Files
roafacturare/docs/cercetare/s4_punct2_registru_cantitate_ramasa.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

616 lines
38 KiB
Markdown

# 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).