# Cercetare — proiectare S4: cautarea articolelor pe server, in linie Investigatie READ-ONLY pentru povestea **S4** din `docs\plan_13_unificare_formular_facturare.md:1865-1874`, plus sectiunea `### C. Incarcarea articolelor — de ce dureaza si ce inlocuieste` (`:589-614`). Fara editari de cod, fara `git_sync.ps1`/`txt2vcx.ps1`, fara commit. `COMUN\clase\ofacturare_comun.vc2` si `COMUN\programe\ofacturare_editare.prg` (perimetrul #6) doar citite, nu atinse. Pe Oracle doar `SELECT`, prin exportul `PACK_FACTURARE` de pe disc — nu s-a rulat nimic pe server. ## Verdict (rezumat) Mecanismul de inlocuire propus in plan **exista deja, dar e un prototip la jumatate**: `grd_factura.cCodMat.cboCodmat` / `cCboDenumire` (`ofacturare.vc2:16751-16797`, wiring la `:19288-19334`) cauta pe server prin `combosql` si scriu `codmat`/`denumire`/`id_articol` in `crsfactura` — dar **nu scriu nimic altceva**, pentru ca sursa lor (`vnom_articole`) nu are pret, TVA, valuta, `id_pol`, `gestionabil`. Asta confirma exact ce zice planul la punctul C: partea Oracle de facut e o **varianta filtrata pe articol a celor cinci cursoare de facturare**, nu o cautare noua. Descoperirea care schimba proiectarea fata de textul din plan: **`crsarticole` nu e doar sursa de populare a gridului — e un registru al cantitatii ramase de facturat**, citit si scris de `do_adauga_tot`, `do_sterge` si `do_scrie_factura` pentru toate tipurile cu document sursa (comanda, aviz, contract-lista-de-preturi). Stergerea unei linii **reface** cantitatea in `crsarticole` (`:14640-14669`), iar la scriere se face `Calculate Sum(cantitate) To lnCantitateRamasa` peste `crsarticole` ca sa se decida daca se inchide automat comanda/avizul (`:14303-14311`, `:14334-14338`). Asta inseamna ca **incarcarea in masa nu poate disparea pentru aceste tipuri**, indiferent de S4 — nu doar pentru ca planul a decis sa pastreze "adauga tot", ci pentru ca bookkeeping-ul de cantitate ramasa e cablat direct pe cursorul incarcat. S4 se aplica deci curat doar pe ramurile de **lista de preturi** (`cursor_preturi`, plus jumatate din `cursor_contract` si `cursor_gestiune`) — vezi punctul 7. A doua descoperire: calea de scriere a pretului la nivel de linie (`adauga_articol_factura`, verificata separat in S10, `docs\cercetare\s10_pret_rederivat.md`) **primeste deja pretul ca parametru** in loc sa-l re-deriveze — exact precedentul pe care trebuie sa-l urmeze si varianta filtrata: cauta pretul o singura data, la alegerea liniei, si il transmite mai departe neschimbat. ## 1. Inventarul celor cinci cursoare Oracle Sursa: `D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql` (corpul pachetului; liniile de mai jos sunt pe corp, `:2138+`, nu pe spec `:335+` — offset **+17** fata de citatele vechi din plan, confirmat si in S10). ### 1.1 `cursor_preturi` — lista de preturi, ramura principala (`:2138-2644`) ``` PROCEDURE cursor_preturi(V_DATA_CURS IN DATE, V_TIP IN NUMBER, V_ID_VALUTA IN NUMBER, V_ID_GESTIUNE_INIT IN NUMBER, V_LUNA IN NUMBER, V_AN IN NUMBER, V_ID_UTIL IN NUMBER, V_ID_SUCURSALA IN NUMBER, V_CURSOR OUT cursor_facturare) ``` **Fara filtru pe articol** — semnatura n-are niciun parametru de cod/denumire. Ramuri pe `V_TIP` (`CASE`, `:2159-2643`): | Ramura | Cand | Sursa randurilor | Observatie | |---|---|---|---| | `V_TIP = 45` | restaurant | `utilizatori_rol_intern` -> `politici_grupuri` -> `crm_politici_preturi` -> `crm_politici_pret_art` -> `nom_articole`, plus `curs`/`nom_valute` | fara filtru de stoc, `cantitate` fixa la 1 (`:2193`) | | `V_TIP IN (1,2)` | factura in lei | acelasi lant de politici, plus `LEFT JOIN` pe `STOC` agregat pe gestiunile utilizatorului | `WHERE` final filtreaza pe stoc > 0 sau `RF_FACTURARE_FARA_STOC` (`:2387-2388`) | | `V_TIP IN (5,6,10,52)` | factura in valuta | `FACT_VPRETURI_UTILIZATOR` (view precalculat, nu politici brute) + `STOC` + `CURS` | `WHERE A.ID_UTIL = V_ID_UTIL AND ((A.ID_VALUTA = V_ID_VALUTA AND A.IN_VALUTA=1) OR A.ID_POL = politica_stoc)` (`:2461-2462`) | | `V_TIP = 7` | credit note | `FACT_VPRETURI_UTILIZATOR`, filtrat suplimentar `NVL(A.nota_discount,0)=1` (`:2538`) | doar articole de discount | | `ELSE` (aviz, tip implicit) | orice alt tip | `FACT_VPRETURI_UTILIZATOR`, fara filtrul de valuta din ramura 5/6/10/52 | ramura cea mai generala | Coloane comune returnate (uniforme pe toate ramurile, ceea ce conteaza pentru mapare — punctul 6): `id_c, id_articol, lot, serie, id_pol, id_valuta, nume_lista_preturi, discount_unitar, discount_unitar_val, codmat, codbare, denumire, um, gestionabil, cantitate, proc_tvav, preturi_cu_tva, curs, multiplicator, pret, pret_val, tip_valuta, nume_val` (+ `modificabil`, `id_gestiune`, `cont` doar pe ramura restaurant). Apeleaza intai `initializeaza_facturare`, `completare_politica_stoc` si **`verifica_cursuri_valute(V_DATA_CURS, V_ID_UTIL)`** (`:2149-2153`) — validare **neconditionata** a cursurilor pentru toate valutele din listele de preturi ale utilizatorului, indiferent de articolul cautat. Relevant pentru S4d (`-20005`). ### 1.2 `cursor_contract` — factura/aviz pe contract (`:2646-2950`) ``` PROCEDURE cursor_contract(V_DATA_CURS, V_TIP, V_ID_VALUTA, V_LISTAID, V_ID_GESTIUNE_INIT, V_LUNA, V_AN, V_ID_UTIL, V_ID_SUCURSALA, V_ID_AGENT OUT, V_NUME_AGENT OUT, V_CURSOR OUT cursor_facturare, V_CURSOR2 OUT cursor_facturare) ``` **Doua cursoare de iesire.** `V_CURSOR2` (`:2718-2938`) e specific contractului: `UNION ALL` intre (a) articole cu `OPT_FACTURARE = 3` din `CTR_ARTICOLE` si (b) rate din `CTR_SCADENTAR` pentru `OPT_FACTURARE IN (1,2)`, cu `id_articol = NULL` pe randurile de rata — filtrate pe `V_LISTAID` (lista de `id_ctr`) prin `charn2collection`. La final (`:2940-2948`) **cheama `cursor_preturi` cu aceiasi parametri** si scrie rezultatul in `V_CURSOR` — deci jumatate din `cursor_contract` e literalmente `cursor_preturi`. In VFP, cele doua cursoare de iesire ajung (observat pe cod, nu documentat explicit in `goExecutor`) in `crsarticole` (=`V_CURSOR`, lista de preturi) si `crsarticole1` (=`V_CURSOR2`, liniile contract + rate) — vezi `ofacturare.vc2:13778` (`lcCursorStoc = Iif(poArticol.opt_facturare = 0, [crsarticole], [crsarticole1])`) si `Destroy` (`:12804-12811`) care inchide ambele. **Randurile de rata nu au `id_articol`** — nu pot fi gasite printr-o cautare cod/denumire; raman legate de gridul `crsarticole1` existent (`grd_contracte`), bounded de numarul de rate ale contractului, nu de catalog. Vezi punctul 7. Valideaza cursurile valutare **doar** pentru valutele prezente in `CTR_SCADENTAR`/`CTR_ARTICOLE` ale contractelor din `V_LISTAID` (`:2679-2716`) — spre deosebire de `cursor_preturi`, care valideaza tot ce are utilizatorul in politici. Filtru deja ingust pe sursa, dar tot pe tot contractul, nu pe articol. ### 1.3 `cursor_comanda` — factura/aviz pe comanda (`:2952-3171`) ``` PROCEDURE cursor_comanda(V_DATA_CURS, V_TIP, V_LISTAID, V_ID_UTIL, V_CURSOR OUT cursor_facturare) ``` `V_LISTAID` e **un singur `id_comanda`** (`TO_NUMBER(V_LISTAID)`, `:2963` — nu o lista, desi parametrul se numeste la fel ca la contract/avize). Doua ramuri identice ca forma (`V_TIP <= 20` = factura, altfel aviz, `:2994-3170`), ambele pe `COMENZI_ELEMENTE` filtrat `WHERE A.ID_COMANDA = V_ID_COMANDA` (`:3078`, `:3166`) — **deja filtrat pe un singur document sursa**, nu pe tot catalogul. `LEFT JOIN` cu suma cantitatilor deja facturate din `VANZARI_DETALII` pe acelasi `ID_COMANDA` (`:3060-3068`) calculeaza `cantitate` ca **ramas de facturat**, nu cantitatea comandata bruta — exact sursa pentru bookkeeping-ul de "adauga tot" / stergere de la punctul urmator. ### 1.4 `cursor_avize` — factura din avize (`:3703-3874`) ``` PROCEDURE cursor_avize(V_LISTAID, V_ID_UTIL, V_DISCOUNT OUT NUMBER, V_CURSOR OUT cursor_facturare) ``` `V_LISTAID` = lista de `id_vanzare` (avize sursa), separate prin virgula. O singura interogare, fara `CASE` pe tip — agrega `VANZARI_DETALII` pe cheie compusa (articol, pol, lot, serie, discount, TVA, gestiune, cont, pret...) si scade ce a fost deja facturat din `VANZARI_CANTITATI` (`WHERE A.CANTITATE <> NVL(B.CANTITATE, 0)`, `:3871`) — acelasi tipar "ramas de facturat" ca la comanda. **Nu are ramuri pe `V_TIP`** — planul o citeaza ca exemplu de "aceleasi ramuri pe tip", dar real e cea mai simpla dintre cele cinci: un singur `SELECT`. ### 1.5 `cursor_gestiune` — transfer intre subunitati pe lista de preturi (`:4158-4310+`) ``` PROCEDURE cursor_gestiune(V_DATA_CURS, V_ID_POL, V_ID_GESTIUNE, V_LUNA, V_AN, V_ID_UTIL, V_ID_SUCURSALA, V_CURSOR OUT cursor_facturare) ``` Foloseste `V_ID_POL` **fix** (nu politica derivata din utilizator ca la `cursor_preturi`), pe `STOC` filtrat `ID_GESTIUNE = V_ID_GESTIUNE` `JOIN` `CRM_POLITICI_PRET_ART WHERE ID_POL = V_ID_POL` (`:4266-4280`) — deja restrans la o singura gestiune si o singura politica, dar tot fara filtru pe articol; `GROUP BY` pe articol agrega stocul. ## 2. De unde sunt executate; cine consuma `crsarticole` **Executia:** un singur loc, `ofacturare.prg:266-311` (`factureaza`), rutare pe `tnTip` (punct de plecare confirmat, `Do Case` la `:266-308`) -> `goExecutor.oExecute(lcSqlCursor, [crsarticole])` (`:310-311`). Acelasi tipar apare a doua oara in `factureaza2` (`ofacturare.prg:824+`, ramura `frm_facturare_articole2`, prototipul). **Consumatorii `crsarticole` in perimetrul S4** (`frm_facturare_articole`, `ofacturare.vc2:11xxx-15300`; lista completa via `vfp_symbols -Grep 'crsarticole\b' -CodeOnly`): | Metoda | Linii | Ce face cu `crsarticole` | Ramane dupa S4? | |---|---|---|---| | `Destroy` | `12804-12811` | inchide cursorul | da, neschimbat | | `do_adauga_articol` | `12813-13167` | citeste **un rand ales** (`Scatter Name poArticol`), il scrie in `crsfactura` | **inlocuit** de randul intors de cautarea pe server, pe ramurile de lista de preturi — vezi punctul 7 | | `do_adauga_tot` | `13169-13198` | `Scan` peste tot cursorul, cheama `do_adauga_articol` pe fiecare rand | **pastrat neschimbat**, dar numai pe tipurile cu document sursa | | `do_cauta` | `13566-13593` | filtru client-side (`Set Filter`) peste cursorul deja incarcat, dupa text tastat in `txtArticole`/`txtCodmat` si politica aleasa | **dispare** pe ramurile de lista de preturi — inlocuit de `combosql` | | `do_scrie_factura` | `14301-14338` | `Calculate Sum(cantitate) To lnCantitateRamasa` peste `crsarticole`, decide daca se inchide comanda/avizul sursa (`pnParametruAditional`) | **pastrat neschimbat** — vezi verdictul | | `do_sterge` | `14608-14669` | la stergerea unei linii din `crsfactura`, **reface** cantitatea in `crsarticole`/`crsarticole1` (`Replace cantitate With cantitate + poArticol.cantitate`) | **pastrat neschimbat** pe tipurile cu bookkeeping; pe lista de preturi nu exista azi replace-back real (cantitatea nu scade la adaugare pe acele tipuri — stocul se verifica separat, prin `cursor_gestiuni_articol*`) | | `do_modifica` | `13778` | alege `crsarticole` sau `crsarticole1` dupa `opt_facturare` pentru verificare stoc | pastrat, tine de gestiunea aleasa la editarea unei linii deja adaugate | | `cb_politici_preturi.InteractiveChange` | `15647-15658` | cheama `do_cauta()` (filtru client-side) | **dispare** — pe varianta noua, schimbarea politicii devine parametru al cautarii pe server (`id_pol` in `WHERE`), nu filtru local | | `KeyPress` (navigare grid) | `15463-15486` | navigare in cursor la taste sageata | **dispare** pe ramurile fara grid de sus | `frm_facturare_articole2` (prototipul, `:17xxx-18xxx`) are exact aceeasi structura pe `do_adauga_articol` / `do_adauga_tot` / `do_scrie_factura` / `do_sterge` — nu difera in aceasta privinta. **Concluzie pentru cat se poate scoate:** `do_cauta` si filtrul din `cb_politici_preturi` dispar integral (inlocuite de cautarea pe server). `do_adauga_articol` isi schimba sursa randului (de la `Scan` in cursorul local la randul ales de `combosql`), dar restul lui (verificare stoc, alegere gestiune via `cursor_gestiuni_articol*`, scriere in `crsfactura`) **nu se schimba** — vezi punctul 6. `do_adauga_tot`, `do_sterge` (partea de bookkeeping) si `do_scrie_factura` (`lnCantitateRamasa`) **nu pot fi scoase** cat timp `crsarticole` ramane sursa lor de adevar pentru "cat a mai ramas" — motiv suplimentar, nu doar UX, pentru care "adauga tot" ramane cablat pe incarcare in masa acolo unde exista document sursa. ## 3. Cat costa azi incarcarea, pe forma interogarii (nu pe timpi masurati — decizia 31) **`cursor_preturi` (ramurile 1/2 si generala) nu are niciun filtru pe articol** — semnatura n-are parametru de cod/denumire (`:2138-2146`). Rezultatul e **intreg catalogul accesibil utilizatorului**: `utilizatori_rol_intern` -> `politici_grupuri` -> `crm_politici_preturi` (toate politicile active la data cursului) -> `crm_politici_pret_art` (toate liniile de pret ale acelor politici) -> `nom_articole`. Numarul de randuri creste cu produsul dintre "cate politici de pret vede utilizatorul" si "cate articole are fiecare politica" — nu cu numarul de articole cautate, care e de regula 1. Trei surse structurale de cost, vizibile direct in forma interogarii, nu masurate: 1. **`LEFT JOIN` pe `STOC` agregat** (`:2359-2378`, ramura 1/2) — subquery cu `GROUP BY ID_ARTICOL` peste tot stocul lunii curente, pe toate gestiunile la care utilizatorul are drept, recalculat la fiecare deschidere de formular, indiferent daca utilizatorul cauta un singur articol. 2. **`FACT_VPRETURI_UTILIZATOR`** (ramurile 5/6/10/52 si 7 si aviz) e un view, nu un tabel — planul nu detaliaza corpul lui aici (ar fi o cercetare separata), dar fiind sursa pentru "toate preturile vizibile utilizatorului", are aceeasi forma: cost proportional cu marimea catalogului, nu cu cautarea. 3. **`verifica_cursuri_valute`** (`:2153`, chemata necondiționat la fiecare apel) valideaza cursul pentru **toate** valutele din politicile utilizatorului, nu doar valuta articolului cautat — cost fix per deschidere, independent de ce se cauta. Concluzia structurala: interogarea de azi calculeaza raspunsul pentru "orice articol ar putea alege utilizatorul", cand formularul are nevoie doar de "articolul pe care tocmai l-a tastat". Filtrarea pe `id_articol`/`codmat`/`denumire` reduce fiecare din cele trei surse la un numar de randuri marginit de cate politici de pret contin acel articol (de regula 1, rar cateva), nu de marimea catalogului. ## 4. Proiectarea variantei filtrate, cursor cu cursor Toate cinci sunt proceduri PL/SQL in `PACK_FACTURARE`, apelate direct din VFP prin `{call pack.proc(...)}` + `goExecutor.oExecute`, fara view intermediar si fara strat ORM — deci minimul de schimbare e **acelasi tipar**: o procedura noua (sau o supraincarcare cu parametru suplimentar) in acelasi pachet, apelata din acelasi loc (`ofacturare.vc2`, metoda nou-introdusa pe `combosql`, nu din `ofacturare.prg`, care ramane neschimbat — el tot incarca varianta "in masa" acolo unde ramane necesara). **Alegere de proiectare, nu fapt verificat:** supraincarcare (aceeasi denumire, parametru nou opțional `V_FILTRU_COD IN VARCHAR2 DEFAULT NULL` / `V_FILTRU_DEN IN VARCHAR2 DEFAULT NULL`) e mai sigura decat o procedura noua, pentru ca **garanteaza** aceleasi ramuri de `CASE`, acelasi `JOIN`, aceeasi logica de rotunjire — orice divergenta viitoare intre "cursor complet" si "cursor filtrat" ar fi un bug de sincronizare greu de prins. Cu supraincarcare, filtrul se adauga o singura data, in `WHERE`-ul final al fiecarei ramuri, nu in logica de business. | Cursor | Ce se adauga | Unde (linia `WHERE`/`CASE` care primeste filtrul) | |---|---|---| | `cursor_preturi` | `AND (V_FILTRU_COD IS NULL OR C.CODMAT LIKE V_FILTRU_COD) AND (V_FILTRU_DEN IS NULL OR UPPER(C.DENUMIRE) LIKE UPPER(V_FILTRU_DEN))` — pe alias-ul articolului, `C` pe patru din cinci ramuri, `A` pe ramurile cu `FACT_VPRETURI_UTILIZATOR` | dupa `WHERE` existent, pe fiecare din cele 5 ramuri (`:2264`, `:2387-2388`, `:2464-2465`, `:2540-2541`, `:2640-2641`) — 5 locuri, nu unul | | `cursor_contract` | acelasi filtru pe `V_CURSOR` (delegat catre `cursor_preturi`, gratuit); pe `V_CURSOR2` filtrul se adauga in `UNION ALL`-ul de la `:2752` (partea cu `id_articol`), **nu** pe partea de rate (`id_articol IS NULL` — nu se pot filtra pe cod, raman needitate de filtru) | `:2825` (join articol) + propagare in `WHERE`-ul de la `:2819` | | `cursor_comanda` | filtru pe `C.CODMAT`/`C.DENUMIRE` in `WHERE A.ID_COMANDA = V_ID_COMANDA` (`:3078`, `:3166`) — **dar nu are sens sa se filtreze**: cf. punctul 7, comanda ramane pe calea "adauga tot" | de proiectat doar daca decizia de la punctul 7 se schimba | | `cursor_avize` | idem — nu are sens, acelasi motiv | idem | | `cursor_gestiune` | filtru pe `C.CODMAT`/`C.DENUMIRE` in interogarea de la `:4234-4237`, inainte de `GROUP BY` | `:4266-4293` | **Parametrii care raman identici** pe varianta filtrata: `V_DATA_CURS`, `V_TIP`, `V_ID_VALUTA`, `V_ID_GESTIUNE_INIT`, `V_LUNA`, `V_AN`, `V_ID_UTIL`, `V_ID_SUCURSALA` — tot ce vine din `poDate` / sesiune la deschiderea formularului, nu se recalculeaza per cautare. Doar filtrul de text e nou, plus (pentru `cursor_preturi` in noua utilizare) eventual `V_ID_POL` daca utilizatorul a ales explicit o lista de preturi in combo-ul `cb_politici_preturi` — azi acel combo doar filtreaza local (`ofacturare.vc2:15647-15658`, comentat inlocuit cu `do_cauta()`); pe varianta noua devine parametru real al interogarii. **Ce nu se poate verifica din cod, ramane de decis de Marius:** daca filtrul pe `codmat` foloseste `LIKE` "incepe cu" (ca `combosql` de azi, `nCharCountBegin`/cautare incrementala) sau match exact la alegerea din lista — combosql de azi face amandoua (tastare = "incepe cu" pe server, alegere din lista = valoare exacta), deci varianta cea mai apropiata de comportamentul actual e sa pastreze acelasi tipar: interogarea filtrata se cheama la fiecare tastare (ca azi, `RefreshData`), iar valoarea finala aleasa vine din randul deja adus, nu dintr-o interogare separata "exact match". ## 5. `combosql` — contract si exemple reale **Clasa:** `combosql AS combobox`, `COMUN\clase\_cb_base.vc2:519-843`. Nu e specifica facturarii — traieste in biblioteca comuna a suitei, dar cautarea nu a gasit nicio alta utilizare, nici in `ROAFACTURARE`, nici in `COMUNROA`/`ROAGEST` (`grep -rn "AS combosql WITH"` — zero potriviri in afara `ofacturare.vc2:16751/16785/16861`, cele trei coloane ale gridului prototip). **Singurul exemplu real de folosire e chiar prototipul din plan** — nu exista alt loc in suita de copiat. **Proprietati relevante** (`_memberdata`, `:560-575`): | Proprietate | Rol | |---|---| | `csourcesql` | `SELECT` fara `WHERE`/`ORDER BY` — baza interogarii | | `csourcewhere` | conditia `WHERE` fixa (ex. `inactiv = 0`) | | `csourceorder` | `ORDER BY`, implicit `pfieldactiv` | | `pcursorname` | numele cursorului cu rezultatele | | `pfieldactiv` | campul dupa care se cauta si se afiseaza | | `psecondfield` | al doilea camp de cautare (optional) | | `ncharcountbegin` | cate caractere minim inainte sa porneasca interogarea pe server | | `llimittolist` | daca valoarea trebuie sa existe in lista | **Mecanismul** (`refreshdata`, `:796-830`): construieste `SELECT ... FROM (cSourceSql) WHERE (cSourceWhere) AND (pFieldActiv LIKE ?pcValue [OR psecondfield LIKE ?pcValue]) ORDER BY ...`, cu `?pcValue` = `cSearchString + '%'` (incepe cu) sau `'%'+cSearchString+'%'` (contine, la Ctrl+Enter) si il executa prin **`goExecutor.oExecuta`** (`selectdata`, `:832-840`) — acelasi executor folosit peste tot in suita pentru apeluri Oracle, deci **niciun mecanism nou de transport**, doar un SQL nou de trimis. `KeyPress` (`:610-776`) gestioneaza incremental tastarea: la fiecare caracter tastat re-executa `RefreshData(1)` daca lungimea depaseste `nCharCountBegin`. **Contractul de legare la o coloana de grid**, dedus din prototip (`ofacturare.vc2:16751-16762` + `:19288-19306`): 1. se adauga ca `CurrentControl` al coloanei (`Column1.CurrentControl = "cCboDenumire"`, `:16621`); 2. `ControlSource` leaga afisarea de campul din cursorul de linii (`crsFactura.codmat`, `:16754`); 3. **evenimentul de reactie e `LostFocus`, nu `Valid` sau `InteractiveChange`** — abia la parasirea celulei se scriu campurile in cursorul de linii, cu paza `If this.Value <> thisform.cOldValue` (setat in `When`, `:19308-19310`) ca sa nu se rescrie fara motiv; 4. `LostFocus` de azi scrie **doar** campurile pe care le are `vnom_articole`: `codmat`, `denumire`, `id_articol` (`:19293`) — nimic despre pret, TVA, valuta. **Aici se opreste prototipul azi**; tot ce trebuie adaugat pentru S4 e sa inlocuiasca `crsCodmat`/`crsDenumire` (rezultatul din `vnom_articole`) cu rezultatul cursorului Oracle filtrat de la punctul 4, care are toate coloanele, si sa extinda `REPLACE`-ul de la `:19293`/`:19317` cu restul campurilor din tabelul de la punctul 6. **Ce nu ofera azi `combosql` si trebuie adaugat, nu doar copiat:** `csourcesql` e un `SELECT` static definit in `.vcx` la design-time, nu poate primi parametrii dinamici ai documentului curent (`poDate.zi_curs`, `poDate.tip`, `poDate.id_valuta`...) direct in proprietate. Pentru cursorul Oracle cu 8 parametri, populate din `poDate`/sesiune, `csourcesql`/`csourcewhere` trebuie construite dinamic in `Init` sau la schimbarea documentului (nu sunt string-uri fixe ca azi), sau — alternativa mai simpla — `refreshdata`/`selectdata` se suprascriu punctual pe instanta din grid, ca sa apeleze `{call pack_facturare.cursor_preturi_filtrat(...)}` in loc de `SELECT ... FROM (cSourceSql)`. A doua varianta pastreaza restul mecanismului (`KeyPress`, incremental, `LostFocus`) neschimbat si e schimbarea minima — de confirmat cu Marius, nu o certitudine de cod. ## 6. Maparea camp-cu-camp Sursa cea mai fiabila pentru "ce completeaza azi `crsarticole` in linie" nu e cursorul Oracle brut, ci `Gather Name poArticol Fields Like ...` din `do_adauga_articol` (`ofacturare.vc2:12945-12957`, identic pe `frm_facturare_articole2` la `:17226+`) — acolo se vede exact ce trece din randul scanat in `crsfactura`. | Camp in `crsfactura` | Vine din `crsarticole` (coloana cursorului Oracle) | Pe varianta filtrata | |---|---|---| | `id_articol`, `codmat`, `codbare`, `denumire`, `um` | direct din cursor | identic — filtrul e chiar pe aceste coloane | | `pret_achizitie` | **nu** din `crsarticole` — vine din `poArtLista`/`crsartselectate` (rezultatul lui `cursor_gestiuni_articol*`, ales dupa `crsarticole`), nu din cursorul de lista de preturi | **neschimbat** — pasul e deja separat azi, ramane separat | | `id_pol` | direct (`crsarticole.id_pol`) | identic | | `Cont` | direct (`crsarticole.cont`, doar ramura restaurant o are explicit; pe restul vine `'371'` implicit sau din `cursor_gestiuni_articol*`) | identic pe ramurile care il au; **de verificat** pe ramurile 1/2/5/6/7/10/52/aviz — cursorul nu are `CONT` explicit in `SELECT` (`:2268-2329` etc.), deci provine din `do_alege_stoc`, nu din `crsarticole` | | `id_gestiune` | direct doar pe ramura restaurant (`A.ID_GESTIUNE`, `:2220`); pe rest vine din alegerea de gestiune (`cursor_gestiuni_articol*`) | identic — pasul de alegere gestiune ramane neschimbat | | `Proc_Tvav`, `pretftva`, `Pretctva`, `discountftva`(`discount_unitar`), `discountctva` | direct din cursor, plus `do_initializeaza_articol` (`:13618-13659`) care deriva `pretftva`/`pretctva`/`tva` reciproc dupa `preturi_cu_tva` | identic — logica de derivare e in VFP, nu se schimba | | `id_valuta`, `tip_valuta`, `nume_val`, `Curs`, `multiplicator` | direct din cursor | identic | | `gestionabil` | direct din cursor | identic (inclusiv regula de proforma, `ofacturare.prg:333-336`, care suprascrie `gestionabil=0` — ramane in `ofacturare.prg`, neschimbata) | | `id_jtva_coloana` | direct doar pe ramurile care il au explicit in `SELECT` (aviz/contract/retur); pe `cursor_preturi` (1/2/5/6/7/10/52) **nu apare in lista de coloane** returnate (`:2264-2329`) | **de verificat cu Marius** — daca lipseste azi, `Gather ... id_jtva_coloana` scrie `NULL`/valoare implicita; filtrarea nu schimba nimic aici, dar merita clarificat inainte de implementare, nu presupus | | `pretv_orig`, `pretd`, `id_valuta_d` | nu apar in `cursor_preturi`; vin din `poArtLista`/`crsartselectate` (alegerea de gestiune) | neschimbat | | `taxcode`, `explicatie`, `id_lucrare_rez`, `id_part_rez`, `id_ctr`, `opt_facturare` | nu din `crsarticole` — din `poArticol`/context (`do_initializeaza_articol`, `do_alege_stoc`) | neschimbat | | `pret_cu_tva` (flag `preturi_cu_tva`) | direct din cursor (`A.PRETURI_CU_TVA` / `D.PRETURI_CU_TVA`) | identic | **Concluzie pentru punctul 6 al cerintei:** nicio coloana din cele cerute explicit (pret, cota TVA, valuta, `id_pol`, `gestionabil`, `pret_cu_tva`) nu ridica probleme — toate vin direct din cursorul Oracle si filtrarea pe articol nu le atinge. Singurul semnal de atentie e `id_jtva_coloana`, absent din `cursor_preturi` insusi (posibil completat in alt pas, neverificat aici — iese din perimetrul "cele cinci cursoare", ar cere citirea intregului `do_adauga_articol`/`calculeaza_totaluri`). ## 7. „Adauga tot" — ce ramane si de ce (nu doar decizia din plan) Planul spune "se pastreaza adauga tot pentru tipurile cu document sursa (comanda, aviz, contract) — acolo setul e marginit". Cercetarea (punctul 2) arata un motiv suplimentar, mai tare decat marimea setului: **bookkeeping-ul de cantitate ramasa** (`do_sterge`, `do_scrie_factura`) citeste si scrie direct in `crsarticole`, deci acel cursor **trebuie sa existe incarcat in memorie** cat timp formularul e deschis, indiferent cum alege utilizatorul liniile. **Vizibilitatea de azi a butonului `but_urmator_tot1`** ("adauga tot"), confirmata pe cod (`ofacturare.vc2:15108-15248`, `Do Case poDate.tip`): | Tip | Vizibil "adauga tot" azi | Sursa cursorului | |---|---|---| | proforma (`eProforma=1`) | da (`:15113`) | `cursor_preturi` sau alt cursor dupa tip, marcat neges­tionabil | | copiere (`lCopiere`) | da (`:15120`) | `cursor_preturi` (varianta copiere, `ofacturare.prg:464-473`) | | `1, 5, 7, 10` — lista de preturi | **nu** | `cursor_preturi` | | `2, 6` — contract | **nu** | `cursor_contract` | | `3` — comanda | da (`:15150`) | `cursor_comanda` | | `4` — avize | da (`:15166`) | `cursor_avize` | | `21, 28, 42, 47` — aviz din comanda | da (`:15177`) | `cursor_comanda` | | `22, 29` — aviz din lista de preturi | **nu** | `cursor_preturi` | | `23, 41` — transfer subunitati | **nu** menționat explicit vizibil | `cursor_gestiune` | | `25` — transfer din comanda | da (`:15215`) | `cursor_comanda` | | `26` — aviz din contract | **nu** | `cursor_contract` | | `8, 9` — retur | da (`:15240`) | `cursor_retur` (nu e in cele 5, iese din perimetru) | | `24` — aviz retur | da (`:15245`) | `cursor_retur` | **Coincide exact** cu impartirea utila pentru S4: butonul e vizibil azi pe tipurile unde `do_adauga_tot` are sens pentru ca setul e mic **si** bookkeeping-ul de ramas il cere (comanda/aviz-din-comanda/retur), si e ascuns pe tipurile de lista de preturi (1/5/7/10, 22/29) si pe contract-lista-de-preturi (2/6/26) — exact ramurile pe care cautarea filtrata inlocuieste incarcarea in masa. **Contractul insa nu are azi "adauga tot" vizibil deloc** (nici pe partea de rate, nici pe partea de articole) — planul semnaleaza asta separat, la S4b, ca gol de acoperit (tipurile 2, 6, 26, 52 lipsesc din conditiile de vizibilitate), nu ca ceva de pastrat. **Concret, ce se schimba per tip:** - **Lista de preturi (1,5,7,10,22,29) si transfer pe lista (23,41,45,48,49):** `crsarticole` nu se mai incarca la deschidere; `combosql` cauta filtrat; "adauga tot" ramane ascuns (ca azi — n-are sens pe catalog intreg). - **Comanda (3,21,25,28,42,47) si avize (4):** `crsarticole` **ramane incarcat in masa**, neschimbat; `combosql` **nu se activeaza** pe aceste tipuri (sau, daca se activeaza pentru UX, cauta *in* cursorul deja incarcat, nu pe server — cautare locala, nu Oracle) pentru ca bookkeeping-ul de cantitate ramasa il cere oricum incarcat. - **Contract (2,6,26,52):** dublu — `crsarticole` (jumatatea `cursor_preturi`) trece pe cautare filtrata ca orice lista de preturi; `crsarticole1` (rate + articole `OPT_FACTURARE=3`) **ramane incarcat in masa**, bounded de contract, neschimbat de S4 (randurile de rata n-au `id_articol`, nu pot fi cautate). - **Retur (8,9,24):** in afara celor cinci cursoare cerute (`cursor_retur`/`cursor_retur_document`), proiectat separat la S4f — nesemnalat aici ca problema, doar exclus din perimetru. ## 8. Interactiunea cu S4b si S10 **Cu S4b (bara de butoane / meniul de adaugare):** S4b proiecteaza meniul `xmenu()` "Adauga articole" cu optiunile pe sursa (tot / alege), inclusiv acoperirea golului de pe contract (tipurile 2/6/26/52). S4 ii da continutul concret pentru ramura "cauta in linie": pe tipurile de lista de preturi (unde S4b nu are nevoie de optiunea "tot" azi), linia de grid cu `combosql` **este** metoda de adaugare — nu exista un dialog separat de ales. Pe tipurile cu document sursa, S4 nu schimba nimic din ce proiecteaza S4b: meniul ramane cablat pe `do_adauga_tot`/alegere din `crsarticole` existent. Punctul de atingere real: daca S4b decide sa afiseze si pe contract un "adauga tot" (gol semnalat la punctul 7), acel "tot" trebuie sa opereze pe `crsarticole1` (rate+articole), nu pe `crsarticole` (lista de preturi) — cele doua cursoare raman distincte si dupa unificare. **Cu S10 (pretul care nu trebuie re-derivat, `docs\cercetare\s10_pret_rederivat.md`):** verificarea S10 arata ca `adauga_articol_factura` **primeste pretul ca parametru** (`V_PRET_ACHIZITIE_TEMP` si restul) si nu-l recalculeaza, cu o singura exceptie tacuta: liniile de contract cu `OPT_FACTURARE = 3`, unde `CTR_ARTICOLE.PRET_UNITAR` **suprascrie** pretul trimis, necondiționat de sursa. Pentru S4, asta inseamna doua lucruri concrete: 1. cursorul filtrat (`cursor_preturi`/`cursor_gestiune` cu filtru pe articol) se cheama **o singura data**, la selectarea liniei in `combosql` (`LostFocus`) — pretul obtinut atunci se scrie in `crsfactura` si **nu se re-interogheaza** la scriere, exact ca azi pe calea `crsarticole` completa; 2. pe **contract**, indiferent daca linia a fost aleasa prin `crsarticole1` (needitat de S4) sau, ipotetic, printr-o cautare filtrata viitoare, riscul de suprascriere tacuta descris in S10 exista deja si S4 nu-l introduce si nu-l rezolva — il mosteneste neschimbat, pentru ca punctul de suprascriere e in `adauga_articol_factura` (scriere), nu in cursorul de cautare (citire). ## 9. Ordinea de executie in pasi 1. **Pas 1 — Oracle, `cursor_preturi` filtrat.** Supraincarcare cu `V_FILTRU_COD`/`V_FILTRU_DEN` opționale, aceleasi 5 ramuri, filtru adaugat in `WHERE`-ul fiecareia (punctul 4). *Gata cand:* apelat cu filtru gol, cursorul e identic randuri-cu-randuri (aceleasi coloane, aceleasi valori, pe fiecare din cele 5 ramuri de `V_TIP`) cu varianta actuala pe acelasi set de parametri — comparatie directa pe SQL, nu pe timp. 2. **Pas 2 — Oracle, `cursor_gestiune` filtrat.** Acelasi tipar, un singur `SELECT`, mai simplu. *Gata cand:* idem, comparatie randuri-cu-randuri cu filtru gol. 3. **Pas 3 — Oracle, `cursor_contract`, doar partea `V_CURSOR`.** Filtrul se propaga gratuit prin apelul catre `cursor_preturi` de la `:2940-2948`; nicio schimbare suplimentara necesara daca Pasul 1 e facut corect. *Gata cand:* `cursor_contract` cu filtru gol produce acelasi `V_CURSOR` ca inainte de Pasul 1 (regresie, nu functie noua). 4. **Pas 4 — VFP, `combosql` pe `cCodMat`/`cCboDenumire`.** Inlocuieste `csourcesql`/`SelectData` cu apelul la cursorul filtrat (punctul 5); extinde `LostFocus` (`:19288-19330`) sa scrie toate campurile din tabelul de la punctul 6, nu doar `codmat`/`denumire`/`id_articol`. *Gata cand:* alegerea unei linii in grid, pe un document de tip lista-de-preturi, produce in `crsfactura` aceleasi valori ca alegerea aceluiasi articol azi din `crsarticole` incarcat complet — comparatie pe date, punctul 10. 5. **Pas 5 — VFP, oprirea incarcarii in masa pe tipurile vizate.** In `ofacturare.prg:266-311`, pentru `tnTip` din ramurile de lista de preturi (1,5,7,10,22,29,23,41,45,48,49) si jumatatea `cursor_preturi` a contractului, `goExecutor.oExecute` nu se mai cheama la deschidere — grid-ul porneste gol, `combosql` populeaza la cerere. *Gata cand:* deschiderea formularului pe aceste tipuri nu executa niciun apel `cursor_preturi`/`cursor_gestiune`/`cursor_contract` (verificabil in log-ul `goExecutor`/`WAIT WINDOW ... NOWAIT` din `combosql.selectdata`, care ramane singurul loc care cheama Oracle pentru articole). 6. **Pas 6 — verificare "adauga tot" neatins.** Pe comanda/aviz/contract-rate, `do_adauga_tot`, `do_sterge`, `do_scrie_factura` raman neschimbate (Pasii 1-5 nu le ating codul). *Gata cand:* regresie manuala pe un document din comanda si unul din aviz — comportament identic cu azi. *Depinde de:* S3 (planul o marcheaza asa; portarea antetului trebuie sa existe inainte, ca sa aiba `poDate` populat corect la deschiderea gridului). ## 10. Cum se verifica „aceleasi valori ca azi" **Testabil pe date (nu doar pe cod), propunere concreta:** pentru fiecare tip reprezentativ (1 sau 5 pentru lista de preturi, 2 pentru contract, 41 pentru transfer-gestiune), se alege un articol prezent in cursorul complet de azi si se compara randul din `crsarticole` (deschidere veche, neschimbata) cu randul intors de cursorul filtrat pe acelasi articol, aceiasi parametri (`zi_curs`, `id_valuta`, `id_gestiune_init`, utilizator) — comparatie coloana cu coloana, in Oracle direct (doua `SELECT`-uri, `MINUS` intre ele pe coloanele comune) sau in VFP prin doua cursoare deschise simultan. Se repeta pe un articol cu politica de discount, unul in valuta si unul fara stoc (`RF_FACTURARE_FARA_STOC`), ca sa acopere ramurile de `CASE` din punctul 1. **Ce nu se poate testa headless — capcana cunoscuta** (`grid-coloane-nu-se-materializeaza-headless`, memorie de proiect): sub `-A -T`, `ColumnCount`/`RecordSource` ale gridului sunt artefacte, nu reflecta ce vede operatorul. Deci **partea VFP** (alegerea din `combosql`, `LostFocus`-ul care scrie in `crsfactura`) nu se poate verifica prin harnessul headless obisnuit — cere fie harnessul UI vizibil existent, fie verificare directa pe cursorul `crsfactura` dupa un apel programatic la metoda `LostFocus` (posibil, pentru ca metoda insasi nu depinde de randare, doar de `This.Value` — de confirmat la implementare). Partea Oracle (comparatia de cursoare de mai sus) **e** testabila headless, prin `SELECT`, fara nicio dependenta de VFP. ## 11. Riscuri si ce ramane de decis de Marius - **`id_jtva_coloana` lipsa din `cursor_preturi`** (punctul 6) — de clarificat inainte de implementare daca e completat in alt pas (nevazut aici) sau ramane `NULL` si azi; filtrarea nu schimba comportamentul, dar nota trebuie inchisa ca sa nu para un bug nou introdus de S4. - **Mecanismul de legare `combosql` -> cursor Oracle parametrizat dinamic** (punctul 5, ultimul paragraf) e o alegere de implementare, nu un fapt de cod — `csourcesql` static din `.vcx` nu poate primi parametrii de sesiune direct; solutia propusa (suprascriere punctuala `refreshdata`) e rezonabila, dar nu e singura posibila si nu exista alt exemplu in suita de comparat. - **Contractul ramane cu doua surse de date pe acelasi grid conceptual** (`crsarticole` filtrat + `crsarticole1` needitat) — de confirmat ca UX-ul (doua zone: cautare + grid rate) e acceptabil, nu doar tehnic corect. - **`cursor_gestiune` (transfer subunitati) nu are azi buton "adauga tot" vizibil** (tabelul din punctul 7 nu-l gaseste in `Do Case`) — de verificat separat daca tipurile 23/41 au alt mecanism de adaugare in masa, neacoperit de cautarea in `crsarticole`/`crsarticole1` directa; nu s-a citit codul suplimentar necesar pentru raspuns cert aici. - **Validarea de curs valutar (`verifica_cursuri_valute`, punctul 1.1) ramane neconditionata** chiar si pe varianta filtrata, daca se pastreaza in procedura supraincarcata — poate produce `-20005` la prima cautare a unui articol, inainte ca utilizatorul sa fi vazut vreun rand, pe o valuta pe care n-o foloseste azi doar pentru ca incarcarea in masa "o gasea" oricum. De discutat cu Marius daca validarea trebuie restransa la valuta articolului cautat sau ramane globala (comportament identic cu azi, doar declansat mai des — la fiecare cautare, nu o data la deschidere). - **Numarul exact de apeluri Oracle pe sesiune de cautare** (un apel per tastare, cu debounce prin `nCharCountBegin`) nu a fost masurat si nu trebuie masurat aici (decizia 31) — dar merita setat explicit `nCharCountBegin` >= 2-3 la implementare, ca sa nu se piarda avantajul filtrarii intr-un numar mare de interogari de o litera. ## Handoff Cercetare incheiata in aceasta sesiune, fara nevoie de predare — toate cele 11 puncte cerute sunt acoperite mai sus, cu citate `fisier:linie` verificate pe fisierul real (nu `.bak`). Niciun fisier de cod atins, niciun `git_sync.ps1`/`txt2vcx.ps1` rulat, nicio scriere pe Oracle.