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

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

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

187 lines
12 KiB
Markdown

# S5 — helper VFP `ScrieArticoleFacturaEditate` (`COMUN\programe\ofacturare_editare.prg`)
Implementare, nu doar cercetare. Singurul fisier atins: `COMUN\programe\ofacturare_editare.prg`
(`.prg` sursa directa, fara binar, fara write-back). Diff: `docs\diff_s5_helper_scriere.patch`.
## 1. Ce s-a facut
- `CreeazaCursorArticoleGol` (deci si `CreeazaCursorTvdGol`): adaugate `id_vanzare_set I NULL` si
`pret_achizitie N(14,4) NULL`, imediat dupa `sters`, inainte de `denumire` — aceeasi pozitie ca in
view-ul `VVANZARI_ARTICOLE` extins de `ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql` (verificat pe
disc, coloanele intra la coada listei proprii tabelei, inainte de join-uri).
- `IncarcaArticoleFactura`: **nicio modificare de cod** — foloseste deja `select v.*, na.in_stoc from
vvanzari_articole v ...` (`:299`), deci cele doua coloane noi ale view-ului ajung automat in `tvd`
prin `v.*` in momentul in care view-ul e aplicat in Oracle. Extinderea cursorului gol (mai sus) e
suficienta pentru ca `tvd` sa poarte ambele coloane si pe ramura de eroare/fallback.
- **A doua definitie a cursorului, `omodificari.vc2:14571` (ramura ROACONT/ROAGEST, `CREATE CURSOR`
duplicat literal) NU a fost atinsa** — confirmarea ceruta explicit. Trebuie tinuta sincron manual de
agentul care lucreaza pe `.vc2`, altfel `tvd` are structuri diferite dupa aplicatie.
- Functie noua `ScrieArticoleFacturaEditate(tnIdVanzare, tcAliasArticole, tcAliasVanzare)` —
implicit `'tvd'`/`'tvanz'`. Patru pasi, in ordinea impusa de decizia 38 (marcheaza tot, invie ce
ramane, insereaza, recalculeaza), fara `COMMIT`/`ROLLBACK`, fara inchidere de cursoare,
salveaza/restaureaza workarea si `Recno()` pe alias.
## 2. Recalculul de totaluri (pasul 4) — LAMURIT de team-lead, acum in helper
Livrarea initiala nu includea apelul la `recalculeaza_totaluri_vanzari` in helper, motivat de
`rec_s5_cale_scriere_vfp.md` 3.3 ("apelul trebuie facut din VFP, ca pas separat, dupa helper") si de
secventa din decizia 38 care arata doua apeluri distincte la nivelul apelantului. **Team-lead a
corectat**: acea schita era la nivel de apelant, nu contractul final; apelul intra in helper tocmai ca
sa ramana un singur punct de scriere (altfel cele doua puncte de intrare, `ofacturare_comun.vc2` si
`comun.vc2`, trebuie amandoua sa-si aminteasca sa-l cheme, cu riscul de a uita unul).
**Implementat acum**: pasul 4, ultimul, dupa `INSERT`-uri, gardat de `IF m.llSucces`:
```
begin pack_facturare.recalculeaza_totaluri_vanzari(<tnIdVanzare>, <discount sau NULL>); end;
```
- discountul se citeste din `<tcAliasVanzare>.discount` (primul rand, garantat unic de garda de
no-op) imediat la inceputul functiei, inainte de pasii 1-3 (pozitia in cursorul de articole nu
intra in conflict, sunt cursoare separate).
- daca `discount` e `.NULL.` in cursor, se trimite literal `NULL` in apelul PL/SQL (pastreaza
discountul curent din baza) — **nu** `0`, care l-ar sterge. Daca are valoare, se trimite prin
`Alltrim(Str(...,18,4))`.
- niciun `UPDATE VANZARI SET DISCOUNT` separat — scrierea discountului ramane exclusiv in
responsabilitatea procedurii Oracle, primit ca parametru, exact cum cere decizia 41.
- acelasi tratament de eroare ca la ceilalti pasi: `goExecutor.oExecuta`, `.F.` la esec, fara mesaj
propriu, fara `COMMIT`/`ROLLBACK`.
## 3. `PRET_ACHIZITIE` pe linia noua — INCHIS: se citeste din cursor, nu se calculeaza in helper
Premisa initiala a deciziei 40 ("se preia din nomenclator / stoc") s-a dovedit gresita si a fost
revizuita de Marius, dupa constatarea de mai jos. Decizia finala: valoarea **o introduce
utilizatorul**, intr-o coloana noua editabila in gridul de pe PAGE3 (`omodificari.vc2`, doar pe
liniile noi — alt agent, nu se atinge aici). Helperul doar **citeste** `pret_achizitie` din cursorul
de articole (`Nvl(pret_achizitie,0)`), fara niciun lookup Oracle propriu.
**Constatarea care a schimbat decizia** (`SELECT` read-only in `MARIUSM_AUTO`, pastrata aici ca sa nu
se redescopere):
- `NOM_ARTICOLE` **nu are nicio coloana de pret de achizitie curent** — singura coloana cu "PRET" sau
"ACHIZ" in nume e `PRETACHCTVA` (`NUMBER`, `DEFAULT 0`), care e un **flag** ("pretul de achizitie
contine TVA?"), nu o valoare — confirmat prin utilizarea ei in
`pack_facturare.scrie_fact_aviz_custodie` (`docs\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:10116,
10168-10169, 10209`), unde e citita alaturi de `V_PRET_ACHIZITIE` (parametru separat), niciodata ca
sursa a lui.
- La emitere, `pret_achizitie` **nu vine dintr-o coloana statica** — vine fie din cursorul de stoc VFP
(`ofacturare_stoc.prg:221`, `a.Pret As pret_achizitie` din `rul_temp`, adica pretul lotului de stoc
ales), fie din cursorul de articole selectate la alegerea din gestiune
(`ofacturare.vc2:13822/13849`, `poArticol.pret_achizitie = Pret` din `crsartselectate`), fie e
re-derivat in Oracle in `adauga_articol_factura` (`docs\ff_...PACK_FACTURARE.sql:5020,
5093-5134`) — numai pe ramura restaurant (`ntip=45`) din `CRM_POLITICI_PRET_ART.PRETFTVA` +
procent de adaos; pe toate celelalte ramuri ramane valoarea primita ca parametru din VFP. Niciuna
din aceste cai exista in fluxul de editare: liniile noi la editare se adauga prin
`CreeazaPoArticolNouTvd` (`ofacturare_editare.prg:356-457`), care fixeaza
**`id_gestiune = -1000` si `gestionabil = 0`** necondtionat — adica editarea NU deschide dialogul
de alegere din stoc, deci n-are un lot/gestiune reala de unde sa citeasca pretul.
- `STOC` (verificat structura, `all_tab_columns`): tabela periodica `AN, LUNA, ID_ARTICOL,
ID_GESTIUNE, PRET, PRETV, CANTS, ...` — un rand pe lot/perioada/gestiune, aceeasi granularitate ca
`RUL`. **Nu exista un "pret curent" unic** fara o formula de agregare (ce perioada, ce gestiuni).
**Ce e implementat acum, in `ScrieArticoleFacturaEditate`**: la pasul `INSERT` al liniei noi,
`PRET_ACHIZITIE = Nvl(<tcAliasArticole>.pret_achizitie, 0)`, citit direct din cursor (coloana
adaugata la punctul 1) — fara `STOC`, fara `NOM_ARTICOLE`, fara functie ajutatoare. La `UPDATE`-ul
liniilor pastrate, coloana ramane **omisa din `SET`**, exact ca in specificatia initiala (pe liniile
existente nu e editabila, valoarea persistata nu se atinge).
## 4. Coloanele `NOT NULL` din `VANZARI_DETALII` — verificat, punctul NEVERIFICAT din cercetare e inchis
`SELECT column_name, nullable, data_default FROM all_tab_columns WHERE owner='MARIUSM_AUTO' AND
table_name='VANZARI_DETALII' AND nullable='N'` (read-only, `MARIUSM_AUTO`/`ROA_CENTRAL`):
| Coloana | `DATA_DEFAULT` | Tratament in `INSERT` |
|---|---|---|
| `ID_VANZARE_DET` | — | nu se scrie, vine din trigger `TRG_VANZARI_DET_BEFOINS` |
| `ID_VANZARE` | — | scris explicit (`tnIdVanzare`) |
| `PRET` | — | scris explicit (din `tvd.pret`) |
| `VALIDAT` | `0` | nescris, ramane `DEFAULT 0` |
| `STERS` | `0` | nescris, ramane `DEFAULT 0` (linie noua = activa) |
| `DIFERENTA` | `0` | nescris, ramane `DEFAULT 0` |
| `CUSTODIE` | `0` | nescris, ramane `DEFAULT 0` |
| `DESCARCAT` | `0` | nescris, ramane `DEFAULT 0` |
Toate cele cinci `NOT NULL` fara valoare din trigger **au `DEFAULT 0` la nivel de coloana** — nu e
nevoie sa fie enumerate in `INSERT`. Singurele `NOT NULL` care trebuie scrise explicit sunt
`ID_VANZARE` si `PRET`, deja acoperite de contract.
## 5. SQL generat, cate un exemplu per categorie
**Pasul 1 — marcheaza tot sters** (`tnIdVanzare = 12345`, `gnIdUtil = 7`):
```sql
update vanzari_detalii set sters = 1, id_utils = 7, dataoras = sysdate where id_vanzare = 12345 and sters = 0
```
**Pasul 2 — linie pastrata** (`id_vanzare_det = 98765`, `cantitate = 2.5`, `pret = 10.5`,
`pret_cu_tva = 0`):
```sql
update vanzari_detalii set sters = 0, cantitate = 2.500, pret = 10.5000, pret_cu_tva = 0, id_utils = 7, dataoras = sysdate where id_vanzare_det = 98765
```
**Pasul 3 — linie noua** (`id_articol = 111`, `cantitate = 1`, `pret = 20`, `pret_cu_tva = 0`,
`proc_tvav = 1.19`, `discount_unitar = 0`, `id_gestiune = -1000`, `cont` gol -> `NULL`, `id_valuta`
`.NULL.` -> `NULL`, `id_jtva_coloana = 3`, `serie`/`lot` goale -> `NULL`, `explicatie = "test 'x'"`,
`taxcode` `.NULL.` -> `NULL`, `pret_achizitie` tastat de utilizator in grid = `15.2300`):
```sql
insert into vanzari_detalii (id_vanzare, id_articol, cantitate, pret, pret_cu_tva, proc_tvav, discount_unitar, id_gestiune, cont, id_valuta, id_jtva_coloana, serie, explicatie, taxcode, lot, pret_achizitie, id_utils, dataoras) values (12345,111,1.000,20.0000,0,1.1900,0.0000,-1000,NULL,NULL,3,NULL,'test ''x''',NULL,NULL,15.2300,7,sysdate)
```
(`OracleSpecialCharacters` a dublat apostroful din `explicatie`.)
**Pasul 4 — recalculul totalurilor** (`tnIdVanzare = 12345`, `tvanz.discount = 5` — cazul cu valoare):
```sql
begin pack_facturare.recalculeaza_totaluri_vanzari(12345,5.0000); end;
```
Cazul `tvanz.discount` `.NULL.` (pastreaza discountul curent din baza):
```sql
begin pack_facturare.recalculeaza_totaluri_vanzari(12345,NULL); end;
```
## 6. Ce a ramas netestat, si de ce
- **Nimic rulat** — nu era in scope ("NU rulezi teste care scriu in baza"). Verificarea de mai sus
(coloane `NOT NULL`, tipuri, existenta view-ului) a fost facuta prin `SELECT`-uri read-only in
`MARIUSM_AUTO`, nu prin executarea functiei.
- Textul SQL generat pentru cei 4 pasi (sectiunea 5) e verificat manual, pe exemple, nu prin rularea
functiei cu un `goExecutor` mock.
- Restul e netestabil headless din motivele deja consemnate in `rec_s5_cale_scriere_vfp.md` sectiunea
7 (ordonarea fata de `actualizeaza_vanzari` cere Oracle real; grid-ul de pe PAGE3 care va scrie
`pret_achizitie` in cursor nu exista inca — depinde de agentul pe `omodificari.vc2`).
## 7. Rezumat pentru revizuire
Ambele puncte semnalate initial ca neconfirmate au fost lamurite de team-lead/Marius:
1. **Recalculul de totaluri** — intra in helper, ca pasul 4 (sectiunea 2).
2. **`PRET_ACHIZITIE`** — nu se mai calculeaza in helper; se citeste din cursor, unde va fi scrisa de
utilizator prin coloana noua din grid (sectiunea 3).
Toata functionalitatea (structura cursorului, cei 4 pasi de scriere, coloanele `NOT NULL`) e
implementata conform contractului final, fara ambiguitate ramasa in acest fisier.
## 8. Corectii din revizuirea team-lead
**1. Garda de no-op extinsa cu `Reccount(tcAliasArticole) = 0` — schimbare de fond fata de cercetare.**
`rec_s5_cale_scriere_vfp.md` sectiunea 6 spunea explicit ca un cursor de articole gol cu
`tnIdVanzare > 0` **nu** e no-op ("s-au sters toate liniile"), plecand de la premisa ca un cursor gol
apare doar prin stergerea tuturor liniilor de catre utilizator. Team-lead a aratat, cu dovada din cod,
ca premisa e incompleta: `IncarcaArticoleFactura` (`ofacturare_editare.prg:305-309`) intoarce **`.T.`
cu cursor gol** si la esec Oracle:
```
IF lnSucces < 0
AMESSAGEBOX(goExecutor.cEroare,0+16,"Eroare")
CreeazaCursorArticoleGol(m.lcAlias)
RETURN .T.
ENDIF
```
Helperul nu poate distinge "utilizatorul a sters tot" de "incarcarea a picat" — ambele ajung cu
`tvd` gol si `.T.` de la apelul de incarcare. Fara garda, al doilea caz ar duce, prin pasul 1
(marcheaza tot sters, fara nimic de inviat la pasul 2), la golirea tacuta a facturii la un simplu esec
de retea/Oracle la deschiderea formularului. Corectat: `Reccount(m.lcAliasArt) = 0` s-a adaugat la
garda initiala de no-op — cursorul gol nu mai scrie nimic. Cazul legitim (utilizatorul chiar sterge
toate liniile) e oricum oprit mai devreme de validarea din `inainte_de_do_termin` (alt agent,
`omodificari.vc2`), inainte ca helperul sa fie apelat.
**2. `UPDATE`-ul liniilor pastrate (pasul 2) ancorat si pe `id_vanzare`.** `WHERE id_vanzare_det = ...`
nu avea garda pe document. Adaugat `and id_vanzare = <tnIdVanzare>` — cost zero, inchide posibilitatea
teoretica de scriere intr-un alt document daca aliasul ar ajunge vreodata sa contina o linie straina
de `tnIdVanzare`.
Nimic altceva schimbat: ordinea pasilor, `NULL` pe discount, omiterea `PRET_ACHIZITIE` din `SET`-ul
de `UPDATE`, `OracleSpecialCharacters`, restaurarea `Recno()`/workarea raman ca in runda anterioara.