# S5 — calea de scriere VFP: unde se agata scrierea in VANZARI_DETALII Cercetare read-only, 09.08.2026. Nu s-a modificat niciun fisier si nu s-a rulat nimic care sa scrie in Oracle. **Surse.** Partea VFP: versiunile text `.vc2` din arbore (`COMUN\clase\*.vc2`), verificate ca fiind la zi — `mtime` identic cu al binarelor (`omodificari.vcx`/`.vc2` = 09.08.2026 18:53, `ofacturare_comun` = 08.08.2026 23:26, `comun` = 09.08.2026 09:45). Atributia clasa/metoda pentru fiecare linie citata e din `vfp_symbols.ps1 -Where`. **Atentie:** alti agenti lucreaza in paralel pe `omodificari.vc2` — numerele de linie din acest raport sunt un instantaneu 09.08.2026 ~21:15. Briefingul dadea `inainte_de_do_termin` la `:13357-13549`; azi e la **`:14249-14441`**. Partea Oracle: surse PL/SQL **de pe disc** — `COMUN\docs\PACK_CONTAFIN.pck` (03.08.2026) si `D:\ROA\ROAACNPRO\PACK_FACTURARE.pck` (10.06.2026, copie mai veche). Corpurile citate mai jos coincid caracter cu caracter cu ce raporta `rec_s5_oracle_vanzari.md` dintr-un export proaspat (08.08.2026), deci sunt de incredere; nu s-a facut un export nou in aceasta sesiune. --- ## 1. Lantul complet de salvare, ambele puncte de intrare ### 1.1 ROAFACTURARE — `frm_facturi.do_editare_factura` (`COMUN\clase\ofacturare_comun.vc2:3715-3869`) | Pas | Linie | Ce face | |---|---|---| | garzi | `:3716-3767` | `lactiv3`, `glLunaInchisa`, `sters=1`, proforma, luna curenta, `ReferinteDocumenteNota`, `EsteInEFactura` | | incarcare nota | `:3769` | `IncarcaCursoareModificareNota(lnCod, pnAn, pnLuna, .F.)` -> `tact`/`trul`/`trul_obinv` | | incarcare articole | `:3793` | `IncarcaArticoleFactura(m.lnIdVanzare, 'crsArticoleFactura')` — **inainte** de `Createobject`, ca `Load()` sa lege gridul pe cursor plin | | formular | `:3796-3797` | `Createobject([frm_modific2024], lnIdSet)` + `.Show()` — modal (`WindowType = 1`, `omodificari.vc2:6892`) | | confirmare | `:3799` | `If buton = 1` | | **tranzactie ON** | **`:3800`** | `Thisform.do_deschide_tranzactie()` | | stergere nota veche | `:3802` | `lnSucces = OSCRIE_IN_FISIERE(2,.T.,.T.)` | | rescriere cursoare | `:3807-3820` | `tact`->`actactan`, `trul`->`RUL_TEMP`, `trul_obinv`->`RUL_TEMP_OBINV` | | scriere nota noua | `:3821` | `lnSucces = OSCRIE_IN_FISIERE(0,.T.,.T.)` | | **finalizare** | **`:3824-3826`** | `begin pack_contafin.finalizeaza_modificare_nota(...); end;` prin `goExecutor.oExecuta`; `lnSucces = Iif(..., 1, -1)` | | **tranzactie OFF** | **`:3828`** | `Thisform.do_inchide_tranzactie(Iif(lnSucces<0, 2, 1))` | | reafisare | `:3830` | `Thisform.do_cauta()` | | curatenie | `:3837-3860` | inchide `actactan`, `tact`, `rul_temp`, `trul`, `rul_temp_obinv`, `trul_obinv`, `crsJtvaTemp`, `crsArticoleFactura` — **NU** inchide `tvd`/`tvanz` | ### 1.2 ROACONT / registru jurnal — `afisjurcom.do_modifica` (`COMUN\clase\comun.vc2:2222-2569`) | Pas | Linie | Ce face | |---|---|---| | garzi | `:2230-2290` | `lactiv3`, `glLunaInchisa`, luna curenta, `id_set` 30000-30009, nota de inventar | | incarcare nota | `:2352-2426` | acelasi SQL ca `IncarcaCursoareModificareNota` (cod duplicat, nu apel) | | incarcare articole | **`:2435-2437`** | `IF "OFACTURARE_EDITARE" $ Upper(Set("Procedure"))` -> `PregatesteArticoleFacturaEditare('tact')` | | formular | `:2439`, `:2445` | `Createobject([frm_modific2024], lnIdSet)` + `.Show()` | | confirmare | `:2447` | `If buton=1` | | **tranzactie ON** | **`:2451`** | `Thisform.do_deschide_tranzactie()` | | stergere nota veche | `:2454` | `oscrie_in_fisiere(2,.T.,llRul)` (sarit daca nota era deja stearsa, `:2456`) | | rescriere cursoare | `:2463-2481` | idem | | scriere nota noua | `:2482` | `oscrie_in_fisiere(0,.T.,llRul)` | | **finalizare** | **`:2487-2489`** | `finalizeaza_modificare_nota` prin `goExecutor.oExecuta` | | **tranzactie OFF** | **`:2538`** | `Thisform.do_inchide_tranzactie(Iif(lnSucces<0, 2, 1))` | | curatenie | `:2546-2566` | inchide aceleasi cursoare + `crsArticoleFactura`; **NU** `tvd`/`tvanz` | **Punctul de intrare 2 e reachable din ROAFACTURARE**, nu doar din ROACONT: `viz_act` (`COMUN\programe\ooperatii_comune.prg:115-126`) instantiaza `AFISJURcom`, iar `ooperatii_comune.prg` e inregistrat in `Programe\roafacturare.prg:218`. `ofacturare_editare.prg` e inregistrat **numai** in ROAFACTURARE (`Programe\roafacturare.prg:214` — singura potrivire in tot `D:\ROA`), deci in ROACONT/ROAGEST garda `"OFACTURARE_EDITARE" $ Set("Procedure")` e falsa, pagina 3 nu apare (`omodificari.vc2:14683`) si nu exista nimic de scris. Codul nou trebuie sa fie no-op acolo, nu doar inofensiv. ### 1.3 Raspunsul explicit: **DA, tranzactia e inca deschisa dupa ce `finalizeaza_modificare_nota` se intoarce** In ambele puncte de intrare `do_inchide_tranzactie` vine **dupa** apelul PL/SQL (`ofacturare_comun.vc2:3826` -> `:3828`; `comun.vc2:2489` -> `:2538`). Intre ele nu se executa nimic. Contractul, verificat in sursa (`COMUN\clase\_frm_base.vc2:252-302`): - `do_deschide_tranzactie()` — `SQLSetprop(gnHandle,"Transactions",2)`; intoarce **`.T.`/`.F.`**; - `do_inchide_tranzactie(tnTip)` — `tnTip = 1` -> `Sqlcommit(gnHandle)`, **orice altceva** -> `Sqlrollback(gnHandle)`; apoi revine pe `Transactions = 1`; intoarce `.T.`/`.F.`. Deci fereastra `:3826-3828` / `:2489-2538` e **exact locul de agatare**: aceeasi conexiune, aceeasi tranzactie manuala, `COMMIT`/`ROLLBACK` inca nedat. **Capcana de contract, preexistenta, de care sa nu depinda codul nou:** garda de commit e `Iif(lnSucces<0, 2, 1)`, deci `lnSucces = 0` **comite**. Codul nou trebuie sa puna explicit `lnSucces = -1` la esec, nu `0`. ### 1.4 Contractele functiilor pe care se sprijina agatarea (deschise, nu presupuse) - **`goExecutor.oExecuta(...)`** (`COMUN\programe\oproceduri_comune.prg:121-158`): intoarce **logic** `.T.`/`.F.`, si **afiseaza singur** `amessagebox(This.oPrelucrareEroare(), 16, "Eroare")` la esec (`:153-156`). Apelantul nu mai trebuie sa afiseze nimic. - **`goExecutor.oExecute(...)`** (`:173-...`): intoarce **numeric** `CT_SUCCES`/`CT_INSUCCES` (`:218-220`), **nu** numar de randuri. Nu le confunda. - **`OSCRIE_IN_FISIERE(tnScrie_Sterge, tlModificare, tlRul, ...)`** (`COMUN\programe\oscrie_in_fisiere.prg:14`): numeric, `>0` = succes; `-1` la esecuri de precon- ditie (`:68-81`), `-5` la verificarea de stoc (`:104`). Parametrul 1: `0` = scriere, `2` = stergere. - **`_frmbase.do_termin`** (`_frm_base.vc2:363-376`): singura poarta care pune `buton = 1` / `gnButon = 1`, si o face **doar daca `this.inainte_de_do_termin()` intoarce `.T.`**. `frm_modific2024` **nu** suprascrie `do_termin` (nu apare in lista de metode proprii), deci mosteneste asta. --- ## 2. Starea purtata de cursorul `tvd` (si de `tvanz`) ### 2.1 Coloanele lui `tvd` `tvd` se creeaza in `frm_modific2024.Load` (`omodificari.vc2:14554-14591`): pe ramura ROAFACTURARE prin `CreeazaCursorTvdGol()` -> `CreeazaCursorArticoleGol('tvd')` (`COMUN\programe\ofacturare_editare.prg:263-282`), pe ramura ROACONT/ROAGEST printr-un `CREATE CURSOR` **duplicat literal** in clasa (`omodificari.vc2:14571`) — doua definitii care trebuie tinute sincron manual. Se umple din `VVANZARI_ARTICOLE` prin `IncarcaArticoleFactura` (`ofacturare_editare.prg:288-323`), cu `where v.id_vanzare = and v.sters = 0` (`:300`) — deci **la incarcare toate randurile au `sters = 0`**. | Coloana | Provenienta | Editabila in grid | |---|---|---| | `id_vanzare`, `id_vanzare_det` | view | nu | | `id_articol` | view | nu | | **`cantitate`** | view | **DA** — `Column5`, `ReadOnly = .F.` (`:12366-12374`) | | **`pret`** | view | **DA** — `Column6`, `ReadOnly = .F.` (`:12375-12383`) | | **`pret_cu_tva`** (flag 0/1) | view | **DA** — `Column7` checkbox, `ReadOnly = .F.`, `Sparse = .F.` (`:12384-12391`) | | `proc_tvav` | view | nu (`Column8.ReadOnly = .T.`) | | `discount_unitar` | view | nu (`Column9.ReadOnly = .T.`) | | `id_gestiune`, `cont`, `id_valuta`, `id_jtva_coloana`, `serie`, `explicatie`, `taxcode`, `lot`, `sters` | view | nu (`cont`, `id_gestiune`, `id_valuta`, `id_jtva_coloana`, `sters` nici macar nu au coloana in grid) | | `denumire`, `codmat`, `nume_gestiune`, `nume_val` | join-uri din view | nu | | `in_stoc` | **nu e in view** — join separat pe `NOM_ARTICOLE` in SQL-ul din `:299` | nu | | **`lmodificat` (L)** | calculat, `.F.` la incarcare (`:316`) | — | | **`valoare` (N 14,2)** | calculat la incarcare (`:316-319`) si recalculat la fiecare editare | nu (`Column14.ReadOnly = .T.`) | Definitia view-ului: `docs\cercetare\ff_view_articole_vanzare.sql` (21 de coloane, valori brute, fara conversie valutara; filtrul `STERS` lasat pe seama apelantului). **Doar 3 coloane sunt editabile: `cantitate`, `pret`, `pret_cu_tva`.** Tot restul e read-only in grid; `explicatie`/`taxcode` se schimba doar pe cealalta cale, `frm_modifica_articol_factura` (punctul 4). ### 2.2 Cum se distinge o linie modificata / stearsa / adaugata - **Modificata**: `tvd.lmodificat = .T.`, **per linie**, nu global. Se pune in exact trei locuri: - `frm_modific2024.calculeaza_valori_articol` (`:13397-13409`, linia `:13407` `REPLACE lmodificat WITH .T., valoare WITH m.lnValoare`) — apelata din `Valid`-ul cantitatii (`:16428-16433`), `Valid`-ul pretului (`:16439-16444`) si `InteractiveChange`-ul checkboxului `pret_cu_tva` (`:16450-16458`); - `cmdStergeArticol.Click` (`:16423`); - `AdaugaLinieTvdDinArticol` (`:13119`). `Valid`-urile compara cu `Thisform.oldvalue` (setat in `When`), deci o retastare a aceleiasi valori **nu** marcheaza linia. Nu exista `lmodificat` la nivel de formular. - **Stearsa**: `tvd.sters = 1`. `cmdStergeArticol.Click` (`:16417-16426`) **comuta** (`REPLACE sters WITH IIF(Nvl(sters,0) = 1, 0, 1)`), deci stergerea e reversibila pana la salvare; linia ramane vizibila, grizata prin `DynamicForeColor = "IIF(tvd.sters=1,RGB(150,150,150),...)"` pe toate cele 14 coloane. Toate randurile incarcate pornesc de la `sters = 0`, deci `sters = 1` in `tvd` inseamna intotdeauna "sters in sesiunea curenta". - **Adaugata**: `tvd.id_vanzare_det = 0`, pus explicit de `AdaugaLinieTvdDinArticol` (`:13108`). Randul vine din `frm_articol_factura` prin `poArticol` (`cmdAdaugaArticol.Click`, `:16374-16415`), cu `id_vanzare` = `This.nIdVanzare` si conversie RON->valuta documentului pe ramura `tip_valuta = 0` (`:13097-13105`). Combinatia `id_vanzare_det = 0 AND sters = 1` e posibila (linie adaugata si apoi stearsa in aceeasi sesiune) si trebuie ignorata la scriere. ### 2.3 Valorile VECHI: **NU se pastreaza nicaieri** Confirmat prin cautare: nu exista niciun cursor de instantaneu (`tvd_orig`, `crsArticoleOrig`, `tvanz_orig`, `nDiscountVechi` etc.) in cod de productie — singura potrivire e intr-un test (`COMUN\utile\Teste\editare_factura\test_ui_bara_totaluri.prg:86`). `tvd` poarta **doar** valorile curente plus flagul `lmodificat`; valoarea dinainte de editare se pierde in momentul in care utilizatorul o schimba. Consecinte: - scrierea nu poate fi restransa la "coloanele efectiv schimbate" — se scriu toate cele trei coloane editabile plus `valoare`, pe randurile cu `lmodificat = .T.`; - cerinta S4b de a **enumera** utilizatorului "linia X: cantitate 5 -> 8" **nu e realizabila azi**; cere un cursor de instantaneu luat imediat dupa `IncarcaArticoleFactura` (ex. `SELECT id_vanzare_det, cantitate, pret, pret_cu_tva FROM tvd INTO CURSOR tvd_initial`). Nu e implementat; e lucrare in plus, nu detaliu. ### 2.4 `tvanz` si discountul de document `tvanz` se creeaza tot in `Load` (`:14574-14582`), prin `CreeazaCursorTvanzGol()` (`ofacturare_editare.prg:148-154`) sau prin `CREATE CURSOR` duplicat (`:14581`, **cu 3 coloane mai putin** — `in_valuta`/`id_valuta`/`nume_val` lipsesc pe ramura ROACONT). Se umple in `Show()` prin `IncarcaVanzareDinNota('tact')` (`:14684`), care cauta randul din `VANZARI` incercand toate tripletele distincte `(cod, nract, serie_act, dataact)` din `tact` (`ofacturare_editare.prg:203-258`). Coloane: `id_vanzare, tip, discount, total_fara_tva, total_tva, total_cu_tva, curs, multiplicator, in_valuta, id_valuta, nume_val`. - **`discount` e editabil direct**: `txtDiscountArt` are `ControlSource = "tvanz.discount"`, `ReadOnly = .F.` (`omodificari.vc2:12825-12836`). `Valid`-ul lui cheama doar `ActualizeazaBaraTotaluri()` (`:16460-16462`). - **Nu exista `lmodificat` pe `tvanz`** si nici valoare veche salvata. Nu se poate sti daca utilizatorul a atins discountul. Singura optiune fara lucrare in plus: scrie discountul **neconditionat** cand pagina a fost activa (`UPDATE VANZARI SET DISCOUNT = ...` e idempotent). Daca se vrea "doar daca s-a schimbat", trebuie retinuta valoarea initiala la incarcare. - `tvanz.total_cu_tva` e afisat ca "Total salvat" (`txtTotalSalvatArt`, `ControlSource = "tvanz.total_cu_tva"`, ReadOnly, `:12895-12907`) — e valoarea din baza, nu una recalculata. - Totalurile calculate stau in proprietati de formular, nu in cursor: `nTotalLiniiRon`, `nTotalNetRon` (`ActualizeazaBaraTotaluri`, `:12934-12976`), `nTotalActRon`, `nTotalRulRon`, `lRulAjustat` (`ActualizeazaVerdictActRul`, `:12978-13079`). Ele dispar odata cu formularul. ### 2.5 Cursoarele supravietuiesc formularului `frm_modific2024` **nu are proprietatea `DataSession`** (nicio potrivire in tot `omodificari.vc2`), deci ruleaza in sesiunea de date implicita: `tvd` si `tvanz`, create in `Load()`, raman deschise dupa `This.Release` din `do_termin`. Niciunul dintre cei doi apelanti nu le inchide (`ofacturare_comun.vc2:3837-3860`, `comun.vc2:2546-2566`). **Asta face agatarea posibila** — dupa `Omodif.Show()`, apelantul citeste `tvd`/`tvanz` direct. Corolar: obiectul `Omodif` e Released, deci **nu se pot citi proprietatile lui** (`lAreArticoleVanzari`, `nIdVanzare`) dupa `Show()`. Semnalul "pagina de articole a fost activa" trebuie dedus din cursoare: `Used('tvanz') AND Reccount('tvanz') = 1 AND Used('tvd')`, iar `id_vanzare` se ia din `tvanz.id_vanzare`. --- ## 3. Unde se agata scrierea — si ce ordonare rezista capcanei ### 3.1 Capcana, verificata in sursa `pack_contafin.finalizeaza_modificare_nota` (`COMUN\docs\PACK_CONTAFIN.pck:8601-8649`): ```sql lnCodNou := pack_contafin.get_cod(); SELECT COUNT(*) INTO lnEInVanzari FROM vanzari WHERE cod = tnCod; IF lnEInVanzari > 0 THEN pack_facturare.actualizeaza_vanzari(tnCod, lnCodNou); END IF; UPDATE atasamente_vanzari SET cod = lnCodNou WHERE cod = tnCod; ... ``` `pack_facturare.actualizeaza_vanzari` (`D:\ROA\ROAACNPRO\PACK_FACTURARE.pck:15951-15961`): ```sql -- de modificat in caz ca il las sa stearga manual inregistrari din VANZARI_DETALII -- acum se marcheaza cu STERS = 1 doar cand se sterge toata factura UPDATE VANZARI_DETALII SET STERS = 0 WHERE ID_VANZARE IN (SELECT ID_VANZARE FROM VANZARI WHERE COD = V_COD_VECHI); UPDATE VANZARI SET COD = V_COD_NOU, STERS = 0 WHERE COD = V_COD_VECHI; ``` Pentru o factura, `COUNT(*) > 0` e intotdeauna adevarat, deci **resetul `STERS = 0` ruleaza la FIECARE editare de nota**. `ID_VANZARE` nu se schimba niciodata (se schimba doar `COD`), deci o cheie `ID_VANZARE` capturata inainte ramane valida dupa. ### 3.2 Doua consecinte, nu una 1. **Evidenta**: o linie marcata `STERS = 1` de VFP *inainte* de `finalizeaza_modificare_nota` e reactivata tacut. Deci scrierea trebuie sa fie **dupa**. 2. **Mai putin evidenta, si mai grava**: la o editare **ulterioara** a aceleiasi facturi, resetul reactiveaza si liniile sterse in sesiuni **anterioare**. `tvd` se incarca doar cu `sters = 0` (`ofacturare_editare.prg:300`), deci VFP nici nu stie ca liniile alea exista si nu le-ar re-marca. Rezultat: **o linie stearsa luna trecuta reapare la prima re-editare a facturii**, si intra si in recalculul de totaluri (care citeste `STERS = 0`). Asta nu e o problema azi, pentru ca azi nu exista stergere per linie — devine problema exact prin #6. ### 3.3 Ordonarea care rezista Punctul de agatare, pentru **ambele** puncte de intrare, e **imediat dupa apelul `finalizeaza_modificare_nota` si inainte de `do_inchide_tranzactie`**, gardat de `lnSucces > 0`: - ROAFACTURARE: intre `ofacturare_comun.vc2:3827` (`Endif`-ul apelului) si `:3828`; - registru jurnal: intre `comun.vc2:2490` (`Endif`-ul apelului) si `:2538`. Secventa completa, o singura tranzactie manuala: ``` do_deschide_tranzactie() OSCRIE_IN_FISIERE(2, ...) -- sterge nota veche OSCRIE_IN_FISIERE(0, ...) -- scrie nota noua finalizeaza_modificare_nota(...) -- realiniaza COD, RESETEAZA STERS = 0 pe toate liniile >>> ScrieArticoleFacturaEditate(...) -- NOU: liniile + discountul >>> recalculeaza_totaluri_vanzari(id_vanzare) -- NOU (S5 Oracle) do_inchide_tranzactie(Iif(lnSucces < 0, 2, 1)) ``` Iar in interiorul helperului, ordinea operatiilor: 1. `INSERT` pentru liniile noi (`id_vanzare_det = 0 AND Nvl(sters,0) <> 1`) — intai, ca ID-urile lor sa poata fi excluse la pasul 3; PK-ul vine din trigger, nu se cere din secventa (`rec_cale_vanzari_detalii.md` 3.2); 2. `UPDATE` pentru liniile existente atinse (`id_vanzare_det > 0 AND lmodificat AND Nvl(sters,0) <> 1`); 3. **stergere ca diferenta de multimi, nu ca lista de linii sterse**: `UPDATE VANZARI_DETALII SET STERS = 1, ID_UTILS = :util, DATAORAS = SYSDATE WHERE ID_VANZARE = :id AND STERS = 0 AND ID_VANZARE_DET NOT IN ()`. Formularea asta e cea care rezista: ea nu enumera ce s-a sters acum, ci **impune** ca multimea activa din baza sa fie exact multimea activa din `tvd`. Repara automat si consecinta (2) din 3.2 — liniile inviate de reset redevin sterse — si e idempotenta. Pentru asta, `` trebuie sa includa si ID-urile randurilor tocmai inserate (de aici ordinea INSERT-inainte), sau, mai simplu, pasul 3 se restrange la `... AND ID_VANZARE_DET NOT IN ( 0 active din tvd>) AND ID_VANZARE_DET NOT IN ()`. Daca lista de ID-uri inserate e greu de obtinut din VFP, alternativa e sa se ruleze pasul 3 **inainte** de INSERT — atunci lista `NOT IN` contine doar ID-uri preexistente si randurile noi nu sunt inca in tabela, deci nu pot fi atinse. **Recomandare: pasul 3 primul, apoi INSERT, apoi UPDATE.** Mai simplu si fara nevoia de `RETURNING`. 4. `UPDATE VANZARI SET DISCOUNT = :discount WHERE ID_VANZARE = :id` (din `tvanz.discount`). **Recalculul de totaluri se muta din `finalizeaza_modificare_nota` in apelantul VFP.** `rec_s5_oracle_vanzari.md` (B) propunea `recalculeaza_totaluri_vanzari` apelata **din interiorul** lui `finalizeaza_modificare_nota`, dupa `actualizeaza_vanzari`. Cu ordonarea de mai sus **asta nu mai merge**: la momentul acela liniile nu sunt inca scrise si `VANZARI.DISCOUNT` inca are valoarea veche, deci totalurile s-ar calcula pe date invechite. Apelul trebuie facut din VFP, ca pas separat, dupa helper. Efectul secundar e favorabil si corespunde motivatiei originale a Variantei B: recalculul devine **strict opt-in pe calea de editare de factura**, nu ruleaza pentru notele ROACONT/ROAGEST care nu ating sumele — ceva ce `finalizeaza_modificare_nota` oricum nu putea distinge. ### 3.4 Ordonari respinse | Varianta | De ce nu | |---|---| | Scriere **inainte** de `finalizeaza_modificare_nota` | `STERS = 1` sters de resetul din `actualizeaza_vanzari`; `INSERT`/`UPDATE` ar supravietui, dar stergerea nu — comportament pe jumatate, cel mai prost caz | | Scriere din `inainte_de_do_termin` (in formular) | ruleaza **inainte** de `do_deschide_tranzactie` (`_frm_base.vc2:364` -> `ofacturare_comun.vc2:3800`), deci in autocommit, in afara tranzactiei; la esec ulterior al notei, liniile raman scrise | | Scriere **dupa** `do_inchide_tranzactie` | tranzactie separata; commit partial daca a doua esueaza | | Modificarea lui `actualizeaza_vanzari` sa nu mai reseteze `STERS` | cod partajat cu toata suita ROA (apelat pentru orice nota cu `cod` in `vanzari`); riscul respins deja de plan | --- ## 4. Modelul existent: `pack_facturare.modifica_explicatie_articol` **Oracle** (`D:\ROA\ROAACNPRO\PACK_FACTURARE.pck:14464-14472`) — corpul complet: ```sql PROCEDURE modifica_explicatie_articol(V_ID_VANZARE_DET IN NUMBER, V_EXPLICATIE IN VARCHAR2, V_ID_UTIL IN NUMBER, V_TAXCODE IN NUMBER DEFAULT NULL) is BEGIN UPDATE VANZARI_DETALII SET EXPLICATIE = V_EXPLICATIE, TAXCODE = V_TAXCODE WHERE ID_VANZARE_DET = V_ID_VANZARE_DET; END modifica_explicatie_articol; ``` **Detaliu de contract, contra-intuitiv:** `V_ID_UTIL` e primit si **complet ignorat** — nu se scriu `ID_UTILS`/`DATAORAS`. Procedura nu e un model bun pentru partea de audit; e model doar pentru forma apelului si pentru granularitatea "un `UPDATE` tintit pe `ID_VANZARE_DET`". **Apelantul VFP** — `frm_modifica_articol_factura.inainte_de_do_termin` (`COMUN\clase\ofacturare_comun.vc2:5230-5241`), integral: ```foxpro PROCEDURE inainte_de_do_termin Local llReturn If amessagebox("Doriti sa salvati modificarile?",4+32,"Confirmare salvare explicatie") = 6 lcSql = [begin pack_facturare.modifica_explicatie_articol(] + Alltrim(Str(poRec.id_vanzare_det)) + [,] + ; ['] + Nvl(Alltrim(OracleSpecialCharacters(poRec.explicatie)),'') + [',?gnIdUtil,?poRec.taxcode); end;] llReturn = goExecutor.oExecuta(lcSql) Else llReturn = .F. Endif Return llReturn ENDPROC ``` Deschis din `frm_facturi.do_modifica_explicatie` (`ofacturare_comun.vc2:4639-4656`): `Scatter Name poRec Memo` din `crsDetalii`, `Createobject("frm_modifica_articol_factura")`, `.Show()`, apoi `actualizeaza_grid2()` daca `gnButon = 1`. **Ce se preia din model:** - bloc `begin ... ; end;` construit ca text, cu numerele injectate prin `Alltrim(Str(...))` si sirurile prin `?`-binding sau literal cu ghilimele simple; - `OracleSpecialCharacters(...)` obligatoriu pe orice sir care ajunge literal in SQL; - **tratarea erorii = niciuna in apelant**: `goExecutor.oExecuta` intoarce `.T.`/`.F.` si afiseaza singur mesajul; apelantul doar propaga booleanul. **Ce NU se preia:** apelul asta ruleaza in **autocommit**, in afara oricarei tranzactii manuale (`do_modifica_explicatie` nu deschide tranzactie). Helperul nou ruleaza **in interiorul** tranzactiei deschise de apelant si nu are voie sa dea `COMMIT`/`ROLLBACK` — se opreste la primul esec si lasa apelantul sa faca rollback prin `do_inchide_tranzactie(2)`. --- ## 5. `inainte_de_do_termin` (`omodificari.vc2:14249-14441`) **Numerotare:** briefingul dadea `:13357-13549`; azi metoda e la `:14249-14441` (aproximativ 90% din corp — `:14299-14437` — e cod comentat, ramas din versiunea veche). **Ce valideaza azi** (codul viu, `:14252-14296`): | Linie | Verificare | |---|---| | `:14252-14253` | `SELECT tact` + `SET FILTER TO` (curata filtrul de grid inainte de salvare) | | `:14257-14259` | completeaza `id_set` gol pe `tact`, `trul`, `trul_obinv` | | `:14265-14271` | pentru `id_set` 99998 / 90024 sare peste verificarea de conturi | | `:14273-14277` | `verificare_note_contabile('tact', ...)` — analitice si parteneri (in `oOperatii_comune`) | | `:14281-14290` | avertisment 4426-4428 / 4428-4427 introdusa fara optiunea dedicata (doar din 2013), cu confirmare Da/Nu | | `:14291-14293` | `This.VerificaAvertizareExigibilizareTVA()` | | `:14296` | `RETURN m.llRet` | **Nu atinge deloc `tvd` sau `tvanz`.** Nicio validare pe pagina de articole. **Recomandare: da, aici trebuie adaugata validarea paginii de articole** — e singura poarta inaintea lui `gnButon = 1` (`_frm_base.vc2:364`), deci singurul loc care poate opri o salvare inainte ca apelantul sa deschida tranzactia. Validari care merita: - **cantitate 0 sau negativa** pe o linie activa (`Nvl(sters,0) <> 1`) — o linie cu cantitate 0 se scrie in `VANZARI_DETALII` cu valoare 0 si strica totalurile fara sa fie vizibila ca eroare; - **`id_articol` nul sau 0** pe o linie activa — se poate produce doar prin `AdaugaLinieTvdDinArticol` cu un `poArticol` incomplet, dar `INSERT`-ul ar cadea pe FK abia in Oracle, dupa ce nota a fost deja rescrisa, deci cu ROLLBACK pe tot; - **`pret` nul** pe o linie activa — `VANZARI_DETALII.PRET` e `NOT NULL` (`rec_cale_vanzari_detalii.md` 2.2); - **zero linii active** cand documentul are rand in `VANZARI` — utilizatorul a sters tot; cerea o confirmare explicita, nu o salvare tacuta care goleste factura. Trei conditii obligatorii pentru adaugare: 1. gardata pe `"OFACTURARE_EDITARE" $ Upper(Set("Procedure")) AND Used('tvd')`, altfel se rupe registrul jurnal din ROACONT/ROAGEST; 2. `RETURN .F.` blocheaza salvarea — se foloseste doar pentru erori reale; pentru situatii discutabile, tiparul existent din `:14286-14288` (mesaj cu 4+32 si continuare la "Da"); 3. plasata **inaintea** lui `RETURN m.llRet` de la `:14296`, nu in blocul comentat de dedesubt. Fara cod aici, doar constatarea si recomandarea, cum s-a cerut. --- ## 6. Contractul minim al helperului nou Locul: `COMUN\programe\ofacturare_editare.prg` — acolo stau deja `IncarcaArticoleFactura`, `IncarcaVanzareDinNota`, `CreeazaPoArticolNouTvd`, si fisierul e inregistrat doar in ROAFACTURARE (`Programe\roafacturare.prg:214`), ceea ce da automat no-op-ul in ROACONT/ROAGEST. ``` FUNCTION ScrieArticoleFacturaEditate LPARAMETERS tnIdVanzare, tcAliasArticole, tcAliasVanzare ``` **Parametri** - `tnIdVanzare` — `ID_VANZARE` al documentului (din `tvanz.id_vanzare`). Nu se ia din `tvd`, ca sa functioneze si cand `tvd` a ramas gol. - `tcAliasArticole` — implicit `'tvd'` (simetric cu `IncarcaArticoleFactura`, care primeste aliasul destinatie; permite testarea pe un cursor construit in test). - `tcAliasVanzare` — implicit `'tvanz'`, pentru `discount`. **Preconditii** (nu le verifica, le documenteaza): - tranzactie manuala deja deschisa de apelant (`do_deschide_tranzactie` a intors `.T.`); - `finalizeaza_modificare_nota` a rulat deja cu succes — deci `actualizeaza_vanzari` si-a facut resetul `STERS = 0`, iar helperul scrie peste el; - `goExecutor` conectat. **Ce scrie**, in ordinea din 3.3: 1. `UPDATE VANZARI_DETALII SET STERS = 1, ID_UTILS = , DATAORAS = SYSDATE WHERE ID_VANZARE = :id AND STERS = 0 AND ID_VANZARE_DET NOT IN ( 0 active din alias>)` — un singur statement, formulat ca diferenta de multimi (motivarea in 3.3); 2. `INSERT INTO VANZARI_DETALII (...)` per linie cu `id_vanzare_det = 0 AND Nvl(sters,0) <> 1`, fara `ID_VANZARE_DET` in lista de coloane (trigger `TRG_VANZARI_DET_BEFOINS`); 3. `UPDATE VANZARI_DETALII SET CANTITATE = ..., PRET = ..., PRET_CU_TVA = ..., ID_UTILS = ..., DATAORAS = SYSDATE WHERE ID_VANZARE_DET = :id_det` per linie cu `id_vanzare_det > 0 AND lmodificat AND Nvl(sters,0) <> 1`; 4. `UPDATE VANZARI SET DISCOUNT = :disc WHERE ID_VANZARE = :id` din `.discount`. `PROC_TVAV` **nu** se recalculeaza — ramane cota deja persistata pe linie (`rec_cale_vanzari_detalii.md`, "Observatie tehnica"). `DISCOUNT_UNITAR` nu e editabil in grid (punctul 2.1), deci se scrie doar la `INSERT`, nu la `UPDATE`. **Ce intoarce**: logic. `.T.` = tot a mers **sau nu era nimic de facut**; `.F.` = primul esec, si se opreste acolo. No-op cu `.T.` cand `tnIdVanzare <= 0`, cand aliasul de articole nu e deschis, sau cand aliasul de vanzare nu are exact un rand — asa apelul poate sta neconditionat in ambele puncte de intrare, fara garda duplicata la apelant. **Exceptie de la "nimic de facut = .T.":** `Reccount(tcAliasArticole) = 0` cu `tnIdVanzare > 0` **nu** e no-op, e "s-au sters toate liniile" — pasul 1 marcheaza tot documentul. Validarea din punctul 5 (confirmare la zero linii active) e ce trebuie sa impiedice cazul accidental. **Cum semnaleaza eroarea**: prin valoarea de retur, atat. Nu afiseaza mesaj — `goExecutor.oExecuta` o face deja (`oproceduri_comune.prg:153-156`). Nu da `COMMIT`/`ROLLBACK`, nu inchide cursoare, nu schimba workarea curenta (o salveaza cu `Select()` si o restaureaza, ca `IncarcaVanzareDinNota`). La apelant: ``` If lnSucces > 0 lnSucces = Iif(ScrieArticoleFacturaEditate(...), 1, -1) Endif ``` — `-1`, nu `0`, ca sa se prinda in `Iif(lnSucces<0, 2, 1)` de la `do_inchide_tranzactie` (capcana din 1.3). **Motivare a formei**: un singur punct de scriere, apelat identic din ambele puncte de intrare (altfel logica se dubleaza in `ofacturare_comun.vc2` si in `comun.vc2`, iar `comun.vc2` e atins de toata suita); parametrizat pe alias, ca testul headless sa-l poata rula pe cursoare construite manual, fara formular; contract boolean, identic cu `goExecutor.oExecuta` si cu `frm_modifica_articol_factura.inainte_de_do_termin`, deci fara conventie noua de erori in cod. **De verificat inainte de a scrie `INSERT`-ul** (ramas deschis din `rec_cale_vanzari_detalii.md` punctul 5, **NEVERIFICAT** si aici): coloanele `NOT NULL` fara valoare din trigger — `STERS`, `VALIDAT`, `DIFERENTA`, `CUSTODIE`, `DESCARCAT` — au sau nu `DEFAULT` la nivel de coloana (`all_tab_columns.data_default`). Daca nu au, trebuie enumerate explicit in `INSERT`. --- ## 7. Ce ramane netestabil headless **Testabil headless** (`vfp9.exe -A -T`), pe cursoare construite in test: - `ScrieArticoleFacturaEditate` cu un `goExecutor` mock: se verifica **textul SQL generat** pentru fiecare din cele 3+1 categorii, ordinea statement-elor, si comportarea la `.F.` din mock; - clasificarea liniilor din `tvd` (modificata / stearsa / adaugata / adaugata-si-stearsa) — logica pura pe cursor; - `AdaugaLinieTvdDinArticol` si `calculeaza_valori_articol` (au deja teste: `COMUN\utile\Teste\editare_factura\test_adauga_linie_articol.prg`, `test_adauga_linie_valuta.prg`); - validarile noi din `inainte_de_do_termin`, apelate direct pe o instanta de formular. **Netestabil headless, cu motivul:** 1. **Coloanele gridului `grdArticoleFactura`.** Sub `-A -T` coloanele nu se materializeaza (`ColumnCount` si `RecordSource` citite acolo sunt artefacte); pentru ele exista deja harnessul cu UI vizibil (`test_ui_grid_articole.prg`, `test_ui_sterge_linie.prg`, `test_ui_fix_editabil_subtotal.prg`). 2. **Interactiunea reala cu gridul** — `Valid`/`When`/`InteractiveChange` pe `cCantitateArt`, `cPretArt`, `cPretCuTvaArt` (`:16428-16458`) depind de focus si de `Thisform.oldvalue`; se pot apela metodele direct, dar asta nu testeaza traseul care pune `lmodificat`. 3. **Dialogul modal `frm_articol_factura`** deschis din `cmdAdaugaArticol.Click` (`:16407-16408`, `loDlg.Show(1)`) — blocheaza headless. De aceea `AdaugaLinieTvdDinArticol` a fost deja separata de `Click` (comentariul de la `:13082-13083`); se testeaza doar partea separata. 4. **Ordonarea fata de `actualizeaza_vanzari`** — inima acestei cercetari. Nu se poate verifica decat pe Oracle real: `finalizeaza_modificare_nota` trebuie sa ruleze efectiv ca sa se vada resetul `STERS = 0`, iar apoi ca helperul il corecteaza. Cere un test tranzactional (`do_deschide_tranzactie` -> pasii -> `SELECT` de verificare -> `Sqlrollback`), care **scrie** temporar in baza. Nu s-a rulat aici. 5. **Regresia liniilor sterse in sesiuni anterioare** (3.2, consecinta 2) — cere doua editari succesive ale aceleiasi facturi, deci doua tranzactii, deci scriere reala. 6. **`amessagebox` din `goExecutor.oExecuta`** la esec Oracle — blocheaza headless; testele care forteaza un esec trebuie sa mocheze `oExecuta`, altfel raman agatate. 7. **Comportamentul in ROACONT/ROAGEST** (pagina absenta, helper neincarcat) — nu se poate testa din ROAFACTURARE, unde `ofacturare_editare.prg` e mereu in `SET("PROCEDURE")`. Se poate aproxima verificand ca garda `"OFACTURARE_EDITARE" $ Upper(Set("Procedure"))` exista in fiecare punct nou, dar nu e acelasi lucru cu o rulare reala. --- ## Rezumat al lucrurilor de decis inainte de implementare 1. **Recalculul de totaluri se muta din `finalizeaza_modificare_nota` in VFP** (3.3) — abatere de la `rec_s5_oracle_vanzari.md` B, impusa de ordonare. De confirmat cu Marius. 2. **Stergerea se scrie ca diferenta de multimi**, nu ca lista de linii sterse (3.3, pasul 1) — asta e ce repara si regresia din 3.2(2). 3. **Discountul se scrie neconditionat** cat timp nu exista valoare initiala salvata pe `tvanz` (2.4); alternativa e un instantaneu la incarcare. 4. **S4b "enumera vechi -> nou" nu are azi de unde lua valorile vechi** (2.3) — cere un cursor de instantaneu, lucrare in plus fata de ce exista. 5. **De verificat `DEFAULT`-urile pe coloanele `NOT NULL`** din `VANZARI_DETALII` inainte de a scrie `INSERT`-ul (punctul 6, NEVERIFICAT).