Files
roafacturare/docs/cercetare/rec_s5_helper_scriere.md
Marius Mutu d9f5ca4226 docs: planurile, proiectarile si rapoartele de lucru intra in versionare
Folderul docs\ era pana acum in afara oricarui control de versiuni - nici git,
nici SVN - desi contine planurile pe puncte, proiectarile si rapoartele de
cercetare pe care se sprijina modificarile din cod. O stergere acolo era
definitiva.

Fisierele intermediare (handoff-uri intre sesiuni, diff-uri deja aplicate) au
fost sterse inainte, nu versionate: ce era durabil in ele a intrat in antetele
fisierelor de test la care se refereau.

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

12 KiB

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(<tnIdVanzare>, <discount sau NULL>); end;
  • discountul se citeste din <tcAliasVanzare>.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(<tcAliasArticole>.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):

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

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

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

begin pack_facturare.recalculeaza_totaluri_vanzari(12345,5.0000); end;

Cazul tvanz.discount .NULL. (pastreaza discountul curent din baza):

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 = <tnIdVanzare> — 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.