Files
roafacturare/docs/cercetare/rec_s5_cale_scriere_vfp.md
Marius Mutu b5a7108f34 docs: cercetarile comune trec in COMUN, reziduul #6 dispare, progres.md la zi
Curatenie ceruta dupa inchiderea lui #6. Folderul cercetare\ NU se putea sterge
in bloc: plan_13 se sprijina pe el cu 85 de trimiteri, deci e baza de dovezi a
planului aflat in lucru. Impartirea:

- 14 cercetari trans-proiect trec in COMUN\docs\cercetare\ - valuta si curs,
  TVA/VANZARI, consumatorii VANZARI din toata suita, integrarile #10/#11/#12,
  watchdog VFP, proiectarea Oracle a lui S5, view-ul VVANZARI_ARTICOLE. Nu sunt
  ale ROAFACTURARE, iar #10/#11/#12 se reiau chiar din ele.
- 20 de rapoarte de executie ale lui #6, nereferite de nimic viu, sterse.
- 91 raman, neatinse.

Trimiterile catre handoff-urile si diff-urile intermediare deja desfiintate au
fost curatate peste tot (39 de fisiere): 50 catre handoff-uri, 37 catre diff-uri
aplicate, plus caile celor mutate in COMUN. Zero trimiteri rupte ramase.

progres.md preia rolul de predare: ce ramane din #6 (cele sase documente
parazite, cele doua esecuri reale din S8 pe factura din aviz), cifrele de test
citite din log, si cele doua capcane de mediu platite - .FXP vechi executat in
locul .prg-ului, si GETFONT() care atarna un formular instantiat fara goApp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SN8snvkk94KuhWwoXUUey3
2026-08-11 22:31:42 +03:00

32 KiB

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 = <id> 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: COMUN\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):

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):

-- 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 (<ID-urile active din tvd>). 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, <ID-urile active> 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 (<ID-urile > 0 active din tvd>) AND ID_VANZARE_DET NOT IN (<ID-urile inserate acum>). 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:

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:

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 = <gnIdUtil>, DATAORAS = SYSDATE WHERE ID_VANZARE = :id AND STERS = 0 AND ID_VANZARE_DET NOT IN (<ID-urile > 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 <tcAliasVanzare>.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).