# S5 — helper VFP `ScrieArticoleFacturaEditate` (`COMUN\programe\ofacturare_editare.prg`) Implementare, nu doar cercetare. Singurul fisier atins: `COMUN\programe\ofacturare_editare.prg` (`.prg` sursa directa, fara binar, fara write-back). Diff: `docs\diff_s5_helper_scriere.patch`. ## 1. Ce s-a facut - `CreeazaCursorArticoleGol` (deci si `CreeazaCursorTvdGol`): adaugate `id_vanzare_set I NULL` si `pret_achizitie N(14,4) NULL`, imediat dupa `sters`, inainte de `denumire` — aceeasi pozitie ca in view-ul `VVANZARI_ARTICOLE` extins de `ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql` (verificat pe disc, coloanele intra la coada listei proprii tabelei, inainte de join-uri). - `IncarcaArticoleFactura`: **nicio modificare de cod** — foloseste deja `select v.*, na.in_stoc from vvanzari_articole v ...` (`:299`), deci cele doua coloane noi ale view-ului ajung automat in `tvd` prin `v.*` in momentul in care view-ul e aplicat in Oracle. Extinderea cursorului gol (mai sus) e suficienta pentru ca `tvd` sa poarte ambele coloane si pe ramura de eroare/fallback. - **A doua definitie a cursorului, `omodificari.vc2:14571` (ramura ROACONT/ROAGEST, `CREATE CURSOR` duplicat literal) NU a fost atinsa** — confirmarea ceruta explicit. Trebuie tinuta sincron manual de agentul care lucreaza pe `.vc2`, altfel `tvd` are structuri diferite dupa aplicatie. - Functie noua `ScrieArticoleFacturaEditate(tnIdVanzare, tcAliasArticole, tcAliasVanzare)` — implicit `'tvd'`/`'tvanz'`. Patru pasi, in ordinea impusa de decizia 38 (marcheaza tot, invie ce ramane, insereaza, recalculeaza), fara `COMMIT`/`ROLLBACK`, fara inchidere de cursoare, salveaza/restaureaza workarea si `Recno()` pe alias. ## 2. Recalculul de totaluri (pasul 4) — LAMURIT de team-lead, acum in helper Livrarea initiala nu includea apelul la `recalculeaza_totaluri_vanzari` in helper, motivat de `rec_s5_cale_scriere_vfp.md` 3.3 ("apelul trebuie facut din VFP, ca pas separat, dupa helper") si de secventa din decizia 38 care arata doua apeluri distincte la nivelul apelantului. **Team-lead a corectat**: acea schita era la nivel de apelant, nu contractul final; apelul intra in helper tocmai ca sa ramana un singur punct de scriere (altfel cele doua puncte de intrare, `ofacturare_comun.vc2` si `comun.vc2`, trebuie amandoua sa-si aminteasca sa-l cheme, cu riscul de a uita unul). **Implementat acum**: pasul 4, ultimul, dupa `INSERT`-uri, gardat de `IF m.llSucces`: ``` begin pack_facturare.recalculeaza_totaluri_vanzari(, ); end; ``` - discountul se citeste din `.discount` (primul rand, garantat unic de garda de no-op) imediat la inceputul functiei, inainte de pasii 1-3 (pozitia in cursorul de articole nu intra in conflict, sunt cursoare separate). - daca `discount` e `.NULL.` in cursor, se trimite literal `NULL` in apelul PL/SQL (pastreaza discountul curent din baza) — **nu** `0`, care l-ar sterge. Daca are valoare, se trimite prin `Alltrim(Str(...,18,4))`. - niciun `UPDATE VANZARI SET DISCOUNT` separat — scrierea discountului ramane exclusiv in responsabilitatea procedurii Oracle, primit ca parametru, exact cum cere decizia 41. - acelasi tratament de eroare ca la ceilalti pasi: `goExecutor.oExecuta`, `.F.` la esec, fara mesaj propriu, fara `COMMIT`/`ROLLBACK`. ## 3. `PRET_ACHIZITIE` pe linia noua — INCHIS: se citeste din cursor, nu se calculeaza in helper Premisa initiala a deciziei 40 ("se preia din nomenclator / stoc") s-a dovedit gresita si a fost revizuita de Marius, dupa constatarea de mai jos. Decizia finala: valoarea **o introduce utilizatorul**, intr-o coloana noua editabila in gridul de pe PAGE3 (`omodificari.vc2`, doar pe liniile noi — alt agent, nu se atinge aici). Helperul doar **citeste** `pret_achizitie` din cursorul de articole (`Nvl(pret_achizitie,0)`), fara niciun lookup Oracle propriu. **Constatarea care a schimbat decizia** (`SELECT` read-only in `MARIUSM_AUTO`, pastrata aici ca sa nu se redescopere): - `NOM_ARTICOLE` **nu are nicio coloana de pret de achizitie curent** — singura coloana cu "PRET" sau "ACHIZ" in nume e `PRETACHCTVA` (`NUMBER`, `DEFAULT 0`), care e un **flag** ("pretul de achizitie contine TVA?"), nu o valoare — confirmat prin utilizarea ei in `pack_facturare.scrie_fact_aviz_custodie` (`docs\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:10116, 10168-10169, 10209`), unde e citita alaturi de `V_PRET_ACHIZITIE` (parametru separat), niciodata ca sursa a lui. - La emitere, `pret_achizitie` **nu vine dintr-o coloana statica** — vine fie din cursorul de stoc VFP (`ofacturare_stoc.prg:221`, `a.Pret As pret_achizitie` din `rul_temp`, adica pretul lotului de stoc ales), fie din cursorul de articole selectate la alegerea din gestiune (`ofacturare.vc2:13822/13849`, `poArticol.pret_achizitie = Pret` din `crsartselectate`), fie e re-derivat in Oracle in `adauga_articol_factura` (`docs\ff_...PACK_FACTURARE.sql:5020, 5093-5134`) — numai pe ramura restaurant (`ntip=45`) din `CRM_POLITICI_PRET_ART.PRETFTVA` + procent de adaos; pe toate celelalte ramuri ramane valoarea primita ca parametru din VFP. Niciuna din aceste cai exista in fluxul de editare: liniile noi la editare se adauga prin `CreeazaPoArticolNouTvd` (`ofacturare_editare.prg:356-457`), care fixeaza **`id_gestiune = -1000` si `gestionabil = 0`** necondtionat — adica editarea NU deschide dialogul de alegere din stoc, deci n-are un lot/gestiune reala de unde sa citeasca pretul. - `STOC` (verificat structura, `all_tab_columns`): tabela periodica `AN, LUNA, ID_ARTICOL, ID_GESTIUNE, PRET, PRETV, CANTS, ...` — un rand pe lot/perioada/gestiune, aceeasi granularitate ca `RUL`. **Nu exista un "pret curent" unic** fara o formula de agregare (ce perioada, ce gestiuni). **Ce e implementat acum, in `ScrieArticoleFacturaEditate`**: la pasul `INSERT` al liniei noi, `PRET_ACHIZITIE = Nvl(.pret_achizitie, 0)`, citit direct din cursor (coloana adaugata la punctul 1) — fara `STOC`, fara `NOM_ARTICOLE`, fara functie ajutatoare. La `UPDATE`-ul liniilor pastrate, coloana ramane **omisa din `SET`**, exact ca in specificatia initiala (pe liniile existente nu e editabila, valoarea persistata nu se atinge). ## 4. Coloanele `NOT NULL` din `VANZARI_DETALII` — verificat, punctul NEVERIFICAT din cercetare e inchis `SELECT column_name, nullable, data_default FROM all_tab_columns WHERE owner='MARIUSM_AUTO' AND table_name='VANZARI_DETALII' AND nullable='N'` (read-only, `MARIUSM_AUTO`/`ROA_CENTRAL`): | Coloana | `DATA_DEFAULT` | Tratament in `INSERT` | |---|---|---| | `ID_VANZARE_DET` | — | nu se scrie, vine din trigger `TRG_VANZARI_DET_BEFOINS` | | `ID_VANZARE` | — | scris explicit (`tnIdVanzare`) | | `PRET` | — | scris explicit (din `tvd.pret`) | | `VALIDAT` | `0` | nescris, ramane `DEFAULT 0` | | `STERS` | `0` | nescris, ramane `DEFAULT 0` (linie noua = activa) | | `DIFERENTA` | `0` | nescris, ramane `DEFAULT 0` | | `CUSTODIE` | `0` | nescris, ramane `DEFAULT 0` | | `DESCARCAT` | `0` | nescris, ramane `DEFAULT 0` | Toate cele cinci `NOT NULL` fara valoare din trigger **au `DEFAULT 0` la nivel de coloana** — nu e nevoie sa fie enumerate in `INSERT`. Singurele `NOT NULL` care trebuie scrise explicit sunt `ID_VANZARE` si `PRET`, deja acoperite de contract. ## 5. SQL generat, cate un exemplu per categorie **Pasul 1 — marcheaza tot sters** (`tnIdVanzare = 12345`, `gnIdUtil = 7`): ```sql update vanzari_detalii set sters = 1, id_utils = 7, dataoras = sysdate where id_vanzare = 12345 and sters = 0 ``` **Pasul 2 — linie pastrata** (`id_vanzare_det = 98765`, `cantitate = 2.5`, `pret = 10.5`, `pret_cu_tva = 0`): ```sql update vanzari_detalii set sters = 0, cantitate = 2.500, pret = 10.5000, pret_cu_tva = 0, id_utils = 7, dataoras = sysdate where id_vanzare_det = 98765 ``` **Pasul 3 — linie noua** (`id_articol = 111`, `cantitate = 1`, `pret = 20`, `pret_cu_tva = 0`, `proc_tvav = 1.19`, `discount_unitar = 0`, `id_gestiune = -1000`, `cont` gol -> `NULL`, `id_valuta` `.NULL.` -> `NULL`, `id_jtva_coloana = 3`, `serie`/`lot` goale -> `NULL`, `explicatie = "test 'x'"`, `taxcode` `.NULL.` -> `NULL`, `pret_achizitie` tastat de utilizator in grid = `15.2300`): ```sql insert into vanzari_detalii (id_vanzare, id_articol, cantitate, pret, pret_cu_tva, proc_tvav, discount_unitar, id_gestiune, cont, id_valuta, id_jtva_coloana, serie, explicatie, taxcode, lot, pret_achizitie, id_utils, dataoras) values (12345,111,1.000,20.0000,0,1.1900,0.0000,-1000,NULL,NULL,3,NULL,'test ''x''',NULL,NULL,15.2300,7,sysdate) ``` (`OracleSpecialCharacters` a dublat apostroful din `explicatie`.) **Pasul 4 — recalculul totalurilor** (`tnIdVanzare = 12345`, `tvanz.discount = 5` — cazul cu valoare): ```sql begin pack_facturare.recalculeaza_totaluri_vanzari(12345,5.0000); end; ``` Cazul `tvanz.discount` `.NULL.` (pastreaza discountul curent din baza): ```sql begin pack_facturare.recalculeaza_totaluri_vanzari(12345,NULL); end; ``` ## 6. Ce a ramas netestat, si de ce - **Nimic rulat** — nu era in scope ("NU rulezi teste care scriu in baza"). Verificarea de mai sus (coloane `NOT NULL`, tipuri, existenta view-ului) a fost facuta prin `SELECT`-uri read-only in `MARIUSM_AUTO`, nu prin executarea functiei. - Textul SQL generat pentru cei 4 pasi (sectiunea 5) e verificat manual, pe exemple, nu prin rularea functiei cu un `goExecutor` mock. - Restul e netestabil headless din motivele deja consemnate in `rec_s5_cale_scriere_vfp.md` sectiunea 7 (ordonarea fata de `actualizeaza_vanzari` cere Oracle real; grid-ul de pe PAGE3 care va scrie `pret_achizitie` in cursor nu exista inca — depinde de agentul pe `omodificari.vc2`). ## 7. Rezumat pentru revizuire Ambele puncte semnalate initial ca neconfirmate au fost lamurite de team-lead/Marius: 1. **Recalculul de totaluri** — intra in helper, ca pasul 4 (sectiunea 2). 2. **`PRET_ACHIZITIE`** — nu se mai calculeaza in helper; se citeste din cursor, unde va fi scrisa de utilizator prin coloana noua din grid (sectiunea 3). Toata functionalitatea (structura cursorului, cei 4 pasi de scriere, coloanele `NOT NULL`) e implementata conform contractului final, fara ambiguitate ramasa in acest fisier. ## 8. Corectii din revizuirea team-lead **1. Garda de no-op extinsa cu `Reccount(tcAliasArticole) = 0` — schimbare de fond fata de cercetare.** `rec_s5_cale_scriere_vfp.md` sectiunea 6 spunea explicit ca un cursor de articole gol cu `tnIdVanzare > 0` **nu** e no-op ("s-au sters toate liniile"), plecand de la premisa ca un cursor gol apare doar prin stergerea tuturor liniilor de catre utilizator. Team-lead a aratat, cu dovada din cod, ca premisa e incompleta: `IncarcaArticoleFactura` (`ofacturare_editare.prg:305-309`) intoarce **`.T.` cu cursor gol** si la esec Oracle: ``` IF lnSucces < 0 AMESSAGEBOX(goExecutor.cEroare,0+16,"Eroare") CreeazaCursorArticoleGol(m.lcAlias) RETURN .T. ENDIF ``` Helperul nu poate distinge "utilizatorul a sters tot" de "incarcarea a picat" — ambele ajung cu `tvd` gol si `.T.` de la apelul de incarcare. Fara garda, al doilea caz ar duce, prin pasul 1 (marcheaza tot sters, fara nimic de inviat la pasul 2), la golirea tacuta a facturii la un simplu esec de retea/Oracle la deschiderea formularului. Corectat: `Reccount(m.lcAliasArt) = 0` s-a adaugat la garda initiala de no-op — cursorul gol nu mai scrie nimic. Cazul legitim (utilizatorul chiar sterge toate liniile) e oricum oprit mai devreme de validarea din `inainte_de_do_termin` (alt agent, `omodificari.vc2`), inainte ca helperul sa fie apelat. **2. `UPDATE`-ul liniilor pastrate (pasul 2) ancorat si pe `id_vanzare`.** `WHERE id_vanzare_det = ...` nu avea garda pe document. Adaugat `and id_vanzare = ` — cost zero, inchide posibilitatea teoretica de scriere intr-un alt document daca aliasul ar ajunge vreodata sa contina o linie straina de `tnIdVanzare`. Nimic altceva schimbat: ordinea pasilor, `NULL` pe discount, omiterea `PRET_ACHIZITIE` din `SET`-ul de `UPDATE`, `OracleSpecialCharacters`, restaurarea `Recno()`/workarea raman ca in runda anterioara.