Files
roafacturare/docs/cercetare/s4_punct2_registru_cantitate_ramasa.md
2026-09-09 22:19:22 +03:00

38 KiB

Cercetare — proiectare S4 punctul 2: decuplarea registrului de cantitate ramasa de crsarticole

Investigatie READ-ONLY pentru punctul 2 al povestii S4 din docs\plan_13_unificare_formular_facturare.md:2099-2113. Continua raportul punctului 1 (docs\cercetare\s4_cautare_articole_server.md) si corectiile din docs\cercetare\s4_puncte_deschise.md. Fara editari de cod, fara git_sync.ps1/txt2vcx.ps1, fara commit, fara scriere pe Oracle (numai SELECT). Nu ating COMUN\clase\ofacturare_comun.vc2 si COMUN\programe\ofacturare_editare.prg (perimetrul altei sarcini). Decizia 39 (S4 ramane integrala, se desface registrul) nu e reargumentata — proiectez CUM, nu DACA.

Sursa VFP: COMUN\clase\ofacturare.vc2 (frm_facturare_articole; frm_facturare_articole2/prototip citit doar pentru confirmare, are aceeasi structura). Sursa Oracle: D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql (corpul pachetului, versiune_db.txt = 2026_08_09_02).

Verdict (rezumat)

Descoperirea centrala: crsarticole.cantitate are azi DOUA roluri distincte, nu unul, si numai unul din ele e "registrul de cantitate ramasa" pe care planul il numeste riscant.

  • Rol A — cantitate ramasa de facturat dintr-un document sursa (doar comanda: tip 3,21,25,28,42,47; si avize: tip 4). Cursoarele Oracle (cursor_comanda, cursor_avize) o calculeaza deja live la deschidere ca cantitate_document - deja_facturat. do_scrie_factura o re-suma din crsarticole (Calculate Sum) doar ca sa decida ce sa trimita mai departe (pnParametruAditional) — dar Oracle recalculeaza acelasi lucru independent, din tabele reale, in inchide_comanda() si marcheaza_facturat(), chiar in procedura care scrie factura. Cursorul VFP e o copie redundanta a unui calcul pe care Oracle il repeta oricum la scriere — exact problema "sursa unica" semnalata in misiune, si exact motivul pentru care decuplarea e posibila fara sa piarda paritatea: mutam citirea "cat a mai ramas" din cursorul local intr-un apel Oracle facut la momentul potrivit, nu intr-o a doua copie tinuta manual.
  • Rol B — plafon de cantitate in sesiune (lista de preturi gestionabila 1,22,29,2-jumatate; transfer 23,41; retur 8,9,24). Decrementat/incrementat in do_adauga_articol/do_modifica/ do_sterge ca sa nu lase operatorul sa adauge mai mult decat vede pe ecran, in aceeasi sesiune. Nu alimenteaza nicio decizie Oracle — nu apare in niciun Calculate Sum in afara celor doua locuri de la Rolul A, si Oracle nu-l citeste niciodata direct. E un plafon UI, nu un registru de business.
  • Corectie fata de raportul punctului 1: pasul 5 de acolo (s4_cautare_articole_server.md:417-424) propune oprirea incarcarii in masa pe tipurile 23,41 (printre altele), presupunand ca ele n-au bookkeeping de pastrat — dar au Rol B (confirmat pe cod, sectiunea 1 de mai jos). Punctul 1 nu poate fi aplicat pe 23,41 fara ca punctul 2 sa acopere intai Rolul B pe aceste doua tipuri.
  • Recomandare de proiectare (detaliata la sectiunea 4): pentru Rolul A, inlocuieste Select crsarticole / Calculate Sum(cantitate) cu un apel Oracle nou, la acelasi moment din do_scrie_factura (dupa do_scrie_articole(), cand VANZARI_DETALII_TEMP e deja populat), care reface exact interogarea pe care inchide_comanda/marcheaza_facturat o repeta oricum. Pentru Rolul B, plafonul devine un apel Oracle la cerere (aceeasi interogare care alimenteaza azi cursor_preturi/cursor_comanda/cursor_gestiune, filtrata pe articol), nu o valoare tinuta in memorie — nu mai e nevoie de sincronizare manuala pentru ca nu mai exista o a doua copie.

1. Inventar exact al scrierilor/citirilor de bookkeeping

Verificat direct pe COMUN\clase\ofacturare.vc2 (nu pe .bak), linii confirmate cu vfp_symbols.ps1.

1.1 do_adauga_articol — scrierea la adaugarea unei linii (:12813-13086)

Dupa ce linia e scrisa in crsfactura, la :13034-13051:

Select (lcCursor)
lnRecno = Recno()
Do Case
    Case gnScadereStoc = 1 And poDate.tip = 41 && aviz retur transfer catre subunitati lista pret
        Replace cantitate With cantitate + poArticol.cantitate For id_c = poArticol.id_c
        Go lnRecno
    Case (gnScadereStoc = 1 And poDate.tip = 23) Or Inlist(poDate.tip, 3, 4, 21, 25, 28, 42, 47)
        Replace cantitate With cantitate - poArticol.cantitate For id_c = poArticol.id_c
        Go lnRecno
    Case Inlist(poDate.tip, 8, 9, 24) && factura retur lei si valuta, aviz retur
        Replace cantitate With cantitate + poArticol.cantitate For id_c = poArticol.id_c
        Go lnRecno
    Case poArticol.gestionabil = 1 And gnScadereStoc = 1 And ;
            (Inlist(poDate.tip, 1, 22, 29) Or (poDate.tip = 2 And poArticol.opt_facturare = 0))
        Replace cantitate With IIF(cantitate - poArticol.cantitate > 0, cantitate - poArticol.cantitate, 0) For id_articol = poArticol.id_articol And gestionabil = 1
        Go lnRecno
Endcase

lcCursor = crsarticole1 daca tlContract, altfel crsarticole (:12839-12845).

1.2 do_sterge — refacerea la stergerea unei linii (:14608-14677)

Select (lcCursor)
lnRecNo = Recno()
Do Case
    Case gnScadereStoc = 1 And poDate.tip = 41
        Select (lcCursor)
        Replace cantitate With cantitate - poArticol.cantitate For id_c = poArticol.id_c
    Case (gnScadereStoc = 1 And poDate.tip = 23) Or Inlist(poDate.tip,3,4,21,25,28,42,47)
        Select (lcCursor)
        Replace cantitate With cantitate + poArticol.cantitate For id_c = poArticol.id_c
    Case Inlist(poDate.tip,8,9,24)
        Replace cantitate With cantitate - poArticol.cantitate For id_c = poArticol.id_c
    Case poArticol.id_gestiune <> - 1000 And gnScadereStoc = 1 And ;
            (Inlist(poDate.tip,1,22,29) Or (poDate.tip = 2 And poArticol.opt_facturare = 0))
        Select (lcCursor)
        Replace cantitate With cantitate + poArticol.cantitate For id_articol = poArticol.id_articol And gestionabil = 1
Endcase

lcCursor = crsarticole1 daca poArticol.opt_facturare <> 0, altfel crsarticole (:14615-14619). Exact inversul semnului fata de do_adauga_articol, pe aceleasi patru grupuri de tipuri — simetrie confirmata, nu presupusa.

1.3 do_modifica — ajustarea la schimbarea cantitatii pe o linie deja adaugata (:13746-13914)

Doua interactiuni distincte cu registrul, in aceeasi metoda:

(a) Calculul plafonului inainte de a arata dialogul (:13775-13798), comentat explicit in cod ca "plafonul de cantitate = stocul disponibil reconstituit din cursorul sursa; fara randul in cursor ramane cantitatea liniei" (:13775):

lnCantitateMax = poArticol.cantitate
lcCursorStoc = Iif(poArticol.opt_facturare = 0, [crsarticole], [crsarticole1])
...
Locate For id_c = poArticol.id_c
If Found()
    Do Case
        Case (gnScadereStoc = 1 And poDate.tip = 23) Or Inlist(poDate.tip, 4, 21, 28, 42, 47)
            lnCantitateMax = cantitate + poArticol.cantitate
        Case Inlist(poDate.tip, 8, 9, 24)
            lnCantitateMax = cantitate - poArticol.cantitate
    Endcase
Endif

Aduna inapoi ce a consumat deja linia curenta, ca sa arate operatorului plafonul real (cat mai era disponibil inainte de aceasta linie), trimis ca parametru la do_alege_stoc/frm_articol_factura.

(b) Ajustarea delta dupa ce operatorul schimba cantitatea (:13887-13904):

lnDeltaCantitate = poArticol.cantitate - lnCantitateVeche
If lnDeltaCantitate <> 0 And Used(lcCursorStoc)
    Do Case
        Case (gnScadereStoc = 1 And poDate.tip = 23) Or Inlist(poDate.tip, 4, 21, 28, 42, 47)
            Replace cantitate With cantitate - lnDeltaCantitate For id_c = poArticol.id_c
        Case Inlist(poDate.tip, 8, 9, 24)
            Replace cantitate With cantitate + lnDeltaCantitate For id_c = poArticol.id_c
    Endcase
Endif

Gasit, nu presupus: do_modifica NU ajusteaza crsarticole pentru tipurile 1,22,29 si 2-lista-jumatate (grupul "gestionabil, plafon local" din do_adauga_articol/do_sterge) — doar pentru grupul comanda/aviz (4,21,23,28,42,47) si retur (8,9,24). E o asimetrie preexistenta in codul de azi (nu introdusa de S4): editarea cantitatii pe o linie de lista-de-preturi deja adaugata nu recalibreaza plafonul local. Nu e de reparat aici — doar de semnalat, ca varianta noua sa nu incerce sa reproduca o simetrie care nu exista azi.

1.4 do_adauga_tot — enumerare, nu bookkeeping propriu (:13169-13198)

Select crsarticole
Scan
    ...
    Thisform.do_adauga_articol(.T.)
    ...
Endscan

Nu scrie direct in crsarticole — cheama do_adauga_articol pe fiecare rand, care face bookkeeping-ul de la 1.1. Rolul lui crsarticole aici e al treilea, distinct de Rolul A/B: sursa de enumerare ("ce randuri exista de adaugat"), necesara oricum pentru populare (S4 punctul 1), nu doar pentru bookkeeping.

1.5 do_scrie_factura — citirea care alimenteaza decizia de inchidere (:14301-14338)

Doua ramuri, singurele doua locuri din intregul fisier unde Calculate Sum(cantitate) ruleaza peste crsarticole (confirmat cu grep pe tot fisierul, nicio a treia aparitie):

Case poDate.Tip = 4
    Select crsarticole
    Calculate Sum(cantitate) To lnCantitateRamasa
    If lnCantitateRamasa > 0
        pnFacturaRetur = 7 - amessagebox("Doriti sa se inregistreze si avizul de retur?",4+32,"Confirmare")
        pnParametruAditional = 1
    Else
        pnFacturaRetur = 0
        pnParametruAditional = 0
    Endif
    ...
Case Inlist(poDate.Tip,3,21,25,28,42,47)
    Select crsarticole
    Calculate Sum(cantitate) To lnCantitateRamasa
    If lnCantitateRamasa <> 0
        pnParametruAditional = 7 - amessagebox("Doriti sa se inchida comanda?",4+32,"Confirmare")
    Endif
    ...

pnParametruAditional default e 0 (:14222, setat inainte de Do Case). Pentru orice alt tip (inclusiv 41, 23, 2/6/26/52, 45/48/49), ramane 0 fara sa fie calculat din crsarticole deloc (exceptie: tip 48 il suprascrie cu poDate.coeficient_k, fara legatura cu inchiderea — :14367-14369).

Concluzie: doar 4 si Inlist(3,21,25,28,42,47) sunt Rolul A. Restul tipurilor nu ating deloc aceasta parte a lui do_scrie_factura — inclusiv 41/23, care au totusi bookkeeping Rol B activ la 1.1-1.3.


2. Semantica registrului azi

2.1 Ce inseamna cantitate la incarcare, per grup

Confirmat pe corpul cursoarelor Oracle (ff_...PACK_FACTURARE.sql):

  • cursor_comanda (:2952-3171 per raportul punctului 1; verificat aici direct la sectiunea "aviz"/comanda a interogarii, :3060-3081): coloana cantitate e A.CANTITATE - NVL(D.CANTITATE, 0) unde D e SUM(cantitate) deja facturat din VANZARI_DETALII pe acelasi id_comanda (:3060-3068), filtrat WHERE ... SIGN(A.CANTITATE) * (A.CANTITATE - NVL(D.CANTITATE, 0)) > 0 (:3079). E deja "ramas de facturat", calculat live la deschidere — nu cantitatea comandata bruta.
  • cursor_avize (:3703-3874): coloana A.CANTITATE - NVL(D.CANTITATE, 0) AS CANTITATE (analog la :3118), plus agregarea finala (:3820-3872) care scade VANZARI_CANTITATI deja consumat din VANZARI_DETALII (WHERE A.CANTITATE <> NVL(B.CANTITATE, 0), :3871). Acelasi tipar: "ramas", nu cantitatea avizata bruta.
  • Restul grupurilor (Rol B: 1,22,29,2-lista, 23,41 transfer, 8,9,24 retur): cantitate vine din cursor_preturi/cursor_gestiune, care e stoc disponibil (LEFT JOIN pe STOC, raportul punctului 1 sectiunea 1.1/1.5), nu "ramas de facturat dintr-un document" — pentru ca aceste tipuri nu au un document sursa cu cantitati de urmarit (lista de preturi n-are document sursa; transferul/returul opereaza pe stoc, nu pe o comanda).

2.2 Cine o scade si cand

Vezi sectiunea 1: la fiecare do_adauga_articol (scade sau creste, dupa tip), la fiecare do_sterge (inversul), si la do_modifica doar pentru grupul Rol A + retur (2.1(b) de mai sus).

2.3 Ce se intampla la adaugare / stergere / modificare cantitate — rezumat comportamental

Actiune Rol A (comanda 3/21/25/28/42/47, aviz 4) Rol B (lista gestionabila 1/22/29/2, transfer 23/41, retur 8/9/24)
Adauga linie crsarticole.cantitate scade (creste la retur/41) cu cantitatea adaugata idem, plafon local
Sterge linie reface exact simetric reface exact simetric
Modifica cantitate pe linie existenta ajusteaza cu delta (23/4/21/28/42/47 si 8/9/24 explicit) nu ajusteaza pe 1/22/29/2-lista (asimetrie preexistenta, 1.3)
La scriere (do_scrie_factura) suma peste crsarticole decide pnParametruAditional (inchidere) nu se suma niciodata — nu alimenteaza nicio decizie Oracle

3. Regula de inchidere automata — exact, cu dovada Oracle

3.1 Ce trimite VFP si ce face Oracle cu el

pnParametruAditional merge la Oracle prin scrie_factura_avize (tip 4) sau scrie_factura2 (toate celelalte, inclusiv comanda). Ambele cheama in interior finalizeaza_factura(...) (ff_...PACK_FACTURARE.sql:14770-14852), care are un CASE unic pe pack_facturare.ntip (setat intern din tipul documentului, nu parametru separat):

CASE
  WHEN V_PARAMETRU_ADITIONAL = 1 AND pack_facturare.ntip IN (3, 21, 25, 28, 42, 47) THEN
    -- comanda: V_PARAMETRU_ADITIONAL este V_INCHIDERE_COMANDA
    pack_facturare.inchide_comanda();
  WHEN pack_facturare.ntip = 4 THEN
    -- aviz: V_PARAMETRU_ADITIONAL este V_VERIFICARE_FACTURAT
    pack_facturare.scrie_cantitati_vanzari_avize;
    pack_facturare.scrie_corespondente_vanzari(1);
    pack_facturare.marcheaza_facturat(V_PARAMETRU_ADITIONAL);
  WHEN pack_facturare.ntip = 24 THEN
    pack_facturare.scrie_corespondente_vanzari(2);
    pack_facturare.marcheaza_facturat(V_PARAMETRU_ADITIONAL);
  WHEN pack_facturare.ntip in (2, 6, 52) THEN
    pack_facturare.scrie_rate_factura(V_DATAORA);
  WHEN pack_facturare.ntip in (8, 9) THEN
    pack_facturare.scrie_corespondente_vanzari(3);
  ELSE
    dbms_output.put_line('---');
END CASE;

(:14818-14839)

41 si 23 nu apar deloc in acest CASE — cad pe ELSE, fara nicio actiune de inchidere. Confirma pe cod ce arata si sectiunea 1.5: transferul intre subunitati nu are un "document sursa" de inchis, doar un plafon de stoc (Rol B). 2/6/26/52 (contract) intra pe ramura scrie_rate_factura, care scrie scadentarul de rate — nu e o "inchidere" in sensul comanda/aviz, e alta operatie.

3.2 Comanda — inchide_comanda() (:5769-5820)

Ruleaza doar cand V_PARAMETRU_ADITIONAL = 1 — adica doar daca crsarticole a aratat cantitate ramasa nenula si operatorul a raspuns "Da" la "Doriti sa se inchida comanda?" (ofacturare.vc2:14336-14337). Cand ramane 0 (fara diferenta), inchide_comanda() nu se cheama deloc — pentru ca o comanda complet livrata se raporteaza deja "inchisa" prin simplul fapt ca interogarea de mai jos nu mai returneaza randuri, fara nicio actiune suplimentara.

Ce face efectiv (:5771-5818): insereaza un rand compensator in COMENZI_ELEMENTE, cu CANTITATE = ramas (calculat live, independent de VFP, din NVL(C.CANTITATE,0) + NVL(B.CANTITATE,0) - A.CANTITATE unde B = deja facturat in aceasta tranzactie (VANZARI_DETALII_TEMP) si C = deja facturat istoric (VANZARI/VANZARI_DETALII)) — asta face ca o interogare ulterioara de tip cursor_comanda (aceeasi forma de WHERE, SIGN(...) * (A.CANTITATE - NVL(D.CANTITATE,0)) > 0) sa nu mai gaseasca randuri ramase, adica sa "inchida" comanda prin recalcul, nu printr-un flag setat. Nu exista o coloana INCHISA/status persistat pe comanda (cautare INCHISA in tot pachetul — zero potriviri) — starea "deschisa/inchisa" e intotdeauna derivata din suma COMENZI_ELEMENTE vs VANZARI_DETALII, nu citita dintr-un camp.

Important pentru paritate: cantitatea trimisa de VFP (pnParametruAditional, un simplu 1/0) nu participa la calculul cantitatii inserate — Oracle o recalculeaza singur din tabele reale. Rolul VFP e strict binar: "a fost cerut sa se forteze inchiderea?".

3.3 Avize — marcheaza_facturat(V_VERIFICARE) (:15381-15418)

IF V_VERIFICARE = 0 THEN
  UPDATE VANZARI SET FACTURAT = 1, ID_UTILFACT = ...
   WHERE ID_VANZARE IN (SELECT id_vanzare_aviz ... WHERE id_vanzare_Fact = pack_facturare.nid_vanzare AND sters=0 AND tip<>3)
     AND FACTURAT = 0;
ELSE
  UPDATE VANZARI SET FACTURAT = 1, ...
   WHERE ID_VANZARE IN (
     SELECT id_vanzare FROM (
       SELECT a.id_vanzare, SUM(a.cantitate - NVL(b.cantitate,0)) AS ramas
       FROM vanzari_detalii a LEFT JOIN (...VANZARI_CANTITATI...) b ON ...
       WHERE a.id_vanzare IN (...avizele referite de aceasta factura...)
       GROUP BY a.id_vanzare
     ) WHERE ramas = 0
   );
END IF;

Aici exista un flag real persistat: VANZARI.FACTURAT. V_VERIFICARE (= pnParametruAditional trimis de VFP) alege intre doua moduri:

  • V_VERIFICARE = 0 (VFP a vazut lnCantitateRamasa = 0, adica a crezut ca tot ce era in avizele referite s-a facturat): marcheaza TOATE avizele referite ca facturate, fara re-verificare — increde in calculul VFP.
  • V_VERIFICARE = 1 (VFP a vazut ceva ramas): recalculeaza per-aviz, din tabele reale (vanzari_detalii/vanzari_cantitati), si marcheaza doar avizele individuale al caror ramas = 0 — mai stric, pentru ca VFP nu mai e sigur.

Observatie relevanta pentru punctul 2 al plafonului de risc: cand un singur crsarticole agrega mai multe avize (facturare din mai multe avize deodata), suma globala poate fi 0 chiar daca un aviz individual din grup mai are ramas si altul are exces care-l compenseaza — caz in care V_VERIFICARE=0 ar marca toate ca facturate, inclusiv cel cu ramas real. Comportament preexistent, nu introdus de decuplare — dar varianta noua trebuie sa-l reproduca identic (aceeasi conditie de trigger: suma globala peste toate liniile din tranzactia curenta, nu per-document), nu sa-l "repare" din greseala.

3.4 Cine altcineva verifica remainder — scrie_cantitati_vanzari_avize (:15420-15479)

Ruleaza necondiționat pe ramura tip=4, indiferent de V_VERIFICARE — scrie in VANZARI_CANTITATI cate din cantitatile avizate au fost efectiv consumate de aceasta factura (mecanismul care alimenteaza ramas la sectiunea 3.3). Nu depinde de crsarticole — foloseste VANZARI_DETALII_TEMP (deja populat de do_scrie_articole(), apelat inaintea acestui Do Case din VFP, :14264) si tabele reale. Confirma independenta totala de crsarticole a partii Oracle — singurul lucru pe care Oracle il primeste de la VFP e semnalul binar pnParametruAditional.


4. Proiectare — variante comparate

Varianta (a) — cursor propriu, minimal, decuplat de populare

Un cursor separat (crsregistru), incarcat cu id_c/id_articol + cantitate ramasa, populat din aceeasi interogare care alimenteaza azi crsarticole, dar independent de campurile de populare (pret, TVA, valuta...) pe care S4 punctul 1 le muta pe cautare filtrata.

Ce se atinge: aceleasi patru metode (do_adauga_articol, do_sterge, do_modifica, do_scrie_factura) — schimba doar numele cursorului tinta, logica ramane identica. Ce se rupe: nimic structural — e schimbarea minima de sintaxa. Cost: mic, dar nu rezolva problema de fond: tot exista o a doua copie a "cat a mai ramas", tinuta manual in memorie VFP, care trebuie sa ramana in sincron cu ce calculeaza Oracle la scriere (sectiunea 3). E acelasi risc de azi, doar cu un cursor mai ingust. Nu e "sursa unica" — e aceeasi sursa dubla, doar mai mica.

Varianta (b) — recalculul pe server la scriere, in loc de suma locala (RECOMANDATA pentru Rolul A)

Inlocuieste cele doua Select crsarticole / Calculate Sum(cantitate) To lnCantitateRamasa din do_scrie_factura (:14303-14304, :14334-14335) cu un apel Oracle nou, facut dupa Thisform.do_scrie_articole() (:14264, care populeaza deja VANZARI_DETALII_TEMP cu exact ce e pe cale sa fie scris) si inainte de Do Case care alege scrie_factura_avize/scrie_factura2.

Doua proceduri Oracle noi, minimale, care reutilizeaza exact interogarea pe care inchide_comanda/marcheaza_facturat o ruleaza oricum peste doua randuri mai jos in acelasi flux — nu o interogare noua, o extragere a celei existente intr-o forma care returneaza un numar in loc sa scrie:

-- comanda: acelasi WHERE ca inchide_comanda (:5771-5818), dar SUM in loc de INSERT
FUNCTION cantitate_ramasa_comanda(V_ID_COMANDA IN NUMBER) RETURN NUMBER IS
  V_RAMAS NUMBER;
BEGIN
  SELECT NVL(SUM(SIGN(A.CANTITATE) *
              GREATEST(SIGN(A.CANTITATE) * (A.CANTITATE - NVL(B.CANTITATE,0) - NVL(C.CANTITATE,0)), 0)), 0)
    INTO V_RAMAS
    FROM COMENZI_ELEMENTE A
    LEFT JOIN (SELECT ID_ARTICOL, ID_POL, ID_VALUTA, PRET, SUM(CANTITATE) AS CANTITATE
                 FROM VANZARI_DETALII_TEMP GROUP BY ID_ARTICOL, ID_POL, ID_VALUTA, PRET) B
      ON A.ID_ARTICOL = B.ID_ARTICOL AND A.ID_POL = B.ID_POL AND A.ID_VALUTA = B.ID_VALUTA AND A.PRET = B.PRET
    LEFT JOIN (... acelasi C ca in inchide_comanda, VANZARI/VANZARI_DETALII istoric ...) C ON ...
   WHERE A.STERS = 0 AND A.ID_COMANDA = V_ID_COMANDA;
  RETURN V_RAMAS;
END;

-- avize: acelasi calcul ca in marcheaza_facturat(V_VERIFICARE=1), dar returnat, nu aplicat
FUNCTION exista_ramas_avize(V_LISTAID IN VARCHAR2) RETURN NUMBER IS ...

Ce se atinge: do_scrie_factura (inlocuieste 4 linii VFP cu un apel Oracle + citire scalar), plus doua functii Oracle noi in PACK_FACTURARE, care extrag logica deja scrisa in inchide_comanda/ marcheaza_facturat, nu o duplica din nimic. do_adauga_tot, do_sterge, do_modifica raman neschimbate pentru Rolul A — nu mai au ce sa faca, pentru ca nimic nu mai citeste suma lor. Ce se rupe: niciun comportament — recalculul Oracle e mai precis decat suma locala (vede concurenta: doi operatori facturand din aceeasi comanda simultan; crsarticole local nu vede modificari facute de alt utilizator dupa deschiderea formularului — bug latent de azi, pe care varianta (b) il elimina ca efect secundar, nu ca scop). Cost: doua functii Oracle noi (SQL, nu logica noua — extrase din proceduri existente), un apel suplimentar goExecutor in do_scrie_factura. De ce e "sursa unica": Oracle calculeaza "cat a ramas" o singura data, in doua locuri (recalcul inainte de scriere + recalcul in inchide_comanda/marcheaza_facturat la scriere), din aceeasi interogare, niciodata dintr-o copie VFP. Nu mai exista sincronizare de mentinut, pentru ca nu mai exista a doua stare.

Varianta (c) — plafon Rol B recalculat la cerere, nu tinut in memorie (RECOMANDATA pentru Rolul B)

Pentru grupul Rol B (1,22,29,2-lista, 23,41, 8,9,24), plafonul din do_modifica (lnCantitateMax, sectiunea 1.3(a)) si decrementul din do_adauga_articol/do_sterge (sectiunea 1.1/1.2) devin un apel Oracle facut la fiecare adaugare/editare, in loc de o valoare tinuta in crsarticole — interogare filtrata pe id_articol, aceeasi forma ca cursor_preturi/ cursor_gestiune filtrate (deja proiectate la S4 punctul 1, sectiunea 4), minus stocul deja rezervat in sesiunea curenta (calculabil din crsfactura insusi, sumand liniile deja adaugate cu acelasi id_articol — cursor local, dar de linii efectiv adaugate, nu de "cat mai era", deci nu mai poate diverge de continutul real al facturii in curs).

Ce se atinge: do_adauga_articol (:13034-13051 dispare, inlocuit cu un apel la cerere din do_alege_stoc, care oricum face verificare de stoc pe server — de confirmat la implementare daca verificarea existenta acopera deja acest plafon sau trebuie extinsa), do_sterge (:14640-14669 Rol B dispare, ramane doar Rolul A), do_modifica (:13775-13798 plafonul se cere pe server, nu se reconstituie din cursor). Ce se rupe: de verificat cu Marius — plafonul Rol B azi e un avertisment soft (do_alege_stoc primeste lnCantitateMax ca parametru, dar verificarea reala de stoc la comitere ramane oricum in Oracle, la adauga_articol_factura/scriere; codul citit aici nu arata un blocaj hard bazat pe crsarticole — de confirmat explicit, nu presupus, ca UX-ul de azi (mesaj/limitare la alegerea cantitatii) nu depinde de faptul ca plafonul persista in memorie intre doua adaugari ale aceluiasi articol in aceeasi sesiune, ci doar de valoarea citita la momentul respectiv). Cost: mediu — mai multe interogari Oracle mici (una per adaugare/editare de linie gestionabila), in loc de aritmetica locala. Justificat de acelasi motiv ca varianta (b): elimina a doua copie.

Comparatie si recomandare finala

(a) cursor propriu minimal (b) recalcul server (Rol A) (c) plafon la cerere (Rol B)
E "sursa unica"? Nu — copie mai mica, tot manuala Da Da
Risc de divergenta fata de Oracle Identic cu azi Eliminat (Oracle citeste Oracle) Eliminat
Cost implementare Mic Mic-mediu (2 functii Oracle) Mediu (mai multe roundtrip-uri)
Rezolva corectia sectiunii 0 (23/41 blocheaza punctul 1) Nu direct N/A (23/41 nu sunt Rol A) Da

Recomandare: (b) pentru Rolul A, (c) pentru Rolul B. Impreuna elimina complet nevoia de a incarca crsarticole in masa pentru bookkeeping — incarcarea ramasa (daca ramane) e strict pentru enumerare la "adauga tot" (sectiunea 1.4), care e un scop diferit si mai ingust (nu mai trebuie sa tina sincron o cantitate, doar sa listeze randuri candidate o singura data, la deschidere).


5. Cazul limita: adauga si sterge inainte de salvare

Azi: do_adauga_articol decrementeaza crsarticole/crsarticole1 (Rol A si/sau B, dupa tip); do_sterge reface exact simetric, inainte de orice salvare (sectiunile 1.1-1.2 sunt perechi simetrice pe fiecare grup de tipuri). Suma finala din crsarticole la momentul do_scrie_factura reflecta corect doar liniile ramase in crsfactura, indiferent cate au fost adaugate si sterse intre timp — pentru ca fiecare stergere anuleaza exact adaugarea corespunzatoare.

In varianta recomandata (b+c): cazul limita devine trivial, nu doar acoperit — pentru ca nu mai exista o stare intermediara de sincronizat. Rolul A: do_scrie_factura calculeaza remainder-ul o singura data, la scriere, din VANZARI_DETALII_TEMP care contine exact liniile ramase in crsfactura la acel moment (populat de do_scrie_articole() chiar inainte, Scan peste crsfactura curent — sectiunea 4 varianta (b)) — liniile adaugate-si-sterse nu ajung niciodata in VANZARI_DETALII_TEMP, deci nu influenteaza calculul, fara nicio actiune suplimentara. Rolul B: plafonul la fiecare adaugare se cere din nou pe server, minus ce e deja in crsfactura in acel moment — o stergere anterioara pur si simplu nu mai apare in acea suma, fara "refacere" explicita. Cazul limita nu mai e un caz special de tratat — e comportamentul implicit al oricarei citiri facute din starea curenta, in loc de dintr-un contor tinut manual.


6. Per tip de document

Tip(uri) Rol azi Ce se schimba in varianta recomandata
Comanda 3,21,25,28,42,47 Rol A (inchidere automata) do_scrie_factura cheama functia Oracle noua (4b) in loc de Calculate Sum; do_adauga_tot/do_sterge raman (enumerare + Rol B nu se aplica pe comanda insasi, doar pe articolele ei individuale daca sunt gestionabile — de verificat, comanda nu apare in niciuna din listele Rol B de la sectiunea 1, deci pare curatata deja)
Avize 4 Rol A (marcheaza_facturat) idem, functia Oracle pentru avize (4b)
Contract 2,6,26,52 Jumatate crsarticole (delegat la cursor_preturi, per raportul punctului 1) e Rol B doar pentru poDate.tip=2 And opt_facturare=0; restul (crsarticole1, rate+articole OPT_FACTURARE=3) nu are bookkeeping in sectiunea 1 (needitat de cautare, ramane cum e azi, per raportul punctului 1 sectiunea 7) Rolul B pe jumatatea opt_facturare=0 trece pe varianta (c); 26,52 nu apar in niciuna din listele Rol B/A gasite in cod — de verificat separat, posibil fara bookkeeping deloc pe aceste doua, nesemnalat ca atare in codul citit aici
Transfer 41 (standard); 23 (doar prototip, standard il trateaza ca lista de preturi) Rol B pur (niciun Rol A — confirmat sectiunea 3.1, 41/23 cad pe ELSE in finalizeaza_factura) Trece integral pe varianta (c); aceasta e corectia care debloca pasul 5 al punctului 1 — fara ea, oprirea incarcarii in masa pe 23,41 (propusa acolo) ar sparge plafonul de stoc existent
Lista de preturi 1,5,7,10,22,29 Rol B doar pentru articole gestionabile (1,22,29; 5,7,10 nu apar in Do Case-urile Rol B — fara bookkeeping, confirmat pe cod) 1,22,29 trec pe varianta (c); 5,7,10 nu au nimic de decuplat, deja curatate
Retur 8,9,24 Rol B (inversul directiei fata de restul) Varianta (c), simetric
Restaurant 45, K 48,49 Nu ating Rol A/B (in afara Do Case-urilor gasite; 45 explicit exclus la poArticol.gestionabil=0 Or gnScadereStoc=0 Or poDate.tip=45 in ambele metode, deci ocoleste bookkeeping-ul indiferent de gestionabilitate) Fara schimbare — deja fara bookkeeping de decuplat

Tipuri semnalate ca neclare, de stabilit separat, nu presupuse aici: 26 (aviz din contract) si 52 (contract in valuta/alt subtip) nu apar explicit in niciuna din listele Rol A/B din sectiunea 1 — codul citit in aceasta sesiune nu confirma nici prezenta, nici absenta bookkeeping-ului pe ele specific; tratamentul lor pare sa urmeze grupul 2,6 din Inlist-urile care le includ, dar niciun Do Case din sectiunea 1 nu le mentioneaza individual in afara de includerea in Inlist(poDate.tip, 2, 6, 52) la do_scrie_articole (rate) — nu la bookkeeping-ul de crsarticole.


7. Pasi de implementare, ordonati

  1. Pas 1 — Oracle: extrage functiile de recalcul remainder (cantitate_ramasa_comanda, exista_ramas_avize) din logica deja scrisa in inchide_comanda/marcheaza_facturat, fara sa modifice acele proceduri. Gata cand: apelate manual cu parametrii unei comenzi/unui grup de avize cunoscute, valoarea returnata coincide cu suma pe care Calculate Sum(cantitate) din VFP o calculeaza azi peste crsarticole, pe acelasi document, in aceeasi stare (comparatie directa, inainte de orice alta schimbare).
  2. Pas 2 — VFP: do_scrie_factura cheama functiile noi in loc de Calculate Sum (sectiunea 4b), pastrand identic restul logicii (pnFacturaRetur, mesajele de confirmare, pnParametruAditional). Gata cand: pe un document de comanda si unul de aviz, cu acelasi scenariu (facturare partiala), dialogul de confirmare apare in acelasi moment si cu acelasi rezultat ca inainte de Pas 2 — regresie manuala, comparatie inainte/dupa pe acelasi document.
  3. Pas 3 — Oracle: functie de plafon Rol B filtrata pe articol, reutilizand WHERE-ul din cursor_preturi/cursor_gestiune (deja proiectat la S4 punctul 1, sectiunea 4), minus ce e deja in crsfactura pentru acelasi articol. Gata cand: pe un articol gestionabil cunoscut, valoarea returnata coincide cu crsarticole.cantitate de azi, la aceeasi stare a sesiunii (inainte de orice adaugare).
  4. Pas 4 — VFP: do_adauga_articol/do_sterge/do_modifica folosesc plafonul cerut pe server pentru grupul Rol B, in loc de Replace cantitate pe crsarticole (sectiunea 4c). Gata cand: cazul limita de la sectiunea 5 (adauga-apoi-sterge inainte de salvare) produce acelasi plafon disponibil ca azi, verificat manual pe un articol cu stoc limitat.
  5. Pas 5 — activarea pasului 5 al punctului 1 pe 23/41 (oprirea incarcarii in masa), acum ca Pasul 4 a mutat Rolul B in afara lui crsarticole. Gata cand: deschiderea formularului pe tip 41 (si 23 pe prototip) nu mai executa cursor_gestiune/cursor_preturi la deschidere, dar adaugarea unui articol tot respecta plafonul de stoc (verificat manual).
  6. Pas 6 — curatare: crsarticole ramane incarcat doar unde e nevoie de enumerare (comanda, aviz, contract-rate — pentru "adauga tot" existent), fara bookkeeping legat de el. Gata cand: grep pe ofacturare.vc2 pentru Replace cantitate.*crsarticole (in afara populare) nu mai gaseste potriviri in do_adauga_articol/do_sterge/do_modifica.
  7. Pas 7 — verificare paritate finala (sectiunea 8), pe toate tipurile cu document sursa.

Depinde de: S4 punctul 1 (Pasii 1-4 din PROIECTAREA acelui punct trebuie sa existe, dar Pasii 1-4 de aici pot fi facuti independent, inaintea sau in paralel cu populare — nu depind de cautarea filtrata, doar de crsarticole/crsfactura existente azi). Pasul 5 de aici depinde explicit de Pasul 4 de aici (nu poate porni inaintea lui).


8. Cum se verifica paritatea inchiderii automate

Comanda (nu exista flag INCHISA persistat — cautat explicit in tot pachetul, zero potriviri; starea se deriva din COMENZI_ELEMENTE vs VANZARI_DETALII):

-- inainte de facturare partiala + inchidere fortata (flux vechi vs nou, aceeasi comanda X)
{call pack_facturare.cursor_comanda(V_DATA_CURS, V_TIP, 'X', V_ID_UTIL, :cursor)}
-- numara randurile ramase (trebuie sa fie 0 dupa inchidere fortata, identic pe ambele fluxuri)
SELECT COUNT(*) FROM (...continutul cursorului de mai sus...);

-- verificare directa a compensatiei inserate de inchide_comanda
SELECT ID_ARTICOL, ID_POL, CANTITATE FROM COMENZI_ELEMENTE
 WHERE ID_COMANDA = :X ORDER BY ID_COMANDA_ELEMENT DESC;  -- randul nou trebuie sa fie identic pe ambele fluxuri (aceeasi cantitate compensatorie)

Avize (flag real: VANZARI.FACTURAT):

SELECT ID_VANZARE, FACTURAT, ID_UTILFACT FROM VANZARI
 WHERE ID_VANZARE IN (:lista_avize_test)
 ORDER BY ID_VANZARE;
-- rulat dupa fiecare flux (vechi, apoi nou, pe date de test resetate identic), FACTURAT trebuie sa coincida rand cu rand

Protocol de comparatie, aplicabil pe fiecare tip cu document sursa (comanda: alege una din 3,21,25,28,42,47; avize: 4):

  1. reseteaza datele de test la aceeasi stare (aceeasi comanda/aviz, aceleasi cantitati ramase);
  2. factureaza partial pe fluxul vechi (cod actual), noteaza rezultatul interogarilor de mai sus;
  3. reseteaza din nou la aceeasi stare initiala;
  4. factureaza identic (aceleasi linii, aceleasi cantitati) pe fluxul nou (dupa Pasii 1-2 din sectiunea 7), noteaza acelasi rezultat;
  5. compara — trebuie sa fie identic, inclusiv pe cazul limita de la sectiunea 5 (adauga-si-sterge inainte de salvare) si pe cazul avizelor multiple agregate intr-o singura factura (sectiunea 3.3, riscul de marcare in bloc).

Zero cazuri testate azi nu e dovada (memorie de proiect) — protocolul de mai sus cere minim un caz per tip din lista, plus cazul limita, nu doar "a mers o data".


9. Riscuri si ce ramane de decis de Marius

  • Corectia sectiunii 0/6: pasul 5 al proiectarii punctului 1 (s4_cautare_articole_server.md:417-424) presupune ca 23,41 n-au bookkeeping de pastrat — gasit aici ca au Rol B activ. De confirmat cu Marius daca punctul 1 se re-deschide pentru aceasta corectie sau ramane cum e, cu mentiunea ca aplicarea pe 23,41 asteapta finalizarea punctului 2 (asa cum recomand la Pasul 5, sectiunea 7).
  • Asimetria do_modifica fata de do_adauga_articol/do_sterge (sectiunea 1.3, grupul 1,22,29,2-lista nu se ajusteaza la editarea cantitatii unei linii existente) — preexistenta, nu introdusa de decuplare. De decis daca varianta noua (Rol B pe server, sectiunea 4c) trebuie sa reproduca exact aceasta asimetrie (plafonul nu se recalculeaza corect la editare pe aceste tipuri, ca azi) sau sa o corecteze ca efect secundar al recalcularii la cerere (care, prin natura ei, ar elimina automat asimetria — orice cerere de plafon citeste starea curenta, indiferent daca a fost o adaugare sau o editare). Recomandare: lasat sa se corecteze de la sine (comportament mai corect, cost zero suplimentar, dar semnaleaza explicit ca S4 schimba acest comportament punctual, nu doar "decupleaza" — de mentionat in changelog daca se alege aceasta cale).
  • Riscul de concurenta multi-utilizator, semnalat ca beneficiu la sectiunea 4b, dar nediscutat cu Marius: varianta recomandata face crsarticole local sa nu mai poata diverge de Oracle la scriere, dar tot poate divarge in timpul editarii (doi operatori pe aceeasi comanda, unul vede plafonul invechit pana la urmatoarea lui adaugare/editare). Nu e un risc nou introdus — e identic cu azi pe cursorul static incarcat o data la deschidere — dar varianta (c) il reduce (cere plafonul proaspat la fiecare adaugare, nu o singura data la deschidere), fara sa-l elimine complet (ramane fereastra intre "am cerut plafonul" si "am scris linia"). De mentionat ca imbunatatire, nu garantie.
  • Cont Rol B pe contract 26,52: nu s-a gasit dovada nici de prezenta, nici de absenta bookkeeping pe aceste doua tipuri in codul citit (sectiunea 6) — de verificat separat inainte de a le include in Pasul 3/4 al implementarii, nu de presupus ca urmeaza grupul 2,6.
  • Functiile Oracle noi (Pas 1, sectiunea 7) sunt o extragere, nu o duplicare — dar tot inseamna cod PL/SQL nou in PACK_FACTURARE, care trebuie revizuit separat de cineva familiar cu pachetul (schema exacta a JOIN-urilor din inchide_comanda, reprodusa aici din citire, nu din executie reala pe Oracle — verificarea Pasului 1 din sectiunea 7 e obligatorie inainte de a continua).

Handoff

Cercetare incheiata in aceasta sesiune. Toate cele 9 puncte cerute sunt acoperite, cu citate fisier:linie verificate direct pe fisierele reale (ofacturare.vc2, nu .bak; corpul PACK_FACTURARE.sql, nu spec-ul comentat). Descoperirea centrala (Rol A vs Rol B, si corectia asupra pasului 5 al punctului 1 pentru 23/41) nu era vizibila din raportul punctului 1 — acela trata crsarticole ca un singur registru omogen; aici s-a aratat ca sunt doua mecanisme cu scopuri diferite, unul (Rol A) recalculat oricum de Oracle la scriere si deci usor de mutat pe server fara pierdere de paritate, celalalt (Rol B) un plafon UI care nu alimenteaza nicio decizie de business.

Niciun fisier de cod atins, niciun git_sync.ps1/txt2vcx.ps1 rulat, nicio scriere pe Oracle (doar Read/Select-String/grep pe fisiere de pe disc).