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

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

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

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

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

542 lines
32 KiB
Markdown

# S5 — calea de scriere VFP: unde se agata scrierea in VANZARI_DETALII
Cercetare read-only, 09.08.2026. Nu s-a modificat niciun fisier si nu s-a rulat nimic care sa scrie
in Oracle.
**Surse.** Partea VFP: versiunile text `.vc2` din arbore (`COMUN\clase\*.vc2`), verificate ca fiind
la zi — `mtime` identic cu al binarelor (`omodificari.vcx`/`.vc2` = 09.08.2026 18:53,
`ofacturare_comun` = 08.08.2026 23:26, `comun` = 09.08.2026 09:45). Atributia clasa/metoda pentru
fiecare linie citata e din `vfp_symbols.ps1 -Where`. **Atentie:** alti agenti lucreaza in paralel pe
`omodificari.vc2` — numerele de linie din acest raport sunt un instantaneu 09.08.2026 ~21:15.
Briefingul dadea `inainte_de_do_termin` la `:13357-13549`; azi e la **`:14249-14441`**.
Partea Oracle: surse PL/SQL **de pe disc** — `COMUN\docs\PACK_CONTAFIN.pck` (03.08.2026) si
`D:\ROA\ROAACNPRO\PACK_FACTURARE.pck` (10.06.2026, copie mai veche). Corpurile citate mai jos
coincid caracter cu caracter cu ce raporta `rec_s5_oracle_vanzari.md` dintr-un export proaspat
(08.08.2026), deci sunt de incredere; nu s-a facut un export nou in aceasta sesiune.
---
## 1. Lantul complet de salvare, ambele puncte de intrare
### 1.1 ROAFACTURARE — `frm_facturi.do_editare_factura` (`COMUN\clase\ofacturare_comun.vc2:3715-3869`)
| Pas | Linie | Ce face |
|---|---|---|
| garzi | `:3716-3767` | `lactiv3`, `glLunaInchisa`, `sters=1`, proforma, luna curenta, `ReferinteDocumenteNota`, `EsteInEFactura` |
| incarcare nota | `:3769` | `IncarcaCursoareModificareNota(lnCod, pnAn, pnLuna, .F.)` -> `tact`/`trul`/`trul_obinv` |
| incarcare articole | `:3793` | `IncarcaArticoleFactura(m.lnIdVanzare, 'crsArticoleFactura')` — **inainte** de `Createobject`, ca `Load()` sa lege gridul pe cursor plin |
| formular | `:3796-3797` | `Createobject([frm_modific2024], lnIdSet)` + `.Show()` — modal (`WindowType = 1`, `omodificari.vc2:6892`) |
| confirmare | `:3799` | `If buton = 1` |
| **tranzactie ON** | **`:3800`** | `Thisform.do_deschide_tranzactie()` |
| stergere nota veche | `:3802` | `lnSucces = OSCRIE_IN_FISIERE(2,.T.,.T.)` |
| rescriere cursoare | `:3807-3820` | `tact`->`actactan`, `trul`->`RUL_TEMP`, `trul_obinv`->`RUL_TEMP_OBINV` |
| scriere nota noua | `:3821` | `lnSucces = OSCRIE_IN_FISIERE(0,.T.,.T.)` |
| **finalizare** | **`:3824-3826`** | `begin pack_contafin.finalizeaza_modificare_nota(...); end;` prin `goExecutor.oExecuta`; `lnSucces = Iif(..., 1, -1)` |
| **tranzactie OFF** | **`:3828`** | `Thisform.do_inchide_tranzactie(Iif(lnSucces<0, 2, 1))` |
| reafisare | `:3830` | `Thisform.do_cauta()` |
| curatenie | `:3837-3860` | inchide `actactan`, `tact`, `rul_temp`, `trul`, `rul_temp_obinv`, `trul_obinv`, `crsJtvaTemp`, `crsArticoleFactura` — **NU** inchide `tvd`/`tvanz` |
### 1.2 ROACONT / registru jurnal — `afisjurcom.do_modifica` (`COMUN\clase\comun.vc2:2222-2569`)
| Pas | Linie | Ce face |
|---|---|---|
| garzi | `:2230-2290` | `lactiv3`, `glLunaInchisa`, luna curenta, `id_set` 30000-30009, nota de inventar |
| incarcare nota | `:2352-2426` | acelasi SQL ca `IncarcaCursoareModificareNota` (cod duplicat, nu apel) |
| incarcare articole | **`:2435-2437`** | `IF "OFACTURARE_EDITARE" $ Upper(Set("Procedure"))` -> `PregatesteArticoleFacturaEditare('tact')` |
| formular | `:2439`, `:2445` | `Createobject([frm_modific2024], lnIdSet)` + `.Show()` |
| confirmare | `:2447` | `If buton=1` |
| **tranzactie ON** | **`:2451`** | `Thisform.do_deschide_tranzactie()` |
| stergere nota veche | `:2454` | `oscrie_in_fisiere(2,.T.,llRul)` (sarit daca nota era deja stearsa, `:2456`) |
| rescriere cursoare | `:2463-2481` | idem |
| scriere nota noua | `:2482` | `oscrie_in_fisiere(0,.T.,llRul)` |
| **finalizare** | **`:2487-2489`** | `finalizeaza_modificare_nota` prin `goExecutor.oExecuta` |
| **tranzactie OFF** | **`:2538`** | `Thisform.do_inchide_tranzactie(Iif(lnSucces<0, 2, 1))` |
| curatenie | `:2546-2566` | inchide aceleasi cursoare + `crsArticoleFactura`; **NU** `tvd`/`tvanz` |
**Punctul de intrare 2 e reachable din ROAFACTURARE**, nu doar din ROACONT: `viz_act`
(`COMUN\programe\ooperatii_comune.prg:115-126`) instantiaza `AFISJURcom`, iar
`ooperatii_comune.prg` e inregistrat in `Programe\roafacturare.prg:218`.
`ofacturare_editare.prg` e inregistrat **numai** in ROAFACTURARE
(`Programe\roafacturare.prg:214` — singura potrivire in tot `D:\ROA`), deci in ROACONT/ROAGEST
garda `"OFACTURARE_EDITARE" $ Set("Procedure")` e falsa, pagina 3 nu apare
(`omodificari.vc2:14683`) si nu exista nimic de scris. Codul nou trebuie sa fie no-op acolo, nu doar
inofensiv.
### 1.3 Raspunsul explicit: **DA, tranzactia e inca deschisa dupa ce `finalizeaza_modificare_nota` se intoarce**
In ambele puncte de intrare `do_inchide_tranzactie` vine **dupa** apelul PL/SQL
(`ofacturare_comun.vc2:3826` -> `:3828`; `comun.vc2:2489` -> `:2538`). Intre ele nu se executa
nimic. Contractul, verificat in sursa (`COMUN\clase\_frm_base.vc2:252-302`):
- `do_deschide_tranzactie()` — `SQLSetprop(gnHandle,"Transactions",2)`; intoarce **`.T.`/`.F.`**;
- `do_inchide_tranzactie(tnTip)` — `tnTip = 1` -> `Sqlcommit(gnHandle)`, **orice altceva** ->
`Sqlrollback(gnHandle)`; apoi revine pe `Transactions = 1`; intoarce `.T.`/`.F.`.
Deci fereastra `:3826-3828` / `:2489-2538` e **exact locul de agatare**: aceeasi conexiune, aceeasi
tranzactie manuala, `COMMIT`/`ROLLBACK` inca nedat.
**Capcana de contract, preexistenta, de care sa nu depinda codul nou:** garda de commit e
`Iif(lnSucces<0, 2, 1)`, deci `lnSucces = 0` **comite**. Codul nou trebuie sa puna explicit
`lnSucces = -1` la esec, nu `0`.
### 1.4 Contractele functiilor pe care se sprijina agatarea (deschise, nu presupuse)
- **`goExecutor.oExecuta(...)`** (`COMUN\programe\oproceduri_comune.prg:121-158`): intoarce
**logic** `.T.`/`.F.`, si **afiseaza singur** `amessagebox(This.oPrelucrareEroare(), 16, "Eroare")`
la esec (`:153-156`). Apelantul nu mai trebuie sa afiseze nimic.
- **`goExecutor.oExecute(...)`** (`:173-...`): intoarce **numeric** `CT_SUCCES`/`CT_INSUCCES`
(`:218-220`), **nu** numar de randuri. Nu le confunda.
- **`OSCRIE_IN_FISIERE(tnScrie_Sterge, tlModificare, tlRul, ...)`**
(`COMUN\programe\oscrie_in_fisiere.prg:14`): numeric, `>0` = succes; `-1` la esecuri de precon-
ditie (`:68-81`), `-5` la verificarea de stoc (`:104`). Parametrul 1: `0` = scriere, `2` = stergere.
- **`_frmbase.do_termin`** (`_frm_base.vc2:363-376`): singura poarta care pune `buton = 1` /
`gnButon = 1`, si o face **doar daca `this.inainte_de_do_termin()` intoarce `.T.`**.
`frm_modific2024` **nu** suprascrie `do_termin` (nu apare in lista de metode proprii), deci
mosteneste asta.
---
## 2. Starea purtata de cursorul `tvd` (si de `tvanz`)
### 2.1 Coloanele lui `tvd`
`tvd` se creeaza in `frm_modific2024.Load` (`omodificari.vc2:14554-14591`): pe ramura
ROAFACTURARE prin `CreeazaCursorTvdGol()` -> `CreeazaCursorArticoleGol('tvd')`
(`COMUN\programe\ofacturare_editare.prg:263-282`), pe ramura ROACONT/ROAGEST printr-un
`CREATE CURSOR` **duplicat literal** in clasa (`omodificari.vc2:14571`) — doua definitii care trebuie
tinute sincron manual.
Se umple din `VVANZARI_ARTICOLE` prin `IncarcaArticoleFactura`
(`ofacturare_editare.prg:288-323`), cu `where v.id_vanzare = <id> and v.sters = 0`
(`:300`) — deci **la incarcare toate randurile au `sters = 0`**.
| Coloana | Provenienta | Editabila in grid |
|---|---|---|
| `id_vanzare`, `id_vanzare_det` | view | nu |
| `id_articol` | view | nu |
| **`cantitate`** | view | **DA** — `Column5`, `ReadOnly = .F.` (`:12366-12374`) |
| **`pret`** | view | **DA** — `Column6`, `ReadOnly = .F.` (`:12375-12383`) |
| **`pret_cu_tva`** (flag 0/1) | view | **DA** — `Column7` checkbox, `ReadOnly = .F.`, `Sparse = .F.` (`:12384-12391`) |
| `proc_tvav` | view | nu (`Column8.ReadOnly = .T.`) |
| `discount_unitar` | view | nu (`Column9.ReadOnly = .T.`) |
| `id_gestiune`, `cont`, `id_valuta`, `id_jtva_coloana`, `serie`, `explicatie`, `taxcode`, `lot`, `sters` | view | nu (`cont`, `id_gestiune`, `id_valuta`, `id_jtva_coloana`, `sters` nici macar nu au coloana in grid) |
| `denumire`, `codmat`, `nume_gestiune`, `nume_val` | join-uri din view | nu |
| `in_stoc` | **nu e in view** — join separat pe `NOM_ARTICOLE` in SQL-ul din `:299` | nu |
| **`lmodificat` (L)** | calculat, `.F.` la incarcare (`:316`) | — |
| **`valoare` (N 14,2)** | calculat la incarcare (`:316-319`) si recalculat la fiecare editare | nu (`Column14.ReadOnly = .T.`) |
Definitia view-ului: `COMUN\docs\cercetare\ff_view_articole_vanzare.sql` (21 de coloane, valori brute,
fara conversie valutara; filtrul `STERS` lasat pe seama apelantului).
**Doar 3 coloane sunt editabile: `cantitate`, `pret`, `pret_cu_tva`.** Tot restul e read-only in
grid; `explicatie`/`taxcode` se schimba doar pe cealalta cale, `frm_modifica_articol_factura`
(punctul 4).
### 2.2 Cum se distinge o linie modificata / stearsa / adaugata
- **Modificata**: `tvd.lmodificat = .T.`, **per linie**, nu global. Se pune in exact trei locuri:
- `frm_modific2024.calculeaza_valori_articol` (`:13397-13409`, linia `:13407`
`REPLACE lmodificat WITH .T., valoare WITH m.lnValoare`) — apelata din `Valid`-ul cantitatii
(`:16428-16433`), `Valid`-ul pretului (`:16439-16444`) si `InteractiveChange`-ul checkboxului
`pret_cu_tva` (`:16450-16458`);
- `cmdStergeArticol.Click` (`:16423`);
- `AdaugaLinieTvdDinArticol` (`:13119`).
`Valid`-urile compara cu `Thisform.oldvalue` (setat in `When`), deci o retastare a aceleiasi
valori **nu** marcheaza linia. Nu exista `lmodificat` la nivel de formular.
- **Stearsa**: `tvd.sters = 1`. `cmdStergeArticol.Click` (`:16417-16426`) **comuta**
(`REPLACE sters WITH IIF(Nvl(sters,0) = 1, 0, 1)`), deci stergerea e reversibila pana la salvare;
linia ramane vizibila, grizata prin `DynamicForeColor = "IIF(tvd.sters=1,RGB(150,150,150),...)"`
pe toate cele 14 coloane. Toate randurile incarcate pornesc de la `sters = 0`, deci `sters = 1`
in `tvd` inseamna intotdeauna "sters in sesiunea curenta".
- **Adaugata**: `tvd.id_vanzare_det = 0`, pus explicit de `AdaugaLinieTvdDinArticol`
(`:13108`). Randul vine din `frm_articol_factura` prin `poArticol`
(`cmdAdaugaArticol.Click`, `:16374-16415`), cu `id_vanzare` = `This.nIdVanzare` si conversie
RON->valuta documentului pe ramura `tip_valuta = 0` (`:13097-13105`).
Combinatia `id_vanzare_det = 0 AND sters = 1` e posibila (linie adaugata si apoi stearsa in
aceeasi sesiune) si trebuie ignorata la scriere.
### 2.3 Valorile VECHI: **NU se pastreaza nicaieri**
Confirmat prin cautare: nu exista niciun cursor de instantaneu (`tvd_orig`, `crsArticoleOrig`,
`tvanz_orig`, `nDiscountVechi` etc.) in cod de productie — singura potrivire e intr-un test
(`COMUN\utile\Teste\editare_factura\test_ui_bara_totaluri.prg:86`). `tvd` poarta **doar** valorile
curente plus flagul `lmodificat`; valoarea dinainte de editare se pierde in momentul in care
utilizatorul o schimba.
Consecinte:
- scrierea nu poate fi restransa la "coloanele efectiv schimbate" — se scriu toate cele trei
coloane editabile plus `valoare`, pe randurile cu `lmodificat = .T.`;
- cerinta S4b de a **enumera** utilizatorului "linia X: cantitate 5 -> 8" **nu e realizabila azi**;
cere un cursor de instantaneu luat imediat dupa `IncarcaArticoleFactura` (ex.
`SELECT id_vanzare_det, cantitate, pret, pret_cu_tva FROM tvd INTO CURSOR tvd_initial`).
Nu e implementat; e lucrare in plus, nu detaliu.
### 2.4 `tvanz` si discountul de document
`tvanz` se creeaza tot in `Load` (`:14574-14582`), prin `CreeazaCursorTvanzGol()`
(`ofacturare_editare.prg:148-154`) sau prin `CREATE CURSOR` duplicat (`:14581`, **cu 3 coloane mai
putin** — `in_valuta`/`id_valuta`/`nume_val` lipsesc pe ramura ROACONT). Se umple in `Show()`
prin `IncarcaVanzareDinNota('tact')` (`:14684`), care cauta randul din `VANZARI` incercand toate
tripletele distincte `(cod, nract, serie_act, dataact)` din `tact`
(`ofacturare_editare.prg:203-258`). Coloane: `id_vanzare, tip, discount, total_fara_tva, total_tva,
total_cu_tva, curs, multiplicator, in_valuta, id_valuta, nume_val`.
- **`discount` e editabil direct**: `txtDiscountArt` are `ControlSource = "tvanz.discount"`,
`ReadOnly = .F.` (`omodificari.vc2:12825-12836`). `Valid`-ul lui cheama doar
`ActualizeazaBaraTotaluri()` (`:16460-16462`).
- **Nu exista `lmodificat` pe `tvanz`** si nici valoare veche salvata. Nu se poate sti daca
utilizatorul a atins discountul. Singura optiune fara lucrare in plus: scrie discountul
**neconditionat** cand pagina a fost activa (`UPDATE VANZARI SET DISCOUNT = ...` e idempotent).
Daca se vrea "doar daca s-a schimbat", trebuie retinuta valoarea initiala la incarcare.
- `tvanz.total_cu_tva` e afisat ca "Total salvat" (`txtTotalSalvatArt`, `ControlSource =
"tvanz.total_cu_tva"`, ReadOnly, `:12895-12907`) — e valoarea din baza, nu una recalculata.
- Totalurile calculate stau in proprietati de formular, nu in cursor: `nTotalLiniiRon`,
`nTotalNetRon` (`ActualizeazaBaraTotaluri`, `:12934-12976`), `nTotalActRon`, `nTotalRulRon`,
`lRulAjustat` (`ActualizeazaVerdictActRul`, `:12978-13079`). Ele dispar odata cu formularul.
### 2.5 Cursoarele supravietuiesc formularului
`frm_modific2024` **nu are proprietatea `DataSession`** (nicio potrivire in tot `omodificari.vc2`),
deci ruleaza in sesiunea de date implicita: `tvd` si `tvanz`, create in `Load()`, raman deschise
dupa `This.Release` din `do_termin`. Niciunul dintre cei doi apelanti nu le inchide
(`ofacturare_comun.vc2:3837-3860`, `comun.vc2:2546-2566`). **Asta face agatarea posibila** — dupa
`Omodif.Show()`, apelantul citeste `tvd`/`tvanz` direct.
Corolar: obiectul `Omodif` e Released, deci **nu se pot citi proprietatile lui**
(`lAreArticoleVanzari`, `nIdVanzare`) dupa `Show()`. Semnalul "pagina de articole a fost activa"
trebuie dedus din cursoare: `Used('tvanz') AND Reccount('tvanz') = 1 AND Used('tvd')`, iar
`id_vanzare` se ia din `tvanz.id_vanzare`.
---
## 3. Unde se agata scrierea — si ce ordonare rezista capcanei
### 3.1 Capcana, verificata in sursa
`pack_contafin.finalizeaza_modificare_nota` (`COMUN\docs\PACK_CONTAFIN.pck:8601-8649`):
```sql
lnCodNou := pack_contafin.get_cod();
SELECT COUNT(*) INTO lnEInVanzari FROM vanzari WHERE cod = tnCod;
IF lnEInVanzari > 0 THEN
pack_facturare.actualizeaza_vanzari(tnCod, lnCodNou);
END IF;
UPDATE atasamente_vanzari SET cod = lnCodNou WHERE cod = tnCod;
...
```
`pack_facturare.actualizeaza_vanzari` (`D:\ROA\ROAACNPRO\PACK_FACTURARE.pck:15951-15961`):
```sql
-- de modificat in caz ca il las sa stearga manual inregistrari din VANZARI_DETALII
-- acum se marcheaza cu STERS = 1 doar cand se sterge toata factura
UPDATE VANZARI_DETALII SET STERS = 0
WHERE ID_VANZARE IN (SELECT ID_VANZARE FROM VANZARI WHERE COD = V_COD_VECHI);
UPDATE VANZARI SET COD = V_COD_NOU, STERS = 0 WHERE COD = V_COD_VECHI;
```
Pentru o factura, `COUNT(*) > 0` e intotdeauna adevarat, deci **resetul `STERS = 0` ruleaza la
FIECARE editare de nota**. `ID_VANZARE` nu se schimba niciodata (se schimba doar `COD`), deci o
cheie `ID_VANZARE` capturata inainte ramane valida dupa.
### 3.2 Doua consecinte, nu una
1. **Evidenta**: o linie marcata `STERS = 1` de VFP *inainte* de `finalizeaza_modificare_nota` e
reactivata tacut. Deci scrierea trebuie sa fie **dupa**.
2. **Mai putin evidenta, si mai grava**: la o editare **ulterioara** a aceleiasi facturi, resetul
reactiveaza si liniile sterse in sesiuni **anterioare**. `tvd` se incarca doar cu `sters = 0`
(`ofacturare_editare.prg:300`), deci VFP nici nu stie ca liniile alea exista si nu le-ar
re-marca. Rezultat: **o linie stearsa luna trecuta reapare la prima re-editare a facturii**, si
intra si in recalculul de totaluri (care citeste `STERS = 0`). Asta nu e o problema azi, pentru
ca azi nu exista stergere per linie — devine problema exact prin #6.
### 3.3 Ordonarea care rezista
Punctul de agatare, pentru **ambele** puncte de intrare, e **imediat dupa apelul
`finalizeaza_modificare_nota` si inainte de `do_inchide_tranzactie`**, gardat de `lnSucces > 0`:
- ROAFACTURARE: intre `ofacturare_comun.vc2:3827` (`Endif`-ul apelului) si `:3828`;
- registru jurnal: intre `comun.vc2:2490` (`Endif`-ul apelului) si `:2538`.
Secventa completa, o singura tranzactie manuala:
```
do_deschide_tranzactie()
OSCRIE_IN_FISIERE(2, ...) -- sterge nota veche
OSCRIE_IN_FISIERE(0, ...) -- scrie nota noua
finalizeaza_modificare_nota(...) -- realiniaza COD, RESETEAZA STERS = 0 pe toate liniile
>>> ScrieArticoleFacturaEditate(...) -- NOU: liniile + discountul
>>> recalculeaza_totaluri_vanzari(id_vanzare) -- NOU (S5 Oracle)
do_inchide_tranzactie(Iif(lnSucces < 0, 2, 1))
```
Iar in interiorul helperului, ordinea operatiilor:
1. `INSERT` pentru liniile noi (`id_vanzare_det = 0 AND Nvl(sters,0) <> 1`) — intai, ca ID-urile
lor sa poata fi excluse la pasul 3; PK-ul vine din trigger, nu se cere din secventa
(`rec_cale_vanzari_detalii.md` 3.2);
2. `UPDATE` pentru liniile existente atinse (`id_vanzare_det > 0 AND lmodificat AND
Nvl(sters,0) <> 1`);
3. **stergere ca diferenta de multimi, nu ca lista de linii sterse**:
`UPDATE VANZARI_DETALII SET STERS = 1, ID_UTILS = :util, DATAORAS = SYSDATE
WHERE ID_VANZARE = :id AND STERS = 0 AND ID_VANZARE_DET NOT IN (<ID-urile active din tvd>)`.
Formularea asta e cea care rezista: ea nu enumera ce s-a sters acum, ci **impune** ca multimea
activa din baza sa fie exact multimea activa din `tvd`. Repara automat si consecinta (2) din
3.2 — liniile inviate de reset redevin sterse — si e idempotenta.
Pentru asta, `<ID-urile active>` trebuie sa includa si ID-urile randurilor tocmai inserate
(de aici ordinea INSERT-inainte), sau, mai simplu, pasul 3 se restrange la
`... AND ID_VANZARE_DET NOT IN (<ID-urile > 0 active din tvd>) AND ID_VANZARE_DET NOT IN
(<ID-urile inserate acum>)`. Daca lista de ID-uri inserate e greu de obtinut din VFP, alternativa
e sa se ruleze pasul 3 **inainte** de INSERT — atunci lista `NOT IN` contine doar ID-uri
preexistente si randurile noi nu sunt inca in tabela, deci nu pot fi atinse.
**Recomandare: pasul 3 primul, apoi INSERT, apoi UPDATE.** Mai simplu si fara nevoia de
`RETURNING`.
4. `UPDATE VANZARI SET DISCOUNT = :discount WHERE ID_VANZARE = :id` (din `tvanz.discount`).
**Recalculul de totaluri se muta din `finalizeaza_modificare_nota` in apelantul VFP.**
`rec_s5_oracle_vanzari.md` (B) propunea `recalculeaza_totaluri_vanzari` apelata **din interiorul**
lui `finalizeaza_modificare_nota`, dupa `actualizeaza_vanzari`. Cu ordonarea de mai sus **asta nu
mai merge**: la momentul acela liniile nu sunt inca scrise si `VANZARI.DISCOUNT` inca are valoarea
veche, deci totalurile s-ar calcula pe date invechite. Apelul trebuie facut din VFP, ca pas separat,
dupa helper. Efectul secundar e favorabil si corespunde motivatiei originale a Variantei B:
recalculul devine **strict opt-in pe calea de editare de factura**, nu ruleaza pentru notele
ROACONT/ROAGEST care nu ating sumele — ceva ce `finalizeaza_modificare_nota` oricum nu putea
distinge.
### 3.4 Ordonari respinse
| Varianta | De ce nu |
|---|---|
| Scriere **inainte** de `finalizeaza_modificare_nota` | `STERS = 1` sters de resetul din `actualizeaza_vanzari`; `INSERT`/`UPDATE` ar supravietui, dar stergerea nu — comportament pe jumatate, cel mai prost caz |
| Scriere din `inainte_de_do_termin` (in formular) | ruleaza **inainte** de `do_deschide_tranzactie` (`_frm_base.vc2:364` -> `ofacturare_comun.vc2:3800`), deci in autocommit, in afara tranzactiei; la esec ulterior al notei, liniile raman scrise |
| Scriere **dupa** `do_inchide_tranzactie` | tranzactie separata; commit partial daca a doua esueaza |
| Modificarea lui `actualizeaza_vanzari` sa nu mai reseteze `STERS` | cod partajat cu toata suita ROA (apelat pentru orice nota cu `cod` in `vanzari`); riscul respins deja de plan |
---
## 4. Modelul existent: `pack_facturare.modifica_explicatie_articol`
**Oracle** (`D:\ROA\ROAACNPRO\PACK_FACTURARE.pck:14464-14472`) — corpul complet:
```sql
PROCEDURE modifica_explicatie_articol(V_ID_VANZARE_DET IN NUMBER,
V_EXPLICATIE IN VARCHAR2,
V_ID_UTIL IN NUMBER,
V_TAXCODE IN NUMBER DEFAULT NULL) is
BEGIN
UPDATE VANZARI_DETALII
SET EXPLICATIE = V_EXPLICATIE, TAXCODE = V_TAXCODE
WHERE ID_VANZARE_DET = V_ID_VANZARE_DET;
END modifica_explicatie_articol;
```
**Detaliu de contract, contra-intuitiv:** `V_ID_UTIL` e primit si **complet ignorat** — nu se scriu
`ID_UTILS`/`DATAORAS`. Procedura nu e un model bun pentru partea de audit; e model doar pentru
forma apelului si pentru granularitatea "un `UPDATE` tintit pe `ID_VANZARE_DET`".
**Apelantul VFP** — `frm_modifica_articol_factura.inainte_de_do_termin`
(`COMUN\clase\ofacturare_comun.vc2:5230-5241`), integral:
```foxpro
PROCEDURE inainte_de_do_termin
Local llReturn
If amessagebox("Doriti sa salvati modificarile?",4+32,"Confirmare salvare explicatie") = 6
lcSql = [begin pack_facturare.modifica_explicatie_articol(] + Alltrim(Str(poRec.id_vanzare_det)) + [,] + ;
['] + Nvl(Alltrim(OracleSpecialCharacters(poRec.explicatie)),'') + [',?gnIdUtil,?poRec.taxcode); end;]
llReturn = goExecutor.oExecuta(lcSql)
Else
llReturn = .F.
Endif
Return llReturn
ENDPROC
```
Deschis din `frm_facturi.do_modifica_explicatie` (`ofacturare_comun.vc2:4639-4656`):
`Scatter Name poRec Memo` din `crsDetalii`, `Createobject("frm_modifica_articol_factura")`,
`.Show()`, apoi `actualizeaza_grid2()` daca `gnButon = 1`.
**Ce se preia din model:**
- bloc `begin ... ; end;` construit ca text, cu numerele injectate prin `Alltrim(Str(...))` si
sirurile prin `?`-binding sau literal cu ghilimele simple;
- `OracleSpecialCharacters(...)` obligatoriu pe orice sir care ajunge literal in SQL;
- **tratarea erorii = niciuna in apelant**: `goExecutor.oExecuta` intoarce `.T.`/`.F.` si afiseaza
singur mesajul; apelantul doar propaga booleanul.
**Ce NU se preia:** apelul asta ruleaza in **autocommit**, in afara oricarei tranzactii manuale
(`do_modifica_explicatie` nu deschide tranzactie). Helperul nou ruleaza **in interiorul** tranzactiei
deschise de apelant si nu are voie sa dea `COMMIT`/`ROLLBACK` — se opreste la primul esec si lasa
apelantul sa faca rollback prin `do_inchide_tranzactie(2)`.
---
## 5. `inainte_de_do_termin` (`omodificari.vc2:14249-14441`)
**Numerotare:** briefingul dadea `:13357-13549`; azi metoda e la `:14249-14441` (aproximativ 90% din
corp — `:14299-14437` — e cod comentat, ramas din versiunea veche).
**Ce valideaza azi** (codul viu, `:14252-14296`):
| Linie | Verificare |
|---|---|
| `:14252-14253` | `SELECT tact` + `SET FILTER TO` (curata filtrul de grid inainte de salvare) |
| `:14257-14259` | completeaza `id_set` gol pe `tact`, `trul`, `trul_obinv` |
| `:14265-14271` | pentru `id_set` 99998 / 90024 sare peste verificarea de conturi |
| `:14273-14277` | `verificare_note_contabile('tact', ...)` — analitice si parteneri (in `oOperatii_comune`) |
| `:14281-14290` | avertisment 4426-4428 / 4428-4427 introdusa fara optiunea dedicata (doar din 2013), cu confirmare Da/Nu |
| `:14291-14293` | `This.VerificaAvertizareExigibilizareTVA()` |
| `:14296` | `RETURN m.llRet` |
**Nu atinge deloc `tvd` sau `tvanz`.** Nicio validare pe pagina de articole.
**Recomandare: da, aici trebuie adaugata validarea paginii de articole** — e singura poarta inaintea
lui `gnButon = 1` (`_frm_base.vc2:364`), deci singurul loc care poate opri o salvare inainte ca
apelantul sa deschida tranzactia. Validari care merita:
- **cantitate 0 sau negativa** pe o linie activa (`Nvl(sters,0) <> 1`) — o linie cu cantitate 0 se
scrie in `VANZARI_DETALII` cu valoare 0 si strica totalurile fara sa fie vizibila ca eroare;
- **`id_articol` nul sau 0** pe o linie activa — se poate produce doar prin `AdaugaLinieTvdDinArticol`
cu un `poArticol` incomplet, dar `INSERT`-ul ar cadea pe FK abia in Oracle, dupa ce nota a fost deja
rescrisa, deci cu ROLLBACK pe tot;
- **`pret` nul** pe o linie activa — `VANZARI_DETALII.PRET` e `NOT NULL`
(`rec_cale_vanzari_detalii.md` 2.2);
- **zero linii active** cand documentul are rand in `VANZARI` — utilizatorul a sters tot; cerea o
confirmare explicita, nu o salvare tacuta care goleste factura.
Trei conditii obligatorii pentru adaugare:
1. gardata pe `"OFACTURARE_EDITARE" $ Upper(Set("Procedure")) AND Used('tvd')`, altfel se rupe
registrul jurnal din ROACONT/ROAGEST;
2. `RETURN .F.` blocheaza salvarea — se foloseste doar pentru erori reale; pentru situatii
discutabile, tiparul existent din `:14286-14288` (mesaj cu 4+32 si continuare la "Da");
3. plasata **inaintea** lui `RETURN m.llRet` de la `:14296`, nu in blocul comentat de dedesubt.
Fara cod aici, doar constatarea si recomandarea, cum s-a cerut.
---
## 6. Contractul minim al helperului nou
Locul: `COMUN\programe\ofacturare_editare.prg` — acolo stau deja `IncarcaArticoleFactura`,
`IncarcaVanzareDinNota`, `CreeazaPoArticolNouTvd`, si fisierul e inregistrat doar in ROAFACTURARE
(`Programe\roafacturare.prg:214`), ceea ce da automat no-op-ul in ROACONT/ROAGEST.
```
FUNCTION ScrieArticoleFacturaEditate
LPARAMETERS tnIdVanzare, tcAliasArticole, tcAliasVanzare
```
**Parametri**
- `tnIdVanzare` — `ID_VANZARE` al documentului (din `tvanz.id_vanzare`). Nu se ia din `tvd`, ca sa
functioneze si cand `tvd` a ramas gol.
- `tcAliasArticole` — implicit `'tvd'` (simetric cu `IncarcaArticoleFactura`, care primeste aliasul
destinatie; permite testarea pe un cursor construit in test).
- `tcAliasVanzare` — implicit `'tvanz'`, pentru `discount`.
**Preconditii** (nu le verifica, le documenteaza):
- tranzactie manuala deja deschisa de apelant (`do_deschide_tranzactie` a intors `.T.`);
- `finalizeaza_modificare_nota` a rulat deja cu succes — deci `actualizeaza_vanzari` si-a facut
resetul `STERS = 0`, iar helperul scrie peste el;
- `goExecutor` conectat.
**Ce scrie**, in ordinea din 3.3:
1. `UPDATE VANZARI_DETALII SET STERS = 1, ID_UTILS = <gnIdUtil>, DATAORAS = SYSDATE
WHERE ID_VANZARE = :id AND STERS = 0 AND ID_VANZARE_DET NOT IN (<ID-urile > 0 active din alias>)`
— un singur statement, formulat ca diferenta de multimi (motivarea in 3.3);
2. `INSERT INTO VANZARI_DETALII (...)` per linie cu `id_vanzare_det = 0 AND Nvl(sters,0) <> 1`,
fara `ID_VANZARE_DET` in lista de coloane (trigger `TRG_VANZARI_DET_BEFOINS`);
3. `UPDATE VANZARI_DETALII SET CANTITATE = ..., PRET = ..., PRET_CU_TVA = ..., ID_UTILS = ...,
DATAORAS = SYSDATE WHERE ID_VANZARE_DET = :id_det` per linie cu
`id_vanzare_det > 0 AND lmodificat AND Nvl(sters,0) <> 1`;
4. `UPDATE VANZARI SET DISCOUNT = :disc WHERE ID_VANZARE = :id` din `<tcAliasVanzare>.discount`.
`PROC_TVAV` **nu** se recalculeaza — ramane cota deja persistata pe linie
(`rec_cale_vanzari_detalii.md`, "Observatie tehnica"). `DISCOUNT_UNITAR` nu e editabil in grid
(punctul 2.1), deci se scrie doar la `INSERT`, nu la `UPDATE`.
**Ce intoarce**: logic. `.T.` = tot a mers **sau nu era nimic de facut**; `.F.` = primul esec, si se
opreste acolo. No-op cu `.T.` cand `tnIdVanzare <= 0`, cand aliasul de articole nu e deschis, sau
cand aliasul de vanzare nu are exact un rand — asa apelul poate sta neconditionat in ambele puncte
de intrare, fara garda duplicata la apelant.
**Exceptie de la "nimic de facut = .T.":** `Reccount(tcAliasArticole) = 0` cu `tnIdVanzare > 0` **nu**
e no-op, e "s-au sters toate liniile" — pasul 1 marcheaza tot documentul. Validarea din punctul 5
(confirmare la zero linii active) e ce trebuie sa impiedice cazul accidental.
**Cum semnaleaza eroarea**: prin valoarea de retur, atat. Nu afiseaza mesaj — `goExecutor.oExecuta`
o face deja (`oproceduri_comune.prg:153-156`). Nu da `COMMIT`/`ROLLBACK`, nu inchide cursoare, nu
schimba workarea curenta (o salveaza cu `Select()` si o restaureaza, ca `IncarcaVanzareDinNota`).
La apelant:
```
If lnSucces > 0
lnSucces = Iif(ScrieArticoleFacturaEditate(...), 1, -1)
Endif
```
— `-1`, nu `0`, ca sa se prinda in `Iif(lnSucces<0, 2, 1)` de la `do_inchide_tranzactie`
(capcana din 1.3).
**Motivare a formei**: un singur punct de scriere, apelat identic din ambele puncte de intrare
(altfel logica se dubleaza in `ofacturare_comun.vc2` si in `comun.vc2`, iar `comun.vc2` e atins de
toata suita); parametrizat pe alias, ca testul headless sa-l poata rula pe cursoare construite
manual, fara formular; contract boolean, identic cu `goExecutor.oExecuta` si cu
`frm_modifica_articol_factura.inainte_de_do_termin`, deci fara conventie noua de erori in cod.
**De verificat inainte de a scrie `INSERT`-ul** (ramas deschis din `rec_cale_vanzari_detalii.md`
punctul 5, **NEVERIFICAT** si aici): coloanele `NOT NULL` fara valoare din trigger —
`STERS`, `VALIDAT`, `DIFERENTA`, `CUSTODIE`, `DESCARCAT` — au sau nu `DEFAULT` la nivel de coloana
(`all_tab_columns.data_default`). Daca nu au, trebuie enumerate explicit in `INSERT`.
---
## 7. Ce ramane netestabil headless
**Testabil headless** (`vfp9.exe -A -T`), pe cursoare construite in test:
- `ScrieArticoleFacturaEditate` cu un `goExecutor` mock: se verifica **textul SQL generat** pentru
fiecare din cele 3+1 categorii, ordinea statement-elor, si comportarea la `.F.` din mock;
- clasificarea liniilor din `tvd` (modificata / stearsa / adaugata / adaugata-si-stearsa) —
logica pura pe cursor;
- `AdaugaLinieTvdDinArticol` si `calculeaza_valori_articol` (au deja teste:
`COMUN\utile\Teste\editare_factura\test_adauga_linie_articol.prg`,
`test_adauga_linie_valuta.prg`);
- validarile noi din `inainte_de_do_termin`, apelate direct pe o instanta de formular.
**Netestabil headless, cu motivul:**
1. **Coloanele gridului `grdArticoleFactura`.** Sub `-A -T` coloanele nu se materializeaza
(`ColumnCount` si `RecordSource` citite acolo sunt artefacte); pentru ele exista deja harnessul
cu UI vizibil (`test_ui_grid_articole.prg`, `test_ui_sterge_linie.prg`,
`test_ui_fix_editabil_subtotal.prg`).
2. **Interactiunea reala cu gridul** — `Valid`/`When`/`InteractiveChange` pe `cCantitateArt`,
`cPretArt`, `cPretCuTvaArt` (`:16428-16458`) depind de focus si de `Thisform.oldvalue`; se pot
apela metodele direct, dar asta nu testeaza traseul care pune `lmodificat`.
3. **Dialogul modal `frm_articol_factura`** deschis din `cmdAdaugaArticol.Click` (`:16407-16408`,
`loDlg.Show(1)`) — blocheaza headless. De aceea `AdaugaLinieTvdDinArticol` a fost deja separata de
`Click` (comentariul de la `:13082-13083`); se testeaza doar partea separata.
4. **Ordonarea fata de `actualizeaza_vanzari`** — inima acestei cercetari. Nu se poate verifica
decat pe Oracle real: `finalizeaza_modificare_nota` trebuie sa ruleze efectiv ca sa se vada
resetul `STERS = 0`, iar apoi ca helperul il corecteaza. Cere un test tranzactional
(`do_deschide_tranzactie` -> pasii -> `SELECT` de verificare -> `Sqlrollback`), care **scrie**
temporar in baza. Nu s-a rulat aici.
5. **Regresia liniilor sterse in sesiuni anterioare** (3.2, consecinta 2) — cere doua editari
succesive ale aceleiasi facturi, deci doua tranzactii, deci scriere reala.
6. **`amessagebox` din `goExecutor.oExecuta`** la esec Oracle — blocheaza headless; testele care
forteaza un esec trebuie sa mocheze `oExecuta`, altfel raman agatate.
7. **Comportamentul in ROACONT/ROAGEST** (pagina absenta, helper neincarcat) — nu se poate testa din
ROAFACTURARE, unde `ofacturare_editare.prg` e mereu in `SET("PROCEDURE")`. Se poate aproxima
verificand ca garda `"OFACTURARE_EDITARE" $ Upper(Set("Procedure"))` exista in fiecare punct nou,
dar nu e acelasi lucru cu o rulare reala.
---
## Rezumat al lucrurilor de decis inainte de implementare
1. **Recalculul de totaluri se muta din `finalizeaza_modificare_nota` in VFP** (3.3) — abatere de la
`rec_s5_oracle_vanzari.md` B, impusa de ordonare. De confirmat cu Marius.
2. **Stergerea se scrie ca diferenta de multimi**, nu ca lista de linii sterse (3.3, pasul 1) — asta
e ce repara si regresia din 3.2(2).
3. **Discountul se scrie neconditionat** cat timp nu exista valoare initiala salvata pe `tvanz`
(2.4); alternativa e un instantaneu la incarcare.
4. **S4b "enumera vechi -> nou" nu are azi de unde lua valorile vechi** (2.3) — cere un cursor de
instantaneu, lucrare in plus fata de ce exista.
5. **De verificat `DEFAULT`-urile pe coloanele `NOT NULL`** din `VANZARI_DETALII` inainte de a scrie
`INSERT`-ul (punctul 6, NEVERIFICAT).