# 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): ```sql 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`) ```sql 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: ```sql -- 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`): ```sql -- 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`): ```sql 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).