docs: runda 6 - cercetari, propuneri si conventia mediului Oracle

Cercetarile si propunerile rundelor 4-6 pe editarea facturii emise. Starea
rundei 6 si ce s-a stabilit intra in progres.md.

Nou: docs/conventii_mediu_oracle.md - pe dev/test se lucreaza numai cu
ROA_CENTRAL si schemele CONTAFIN_ORACLE si MARIUSM_AUTO; schema ACN nu se
foloseste. Scripturile de pachet sunt necalificate, deci schema tinta o
decide sirul de conectare, nu fisierul.

roafacturare.pj2 regenerat de git_sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gYCNE26G1G7UoGu2ju7aS
This commit is contained in:
2026-08-20 16:35:03 +03:00
parent b5a7108f34
commit ca3c5d7eea
26 changed files with 5316 additions and 103 deletions

View File

@@ -0,0 +1,125 @@
# Cercetare pe date: `cantitate < 0` / `cantitate = 0` pe articole de vânzări (validarea de la `omodificari.vc2:14386`)
Schema verificată: `MARIUSM_AUTO`@`ROA_CENTRAL` (schema de dezvoltare, folosită și pentru rulări de
teste automate — vezi caveat la final). Toate interogările: `SET TRANSACTION READ ONLY`, încheiate
cu `ROLLBACK` (confirmat: „Rollback complete." la fiecare rulare, zero scrieri).
Sursă date: view-ul `vvanzari_articole` (JOIN `VANZARI_DETALII`+articol, expune deja `denumire`,
`pret_achizitie`), filtrat `sters = 0` (= liniile active), înjumătățit cu `VANZARI` pe `id_vanzare`
pentru `tip`/`cod`/`data_act`. Nu trag concluzii de produs — doar cifre și exemple, cerute punct cu punct.
## 1. Câte linii, pe `VANZARI.TIP`
**`cantitate < 0`** (linii active, sparte pe tip):
| TIP | linii | documente | etichetă (dacă am confirmat-o în cod) |
|---|---|---|---|
| -13 | 1 | 1 | necunoscută — vezi caveat |
| -12 | 9 | 8 | necunoscută — vezi caveat |
| -8 | 1 | 1 | necunoscută — vezi caveat |
| -7 | 1 | 1 | necunoscută — vezi caveat |
| -6 | 25 | 24 | necunoscută — vezi caveat |
| -4 | 1 | 1 | necunoscută — vezi caveat |
| -2 | 1 | 1 | necunoscută — vezi caveat |
| -1 | 3 | 2 | necunoscută — vezi caveat |
| 1 | 3 | 2 | lista de preturi (confirmat de team-lead) |
| 3 | 10 | 4 | necunoscută — nu am verificat-o în cod |
| 8 | 2 | 1 | necunoscută — nu am verificat-o în cod |
| 24 | 1 | 1 | necunoscută — nu am verificat-o în cod |
| 41 | 4 | 4 | necunoscută — nu am verificat-o în cod |
| 51 | 3 | 1 | ROAACNPRO / import (menționat în `omodificari.vc2:13125-13127`, cercetarea `rec_s8_rulaje_tip4.md`) |
Total: **65 linii active cu `cantitate < 0`, pe 52 documente distincte.**
**`cantitate = 0`** (linii active, sparte pe tip):
| TIP | linii | documente | etichetă |
|---|---|---|---|
| -6 | 2 | 2 | necunoscută — vezi caveat |
| 2 | 1 | 1 | contract (confirmat de team-lead) |
| 22 | 5 | 5 | aviz (confirmat în cod, `oproceduri_facturare.prg` etc.) |
Total: **8 linii active cu `cantitate = 0`, pe 8 documente distincte.**
Tabelele de mai sus conțin `TIP = -6`, nu `TIP = 6` — sunt valori distincte, nu le-am confundat.
> **CORECTIE a sesiunii principale, 12.08.2026 — afirmatia „doar `TIP = 4` trece prin validarea de la
> `:14386`" era GRESITA si a fost stearsa de aici si din §6.** Contaminare din misiunea anterioara a
> aceluiasi agent, care era chiar despre tip 4.
>
> Pagina de articole **nu e conditionata de tip**. `omodificari.vc2:14842`:
> `IF "OFACTURARE_EDITARE" $ Upper(Set("Procedure")) AND Reccount('tact') > 0`, apoi
> `IF Reccount('tvanz') = 1` -> `lAreArticoleVanzari = .T.`. `nTipVanzare` e doar **retinut**
> (`:14841`), niciodata folosit ca poarta. Deci **orice document cu rand in `VANZARI`** primeste `tvd`
> si trece prin `:14386`.
>
> Dovedit si empiric de matricea S8 (`rec_s8_matrice.md`): asertia „tvd incarcat cu liniile active
> reale" a trecut pe `1048` (tip 1), `1055` (tip 2), `1052` (tip 22) **si** `1054` (tip 4).
>
> **Consecinta**: absenta lui `TIP = 4` din tabele **nu e o veste buna** si nu arata ca validarea n-a
> respins nimic real. Cele 52 de documente cu `cantitate < 0` sunt exact populatia pe care validarea o
> blocheaza. Tipurile `-6` si `41` sunt **retur-transfer** (`docs\progres.md`, sectiunea #8/S8), adica
> fix cazul semnalat de Marius.
## 2. Exemple concrete (3 + 3, cele mai recente după `data_act`)
**`cantitate < 0`:**
| id_vanzare | tip | cod | data_act | denumire | cantitate | pret | id_articol | pret_achizitie |
|---|---|---|---|---|---|---|---|---|
| 1035 | 3 | 1140727 | 26-MAR-26 | DISCOUNT | -3 | 4.8 | 4294507213 | 0 |
| 1035 | 3 | 1140727 | 26-MAR-26 | DISCOUNT | -1 | 3.15 | 4294507213 | 0 |
| 1035 | 3 | 1140727 | 26-MAR-26 | DISCOUNT | -3 | 3.3 | 4294507213 | 0 |
**`cantitate = 0`:**
| id_vanzare | tip | cod | data_act | denumire | cantitate | pret | id_articol | pret_achizitie |
|---|---|---|---|---|---|---|---|---|
| 1051 | 22 | 1140907 | 10-AUG-26 | SERVICII PER BUC | 0 | 0 | 4294507490 | 0 |
| 1057 | 2 | 1140913 | 10-AUG-26 | SERVICII PER BUC | 0 | 0 | 4294507490 | 0 |
| 1056 | 22 | 1140912 | 10-AUG-26 | SERVICII PER BUC | 0 | 0 | 4294507490 | 0 |
(Toate cele 3 exemple de `cantitate=0` provin sunt pe `id_articol` completat, `pret = 0`.)
## 3. Recență
| categorie | `MAX(data_act)` | linii în ultimele 12 luni |
|---|---|---|
| `cantitate < 0` | 26-MAR-26 | 10 din 65 |
| `cantitate = 0` | 10-AUG-26 | 6 din 8 |
## 4. `pret = 0` pe linii active
**19 linii, pe 17 documente distincte**, `MAX(data_act) = 10-AUG-26`. Toate cele 19 au `id_articol`
completat (`linii_cu_articol = 19` din `19` — deci nu sunt rânduri-rest fără articol).
## 5. `pret IS NULL` pe linii active
**0 linii.** Confirmă structural ce ați bănuit: `VANZARI_DETALII.PRET` e `NOT NULL` (verificat cu
`DESC vanzari_detalii` — coloana `PRET NUMBER(22,6) NOT NULL`), deci interogarea pe `pret IS NULL`
nu putea da altceva decât 0.
## 6. `cantitate < 0` pe documente cu pagină de articole (validarea `:14386`)
Agentul a raportat aici ca „doar `TIP = 4` trece prin validare" si ca `TIP = 4` lipsind din date ar
insemna ca validarea n-a respins nimic real. **Ambele sunt gresite** — vezi caseta de corectie din §1.
Raspunsul corect, stabilit de sesiunea principala din cod: pagina de articole **nu are filtru pe
tip** (`omodificari.vc2:14842`), deci **toate** cele 52 de documente cu `cantitate < 0` si cele 8 cu
`cantitate = 0` trec prin `:14386` la editare, si toate ar fi respinse.
## Caveat pe date
- Schema e cea de **dezvoltare** (`MARIUSM_AUTO`), folosită și pentru rulările automate de teste
(vezi `docs/cercetare/rec_s8_rulaje_tip4.md` — același `id_vanzare` a fost editat de un test).
Recența (`10-AUG-26` = practic azi) poate reflecta rulări de test, nu neapărat uz curent real.
- **Valorile negative de `TIP`** (-1, -2, -4, -6, -7, -8, -12, -13) nu corespund niciunui tip de
document cunoscut din codul citit până acum — `VANZARI.TIP` e `NUMBER(2) NOT NULL`, dar coloana nu
are o constrângere de semn în schema Oracle, deci acceptă tehnic valori negative. N-am cercetat
originea lor (ar necesita căutare suplimentară în cod, în afara scopului acestei misiuni) — le
raportez ca atare, fără să presupun că sunt date de test sau eroare.
## Fișiere atinse
Niciunul (misiune read-only). Interogări Oracle read-only pe `MARIUSM_AUTO`@`ROA_CENTRAL`, cu
`ROLLBACK` la final pe toate scripturile.

View File

@@ -0,0 +1,176 @@
# FACT-008 la vending: pretul de achizitie se trimite cu 3 zecimale, stocul il tine cu 4
Investigatie pe productia VENDING (tunel SSH, strict citiri). Articol reclamat:
`ZAHAR STICK BRUN 4G 200BUC/SET`, `id_articol = 5155`.
## Simptom
La salvarea facturii: `Articolul ZAHAR STICK BRUN 4G 200BUC/SET nu mai e in stoc! (FACT-008)`,
desi articolul are 500 buc pe stoc si e in lista de preturi.
## Cauza
`pack_facturare.descarca_gestiune` cauta randul din `STOC` prin egalitate exacta pe pretul de
achizitie (productie, `PACK_FACTURARE` body linia **7186**, ramura `ELSE`):
```sql
AND A.PRET = V_PRET_ACHIZITIE
```
Daca `BULK COLLECT` nu aduce niciun rand, se ridica FACT-008 (linia 7247).
Datele din productie:
| sursa | valoare |
|---|---|
| `STOC.PRET` (id_articol 5155, gest. 1001, cont 371, luna 8 / 2026) | **8.5586** |
| `RUL` receptia din 06.08.2026 (id_rul 568464) | cant 500, valoare 4279.28 -> 4279.28/500 = 8.55856 -> **8.5586** |
| `OPTIUNI.PPRET` (precizia pretului de achizitie) | **3** |
Clientul VFP tine pretul corect (cursorul e definit `pret_achizitie N(20, max(gnPPret,4))`, deci 4
zecimale), dar **il serializeaza in SQL cu `gnPPret` zecimale**:
- `COMUN\clase\ofacturare.vc2:14073` — `frm_facturare_articole.do_scrie_articole`
- `COMUN\clase\ofacturare.vc2:18108` — `frm_facturare_articole2.do_scrie_articole`
- `COMUN\programe\ofacturare_stoc.prg:360` — `adauga_articol_factura_stoc`
```foxpro
Alltrim(Str(poArt.pret_achizitie,18,gnPPret))
```
`Str(8.5586, 18, 3)` = `8.559`. In `VANZARI_DETALII_TEMP.PRET_ACHIZITIE` (NUMBER(22,6)) ajunge
8.559, iar predicatul `A.PRET = V_PRET_ACHIZITIE` compara 8.5586 cu 8.559 -> 0 randuri -> FACT-008.
Verificat pe productie:
```
pret = 8.559 -> 0 randuri
pret = 8.5586 -> 1 rand
```
Ramura `tip = 3` (articole fara stoc) nu salveaza situatia: `OPTIUNI.RF_FACTURARE_FARA_STOC = 0`.
## Intinderea problemei
Nu e un caz izolat. In `STOC` exista **15 randuri** cu pret pe mai mult de 3 zecimale, toate in
luna 8 / 2026, si **toate vor da FACT-008** la facturare:
```
1070 CAFEA COVIM OROCREMA 1KG 42.3203 360 buc
1367 PULBERE ALBA -ADAOS BAUTURI CALDE 25.3857 120 buc
1433 ZAHAR STICK 5G 200BUC/SET 7.2072 1000 buc
1537 CONTOR VOLUMETRIC NECTA 19.8347 30 buc
1677 GARNITURA BUCSA MIXER NECTA NEW .4958 60 buc
1965 MOTOREDUCTOR GRUP CAFEA NECTA 152.0667 3 buc
2020 LAVAZZA BBE EXPERT GUSTO FORTE 55.8815 1188 buc (cants)
2057 LAVAZZA BBE EXPERT GUSTO PIENO 61.5397 396 buc
2261 FURTUN SILICONIC 6X9 6.6116 50 buc
2934 BUCSA TEFLON MIXER NECTA 4.7107 60 buc
3680 ZAHAR MARGARITAR 1 KG 3.3243 100 buc
4282 GARNITURA NECTA OR2037 WITT 9100 1.6528 60 buc
5133 ZAHAR STICK MXA 5G 100BUC/SET 3.6036 1000 buc
5155 ZAHAR STICK BRUN 4G 200BUC/SET 8.5586 500 buc
```
In `RUL`, preturile cu peste 3 zecimale apar **doar din 09.07.2026** incoace (20 randuri, ultimul
14.08.2026). Inainte de aceasta data nu exista niciunul — deci comportamentul a aparut recent, pe
calea de receptie (pret unitar = valoare / cantitate, rotunjit la scale-ul coloanei `STOC.PRET`,
care e 4).
## Solutii
**Rapid, fara cod (productie):** `OPTIUNI.PPRET` de la 3 la 4. `Str()` incepe sa emita 4 zecimale
si predicatul se potriveste. Aliniaza precizia de transport cu scale-ul real al `STOC.PRET`.
**Durabil, in cod:** in cele 3 puncte de mai sus, serializarea sa foloseasca aceeasi precizie ca
definitia cursorului:
```foxpro
Alltrim(Str(poArt.pret_achizitie,18,Max(gnPPret,4)))
```
Nu se recomanda relaxarea predicatului in `descarca_gestiune` (`round(A.PRET, ...)`): pe gestiuni
cu mai multe loturi ale aceluiasi articol ar putea potrivi randul gresit.
## Risc similar, neatins inca
`pretv_orig` se trimite cu `gnPPretV` (= 2 la vending), iar `STOC.PRETV` are scale 4, cu acelasi
predicat de egalitate exacta (`A.PRETV = V_PRETV_ALES`). La vending toate randurile din stoc au
`pretv = 0`, deci nu se manifesta; pe o gestiune tinuta la pret de vanzare cu pret pe 3-4 zecimale
ar da acelasi FACT-008.
## De unde vin zecimalele: importul eFactura din ROACONT (confirmat)
Ipoteza s-a confirmat pe date. Receptia care a creat stocul articolului 5155 vine din eFactura:
```
ANAF_EFACTURA_DETALII / ANAF_EFACTURA
furnizor NAVIS PROD CONCEPT SRL, NVS 1427, 06.08.2026
id_articol 5155, cantitate 500, PRET = 8.5586, VALOAREFARATVA = 4279.28
```
Sunt **doua** surse de zecimale, ambele ocolind `gnPPret`:
**1. Pretul din XML-ul furnizorului are el insusi 4 zecimale.** Cazul 5155 (8.5586) si 1433
(7.2072, NVS 1406). Se preia ca atare in `ANAF_EFACTURA_DETALII.PRET` si de acolo in NIR.
**2. Importul recalculeaza pretul unitar dupa distribuirea discounturilor, cu numarul de zecimale
scris in cod (4, respectiv 6), nu cu `gnPPret`** — `frm_import_efactura.importmodifica`,
`D:\ROA\ROACONT\COMUN\clase\anaf_efactura.vc2`:
| linie | cod | efect |
|---|---|---|
| 12727 | `Set pret = ROUND(ROUND(pret / lnTotalFaraTVAx, 6) * lnTotalCuTVAx, 6)` | 6 zecimale |
| 12744 | `lnPret = pret - ROUND(lnDiscount / cantitate, 4)` | 4 zecimale — discount pe linie |
| 12786 | `Set Pret = Round(Pret * lnProcent, 4)` | 4 zecimale — discount/transport global |
Verificare pe date, cazuri unde XML-ul avea 2 zecimale si stocul a ajuns cu 4 (deci recalculul de
la 12744 e cel care a produs zecimalele):
| articol | XML: cant / pret / valoare | valoare/cant | `STOC.PRET` |
|---|---|---|---|
| 1677 GARNITURA BUCSA MIXER | 60 / 0.50 / 29.75 | 0.4958333 | **0.4958** |
| 1965 MOTOREDUCTOR GRUP CAFEA | 3 / 152.07 / 456.20 | 152.066666 | **152.0667** |
| 1537 CONTOR VOLUMETRIC | 30 / 19.83 / 595.04 | 19.834666 | **19.8347** |
| 2934 BUCSA TEFLON MIXER | 30 / 4.71 / 141.32 | 4.7106666 | **4.7107** |
Mai departe, `frm_import_efactura.importmodifica` (`anaf_efactura.vc2:12810`) duce `d.Pret` in
cursorul NIR-ului **fara nicio rotunjire**, desi toate valorile vecine din acelasi `SELECT` trec
prin `Round(..., gnPC)`.
### De ce rotunjirea la import nu e solutia buna
Daca la import s-ar rotunji pretul la `gnPPret = 3`, pentru 5155 ar rezulta 8.559 x 500 = 4279.50,
fata de valoarea reala a facturii, 4279.28 — o diferenta de 0.22 lei intre valoarea receptionata si
cea care se descarca ulterior din stoc. Exact de aceea `STOC.PRET` are scale 4 si cursoarele din
facturare sunt declarate `N(20, max(gnPPret,4))`.
Concluzia ramane: reparatia corecta e la **transportul din facturare** (`Str(..., 18, gnPPret)` ->
`Max(gnPPret,4)`), sau, ca masura imediata, `OPTIUNI.PPRET = 4`. Importul eFactura e sursa
zecimalelor, dar zecimalele in sine sunt legitime.
## Al patrulea punct de emisie, in afara COMUN (ROAGEST)
`D:\ROA\ROAGEST\Programe\ofactureaza.prg`, liniile **267, 268, 280**, construieste acelasi apel
`pack_facturare.adauga_articol_factura(...)` cu aceeasi trunchiere (`gnPPret`, `gnPPretVal`,
`gnPPretV`). E cod propriu ROAGEST, nu in `COMUN`, deci reparatia din `COMUN` nu-l acopera. Acelasi
tipar, aceeasi solutie.
In plus, copiile `COMUN` din ROAGEST / ROACONT / ROAACNPRO sunt in urma celei din ROAFACTURARE
(`ofacturare.vc2` are acolo liniile la 13857 / 17889) — primesc reparatia la urmatorul `svn update`
plus recompilare.
## Ce mai atinge PPRET, in afara transportului
Nu e doar precizie de transport. Aceeasi optiune conduce:
- **rotunjirea pretului unitar la NIR-ul introdus manual**: `COMUN\clase\ointroduceri.vc2`, liniile
1459, 1495, 15533, 19159 — `Round(valoare / cantitate, gnPPret)`. Deci calea manuala de receptie
respecta deja `gnPPret`; doar importul eFactura o ocoleste. Cu `PPRET = 4`, si NIR-urile
introduse manual vor stoca preturi cu 4 zecimale, nu 3.
- **mastile de editare/afisare** ale pretului de achizitie: `ofacturare.vc2:3490`,
`ofacturare_comun.vc2:1643`, `ointroduceri.vc2:10071, 16390, 19638` (`get_mask(..., gnPPret)`).
De aici si concluzia despre scriptul global: dupa ce intra versiunile cu `Max(gnPPret,4)`, ridicarea
lui PPRET la 4 nu mai repara nimic, dar schimba rotunjirea receptiilor manuale si afisarea la toti
clientii.

View File

@@ -0,0 +1,451 @@
# Runda 5 - editare inline pe pagina Articole (proiectare, fara aplicare)
Document de proiectare pentru cerinta: pe formularul unificat `frm_modific2024`, pagina 3
"Articole factura" (`pgfArticole.PAGE3.grdArticoleFactura`, cursor `tvd`), sa devina editabile
inline `serie`, `lot`, `explicatie` si `proc_tvav`, iar coloanele cu nomenclator (`denumire`,
`nume_gestiune`, `nume_val`, `taxcode`, `id_jtva_coloana`) sa deschida dialogul de cautare si pe
`InteractiveChange`, nu doar din `But_modificaR.Click`.
Linii citate din `COMUN\clase\omodificari.vc2` (17059 linii) si
`COMUN\programe\ofacturare_editare.prg` (1140 linii), stare de pe disc la data cercetarii -
fisierul e in lucru, **reconfirma numerele de linie cu `vfp_symbols.ps1` inainte de orice
`txt2vcx.ps1`**.
Fiecare bloc de cod de mai jos e marcat **VERIFICAT** (citit direct din sursa) sau **PRESUPUS**
(judecata de proiectare, nu extrasa din cod existent).
---
## 0. Constatare care schimba premisa punctului A din brief
**VERIFICAT.** Intrebarea "`cCantitateArt`/`cPretArt` marcheaza `lmodificat` si conteaza asta la
salvare" are raspuns clar: **`lmodificat` nu e citit nicaieri ca filtru de salvare.**
`ScrieArticoleFacturaEditate` (`ofacturare_editare.prg:473-571`) are doar doua `SCAN`-uri pe
`tvd`:
- `ofacturare_editare.prg:502` - `SCAN FOR id_vanzare_det > 0 AND Nvl(sters,0) <> 1` (UPDATE
liniilor existente)
- `ofacturare_editare.prg:534` - `SCAN FOR id_vanzare_det = 0 AND Nvl(sters,0) <> 1` (INSERT
liniilor noi)
Niciunul nu conditioneaza pe `lmodificat`. Comentariul de la `ofacturare_editare.prg:498` o spune
explicit: *"invie si actualizeaza liniile pastrate - toate cele active din alias, nu doar
lmodificat, pasul de mai sus le-a marcat pe toate"*. Singurele scrieri ale campului
(`AplicaAdaugareTvd:857`, `ComutaSters:1034`, `DuplicaLinie:1053`,
`ModificaNomenclator:1108`, `calculeaza_valori_articol` in `omodificari.vc2:13521`) nu au nicio
citire-pereche - `lmodificat` e scris dar niciodata citit. E camp vestigial (poate util pentru o
extensie viitoare, gen indicator vizual "linie modificata"), **nu un defect de blocat livrarea**.
Consecinta pentru A: coloanele noi (`serie`, `lot`, `explicatie`, `proc_tvav`) **nu au nevoie sa
seteze `lmodificat` ca sa se salveze** - se salveaza oricum, ca toate liniile active. Recomand
totusi sa-l seteze, din consecventa cu restul coloanelor editabile si ca sa nu ramana singurele
exceptii daca cineva incepe sa-l citeasca in viitor - cost zero, un `REPLACE` in plus.
---
## 1. Defect real gasit pe drum: UPDATE-ul nu scrie `serie`/`lot`/`explicatie`
**VERIFICAT - trebuie reparat in aceeasi livrare**, exact cum a semnalat brief-ul.
UPDATE-ul liniilor existente (`ofacturare_editare.prg:505-517`):
```foxpro
lcSql = [update vanzari_detalii set sters = 0, cantitate = ] + Alltrim(Str(Nvl(cantitate,0),18,3)) + ;
[, pret = ] + Alltrim(Str(Nvl(pret,0),18,4)) + [, pret_cu_tva = ] + Alltrim(Str(Nvl(pret_cu_tva,0))) + ;
[, pret_achizitie = ] + Alltrim(Str(Nvl(pret_achizitie,0),18,4)) + ;
[, proc_tvav = ] + Alltrim(Str(Nvl(proc_tvav,0),18,4)) + ;
[, discount_unitar = ] + Alltrim(Str(Nvl(discount_unitar,0),18,4)) + ;
[, id_articol = ] + Alltrim(Str(Nvl(id_articol,0))) + ;
[, id_gestiune = ] + Iif(Isnull(id_gestiune),[NULL],Alltrim(Str(id_gestiune))) + ;
[, id_valuta = ] + Iif(Isnull(id_valuta),[NULL],Alltrim(Str(id_valuta))) + ;
[, id_jtva_coloana = ] + Iif(Isnull(id_jtva_coloana),[NULL],Alltrim(Str(id_jtva_coloana))) + ;
[, taxcode = ] + Iif(Isnull(taxcode),[NULL],Alltrim(Str(taxcode))) + ;
[, cont = ] + Iif(Empty(Nvl(cont,'')),[NULL],['] + OracleSpecialCharacters(Alltrim(Nvl(cont,''))) + [']) + ;
[, id_utils = ] + Alltrim(Str(gnIdUtil)) + [, dataoras = sysdate ] + ;
[where id_vanzare_det = ] + Alltrim(Str(id_vanzare_det)) + [ and id_vanzare = ] + Alltrim(Str(m.tnIdVanzare))
```
`proc_tvav` **e deja in lista** (linia 508) - editarea cotei TVA persista deja corect pe linii
existente, nimic de reparat acolo. Lipsesc doar `serie`, `lot`, `explicatie` - prezente in
INSERT-ul liniilor noi (`ofacturare_editare.prg:535-544`, aceeasi forma
`Iif(Empty(Nvl(...,'')),[NULL],['] + OracleSpecialCharacters(...) + [']`) dar absente din UPDATE.
Fara reparatie, editarea inline propusa la punctul A de mai jos s-ar pierde tacut la salvare pe
orice linie deja salvata (`id_vanzare_det > 0`) - exact scenariul cel mai comun (factura cu
articole sincronizate din rulaj).
### Propunere - `ofacturare_editare.prg`, dupa linia 515 (`, cont = ...`), inainte de linia 516
(`, id_utils = ...`)
```foxpro
[, cont = ] + Iif(Empty(Nvl(cont,'')),[NULL],['] + OracleSpecialCharacters(Alltrim(Nvl(cont,''))) + [']) + ;
[, serie = ] + Iif(Empty(Nvl(serie,'')),[NULL],['] + OracleSpecialCharacters(Alltrim(Nvl(serie,''))) + [']) + ;
[, lot = ] + Iif(Empty(Nvl(lot,'')),[NULL],['] + OracleSpecialCharacters(Alltrim(Nvl(lot,''))) + [']) + ;
[, explicatie = ] + Iif(Empty(Nvl(explicatie,'')),[NULL],['] + OracleSpecialCharacters(Alltrim(Nvl(explicatie,''))) + [']) + ;
[, id_utils = ] + Alltrim(Str(gnIdUtil)) + [, dataoras = sysdate ] + ;
```
Trei linii noi, tipar identic cu cel deja folosit pentru `cont` pe acelasi UPDATE si pentru
`serie`/`explicatie`/`lot` pe INSERT (`:538-540`). Comentariul de la `:503-504` ("toate campurile
pe care grila si nomenclatoarele le pot schimba") ramane valabil fara modificare.
---
## 2. Coloane care devin editabile inline (`serie`, `lot`, `explicatie`)
**VERIFICAT** - definitiile actuale, `omodificari.vc2`:
| coloana | linie definitie | `ControlSource` | `ReadOnly` azi |
|---|---|---|---|
| `cSerieArt` (Column3) | `:12359-12365` | `tvd.serie` | `.T.` |
| `cLotArt` (Column4) | `:12366-12372` | `tvd.lot` | `.T.` |
| `cExplicatieArt` (Column13) | `:12441-12447` | `tvd.explicatie` | `.T.` |
Niciuna nu are azi `Text1.When`/`Text1.Valid` propriu (singurele evenimente pe pagina 3 sunt cele
listate la `omodificari.vc2:16678-16770`, verificat prin citire directa a intervalului).
### Garda de editare - condifia de baza vs. cea extinsa a lui `cPretAchizitieArt`
**VERIFICAT.** Doua garde diferite exista azi pe pagina 3:
```foxpro
* cCantitateArt.Text1.When si cPretArt.Text1.When - garda de baza
IF Thisform.lArticoleReadOnly OR Nvl(tvd.id_vanzare_set,0) <> 0
RETURN .F.
ENDIF
* cPretAchizitieArt.Text1.When - garda extinsa, cu o conditie in plus
IF Thisform.lArticoleReadOnly OR Nvl(tvd.id_vanzare_set,0) <> 0 OR Nvl(tvd.id_vanzare_det,0) <> 0
RETURN .F.
ENDIF
```
**PRESUPUS** (nu exista comentariu care sa explice; judecata de proiectare pe baza codului
inconjurator): conditia suplimentara `Nvl(tvd.id_vanzare_det,0) <> 0` de pe pret de achizitie
blocheaza editarea manuala pe liniile **deja legate de o vanzare persistata** (`id_vanzare_det`
nenul inseamna linie care exista deja in `VANZARI_DETALII`, deci a venit dintr-o miscare de stoc
reala). Sustinut de garda de la salvare (`omodificari.vc2:14457-14460`): un avertisment de "pret
de achizitie 0" apare doar pentru linii **noi** (`id_vanzare_det = 0`) - semn ca liniile vechi au
deja o valoare de incredere, mostenita din stoc, si nu trebuie rescrisa manual dupa fapt (ar
strica marja/COGS calculat la vremea vanzarii). E o garda de **integritate financiara pe o
valoare derivata**, nu una generica de editare.
`serie`/`lot`/`explicatie` sunt campuri **descriptive/text**, nu valori derivate din stoc -
corectarea unei greseli de tastare pe o linie deja salvata (serie gresita, lot gresit, o
explicatie de corectat) e exact scenariul uzual de folosit al acestei livrari, inclusiv pe linii
vechi. Recomand garda de baza (fara conditia `id_vanzare_det`) pentru toate trei. Daca exista un
motiv de trasabilitate (serie/lot leaga factura de un lot fizic expediat, iar schimbarea lui dupa
livrare ar fi o problema de conformitate) - e o decizie de business, nu una pe care o pot confirma
din cod; semnalez-o explicit ca punct de validat cu tine inainte de aplicare.
### Propunere - `omodificari.vc2`, dupa `cPretArt.Text1.Valid` (`:16759`, inainte de
`cPretArt.Text1.When`)
```foxpro
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cSerieArt.Text1.When
IF Thisform.lArticoleReadOnly OR Nvl(tvd.id_vanzare_set,0) <> 0
RETURN .F.
ENDIF
Thisform.oldvalue = This.Value
ENDPROC
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cSerieArt.Text1.Valid
IF NVL(Thisform.oldvalue,'') <> NVL(This.Value,'')
REPLACE lmodificat WITH .T. IN tvd
Thisform.oldvalue = This.Value
ENDIF
ENDPROC
```
Identic (nume schimbat) pentru `cLotArt` (`tvd.lot`) si `cExplicatieArt` (`tvd.explicatie`).
Coloana 3 din `Column3.ReadOnly = .T.`, 4 din `Column4.ReadOnly = .T.`, 13 din
`Column13.ReadOnly = .T.` trec pe `.F.` (`omodificari.vc2:12365`, `:12372`, `:12447`).
---
## 3. Cazul delicat: `proc_tvav` (Column9, `cProcTvavArt`)
### 3.1 Forma stocata - VERIFICAT, nu presupus
`tvd.proc_tvav` e in forma multiplicativa (1.21, nu 21), confirmat in trei locuri independente:
- `CreeazaPoArticolNouTvd`... `calculeaza_valori_articol` (`omodificari.vc2:13511-13521`):
`lnProcTvav = NVL(proc_tvav, 1)` si `lnValoare = ... * lnPretNet * lnProcTvav` - daca ar fi
forma "21", valoarea liniei ar fi de 21x mai mare, evident gresit.
ei
- `ArticoleNotaEditor.ModificaNomenclator`, ramura `id_jtva_coloana`
(`ofacturare_editare.prg:1099-1101`): `proc_tvav WITH (Nvl(loCauta.cota_tva, 0) + 100) / 100` -
`cota_tva` vine din nomenclator ca "21", transformarea explicita in "1.21" e in cod.
- `CautaExplicatieTva` (`ofacturare_editare.prg:1117-1120`):
`lnCotaFiltru = Round((Nvl(tvd.proc_tvav, 0) - 1) * 100, 2)` - transformarea inversa, "1.21" ->
"21".
### 3.2 Precedent direct in acelasi grid family: editare libera in forma "1.xx"
**VERIFICAT.** Coloana `cProc_tva` de pe grila de rulaje (`pgfArticole.PAGE1.grdRulaje`,
`omodificari.vc2:8956-8963`, si dubletul ei pe `PAGE2.grdRulajeObinv`) e **deja editabila
direct**, in forma bruta:
```foxpro
Column24.ControlSource = "proc_tva", ;
Column24.Format = "R", ;
Column24.InputMask = "9.99", ;
Column24.Name = "cProc_tva", ;
Column24.ReadOnly = .F., ;
```
cu evenimentul (`omodificari.vc2:16337-16343`):
```foxpro
PROCEDURE pgfArticole.PAGE1.grdRulaje.cProc_tva.Text1.Valid
IF NVL(thisform.oldvalue, 0) <> NVL(this.Value, 0)
Thisform.calculeaza_valori_rul(this.ControlSource)
thisform.oldvalue = this.Value
ENDIF
ENDPROC
```
Utilizatorul tasteaza deja "1.21", nu "21", pe grila de rulaje din **acelasi formular**.
`cProcTvavArt` de pe grila de articole are deja azi `Column9.InputMask = "9.99"`
(`omodificari.vc2:12417`, mostenit inca de cand coloana era doar de afisare) - acelasi format,
gata pregatit.
### 3.3 Recomandare: pastreaza forma "1.21", nu converti la "21"
**Argument**: forma "1.21" e cea stocata in `proc_tvav`, cea folosita direct de
`calculeaza_valori_articol`, si cea deja tastata de utilizatori pe grila de rulaje din acelasi
formular - e conventia existenta, nu una noua. Varianta "utilizatorul tasteaza 21" ar cere:
- o proprietate ajutatoare separata pe care sa se afiseze/editeze (`Text1` legat printr-o
expresie de transformare, `ControlSource` nu mai poate fi direct `tvd.proc_tvav`), pentru ca
grid column text boxes nu pot face transformarea la afisare fara sa piarda editarea directa pe
`ControlSource`;
- conversia dus-intors (`/100+1` la citire, `(x-1)*100` la scriere) e exact riscul semnalat -
o greseala de semn sau de ordine ar strica toate liniile la urmatoarea recalculare, silentios
(nu exista validare care sa prinda "1900" in loc de "19" introdus gresit);
- ar fi **singura** coloana din tot formularul cu conventia asta, cand exact aceeasi valoare, pe
aceeasi pagina, la un tab distanta (rulaje), se editeaza deja in forma "1.21".
Nu recomand conversia. Ramane `InputMask = "9.99"`, neschimbat.
### 3.4 Riscul real: corelarea cu `id_jtva_coloana`/`taxcode` se poate dezincroniza
**VERIFICAT ca risc, nu ca deja tratat.** Spre deosebire de `cProc_tva` de pe rulaje (unde
`trul` nu are corelare SAF-T de taxcode legata de cota), pe `tvd` cota TVA e legata explicit de
`id_jtva_coloana` si de `taxcode`:
- `ModificaNomenclator`, ramura `id_jtva_coloana` (`ofacturare_editare.prg:1099-1104`): alegerea
unei explicatii TVA scrie **impreuna** `id_jtva_coloana` si `proc_tvav`, apoi cheama
`UpdateExplicatieSAFTArt()` care recoreleaza `taxcode` din `id_jtva_coloana`
(`omodificari.vc2:15049-15073`, verificat: `lnIdJtva = tvd.id_jtva_coloana`,
`GetTaxCodeIdPart(...)`, `Replace taxcode With m.lnTaxCode In tvd`).
- Daca utilizatorul editeaza `proc_tvav` direct (tasteaza "1.19" peste "1.21"), fara sa treaca si
prin nomenclatorul de explicatie TVA, **`id_jtva_coloana` si `taxcode` raman la cota veche** -
`cExplicatieTvaArt` (coloana 16) ar continua sa arate explicatia de 21% langa o cota de 19%, iar
`taxcode`-ul folosit la raportarea SAF-T ar fi cel corelat cu explicatia gresita. E o
inconsistenta silentioasa, nu doar cosmetica - `taxcode` alimenteaza raportarea SAF-T 406.
Pe grila de rulaje, editarea directa a lui `proc_tva`/`proc_tvav` **nu recoreleaza nimic** legat
de TVA (`trul` nu poarta `taxcode`/`id_jtva_coloana` in acelasi fel) - acolo riscul asta nu
exista, deci precedentul de la 3.2 nu acopera si problema asta.
### 3.5 Doua variante, cu recomandare
**Varianta A (recomandata) - blocheaza corelarea veche in loc s-o lase gresita**
```foxpro
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cProcTvavArt.Text1.When
IF Thisform.lArticoleReadOnly OR Nvl(tvd.id_vanzare_set,0) <> 0
RETURN .F.
ENDIF
Thisform.oldvalue = This.Value
ENDPROC
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cProcTvavArt.Text1.Valid
IF NVL(Thisform.oldvalue, 0) <> NVL(This.Value, 0)
SELECT tvd
REPLACE id_jtva_coloana WITH NULL, taxcode WITH NULL
Thisform.calculeaza_valori_articol()
Thisform.oldvalue = This.Value
This.Parent.Parent.Refresh()
ENDIF
ENDPROC
```
Dupa editare manuala a cotei, `cExplicatieTvaArt` si `cTaxcodeArt` raman **goale** pe randul
respectiv - semnal vizibil, imediat, ca utilizatorul trebuie sa aleaga din nou explicatia TVA
(punctul 4 de mai jos ii da exact calea: click sau tastare pe coloana Explicatie TVA deschide
dialogul, filtrat deja pe noua cota introdusa). `taxcode WITH NULL` inseamna insa ca linia nu mai
are cod SAF-T pana la re-alegere - de verificat cu tine daca exista vreo validare la salvare care
sa avertizeze pe `taxcode` nul (nu am gasit una explicita pentru `tvd`, doar cea de pret de
achizitie 0 de la `:14457`); daca nu exista, merita adaugata odata cu asta, ca sa nu scape o
factura fara cod SAF-T la salvare.
**Varianta B (respinsa) - lasa corelarea veche neatinsa, ca pe rulaje**
Doar `Thisform.calculeaza_valori_articol()` in `Valid`, fara sa atinga
`id_jtva_coloana`/`taxcode`. Simetrica cu precedentul de la 3.2, dar pe `tvd` inseamna taxcode
SAF-T incorect ramas silentios dupa o editare manuala de cota - risc pe raportare fiscala, nu doar
UX. Nu o recomand.
**Nu am incercat o a treia varianta ("re-coreleaza automat")** - ar insemna sa caut in
`crsJtvaTemp` o explicatie care sa aiba exact noua cota si sa o aplic fara dialog; las-o
deoparte pentru ca poate exista mai mult de o explicatie pe aceeasi cota (JC vs JV, exigibil vs
neexigibil - vezi parametrii `tlTipEx` din `caut_explicatie_tva`,
`ocautare.prg:3174-3181`), iar alegerea automata a uneia ar fi arbitrara exact acolo unde conteaza
sa fie corecta.
---
## 4. Nomenclatoarele pe `InteractiveChange`
**VERIFICAT** - sablonul de referinta indicat de tine (`Grid1.Column15/16/17` pe grila de note,
`omodificari.vc2:5890-5940`) e construit pe **`do_modifica`** generic (parametri `pcx_item`,
`pccontrol`, `pnid_item`, cautare in cursorul `xitems`) - un mecanism diferit fata de
`ArticoleNotaEditor.ModificaNomenclator`, care primeste **doar `pccontrol`** si decide singur ce
cautare deschide printr-un `DO CASE` pe numele campului
(`ofacturare_editare.prg:1073-1090`). **Sablonul se preia doar partial**: pattern-ul
`GotFocus` seteaza `pccontrol`+`pncolumnorder`, `InteractiveChange` cheama dialogul - dar apelul e
catre `ArticoleNotaEditor.ModificaNomenclator(pccontrol)`, fara `pcx_item`/`pnid_item` (nu exista
in semnatura ei).
Cele 5 coloane cu nomenclator (`AreNomenclator`, `ofacturare_editare.prg:1004-1007`:
`'denumire', 'nume_gestiune', 'nume_val', 'taxcode', 'id_jtva_coloana'`) au deja `Text1.GotFocus`
care seteaza `pccontrol`+`pncolumnorder` (`omodificari.vc2:16700-16704` `cDenumireArt`,
`:16723-16727` `cExplicatieTvaArt` cu `pccontrol` explicit `'tvd.id_jtva_coloana'` pentru ca
`ControlSource` e o expresie, `:16729-16733` `cGestiuneArt`, `:16750-16754` `cTaxcodeArt`,
`:16755-16759` `cValutaArt`) - functioneaza deja azi desi coloanele sunt `ReadOnly = .T.` (un
textbox readonly primeste `GotFocus` la navigare cu sagetile, doar nu accepta tastare). Asta e
mecanismul care tine `But_modificaR` sincronizat cu coloana curenta (punctul 5).
### Propunere - adauga `Text1.InteractiveChange` pe fiecare, `ReadOnly` -> `.F.`
```foxpro
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cDenumireArt.Text1.InteractiveChange
LOCAL loEditor
loEditor = Createobject('ArticoleNotaEditor', Thisform)
loEditor.ModificaNomenclator(Thisform.pccontrol)
ENDPROC
```
Identic (nume schimbat) pentru `cGestiuneArt`, `cValutaArt`, `cTaxcodeArt`, `cExplicatieTvaArt` -
`Thisform.pccontrol` e deja setat corect de `GotFocus`-ul existent al fiecareia. `Column1.ReadOnly`
(`:12354`), `Column11.ReadOnly` (`:12428`), `Column12.ReadOnly` (`:12434`), `Column14.ReadOnly`
(`:12453`), `Column16.ReadOnly` (`:12474`) trec pe `.F.`.
### Risc: caracterul tastat inainte de deschiderea dialogului
**VERIFICAT ca exista deja acelasi risc, neremediat, in sablonul citat.** In `do_modifica`
(`omodificari.vc2:13960` si dublurile ei), `REPLACE`-ul cu valoarea aleasa se face **doar** in
ramura `IF gnButon = 1` (`omodificari.vc2:14019-14025` pentru `tact`) - la anulare (`gnButon <> 1`
sau formular inchis fara alegere), campul **nu e restaurat**. Cum coloana e `ReadOnly = .F.`,
`InteractiveChange` se declanseaza pe **prima tasta** apasata (Value s-a schimbat deja cu acel
caracter inainte ca evenimentul sa ruleze) - daca utilizatorul apasa o litera si apoi renunta la
dialog, acel caracter ramane in camp.
Pe `ArticoleNotaEditor.ModificaNomenclator` e identic: `REPLACE`-ul final
(`ofacturare_editare.prg:1093-1107`) e dupa `IF gnButon <> 1: RETURN .F. ENDIF`
(`:1082-1084`) - la anulare, campul nu se atinge, deci caracterul parazit ramane in `tvd.denumire`
(sau alt camp editat). Fara refresh explicit pe ramura de anulare (`RefreshGrid()` e apelat doar
dupa `REPLACE`, in afara ramurii de `RETURN` timpuriu), gridul ramane cu textul modificat vizual
pana la urmatoarea reimprospatare.
**Nota:** `lmodificat` nu se atinge pe ramura de anulare, dar cum am aratat la punctul 0, asta nu
opreste totusi salvarea - daca linia era oricum activa (`sters<>1`), caracterul parazit ar fi
scris in Oracle la urmatoarea salvare a documentului, indiferent de `lmodificat`.
**Propunere de atenuare (in plus fata de sablon, nu cerinta din brief):** in
`ArticoleNotaEditor.ModificaNomenclator`, muta `This.RefreshGrid()` inainte de
`RETURN .F.` din garda de anulare, ca sa readuca vizual valoarea corecta din cursor peste orice
caracter tastat:
```foxpro
IF gnButon <> 1
This.RefreshGrid()
RETURN .F.
ENDIF
```
`RefreshGrid()` doar re-deseneaza gridul din cursor (`This.oForm.pgfArticole.PAGE3.grdArticoleFactura.Refresh()`,
`ofacturare_editare.prg:1136-1138`) - nu scrie nimic, deci sigur de adaugat si pe ramura de
anulare. Nu rezolva 100% (daca utilizatorul apasa doua taste rapid inainte ca dialogul sa apuce sa
se deschida, a doua tasta tot ar intra), dar acopera cazul uzual (o tasta, apoi Esc pe dialog).
`But_modificaR` ramane functional neschimbat: click-ul lui cheama tot
`ArticoleNotaEditor.ModificaNomenclator(thisform.pccontrol)` (`omodificari.vc2:15319-15322`,
verificat), acelasi `pccontrol` pe care acum si `InteractiveChange`-ul il foloseste - **nu se
dubleaza deschiderea dialogului**, sunt doua declansatoare catre aceeasi metoda, niciodata
concurente (butonul se apasa explicit, `InteractiveChange` doar la tastare in celula).
---
## 5. `BeforeRowColChange`/`AfterRowColChange` - raman neschimbate
**VERIFICAT.**
```foxpro
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.AfterRowColChange
Lparameters nColIndex
DODEFAULT(m.nColIndex)
Thisform.but_modificaR.Enabled = (Thisform.pncolumnorder = m.nColIndex) AND !Thisform.lArticoleReadOnly
ENDPROC
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.BeforeRowColChange
LPARAMETERS nColIndex
IF Thisform.pncolumnorder # m.nColIndex
STORE [] TO Thisform.pccontrol
ENDIF
ENDPROC
```
Nu au nevoie de nicio schimbare:
- Coloanele cu nomenclator continua sa seteze `pncolumnorder` in `GotFocus` (neschimbat la
punctul 4) - `But_modificaR` ramane activat/dezactivat exact ca azi.
- Coloanele noi editabile fara nomenclator (`cSerieArt`, `cLotArt`, `cExplicatieArt`,
`cProcTvavArt`) **nu primesc `GotFocus` care sa seteze `pccontrol`/`pncolumnorder`** - la fel ca
`cCantitateArt`/`cPretArt`/`cPretAchizitieArt` azi (verificat: niciuna dintre astea trei nu are
`Text1.GotFocus` in intervalul `:16678-16770`). Cand utilizatorul navigheaza pe una din ele,
`pncolumnorder` ramane la valoarea din ultima coloana cu nomenclator vizitata, deci
`Thisform.pncolumnorder = m.nColIndex` e fals si `But_modificaR` ramane dezactivat - corect,
pentru ca aceste patru coloane n-au nomenclator si nu trebuie sa activeze butonul.
Nu propun sa le adaug `GotFocus` - ar activa gresit `But_modificaR` pe coloane fara cautare.
---
## 6. Ordinea coloanelor - doar de retinut, nu de aplicat acum
`cExplicatieTvaArt` (Column16) e ultima coloana din grid, dupa `cValoareArt` (Column15) -
`ColumnOrder`-ul azi urmeaza indicii (1-16). Mutarea ei langa `cTaxcodeArt` (Column14) ar cere
renumerotarea `ColumnOrder` pe toate cele 16 coloane (nu doar schimbarea pozitiei uneia), cost
deja notat in `docs\propunere_runda4_note_sincronizare_tva.md:242-244`. Ramane a doua iteratie,
separata de livrarea asta.
---
## Rezumat - ce e usor, ce e delicat
**Usor, cu incredere mare:**
- `serie`/`lot`/`explicatie` editabile inline (punctul 2) - tipar identic cu `cPretArt`, fara
recalcul necesar.
- UPDATE-ul din `ScrieArticoleFacturaEditate` (punctul 1) - trei linii, tipar deja folosit alaturi
(`cont` pe acelasi UPDATE, `serie`/`lot`/`explicatie` pe INSERT-ul de doua ori mai jos).
- Nomenclatoarele pe `InteractiveChange` (punctul 4) - `GotFocus` deja exista pe toate cele 5
coloane, doar `ReadOnly` si `InteractiveChange` lipsesc.
- `BeforeRowColChange`/`AfterRowColChange` (punctul 5) - zero schimbari, verificat ca raman
corecte.
**Delicat, cere o decizie a ta inainte de aplicare:**
- `proc_tvav` (punctul 3) - forma "1.21" e clar cea corecta de pastrat (precedent direct pe
aceeasi pagina, la rulaje), dar corelarea cu `id_jtva_coloana`/`taxcode` la editare manuala e un
risc real de raportare SAF-T netratat de niciun precedent existent. Recomand Varianta A
(goleste corelarea, forteaza re-alegere) - confirma daca vrei asta sau preferi sa nu atingi
`id_jtva_coloana`/`taxcode` deloc (Varianta B, mai simpla dar cu riscul asumat).
- Garda de editare pe `serie`/`lot`/`explicatie` - am recomandat garda de baza (fara restrictia
`id_vanzare_det<>0` de la pret de achizitie), dar e o decizie de business (trasabilitate lot pe
linii deja livrate), nu una pe care am putut-o confirma din cod.
- Riscul caracterului parazit la deschiderea nomenclatorului pe `InteractiveChange` (punctul 4) -
exista deja, neremediat, in sablonul `do_modifica` de pe grila de note; am propus o atenuare de
o linie (`RefreshGrid()` pe ramura de anulare) care nu era in cerinta ta, spune daca o vrei in
livrare sau ramane pentru alta runda.

View File

@@ -0,0 +1,427 @@
# Diagnostic — linii fara articol pe facturi emise din CONTRACT (rate)
**Scop.** Doua erori pe facturi emise din contract (explicatie "CONTRACT"), pe formularul unificat
`frm_modific2024` (`COMUN\clase\omodificari.vc2`) + `COMUN\programe\ofacturare_editare.prg`:
- **EROAREA 1** (la deschidere): `Field ID_ARTICOL does not accept null values`,
`CONSTRUIESTEPROPUNERESINCRONIZARE`, linia 652 (INSERT in `agg_tvd`).
- **EROAREA 2** (la salvare): `Linia '' nu are articol asociat.`
Cazul de test: factura 10/08/2026, SSS 549, ROMFAST S.R.L., explicatie "CONTRACT", contract
"1/21.07.2020" — grid cu linia "RATA 2" (fara codmat, fara pret de achizitie) si linia "A2" (cu
codmat 8003510000203). Doar diagnostic — nu s-a atins niciun fisier de cod.
## Concluzia scurta
**Nu e un defect de generare.** Liniile de tip "rata" dintr-un contract sunt, prin proiectare, linii
`VANZARI_DETALII` fara articol de nomenclator — `id_articol` chiar e `NULL` in Oracle pe randul
respectiv, iar denumirea vizibila ("RATA 2") vine din `explicatie`, nu din `nom_articole.denumire`.
Asta era deja documentat in `docs\plan_13_unificare_formular_facturare.md:2275` inainte sa apara
eroarea curenta — planul #13 stia despre ele, dar guarda de la salvare (`omodificari.vc2:14450`) si
cursoarele de agregare pentru sincronizare RUL<->TVD (`ofacturare_editare.prg:617,640`) au fost scrise
pornind de la premisa contrara, ca `id_articol` "vine mereu completat din sursa"
(`docs\progres.md:166`). Premisa aia era gresita exact pentru liniile de rata, si cele doua erori sunt
consecinta directa.
---
## 1. De unde vine linia fara articol — proiectare, nu defect
Generarea facturii din contract (butonul de facturare al `ROACONTRACTE`, `goContract`, tipurile de
document `2/6/52` — `ofacturare_comun.prg:261-297`) construieste liniile prin
`creeaza_facturacrs` (`ofacturare_comun.prg:1793`, cursorul `crsfacturacrs`, camp
`id_articol N(20) null` — **explicit nullable**) si le populeaza prin `prelucreaza_facturacrs`
(`ofacturare_comun.prg:1836-1840`):
```
Insert Into (tcCursorDestinatie) (id_articol, ...) ;
SELECT CAST(IIF(TYPE(tcCursorSursa+".id_articol")='U',0,id_articol) as N(20)) as id_articol, ...
```
`TYPE(...)='U'` verifica daca **exista coloana** `id_articol` in cursorul sursa (0 doar cand lipseste
complet), nu daca valoarea e NULL — deci daca sursa are coloana `id_articol` si pe randul de rata ea
e NULL, NULL trece mai departe neschimbat, nu devine 0.
Structura de rate a contractului insasi confirma asta — cursorul istoric pentru rate
(`ofacturare_comun.prg:1905-1908`, comentat, dar pastrat ca document al formei) nu are deloc coloana
`id_articol`:
```
*!* Create Cursor crsfactura(id_rata N(20), id_temp N(20), den_rata c(100), ..., denumire c(100), ...)
```
**Deja documentat inainte de eroarea curenta.** Cercetarea S4 din planul #13 a stabilit exact acelasi
lucru cand a analizat `cursor_contract`:
> `docs\plan_13_unificare_formular_facturare.md:2273-2275`:
> „`cursor_contract` produce deja doua cursoare — `V_CURSOR` (`crsarticole`, prin delegare la
> `cursor_preturi`) si `V_CURSOR2` (`crsarticole1`, articole **sau** rate). Filtrarea se aplica curat
> doar pe jumatatea `crsarticole`; **randurile de rata n-au `id_articol`** si raman needitate."
Concluzie punct 1: **legitim, prin proiectare**. Randul de rata reprezinta o transa de plata dintr-un
contract (esalonare), nu un articol din nomenclator — nu exista ce `id_articol` sa i se puna, si codul
de generare stie asta de multa vreme.
## 2. Ce accepta baza de date
**Confirmat pe Oracle** (`MARIUSM_AUTO@ROA_CENTRAL`, interogare proprie `ALL_TAB_COLUMNS`, SELECT
strict, plus datele masurate de team-lead pe `VANZARI`/`VANZARI_DETALII`):
| Coloana | Tip | Nullable | Observatie |
|---|---|---|---|
| `ID_ARTICOL` | NUMBER | **Y** | are FK `FK_VANZARE_DET002 -> NOM_ARTICOLE.PK_ARTICOL`; in `NOM_ARTICOLE` **nu exista niciun articol cu `id_articol = 0`** (count = 0) |
| `PRET` | NUMBER | **N** | singura coloana NOT NULL din setul verificat |
| `PRET_CU_TVA` | NUMBER | Y | flag 0/1, nu pret |
| `PRET_ACHIZITIE` | NUMBER | Y | |
| `PROC_TVAV` | NUMBER | Y | multiplicator TVA (ex. 1.21), nu procent |
| `DISCOUNT_UNITAR` | NUMBER | Y | |
| `CANTITATE` | NUMBER | Y | |
| `SERIE` / `LOT` / `EXPLICATIE` | VARCHAR2 | Y | |
| `ID_VANZARE_SET` / `ID_GESTIUNE` | NUMBER | Y | |
Confirmare directa pe date reale (interogare team-lead): factura `VANZARI.id_vanzare=1055` (SSS 549,
tip=2, `id_ctr=234`), linia `id_vanzare_det=1603` are efectiv `id_articol = NULL`,
`id_gestiune = NULL`, `pret_achizitie = NULL`, `explicatie = 'RATA 2'`, `cantitate=1`, `pret=100`,
`proc_tvav=1.21`, `id_valuta=3` — deja salvata asa, deci coloana accepta NULL in productie (nu doar
teoretic in DDL). In toata baza exista deja **20 de linii active** (`sters=0`) cu `id_articol IS
NULL`, din 1124 in total — starea nu e un caz izolat.
Concluzie punct 2: `ID_ARTICOL` e **nullable, cu FK** catre `NOM_ARTICOLE`. Asta e critic pentru
reparatie (punctul 6, mai jos): daca s-ar scrie `0` in loc de `NULL` pe o linie fara articol, FK-ul ar
respinge scrierea (`ORA-02291`), pentru ca `id_articol = 0` nu exista in `NOM_ARTICOLE`.
## 3. De unde se incarca `tvd` si de ce `denumire` iese goala
`tvd` se incarca prin `IncarcaArticoleFactura` (`ofacturare_editare.prg:305-`), din view-ul
`VVANZARI_ARTICOLE`, definit recent chiar pentru acest formular:
```sql
-- D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql:7-34
CREATE OR REPLACE VIEW VVANZARI_ARTICOLE AS
select vd.id_vanzare, ..., vd.id_articol, ..., vd.explicatie, ...,
na.denumire, na.codmat, ng.nume_gestiune, nv.nume_val
from vanzari_detalii vd
left join nom_articole na on na.id_articol = vd.id_articol
...
```
Spre deosebire de `fact_vfacturi_detalii` (punctul 2), **acest view nou nu are fallback** pe
`vd.explicatie` cand `vd.id_articol` e NULL — `na.denumire` vine direct dintr-un `LEFT JOIN` pe
`nom_articole`, iar cand `id_articol` e NULL, join-ul nu gaseste nimic si `denumire` iese **NULL**.
Cursorul `tvd` insusi e declarat cu `id_articol N(20) NULL` (`ofacturare_editare.prg:278`,
`omodificari.vc2:14769`), asa ca incarcarea nu crapa aici — doar `denumire` ramane goala pe randul de
rata.
Asta explica exact mesajul din EROAREA 2: `Alltrim(Nvl(denumire,''))` (`omodificari.vc2:14450`) da
sir gol pentru ca `tvd.denumire` e cu adevarat NULL pe acel rand, nu pentru ca s-a gasit randul gresit.
In grid, textul vizibil "RATA 2" **nu vine din coloana Denumire** (`Column1.ControlSource =
"tvd.denumire"`, `omodificari.vc2:12349`), ci din coloana **Explicatie** de mai la dreapta
(`Column13.ControlSource = "tvd.explicatie"`, `omodificari.vc2:12444`) — coloana Denumire e goala pe
acel rand, exact ca in mesajul de eroare.
Concluzie punct 3: `denumire` **poate** iesi NULL pe linia de rata, si chiar iese — cauza e absenta
fallback-ului pe `explicatie` in `VVANZARI_ARTICOLE`, spre deosebire de view-ul mai vechi
`fact_vfacturi_detalii` care il are.
## 4. Cine a pus garda `Nvl(id_articol,0) = 0` si de ce
Garda de la `omodificari.vc2:14450` (in `frm_modific2024.inainte_de_do_termin`) a intrat in
`COMUN` prin commit-ul `1c42ae0` — *"#6 editare factura emisa: S5 - scrierea sumelor editate in
Oracle"*, 10.08.2026 — inainte de S4 (cautarea articolelor pe server) si inainte de decizia 18/S4b
despre liniile sintetice. Nu exista niciun commit ulterior care sa modifice acel `IF`.
**Nu era gandita ca plasa pentru rate.** Documentatia proprie a proiectului o descrie explicit ca
ramura considerata **imposibil de declansat** pe fluxul normal:
> `docs\progres.md:157,166`:
> | `:14402` | `Nvl(id_articol,0) = 0` | blocheaza |
> ...
> „Din cele trei, `Isnull(pret)` e ramura moarta si `id_articol` vine **mereu completat din sursa**,
> deci singura care ar fi putut pica e chiar cea de cantitate."
Asta e in contradictie directa cu ce arata investigatia S4 din acelasi plan (`plan_13:2275`, punctul 1
de mai sus), scrisa in aceeasi fereastra de timp — **liniile de rata nu au `id_articol` de la sursa**.
Contradictia nu a fost observata pentru ca cele doua fire de lucru (S5/garda de salvare, si S4/cautarea
articolelor din `cursor_contract`) nu s-au intersectat pana acum, pe date reale de contract cu rate.
Concluzie punct 4: garda **nu** a fost pusa pentru ca cineva stia ca `VANZARI_DETALII.ID_ARTICOL` e
NOT NULL in Oracle (ar fi contrazis chiar codul de generare din `ofacturare_comun.prg`, punctul 1) —
a fost pusa ca validare generica de continut ("linia trebuie sa aiba un articol"), pe premisa (falsa
pentru rate) ca `id_articol` vine intotdeauna populat. E o garda prea stricta pentru liniile de rata,
nu o reflectare a unei constrangeri de baza de date.
## 5. Ce mai crapa in aval, cu `id_articol` NULL pe un rand `tvd`/`trul`
Lista completa, `fisier:linie`, a locurilor care folosesc `id_articol` fara `Nvl` sau il compara cu
`=` (deci sensibile la NULL):
| Loc | Cod | Efect cu `id_articol` NULL |
|---|---|---|
| `ofacturare_editare.prg:617` | `CREATE CURSOR agg_rul (id_articol N(20), ...)` | camp declarat **fara** `NULL` — pe hartie acelasi tipar ca la `agg_tvd`, dar **confirmat inaccesibil in practica** (vezi punctul 7: `RUL.ID_ARTICOL` e `NOT NULL` in Oracle, 0 randuri active cu NULL) |
| `ofacturare_editare.prg:626,630-631` | `LOCATE FOR id_articol = trul.id_articol` apoi `INSERT INTO agg_rul ... VALUES (trul.id_articol, ...)` | fara risc practic — `trul.id_articol` nu poate fi NULL (punctul 7) |
| `ofacturare_editare.prg:640,647,651-652` | `CREATE CURSOR agg_tvd (id_articol N(20), ...)` + `INSERT INTO agg_tvd ... VALUES (tvd.id_articol, ...)` | **cauza directa a EROAREA 1** — camp NOT NULL, valoare NULL |
| `ofacturare_editare.prg:660-663` | `SELECT id_articol FROM agg_rul UNION SELECT id_articol FROM agg_tvd INTO CURSOR crs_articole_unite` | **verificat empiric (punctul 7)**: `UNION` trateaza NULL=NULL ca echivalent la deduplicare — oricate randuri NULL ar aduce `agg_tvd`, `crs_articole_unite` primeste **un singur** rand NULL |
| `ofacturare_editare.prg:671,687` | `LOCATE FOR id_articol = crs_articole_unite.id_articol` (o data pe `agg_rul`, o data pe `agg_tvd`) | **verificat empiric (punctul 7), corecteaza ipoteza initiala**: `LOCATE FOR camp = NULL` **nu gaseste niciodata**, in VFP, nici cand campul chiar contine NULL pe randul cautat — nu doar cand valorile difera. Efectul nu e o pereche falsa „Adaugare”+„Semnalare” (ipoteza initiala, neconfirmata) — e mai simplu si mai tacut: randul NULL din `crs_articole_unite` iese cu `llGasitSursa=.F.` **si** `llGasitTinta=.F.` pe ambele ramuri, cade in `OTHERWISE` cu 0=0, si **nu genereaza nicio linie in `propunere_sincronizare`** — dispare complet din sincronizare, fara eroare, fara semnalare |
| `ofacturare_editare.prg:849,887,923` | `LOCATE FOR id_articol = m.tnIdArticol` in `AplicaModificareTvd`, `AplicaAdaugareTvd`, `AplicaModificareTrul` | irelevant in practica daca se aplica reparatia recomandata la punctul 7 (Varianta B) — `propunere_sincronizare` nu mai ajunge sa contina randuri cu `id_articol` NULL, deci aceste functii nu sunt niciodata chemate cu `tnIdArticol` NULL |
| `omodificari.vc2:14450` | `IF Nvl(id_articol,0) = 0` | **cauza directa a EROAREA 2** — garda generica, prea stricta pentru rate |
Concluzie punct 5: reparatia nu se opreste la `agg_tvd` (linia care a crapat primul) — `agg_rul` are
aceeasi lipsa de `NULL` pe declaratie, dar dovedit inaccesibila (punctul 7). Mecanismul de potrivire
pe `id_articol =` din sincronizarea RUL<->TVD (`:671`, `:687`) e **verificat empiric ca nu functioneaza
pentru NULL**, cu efect de disparitie tacuta din propunere, nu de eroare — vezi analiza completa si
reparatia recomandata la punctul 7.
## 6. `Nvl(...,0)` la scriere in `ScrieArticoleFacturaEditate` — campuri care distrug informatie
Chiar daca EROAREA 1 si EROAREA 2 s-ar repara (cursoare + garda), `ScrieArticoleFacturaEditate`
(`ofacturare_editare.prg`) tot ar strica un rand de rata la prima salvare reusita, pentru ca **atat
UPDATE-ul liniilor pastrate (`:505-517`), cat si INSERT-ul liniilor noi (`:535-547`)** trec mai multe
campuri prin `Nvl(camp,0)` inainte sa le scrie in Oracle — convertind orice NULL legitim in `0`
literal:
```
-- UPDATE (linii existente), ofacturare_editare.prg:505-510
lcSql = [update vanzari_detalii set sters = 0, cantitate = ] + Alltrim(Str(Nvl(cantitate,0),18,3)) + ;
[, pret = ] + Alltrim(Str(Nvl(pret,0),18,4)) + [, pret_cu_tva = ] + Alltrim(Str(Nvl(pret_cu_tva,0))) + ;
[, pret_achizitie = ] + Alltrim(Str(Nvl(pret_achizitie,0),18,4)) + ;
[, proc_tvav = ] + Alltrim(Str(Nvl(proc_tvav,0),18,4)) + ;
[, discount_unitar = ] + Alltrim(Str(Nvl(discount_unitar,0),18,4)) + ;
[, id_articol = ] + Alltrim(Str(Nvl(id_articol,0))) + ;
[, id_gestiune = ] + Iif(Isnull(id_gestiune),[NULL],Alltrim(Str(id_gestiune))) + ;
...
-- INSERT (linii noi), ofacturare_editare.prg:535-546
lcSql = [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 (] + ;
Alltrim(Str(m.tnIdVanzare)) + [,] + Alltrim(Str(Nvl(id_articol,0))) + [,] + Alltrim(Str(Nvl(cantitate,0),18,3)) + [,] + ;
Alltrim(Str(Nvl(pret,0),18,4)) + [,] + Alltrim(Str(Nvl(pret_cu_tva,0))) + [,] + Alltrim(Str(Nvl(proc_tvav,0),18,4)) + [,] + ;
Alltrim(Str(Nvl(discount_unitar,0),18,4)) + [,] + Iif(Isnull(id_gestiune),[NULL],Alltrim(Str(id_gestiune))) + [,] + ;
...
Alltrim(Str(Nvl(pret_achizitie,0),18,4)) + [,] + Alltrim(Str(gnIdUtil)) + [, sysdate)]
```
Observatie de proiectare: **`id_gestiune`, `id_valuta`, `id_jtva_coloana`, `taxcode`, `cont`, `serie`,
`explicatie`, `lot` sunt deja tratate corect**, cu `Iif(Isnull(...),[NULL],...)` — modelul corect
exista deja in acelasi bloc de cod, doar ca nu s-a aplicat si celor sase campuri de mai jos.
Evaluare camp cu camp, cu nullabilitatea confirmata la punctul 2:
| Camp | Nullable in Oracle | `Nvl(...,0)` e corect? | Motiv |
|---|---|---|---|
| **`id_articol`** | Y, **cu FK** spre `NOM_ARTICOLE` | **NU — critic** | `0` nu exista in `NOM_ARTICOLE` (count=0) — pe o linie de rata cu `id_articol` real NULL, scrierea ar da **`ORA-02291`** (violare FK), nu doar ar corupe tacut o valoare. Chiar daca EROAREA 2 s-ar repara la nivel de garda VFP, salvarea tot ar crapa aici, doar cu un mesaj Oracle mai putin clar decat "nu are articol asociat". **Singurul din lista care produce eroare dura, nu doar corupere tacuta.** |
| **`pret_achizitie`** | Y | **NU** | Pe randul real `id_vanzare_det=1603` e deja NULL in productie — "cost de achizitie necunoscut/nu se aplica" (rata n-are cost de achizitie, nu e marfa). `Nvl(...,0)` il transforma in "cost de achizitie efectiv zero", care e o valoare falsa pentru orice calcul de marja/profit facut ulterior direct din `VANZARI_DETALII` (in afara VFP) |
| **`proc_tvav`** | Y | **Probabil nu, cu rezerva** | E un multiplicator TVA (1.21, nu 21%) — `0` nu inseamna "fara TVA" in aceasta conventie, ci ar corupe orice calcul care-l inmulteste (baza * 0 = 0). Pe randul de test `proc_tvav` e deja completat (1.21), deci `Nvl` nu se declanseaza acolo — dar daca exista/ar exista vreun rand cu `proc_tvav` NULL, scrierea lui ca `0` e o valoare periculoasa, nu neutra. **NEVERIFICAT**: daca exista azi randuri reale cu `proc_tvav` NULL (n-am interogat) |
| **`discount_unitar`** | Y | **Risc scazut** | NULL si 0 sunt aproape echivalente semantic ("fara discount") — conversia schimba tipul valorii, nu sensul ei practic. Recomandat de aliniat la tiparul `Isnull` din acelasi bloc, mai mult pentru consistenta decat pentru un bug observat |
| **`cantitate`** | Y | **Risc scazut, deja filtrat in amonte** | Garda `omodificari.vc2:14386` (`Nvl(cantitate,0) <= 0` -> blocheaza cu "are cantitatea 0") ruleaza **inaintea** lui `ScrieArticoleFacturaEditate` si opreste salvarea pe orice rand cu cantitate NULL sau <= 0. Pana la aceasta functie, `cantitate` e deja garantat non-NULL si non-zero pe calea normala — `Nvl(...,0)` de aici e defensiv, nu activ distructiv |
| **`pret`** | **N (NOT NULL)** | **DA — corect** | Coloana Oracle nu accepta NULL; garda `omodificari.vc2:14394` (`Isnull(pret)` -> blocheaza) opreste deja NULL inainte de scriere. `Nvl(pret,0)` e aici plasa de siguranta potrivita pentru o coloana NOT NULL, nu o corupere |
| **`pret_cu_tva`** | Y | **Risc scazut** | E flag boolean 0/1 (comentariu `:501` in acelasi fisier: "pret_cu_tva e flag (0/1), nu pret"), nu o valoare cu semnificatie de "lipsa" — `Nvl(...,0)` echivaleaza NULL cu "fara TVA in pret", o valoare implicita rezonabila pentru un flag |
Concluzie punct 6: din cele sase campuri, **doar `id_articol` produce o eroare Oracle dura** (FK) daca
nu se repara — e blocajul real, dincolo de garda VFP. **`pret_achizitie` corupe tacut date reale deja
existente** (randul 1603 chiar are NULL azi) fara sa arunce nicio eroare. `proc_tvav` e risc teoretic,
neconfirmat pe date. Restul (`discount_unitar`, `cantitate`, `pret_cu_tva`) sunt scrieri defensive
fara efect practic distructiv, iar `pret` e deja corect (coloana NOT NULL + garda VFP dedicata).
## 7. Sarim liniile fara articol la sursa, sau reparam matching-ul cu NULL — analiza cu dovada VFP
**Date suplimentare masurate de team-lead pe Oracle**: `RUL.ID_ARTICOL` e **NOT NULL** in Oracle
(`all_tab_columns.nullable = 'N'`), si exista **0** randuri `RUL` active cu `id_articol IS NULL`.
Documentul de test (SSS 549, cod 1140920) are **un singur rulaj**: `RUL.id_rul=10947,
id_articol=4294507173, cant=0, cante=1, pretvtva=121, id_tip_rulaj=0` — si **doua** linii `tvd`
(rata fara articol + articolul 4294507173). Deci pe acest document, `agg_rul` n-ar primi niciodata
un rand NULL — doar `agg_tvd` (din `tvd`) aduce randul de rata.
**Concluzie directa**: rândul `ofacturare_editare.prg:617` (`agg_rul` declarat fara `NULL`) e nesigur
pe hartie, dar **dovedit inaccesibil** — `trul`, incarcat din `RUL`, nu poate aduce NULL, constrangerea
Oracle il blocheaza la sursa. Merita uniformizat pentru consistenta cu `tvd`/`agg_tvd` (defensiv,
cost zero), dar nu e un bug activ.
### Testat empiric, nu presupus
Am rulat un test izolat in VFP (`vfp9.exe -A -T`, fara Oracle, fara formulare), reproducand exact
structura din `ConstruiestePropunereSincronizare` — `test_null_locate_union.prg`, pastrat in
scratchpad, log complet in `test_null_locate_union.log` (acelasi director). Rezultate:
| Test | Ce verifica | Rezultat masurat |
|---|---|---|
| 1 | `SELECT id_articol FROM agg_rul UNION SELECT id_articol FROM agg_tvd` cu 1 rand NULL in `agg_tvd` | **2 randuri** in rezultat: unul NULL, unul cu valoare — `UNION` pastreaza NULL-ul, nu-l pierde |
| 2 | Acelasi UNION, dar cu **2** randuri NULL in `agg_tvd` (simuleaza doua rate pe acelasi document) | Tot **2 randuri** in rezultat — `UNION` trateaza cele doua NULL-uri **ca echivalente** la deduplicare (comportament SQL standard de grupare, diferit de semantica lui `=`) |
| 3 | `LOCATE FOR id_articol = m.lnTest`, cu `m.lnTest = .NULL.`, pe un cursor care chiar are un rand cu `id_articol` NULL | **`FOUND() = .F.`** — nu gaseste randul, desi acesta exista |
| 4 | `LOCATE FOR id_articol = crs_articole_unite.id_articol`, exact tiparul de la `:671`/`:687`, cu ambele parti NULL | **`FOUND() = .F.`** — confirma acelasi lucru intre doua cursoare, nu doar cu un memvar |
**Interpretare, aplicata pe `ConstruiestePropunereSincronizare`**: daca EROAREA 1 s-ar repara doar prin
adaugarea `NULL` la declaratiile `agg_rul`/`agg_tvd` (Varianta A propusa initial), randul NULL din
`crs_articole_unite` (Test 1/2 arata ca **exista**, indiferent de cate rate sunt) ar ajunge in bucla
principala de la `:665-773`. Acolo, `LOCATE FOR id_articol = crs_articole_unite.id_articol` **pe
`agg_rul` si pe `agg_tvd`, ambele** (Test 3/4) **nu gaseste nimic**, chiar daca `agg_tvd` chiar contine
randul cautat. Rezultatul: `lnNrandRul=0` si `lnNrandTvd=0` raman la valorile implicite, deci
`llGasitSursa=.F.` si `llGasitTinta=.F.` **pe ambele ramuri** — nu se potriveste niciun `CASE` din
`DO CASE` (nici „Adaugare", nici „Semnalare"), cade in `OTHERWISE` cu `0=0`, si **liniei de rata nu i
se genereaza nicio linie in `propunere_sincronizare`**. **Corectez aici ipoteza din punctul 5 al
raportului initial** („pereche falsa Adaugare+Semnalare") — nu e o pereche falsa, e o **disparitie
tacuta**, mai greu de observat: randul nu genereaza nici eroare, nici avertizare, doar lipseste din
propunere, din `SemnaturaDivergenteSincronizare` (`omodificari.vc2:14839`, filtreaza pe aceleasi trei
actiuni) si deci din dialogul de sincronizare.
### Raspunsul la intrebare: Varianta B (excludere la sursa) e cea corecta
Team-lead a propus alternativa: `ConstruiestePropunereSincronizare` sare complet peste liniile `tvd`
(si, defensiv, `trul`) fara `id_articol`, cu `SCAN FOR Nvl(sters,0) <> 1 AND !Isnull(id_articol)` in
loc de `SCAN FOR Nvl(sters,0) <> 1` la `:619` (trul->agg_rul) si `:642` (tvd->agg_tvd).
**E varianta corecta**, din trei motive, toate confirmate mai sus:
1. **`id_articol` e chiar cheia de comparatie a mecanismului** — comentariul din capul fisierului
(`ofacturare_editare.prg:14`: "agregate pe id_articol, in ambele sensuri") o spune explicit. O
linie fara cheie nu poate, prin definitie, sa aiba corespondent — nu e un caz special de tratat cu
grija, e un rand care nu apartine acestui mecanism.
2. **Rezultatul practic al Variantei B e identic cu ce se intampla azi accidental prin Varianta A**
(randul de rata nu ajunge in `propunere_sincronizare`, deci nu apare in dialog, nu declanseaza
„Aplica" pe el) — dar **prin filtrare explicita**, nu prin efectul secundar, neverificat de nimeni
pana acum, al lui `LOCATE FOR ... = NULL`. Daca cineva „repara" mai tarziu comparatiile cu
`Nvl(id_articol,-1) = Nvl(...,-1)` (Varianta A din reparatia initiala, sau orice alt developer care
descopera si "repara" fara sa stie de aceasta analiza), comportamentul s-ar schimba brusc de la
"dispare tacut" la "participa la matching" — cu riscul semnalat deja in reparatia initiala, ca mai
multe rate distincte de pe acelasi document s-ar agrega/compara laolalta (confirmat acum de Testul
2: `UNION` le trateaza deja ca un singur grup NULL). Varianta B evita complet acest risc, pentru ca
liniile de rata nici nu ajung sa fie agregate.
3. **Varianta B rezolva si EROAREA 1 la radacina**, fara sa mai fie nevoie sa se adauge `NULL` la
declaratiile `agg_rul`/`agg_tvd` — daca linia cu `id_articol` NULL nu mai intra in bucla de `SCAN`
care face `INSERT INTO agg_tvd`, nu mai exista nicio incercare de a insera NULL intr-un camp NOT
NULL. (Adaugarea `NULL` la declaratii ramane totusi recomandata, ca plasa de siguranta ieftina,
independent de asta.)
**Efect pe cazul concret SSS 549, cu Varianta B**: `agg_rul` are 1 rand (`4294507173`), `agg_tvd` are
1 rand (`4294507173`, linia de rata fiind sarita la `SCAN`). `crs_articole_unite` are 1 rand. Randul
de rata nu apare nicaieri in `propunere_sincronizare` — nici „Adaugare", nici „Semnalare", nici „N-A".
La `SemnaturaDivergenteSincronizare`, semnatura nu contine nimic despre rata — deschiderea/inchiderea
dialogului de sincronizare nu e afectata de ea, la deschidere sau la salvare. La „Aplica" (daca
utilizatorul il apasa pentru articolul real), `AplicaModificareTrul`/`AplicaAdaugareTvd`/
`AplicaModificareTvd` nu sunt niciodata chemate cu `tnIdArticol` NULL, pentru ca randul de rata nu
ajunge in `propunere_sincronizare` ca sa fie scanat de `AplicaSincronizareArticole`
(`ofacturare_editare.prg:785-822`, bucla `SCAN FOR Inlist(Alltrim(actiune), 'Modificare',
'Adaugare')` la `:802`) — deci ingrijorarea din punctul 5 despre `:849/887/923` nu se mai
materializeaza.
---
## Reparatie propusa (NU aplicata)
### EROAREA 1 — cursoarele de agregare
**Recomandare finala (dupa analiza si testul din punctul 7): Varianta B — exclude liniile fara
`id_articol` la sursa**, in `ConstruiestePropunereSincronizare`:
```
SELECT trul
SCAN FOR Nvl(sters,0) <> 1 AND !Isnull(id_articol)
...
```
(`ofacturare_editare.prg:619`, azi `SCAN FOR Nvl(sters,0) <> 1`)
```
SELECT tvd
SCAN FOR Nvl(sters,0) <> 1 AND !Isnull(id_articol)
...
```
(`ofacturare_editare.prg:642`, azi `SCAN FOR Nvl(sters,0) <> 1`)
E coerent cu faptul ca `id_articol` e chiar cheia de comparatie a mecanismului (comentariul din capul
fisierului, `:14`) — o linie fara cheie n-are cum sa aiba corespondent, la fel cum deja se trateaza
separat cazul "Articol nestocat, fara corespondent in rulaje" (`:730-732`). Filtrul de mai sus **repara
si EROAREA 1** — nu mai ajunge nicio valoare NULL la `INSERT INTO agg_tvd`/`agg_rul`, deci declaratiile
cursoarelor n-ar mai avea nevoie sa accepte `NULL` ca sa nu crape. Recomandat totusi, ca plasa de
siguranta ieftina si pentru consistenta cu `tvd`:
```
CREATE CURSOR agg_rul (id_articol N(20) NULL, denumire C(100), codmat C(30), cant N(12,3), val N(16,4), nrand I)
CREATE CURSOR agg_tvd (id_articol N(20) NULL, denumire C(100), codmat C(30), cant N(12,3), val N(16,4), nrand I, in_stoc I)
```
(`ofacturare_editare.prg:617`, `:640`)
**Varianta alternativa, respinsa**: doar adauga `NULL` la declaratii si repara comparatiile `id_articol
= X` cu o forma NULL-safe (`Nvl(id_articol,-1) = Nvl(m.tnIdArticol,-1)`, acelasi tipar folosit in
`ofacturare_comun.prg:1334,1349`). Tehnic ar functiona, dar **verificat empiric (punctul 7, Test 2)**:
`UNION`-ul deja trateaza toate liniile NULL ca **un singur grup** — cu aceasta varianta, doua sau mai
multe rate distincte de pe acelasi document s-ar agrega si compara laolalta, ca si cum ar fi acelasi
"articol". Nu aduce niciun beneficiu fata de Varianta B (rezultatul pentru randul de rata tot nu
trebuie sa apara in sincronizare), doar risc suplimentar — de aceea nu e recomandata.
### EROAREA 2 — garda de la salvare
Trei variante, e o decizie de produs:
- **Varianta A — restrange garda la liniile cu articol real posibil**: cere articol doar cand linia
nu e de tip rata — de exemplu cand `id_vanzare_set = 0` **si** linia are `pret_achizitie` (semn ca
vine dintr-un flux de articole reale), sau invers, sare garda cand `Isnull(id_articol) AND
!Empty(explicatie)` (semnul unei linii "sintetice" descrise prin `explicatie`, ca ratele).
- **Varianta B — scoate garda complet** pentru randuri cu `id_articol` NULL de la incarcare (nu
adaugate manual in sesiunea curenta) — presupune sa distingi in `tvd` intre "NULL de la Oracle" si
"NULL pentru ca utilizatorul a adaugat un rand si n-a ales inca un articol" (al doilea caz chiar
trebuie blocat).
- **Varianta C — transforma in intrebare (confirmare), nu blocaj** — la fel ca gardul de pret de
achizitie 0 de doua linii mai jos (`:14458-14464`), las utilizatorul sa decida daca salveaza cu
randul fara articol.
Recomandarea de continut (nu de aplicat acum): **Varianta A**, pentru ca pastreaza garda utila pe
cazul ei real (rand adaugat manual din nomenclator, fara articol ales din greseala), fara sa oblige
verificarea de tip "e linie de rata veche" peste tot.
### Scrierea in Oracle — `ScrieArticoleFacturaEditate` (punctul 6)
Minim, pe ambele blocuri (UPDATE `:505-510`, INSERT `:535-546`), aliniaza `id_articol` si
`pret_achizitie` la tiparul `Iif(Isnull(...),[NULL],...)` deja folosit pentru `id_gestiune`/
`id_valuta`/`id_jtva_coloana`/`taxcode` in acelasi bloc:
```
[, id_articol = ] + Iif(Isnull(id_articol),[NULL],Alltrim(Str(id_articol))) + ;
...
[, pret_achizitie = ] + Iif(Isnull(pret_achizitie),[NULL],Alltrim(Str(pret_achizitie,18,4))) + ;
```
**`id_articol` e obligatoriu de facut** — altfel, chiar dupa ce EROAREA 1 si EROAREA 2 s-ar repara,
prima salvare a unei facturi cu linie de rata ar cadea pe `ORA-02291` (FK spre `NOM_ARTICOLE`, care
n-are rand cu `id=0`). `pret_achizitie` e recomandat, ca sa nu se scrie tacut "cost de achizitie 0" pe
un rand care azi are NULL. `proc_tvav` — de decis dupa ce se clarifica NEVERIFICAT-ul de mai jos (daca
poate fi NULL pe un rand real, acelasi tipar se aplica si lui). `discount_unitar`, `cantitate`,
`pret_cu_tva` — opional, doar pentru consistenta cu restul blocului, fara bug observat.
### `VVANZARI_ARTICOLE` — fallback pe `explicatie`
Independent de cele doua erori, view-ul (`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql`)
ar putea capata acelasi fallback ca `fact_vfacturi_detalii`:
`NVL2(vd.id_articol, na.denumire, vd.explicatie) as denumire` — ca sa nu mai iasa coloana Denumire
goala pe randurile de rata (in prezent doar coloana Explicatie arata ceva, iar Denumire ramane goala
in grid, ceea ce probabil nu era intentionat cand s-a proiectat view-ul).
## NEVERIFICAT
- **Constrangerea DDL exacta pe `VANZARI_DETALII.ID_ARTICOL` — REZOLVAT.** Confirmat direct pe Oracle
(`ALL_TAB_COLUMNS`, `MARIUSM_AUTO@ROA_CENTRAL`): `ID_ARTICOL` e `NULLABLE = Y`, cu FK
`FK_VANZARE_DET002` spre `NOM_ARTICOLE.PK_ARTICOL`; `NOM_ARTICOLE` n-are niciun rand cu `id_articol =
0`. Vezi tabelul din punctul 2. (Scriptul original de `CREATE TABLE` tot nu e in
`D:\ROA\DATABASE\SCRIPTURI_CLAR` — dar nu mai e nevoie de el, DDL-ul curent s-a citit direct din
dictionarul de date.)
- **Daca exista azi randuri reale in `VANZARI_DETALII` cu `proc_tvav` NULL** — n-am interogat asta
direct (doar am confirmat ca *poate* fi NULL, `NULLABLE=Y`). Pe randul de test `proc_tvav=1.21`, deci
nu se declanseaza acolo. Daca nu exista niciodata NULL pe randuri reale, `Nvl(proc_tvav,0)` din
punctul 6 e o plasa de siguranta fara efect, la fel ca la `pret`/`cantitate`; daca exista, e periculos
(baza * 0 = 0 in orice recalcul din afara VFP). Se poate lamuri cu un `SELECT count(*) FROM
vanzari_detalii WHERE sters=0 AND proc_tvav IS NULL`.
- **De ce n-a crapat `agg_rul` — REZOLVAT.** `RUL.ID_ARTICOL` e **NOT NULL** in Oracle
(`all_tab_columns.nullable = 'N'`, masurat de team-lead), si exista **0** randuri `RUL` active cu
`id_articol IS NULL`. Documentul de test (SSS 549) are un singur rulaj, cu articolul real
`4294507173` — rata n-are corespondent in `RUL`. Deci `trul` nu poate aduce niciodata NULL, iar
declaratia `agg_rul (id_articol N(20), ...)` fara `NULL` (`:617`), desi nesigura pe hartie, e
dovedit inaccesibila pe calea normala — nu doar "n-a crapat inca", ci "nu poate crapa" atat timp cat
constrangerea Oracle ramane in vigoare. Vezi punctul 7.
- **Comportamentul VFP la NULL in `UNION` si `LOCATE FOR` — REZOLVAT, testat empiric.** Vezi punctul 7:
test izolat `vfp9.exe -A -T`, script pastrat in
`C:\Users\mmari\AppData\Local\Temp\claude\D--ROA-ROAFACTURARE\81fbd7c8-41d4-4341-a83a-95736e6dc6d3\scratchpad\test_null_locate_union.prg`,
log in acelasi director (`test_null_locate_union.log`) — fisiere temporare de sesiune, nu in
arborele proiectului. `UNION` trateaza NULL=NULL ca echivalent la deduplicare; `LOCATE FOR camp =
NULL` nu gaseste niciodata, nici cand campul chiar contine NULL pe randul cautat.
- **Cate linii de rata existente in productie ar fi afectate** de reparatia recomandata (Varianta B) —
nu am interogat Oracle pentru un numar total (team-lead a masurat deja 20 de linii active cu
`id_articol IS NULL` in toata baza, punctul 2 — dar nu si cate din ele sunt pe facturi cu rulaje
care ar trece prin `ConstruiestePropunereSincronizare`).

View File

@@ -0,0 +1,411 @@
# Cercetare: totaluri goale pe factura editata din aviz (LISTA FACTURI, AVIZE SI PROFORME)
Simptom: dupa editarea din formularul unificat a unei facturi emise DIN AVIZ, randul facturii in
lista principala apare cu **Total fara TVA / Total TVA / Total cu TVA GOALE** (NULL, nu 0.00).
Randul avizului de dedesubt ramane corect. Regresie fata de comportamentul anterior.
Diagnostic STRICT — nu s-a modificat niciun fisier de cod, nu s-a facut write-back, nu s-a comis
nimic.
> **Depasit partial.** Acest raport se opreste la "cauza probabila, NEVERIFICAT pe date".
> Interogarea pe `MARIUSM_AUTO@ROA_CENTRAL` a inchis intre timp intrebarea: linia facturii are
> `ID_VALUTA = 2` (EURO) fara rand corespunzator in `VANZARI_CURSURI`, iar ramura de conversie
> valutara a procedurii produce NULL. Concluzia si reparatia sunt in
> `docs\propunere_runda5_articole_valuta_rate.md`, punctul 2. Ce ramane valabil aici: analiza
> lantului de scriere si infirmarea ipotezei cu `id_vanzare_set`/`id_vanzare_det`.
## 1. De unde vin coloanele Total fara TVA / Total TVA / Total cu TVA
Gridul `grid_facturi` din `frm_facturi` (`COMUN\clase\ofacturare_comun.vc2:1172` obiectul,
`:1203-1208` coloanele `cTotal_cu_tva`/`cTotal_fara_tva`/`cTotal_tva`) e legat pe cursorul
`crsfacturi` (`RecordSource = "crsFacturi"`, `COMUN\clase\ofacturare_comun.vc2:2238`), populat prin
`gencursor('poFacturi','crsfacturi', lcSelect, ...)`.
Cursorul e umplut din UNA din doua proceduri Oracle, **alese la RUNTIME de utilizator**, printr-un
prompt (`Clase\ofundal_facturare.vc2:944-953`, `Page4.Cw1.do_actiune`):
```
lnOptiune = xmenu('Vizualizare \<standard;Vizualizare \<experimentala (mai rapida)')
If m.lnOptiune = 1
Do vizualizare_facturi In oproceduri_facturare.prg && FACT_VFACTURI
Else
Do vizualizare_facturi2 In oproceduri_facturare.prg && FACT_VFACTURI2
Endif
```
Cele doua proceduri (`COMUN\programe\oproceduri_facturare.prg:314` si `:398`) selecteaza
`total_fara_tva, total_tva, total_cu_tva` din **doua view-uri Oracle diferite** cu semantica
diferita — asta e cheia problemei:
- **`FACT_VFACTURI`** ("standard"): coloanele sunt **calculate live**, printr-un `SUM(...)`
peste `vanzari_detalii` (si `vanzari_seturi` pentru liniile de tip set). Nu citeste niciodata
coloanele stocate din `VANZARI`.
Definitie: `D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_06_11_COMUN_FACT_VFACTURI.sql:192-194`
(`a.suma_fara_tva - a.disc_fara_tva_ron as total_fara_tva`, etc.), agregarea la
`:408-460` (subquery `vd`, `LEFT JOIN` intre `v.id_vanzare` si rezultatul agregat).
- **`FACT_VFACTURI2`** ("experimentala"): coloanele sunt citite **direct** din `VANZARI` (coloane
stocate), fara recalcul la interogare — comentariul din cod spune explicit de ce:
"*totalurile ... sunt calculate in vanzari, in loc sa fie luate din vanzari_detalii, pentru
rapiditate; totalurile sunt completate in vanzari la insert into vanzari*"
(`COMUN\programe\oproceduri_facturare.prg:397-399`).
Definitie view: `ff_2026_08_06_11_COMUN_FACT_VFACTURI.sql:682-684`
(`a.total_fara_tva, a.total_tva, a.total_cu_tva` din `vanzari a`).
**NEVERIFICAT**: nu stiu ce optiune a ales Marius la momentul screenshot-ului (`standard` sau
`experimentala`) — promptul e interactiv, nu am gasit un default hard-codat. Concluzia de mai jos
e valabila pentru amandoua variantele, din motive diferite (vezi §3).
## 2. Ce scrie salvarea din formularul unificat pe partea de factura
`ScrieArticoleFacturaEditate` (`COMUN\programe\ofacturare_editare.prg:473-571`), apelata cu
`tvanz.id_vanzare` (id-ul corect al FACTURII, verificat — `tvanz` vine din
`IncarcaVanzareNota`/`IncarcaVanzareDinNota`, care interogheaza `VANZARI` dupa
`cod/nract/serie_act/data_act` ale notei editate, deci nu e confuzie aviz/factura; apelurile reale
sunt in `COMUN\clase\comun.vc2:2492` si `COMUN\clase\ofacturare_comun.vc2:3829`, ambele in ramura
`"OFACTURARE_EDITARE" $ Upper(Set("Procedure"))`):
1. **Marcheaza sters=1 TOATE liniile active** din `vanzari_detalii` pentru acel `id_vanzare`
(`:492-495`).
2. **Reinvie/actualizeaza** liniile pastrate, cele cu `id_vanzare_det > 0` din cursorul `tvd`
(`:498-518`) — UPDATE pe cheia `id_vanzare_det`, camp cu camp: `sters, cantitate, pret,
pret_cu_tva, pret_achizitie, proc_tvav, discount_unitar, id_articol, id_gestiune, id_valuta,
id_jtva_coloana, taxcode, cont, id_utils, dataoras`. **NU** atinge `id_vanzare_set` — coloana nu
apare in lista SET, deci valoarea existenta in DB ramane neschimbata la UPDATE.
3. **Insereaza** liniile noi (`id_vanzare_det = 0`, `:522-540`) — la fel, **fara** `id_vanzare_set`
in lista de coloane (linii noi ies cu `id_vanzare_set` implicit NULL in DB).
4. **Doar daca 1-3 au reusit complet** (`IF m.llSucces`, verificat dupa fiecare pas, `EXIT` la
primul esec Oracle — deci un esec partial NU ajunge la pasul urmator), cheama procedura Oracle
de recalcul totaluri (`:559-563`, vezi §3).
Pe o factura din aviz **fara** articole compuse ("seturi"), toate liniile din `tvd` au
`id_vanzare_set = 0/NULL` — confirmat de cercetarea anterioara `docs/livrare_s5.md:117-118`:
"*Liniile din seturi de articole (id_vanzare_set nenul) — neacoperibil pe datele actuale, nu din
omisiune: interogare pe MARIUSM_AUTO, zero documente cu id_vanzare_set nenul*". Deci pentru cazul
din screenshot (SSS 100037), branch-ul de "seturi" din view/procedura Oracle (vezi §3) probabil nu
se activeaza — **ramane insa NEVERIFICAT direct pe acest document**, n-am rulat SQL pe schema
reala.
**Nu am gasit nicio scriere directa pe antetul `VANZARI` in `ScrieArticoleFacturaEditate` in afara
apelului la procedura Oracle de la pasul 4** — totalurile de antet nu sunt niciodata calculate in
VFP, sunt lasate integral pe seama Oracle.
## 3. Procedura Oracle de recalcul — cea mai probabila cauza
`ScrieArticoleFacturaEditate` (`ofacturare_editare.prg:559-563`) cheama necondiționat:
```
lcSql = [begin pack_facturare.recalculeaza_totaluri_vanzari(] + Alltrim(Str(m.tnIdVanzare)) + [,] + ;
Iif(m.llDiscountNul,[NULL],Alltrim(Str(m.lnDiscount,18,4))) + [); end;]
llSucces = goExecutor.oExecuta(m.lcSql)
```
Aceasta procedura este **cod nou**, introdus in exact aceeasi runda de modificari ca bug-ul
raportat:
- Script-ul care o documenteaza in `SCRIPTURI_CLAR` e
`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql`
(comentariu la inceput: "*adauga procedura recalculeaza_totaluri_vanzari*").
- Apelul din VFP a fost adaugat in commit-ul `1c42ae0` din `COMUN`
("*#6 editare factura emisa: S5 - scrierea sumelor editate in Oracle*"), **10.08.2026**, adica o
zi dupa ce procedura Oracle a fost capturata in script (09.08.2026) — coerent cu "procedura noua
in DB, apoi codul VFP care o cheama".
Corpul procedurii (`ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:16024-16228`) face un
`SELECT ... INTO` cu agregare (SUM) peste vanzari_detalii/vanzari_seturi — **aceeasi structura**
ca formula din `FACT_VFACTURI` (§1): UNION ALL intre liniile normale
(`nvl(a.id_vanzare_set,0)=0 and a.id_vanzare=V_ID_VANZARE and a.sters=0`) si liniile "set"
(`vanzari_seturi` join `vanzari_detalii`), apoi `SUM(pack_facturare.calculeaza_total_fara_tva_fact(...))`
etc. Rezultatul e scris **necondiționat**, fara nicio garda `NVL`:
```
update vanzari
set discount = lnDiscountFactura,
...
total_fara_tva = lnTotalFaraTVA,
total_tva = lnTotalTVA,
total_cu_tva = lnTotalCuTVA,
...
where id_vanzare = V_ID_VANZARE;
```
(`ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:16211-16223`)
Contrast direct cu scriptul de backfill istoric din aceeasi runda
(`ff_2026_08_06_12_COMUN_VANZARI_BACKFILL.sql`), care recalculeaza aceleasi coloane dar explicit
**"fill-only"**: `v.total_fara_tva = NVL(v.total_fara_tva, src.c_ftva)`, cu comentariu propriu care
spune raspicat ca scrierea trebuie sa fie doar completare de goluri, "*niciodata peste o valoare
deja scrisa*". `recalculeaza_totaluri_vanzari` **nu are aceasta garda** — daca `SELECT INTO`
calculeaza `NULL` pe oricare coloana (SUM peste zero randuri, sau peste randuri toate NULL),
UPDATE-ul suprascrie o valoare corecta deja existenta cu NULL.
**Cand poate iesi NULL din SUM**: in Oracle, `SUM()` peste un set de randuri produs de un
`LEFT JOIN` fara nicio potrivire da NULL (nu 0), iar daca TOATE liniile relevante ale documentului
ies cu pret/discount NULL din subquery (de ex. `vanzari_seturi` fara rand pentru
`id_vanzare_set`-ul cerut, in branch-ul de "seturi"), rezultatul agregat pe acel document e NULL.
Pentru un document fara linii de tip "set" (cazul obisnuit, §2), acest branch nu ar trebui sa se
activeze — dar **nu am verificat pe date reale** daca liniile facturii SSS 100037 au ramas cu
`sters=0` dupa salvare sau daca vreo alta conditie (curs valutar lipsa in `vanzari_cursuri` pentru
`id_valuta`-ul liniei, de ex.) a produs NULL in agregat.
## 4. Regresia, concret — NEVERIFICAT complet, dar convergenta puternica
Nu exista fisiere `.bak.vc2` pentru `ofacturare_editare.prg` (bak-urile mentionate in cerere —
`omodificari.pre_cantitate.bak.vc2` etc. — sunt pentru `omodificari.vc2`, o clasa diferita; codul
de scriere efectiva a articolelor si totalurilor traieste in `ofacturare_editare.prg`, care nu are
un `.bak`). Istoricul git (`COMUN`) arata insa clar ca **intreg mecanismul de recalcul al
totalurilor pe factura editata e cod nou din 09-10.08.2026** (S5), introdus in acelasi pachet cu
extinderea UPDATE-ului mentionata in cerere (`id_articol, id_gestiune, id_valuta, proc_tvav,
taxcode, cont, pret_achizitie, discount_unitar, id_jtva_coloana`) — commit-urile ulterioare
(S4b etapa 2, 11.08.2026) nu ating aceasta zona.
**Ipoteza suspectata explicit de cerere** (UPDATE-ul extins scrie campuri NULL peste linii
existente si strica `id_vanzare_set`/`id_vanzare_det`) **NU se confirma din citirea codului**:
UPDATE-ul de la pasul 2 (§2) nu atinge deloc coloana `id_vanzare_set`, iar `id_vanzare_det` e
folosit doar in clauza `WHERE`, niciodata scris. Round-trip-ul cursorului `tvd` (incarcat din
`VVANZARI_ARTICOLE`, view 1:1 pe `vanzari_detalii` — vezi
`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql`, fara nicio
agregare) e coerent camp cu camp.
**Cauza cea mai probabila, cu incredere medie-mare**: nu o eroare in codul VFP de scriere a
liniilor, ci procedura Oracle noua `pack_facturare.recalculeaza_totaluri_vanzari`
(§3) — cod introdus in aceeasi runda, apelat necondiționat dupa fiecare salvare de factura editata
din formularul unificat, care scrie fara garda NULL peste coloanele de total din `VANZARI`. Pe
"vizualizarea standard" (`FACT_VFACTURI`), acelasi tip de agregare NULL-propagabila se repeta live
la fiecare afisare a listei, deci simptomul ar aparea indiferent care view e activ.
## 5. Trigger/procedura declansata la INSERT — nu se aplica aici
Nu exista trigger Oracle care sa recalculeze totalurile la INSERT in `vanzari_detalii` — scrierea
e facuta explicit, o singura data, prin apelul manual la `recalculeaza_totaluri_vanzari` de la
finalul lui `ScrieArticoleFacturaEditate` (§3). Punctul 4 din cererea initiala nu se aplica.
## Reparatie propusa (fara aplicare)
Minimal, in `D:\ROA\DATABASE\SCRIPTURI_CLAR\...\PACK_FACTURARE.sql` /
`recalculeaza_totaluri_vanzari`: inlocuit UPDATE-ul necondiționat cu varianta "fill-safe" folosita
deja in scriptul de backfill — fie NVL pe fiecare coloana (`total_fara_tva = NVL(lnTotalFaraTVA,
total_fara_tva)` etc., pastreaza valoarea veche daca recalculul da NULL), fie un `RAISE`/log
explicit cand agregatul iese NULL, ca sa nu treaca neobservat. Inainte de asta, e nevoie de o
verificare directa pe schema reala (SQL pe `vanzari_detalii`/`vanzari_seturi` pentru
`id_vanzare`-ul facturii SSS 100037) ca sa se confirme ce ramura produce NULL — recomand asta ca
prim pas al oricarei remedieri, nu modificarea pe ghicite.
## Ce ramane NEVERIFICAT (runda 1)
- Ce optiune de vizualizare (standard/experimentala) a fost activa la momentul screenshot-ului.
- Continutul real al `vanzari_detalii`/`vanzari_seturi` pentru factura SSS 100037 dupa editare —
**verificat in runda 2** (mai jos), pe baza interogarilor SQL facute de team-lead + interogari
proprii read-only, cu `sqlplus`.
---
# Runda 2 — cauza confirmata prin SQL pe date reale
Team-lead a interogat direct Oracle (`MARIUSM_AUTO@ROA_CENTRAL`) si a stabilit cauza imediata:
linia `VANZARI_DETALII.id_vanzare_det = 1602` (singura linie activa a facturii `id_vanzare = 1054`,
SSS 100037) are `ID_VALUTA = 2` (EURO), desi documentul e `IN_VALUTA = 0` si linia-sursa din aviz
avea `ID_VALUTA = 3` (RON). `VANZARI_CURSURI` nu are niciun rand pentru `id_vanzare = 1054`, deci
`recalculeaza_totaluri_vanzari` calculeaza `pret_ron = NULL` pentru acea linie (branch-ul de
conversie valutara, fara curs) si `total_fara_tva/total_tva/total_cu_tva` ies `NULL`. Am confirmat
independent, cu SQL read-only propriu (`sqlplus`, vezi mai jos), amploarea si mecanismul.
## 1. Cine scrie `id_valuta` pe linia de articol — CONFIRMAT
Singura cale prin care `tvd.id_valuta` se schimba pe o linie **existenta** e nomenclatorul de
valuta din grid: `ArticoleNotaEditor.ModificaNomenclator`, ramura `nume_val`
(`COMUN\programe\ofacturare_editare.prg:1093-1094`):
```
CASE m.lcCamp == 'nume_val'
REPLACE nume_val WITH loCauta.nume_val, id_valuta WITH loCauta.id_valuta
```
`loCauta = caut_valuta()` — nomenclatorul standard de valute, deschis fara nicio conditie legata
de `tvanz.in_valuta`. Singura garda de la inceputul procedurii (`:1069`) e
`Nvl(tvd.id_vanzare_set,0) <> 0` (blocheaza doar liniile din seturi de articole) — **nu exista
nicio garda legata de tipul documentului**, deci utilizatorul poate alege orice valuta pe orice
linie normala, indiferent daca factura e in RON sau in valuta.
UPDATE-ul care duce `id_valuta` in `VANZARI_DETALII` e in
`ScrieArticoleFacturaEditate` (`COMUN\programe\ofacturare_editare.prg:512`):
```
[, id_valuta = ] + Iif(Isnull(id_valuta),[NULL],Alltrim(Str(id_valuta))) + ;
```
**Confirmat prin diff cu `COMUN\programe\ofacturare_editare.prg.pre_runda_butoane.bak:501-505`**:
inainte de runda curenta, UPDATE-ul pe liniile existente scria doar
`sters, cantitate, pret, pret_cu_tva, id_utils, dataoras` — **`id_valuta` nu era in lista**. Deci
inainte, o alegere de valuta pe o linie existenta (facuta prin `nume_val`) se pierdea silentios la
salvare (UI arata schimbarea, dar UPDATE-ul n-o scria) — inofensiv, dar si fara efect. Extinderea
UPDATE-ului (runda curenta, aceeasi runda care a adaugat si `recalculeaza_totaluri_vanzari`, vezi
runda 1 §3-4) e cea care face ca alegerea gresita de valuta sa ajunga acum in `VANZARI_DETALII` si
sa strice totalurile. **Simptomul e nou din exact acest motiv — confirmat pe cod, nu presupunere.**
## 2. Ce ar trebui sa fie corect pe un document `in_valuta = 0` — CONFIRMAT
`VANZARI_CURSURI` e scris o singura data, la EMITERE, de `pack_facturare.scrie_cursuri`
(`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:14538-14548`):
```sql
PROCEDURE scrie_cursuri(V_ID_VANZARE IN NUMBER) IS
BEGIN
INSERT INTO VANZARI_CURSURI (ID_VANZARE, ID_VALUTA, CURS, MULTIPLICATOR)
SELECT DISTINCT V_ID_VANZARE, ID_VALUTA, CURS, MULTIPLICATOR
FROM VANZARI_DETALII_TEMP
WHERE ID_VALUTA <> pack_facturare.nid_moneda_nationala;
END scrie_cursuri;
```
Sursa e tabela de STAGING folosita la emitere (`VANZARI_DETALII_TEMP`), nu `VANZARI_DETALII` live —
deci **nu exista niciun mecanism care sa adauge/actualizeze `VANZARI_CURSURI` cand se schimba
`id_valuta` pe o linie DUPA emitere**, din editarea unificata sau din oricare alt punct. Raspuns
direct la intrebare: **da, e conceptual posibil ca o linie sa aiba alta valuta decat RON pe un
document in lei** (mecanismul de facturare mixta exista, cu curs propriu per linie in
`VANZARI_CURSURI`) — dar **doar daca acea alegere s-a facut la emitere**, cand `scrie_cursuri`
inca ruleaza si scrie perechea `(id_vanzare, id_valuta) -> curs`. O schimbare **post-emitere**,
prin editarea unificata, nu are nicio cale sa creeze acel rand — deci orice `id_valuta` diferit de
RON ales dupa emitere e, prin constructie, orfan (fara curs), garantand `pret_ron = NULL` in
`recalculeaza_totaluri_vanzari`.
## 3. Toate caile prin care agregatele pot iesi NULL — enumerare + masurare pe date reale
Din citirea corpului `recalculeaza_totaluri_vanzari`
(`ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:16024-16228`):
1. **Zero randuri active in `VANZARI_DETALII`** pentru `id_vanzare` (toate `sters=1`, sau
document fara nicio linie) — `SUM()`/`MAX()` peste zero randuri = NULL pentru toate coloanele.
2. **Linie cu `id_valuta` diferit de moneda nationala, fara rand in `VANZARI_CURSURI`** pentru acea
pereche `(id_vanzare, id_valuta)` — cazul masurat mai jos (Marius). `LEFT JOIN vc` nu gaseste
nimic, `vc.curs`/`vc.multiplicator` = NULL, `pret_ron` = NULL pentru acea linie. Daca **toate**
liniile documentului sunt afectate, `SUM()` total iese NULL; daca doar unele, `SUM()` ignora
randurile NULL si totalul iese **trunchiat, nu NULL** (lipseste contributia liniei respective,
fara sa fie vizibil ca eroare).
3. **Linie din "set de articole" (`id_vanzare_set <> 0`) fara rand corespunzator in
`VANZARI_SETURI`** — acelasi tipar ca #2, prin `LEFT JOIN vanzari_seturi b`. Neconfirmat pe date
reale (branch aproape neutilizat — vezi `docs/livrare_s5.md:117-118`, zero documente cu
`id_vanzare_set` nenul cunoscute anterior).
4. **`VANZARI.in_valuta` NULL** — `nvl(lnInValuta,-1) > -1` devine fals, exclude tot branch-ul de
"seturi"; nu produce NULL pe branch-ul normal, dar poate goli branch-ul 2 pe un document compus
doar din linii de set.
5. **Argument NULL in `pack_facturare.calculeaza_total_fara_tva_fact`/`_tva_fact`** (ex.
`proc_tvav` sau `cantitate` NULL pe o linie) — acelasi tipar trunchiat/total, dupa cate linii
sunt afectate.
**Masurat pe schema reala** (`MARIUSM_AUTO@ROA_CENTRAL`, SELECT-uri read-only, script-urile
folosite raman in `scratchpad`, nu s-a scris nimic in baza):
| Categorie (din cele 235 `VANZARI.total_cu_tva IS NULL`) | Numar documente |
|---|---|
| Total `VANZARI` cu `total_cu_tva IS NULL` | **235** |
| Fara nicio linie activa in `VANZARI_DETALII` (cauza #1, veche — carve-out cunoscut, vezi
`ff_2026_08_06_12_COMUN_VANZARI_BACKFILL.sql`, "facturile fara nicio linie activa raman
neatinse, prin decizie de produs") | **120** |
| Linie cu `id_valuta` diferit de RON si fara rand in `VANZARI_CURSURI` (cauza #2 — **exact
bug-ul curent**) | **1** (chiar SSS 100037 / `id_vanzare=1054`, confirmat `IN_VALUTA=0`) |
| Neexplicat de #1 sau #2 (alta cauza, preexistenta, in afara scopului acestei cercetari) |
**114** — din acestea, 109 au `VANZARI.discount_evidentiat IS NULL` (posibil o cauza separata,
nelegata de `id_valuta`; neinvestigat in continuare, iese din scopul cererii) |
**Concluzie importanta**: bug-ul de `id_valuta` orfan e **izolat la 1 singur document in toata
baza**, in acest moment — consistent cu faptul ca UPDATE-ul extins care il face vizibil e cod de
cateva zile (runda curenta). Nu e nevoie de o reparare in masa; celelalte 234 de documente cu total
NULL au alta cauza (majoritar #1, cunoscuta si acceptata prin design) si nu trebuie atinse de
reparatia acestui bug.
## 4. Reparatie propusa, in trei straturi (fara aplicare)
### Client (VFP)
In `ArticoleNotaEditor.ModificaNomenclator`
(`COMUN\programe\ofacturare_editare.prg:1069`), extinde garda existenta ca sa blocheze
nomenclatorul de valuta pe liniile unui document care nu e in valuta:
```
IF !This.Editabil() OR Reccount('tvd') = 0 OR !This.AreNomenclator(m.lcCamp) OR Nvl(tvd.id_vanzare_set, 0) <> 0 ;
OR (m.lcCamp == 'nume_val' AND Nvl(tvanz.in_valuta,0) <> 1)
RETURN .F.
ENDIF
```
(presupune ca `tvanz` e vizibil din contextul clasei — de verificat la implementare; alternativ,
o proprietate `This.oForm.lDocInValuta` populata la incarcare). Efect: pe o factura in RON,
coloana `nume_val` devine needitabila, la fel ca liniile din seturi — elimina posibilitatea de a
introduce `id_valuta` orfan prin UI.
### DB — `recalculeaza_totaluri_vanzari`
Doua schimbari, ambele minime:
1. **Restrange conditia de conversie** la cazul in care documentul insusi e in valuta, nu cand
linia are o valuta diferita de RON pe un document RON (elimina exact vulnerabilitatea gasita):
```sql
-- inainte:
(case when (lnInValuta = 1 or vd.id_valuta <> pack_def.GetIdMonedaNationala())
then ROUND(vc.curs * vd.pret / vc.multiplicator, lnPreciziePretV)
else vd.pret end) as pret_ron
-- dupa:
(case when lnInValuta = 1
then ROUND(NVL(vc.curs,1) * vd.pret / NVL(vc.multiplicator,1), lnPreciziePretV)
else vd.pret end) as pret_ron
```
Motivatie: pe un document `in_valuta=0`, `vd.pret` e prin definitie deja in RON (asa scrie
`ScrieArticoleFacturaEditate` liniile, indiferent de `id_valuta` afisat) — conversia n-are ce sa
faca acolo, indiferent ce `id_valuta` a ajuns (corect sau nu) pe linie. Pastrez `NVL(vc.curs,1)`
doar ca ultima plasa de siguranta pe ramura `lnInValuta=1` (document CHIAR in valuta) — daca
acolo lipseste cursul e o eroare de date reala care merita semnalata, nu ascunsa; de discutat cu
Marius daca `NVL(...,1)` e acceptabil acolo sau daca ar trebui sa opreasca salvarea cu eroare in
loc sa scrie o suma gresita tacut. **Nu propun `NVL` necondiționat pe ramura de conversie
reala** — ar masca o factura in valuta cu curs lipsa, scriind un total fals dar nenul, mai greu
de observat decat un NULL.
2. **Plasa finala de siguranta la UPDATE**, dupa modelul "fill-safe" deja folosit in
`ff_2026_08_06_12_COMUN_VANZARI_BACKFILL.sql`, ca sa nu se mai poata suprascrie tacut o valoare
corecta cu NULL, indiferent ce cauza noua ar aparea in viitor:
```sql
update vanzari
set total_fara_tva = NVL(lnTotalFaraTVA, total_fara_tva),
total_tva = NVL(lnTotalTVA, total_tva),
total_cu_tva = NVL(lnTotalCuTVA, total_cu_tva),
...
where id_vanzare = V_ID_VANZARE;
```
Cu un `dbms_output`/log undeva (tabela de erori aplicative, daca exista una) cand recalculul
iese NULL, ca sa nu treaca neobservat un caz nou.
### Date (script propus, NU RULAT)
```sql
-- 1. Corectie punctuala pentru SSS 100037 (id_vanzare=1054): id_valuta-ul liniei 1602 revine la
-- RON, aliniat cu documentul (in_valuta=0) si cu linia-sursa din aviz (id_vanzare_det=1599,
-- care avea deja id_valuta=3).
UPDATE VANZARI_DETALII
SET ID_VALUTA = pack_def.GetIdMonedaNationala()
WHERE ID_VANZARE_DET = 1602
AND ID_VANZARE = 1054;
COMMIT;
-- 2. Retrigger recalcul dupa fix (acelasi apel pe care il face si formularul la salvare)
BEGIN
pack_facturare.recalculeaza_totaluri_vanzari(1054);
END;
/
COMMIT;
```
Restrans STRICT la acest document — cele 234 de alte randuri cu total NULL au alta cauza (§3) si nu
intra in scopul acestei reparatii. Daca se doreste o baleiere completa dupa ce fix-ul de la client
e livrat (sa nu mai apara alte cazuri noi), propun mai intai o interogare de monitorizare
periodica (varianta de query C/F de mai sus), nu o corectie automata in masa.
## Ce ramane NEVERIFICAT (runda 2)
- Cauza celor 114 documente "neexplicate" (mult mai vechi, posibil legate de
`discount_evidentiat IS NULL`) — iese din scopul cererii, semnalez doar ca exista.
- Daca `tvanz` e efectiv vizibil ca alias in contextul `ArticoleNotaEditor.ModificaNomenclator` la
momentul apelului (necesar pentru fix-ul de client propus) — de verificat la implementare, nu am
urmarit domeniul complet de vizibilitate al claselor `.vc2`.

View File

@@ -0,0 +1,327 @@
# R6 - butonul "Modificare articol" din frm_facturi + explicatie TVA - diagnostic
Raspuns la cerinta: pe formularul de facturare `frm_facturi` (lista facturi/avize/proforme, NU
`frm_modific2024`), butonul care edita azi `explicatie`+`taxcode` sa expuna si "explicatie TVA",
cu sincronizare automata `explicatie TVA -> taxcode`. Fiecare afirmatie e **VERIFICAT** (citit din
sursa, `fisier:linie`) - nu apare nimic marcat NESTABILIT, tot ce s-a cerut s-a putut confirma din
cod.
---
## A. Localizarea `frm_facturi`
Clasa `frm_facturi` (nu form `.scx` separat) - definita in
`COMUN\clase\ofacturare_comun.vc2:1168`:
```
DEFINE CLASS frm_facturi AS _frmbase OF "_frm_base.vcx"
```
Lant de mostenire: `frm_facturi -> _frmbase (_frm_base.vc2:7) -> _form (_baza.vc2:157) -> form`.
Instantiat din `roafacturare.prg` (meniul principal), nu are `.scx` propriu - toata definitia e in
binarul `ofacturare_comun.vcx`.
---
## B. Butonul "Modificare articol"
Obiect **`But_modifica2`**, definit in `frm_facturi` la `ofacturare_comun.vc2:1436-1444`:
```
ADD OBJECT 'But_modifica2' AS but_modifica WITH ;
Anchor = 12, ;
caction = do_modifica_explicatie, ;
Caption = "", ;
Left = 710, ;
Name = "But_modifica2", ;
TabIndex = 27, ;
ToolTipText = "Modificare explicatie articol", ;
Top = 341
*< END OBJECT: ClassLib="cmd_butoane.vcx" BaseClass="commandbutton" />
```
`Caption` e gol (e un buton cu picture, nu text); `ToolTipText` = "Modificare explicatie
articol". Clasa `but_modifica` (`cmd_butoane.vc2:184-197`) nu are `Click` propriu - mosteneste
`buton.Click` din `_cmd_base.vc2:41-71`, care e un dispatcher generic:
```
PROCEDURE Click
Local lcAction, lcCommand, lcListaParametri
lcAction = This.cAction
...
If Type('this.parent') = 'O' And Pemstatus(This.Parent,lcAction,5)
lcCommand = [this.Parent.] + lcAction + lcListaParametri
&lcCommand
Else
If Type('this.parent.PARENT') = 'O' And Pemstatus(This.Parent.Parent,lcAction,5)
lcCommand = [this.Parent.Parent.] + lcAction + lcListaParametri
&lcCommand
Else
If Pemstatus(Thisform,lcAction,5)
lcCommand = [thisform.] + lcAction + lcListaParametri
&lcCommand
Endif
Endif
Endif
ENDPROC
```
Cu `caction = do_modifica_explicatie` si parintele `But_modifica2` fiind direct `frm_facturi`
(fara container intermediar cu aceeasi metoda), rezultatul e efectiv `thisform.do_modifica_explicatie()`.
Corpul integral al metodei (`ofacturare_comun.vc2:4642-4659`):
```
PROCEDURE do_modifica_explicatie
If Reccount('crsDetalii') > 0
update_saft_taxtable()
Select crsDetalii
Scatter Name poRec Memo
poRec.explicatie = NVL(poRec.explicatie, ' ')
If poRec.sters = 0
ofrmmodificare = Createobject("frm_modifica_articol_factura")
ofrmmodificare.Show()
If gnButon = 1
Thisform.actualizeaza_grid2()
Endif
Endif
Endif
ENDPROC
```
---
## C. Ce deschide butonul
Un formular modal separat: **`frm_modifica_articol_factura`**
(`ofacturare_comun.vc2:5129-5253`), clasa `AS frm_termin_renunt OF "_frm_child.vcx"` (dialog
copil standard, cu `But_termin1`/`But_renunt1`). Titlu: `Lb_titlu_alb_b121.Caption = "Modifică
explicație articol"` (`:5163`).
Campuri editabile azi, cu `ControlSource` si linii:
| control | tip | ControlSource | label | linie |
|---|---|---|---|---|
| `Ed_tx_simplu1._edbase1` | editbox | `porec.explicatie` | "Explicație" (+ "( maxim N caractere )" adaugat in `Init`) | `:5213-5223` |
| `cbo_saft` | combobox | `poRec.taxcode` | "Cod taxa" (`lbSaft`) | `:5185-5199` |
`cbo_saft` e legat direct pe `poRec.taxcode` (BoundColumn=4), cu:
```
RowSource = "Select taxname, tip, procent_taxa, taxcode From saft_taxtable where tva = 1 order by taxcode Into Cursor crsTaxTableX"
```
adica lista tuturor codurilor SAF-T (cursor local `saft_taxtable`, populat de
`update_saft_taxtable()` apelat inainte de deschidere) - **nu exista azi nicio legatura cu
explicatia TVA**; userul alege direct un `taxcode` dintr-o lista plata de coduri fiscale.
Niciun alt camp (cantitate, pret, articol, id_jtva_coloana) nu e pe acest dialog.
---
## D. Cum se scrie inapoi - si confirmarea premisei "nu modifica valorile"
Salvare in `inainte_de_do_termin` (`ofacturare_comun.vc2:5225-5233`):
```
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
```
Scrierea e **direct in Oracle**, prin `pack_facturare.modifica_explicatie_articol(id_vanzare_det,
explicatie, id_util, taxcode)`. Cercetare anterioara deja confirmase corpul acestei proceduri
(`docs\cercetare\rec_cale_vanzari_detalii.md:195-199`, citat identic aici pentru trasabilitate):
```sql
UPDATE VANZARI_DETALII SET EXPLICATIE = V_EXPLICATIE, TAXCODE = V_TAXCODE
WHERE ID_VANZARE_DET = V_ID_VANZARE_DET
```
**Confirmat: doar 2 coloane se scriu** (`explicatie`, `taxcode`). Nu se ating `cantitate`, `pret`,
`pret_cu_tva`, `proc_tvav`, `id_jtva_coloana` si nu exista niciun apel de recalcul de totaluri
(`gcs.vact_tot`/`vrul_tot`, note contabile, rulaje) in acest flux - premisa utilizatorului
("campuri care nu modifica valorile") **e corecta pentru starea de azi a acestor doua campuri**.
---
## E. "Explicatie TVA" ca data + sincronizarea cu taxcode - sablonul deja aplicat in frm_modific2024
Campul din baza este **`id_jtva_coloana`** (nu `taxcode`) - nomenclator `vjtva_coloane`
(`id_jtva_coloana, denumire, cota_tva`). `taxcode` e un cod SAF-T/e-Factura separat, derivat din
`id_jtva_coloana` + context (partener, data, tip TVA), nu ales direct de nomenclatorul de
explicatii.
Sablonul de sincronizare cerut de utilizator **exista deja**, aplicat in runda 5 pe
`frm_modific2024` / `pgfArticole.PAGE3.grdArticoleFactura` (cursor `tvd`), documentat in
`docs\raport_runda5_omodificari.md` si `docs\propunere_runda4_note_sincronizare_tva.md` (sectiunea
5, "varianta B"). Cod citit direct din sursa curenta (`COMUN\programe\ofacturare_editare.prg`,
`ArticoleNotaEditor.ModificaNomenclator`, `:1100-1108`):
```
CASE m.lcCamp == 'id_jtva_coloana'
*!* cota vine din explicatia aleasa, ca la randul de nota; taxcode-ul SAF-T se recoreleaza dupa ea
REPLACE id_jtva_coloana WITH loCauta.id_jtva_coloana, ;
proc_tvav WITH (Nvl(loCauta.cota_tva, 0) + 100) / 100
This.oForm.UpdateExplicatieSAFTArt()
This.oForm.calculeaza_valori_articol()
```
`UpdateExplicatieSAFTArt` (`omodificari.vc2:15049-15074`) e "jumatatea taxcode" a sincronizarii -
deriva `taxcode` din `id_jtva_coloana` prin functia comuna `GetTaxCodeIdPart`:
```
PROCEDURE updateexplicatiesaftart
IF !m.gl406
RETURN
ENDIF
...
lnIdJtva = tvd.id_jtva_coloana
lnIdPart = Nvl(tAct.id_partc, tAct.id_partd)
...
lnTaxCode = GetTaxCodeIdPart(m.gnAn, m.gnLuna, m.ldDataAct, m.lnIdJtva, m.lnIdPart, m.llN50, m.llN100, m.llNeexigibil)
Replace taxcode With m.lnTaxCode In tvd
This.pgfArticole.PAGE3.grdArticoleFactura.cTaxcodeArt.Refresh()
ENDPROC
```
Relatia exacta explicatie TVA -> taxcode: **`id_jtva_coloana` (+ an/luna/data act/partener/steaguri
N50-N100/neexigibil) -> `GetTaxCodeIdPart()` -> `taxcode`**. Nu e un `Do Case` simplu pe
`id_jtva_coloana`; `GetTaxCodeIdPart` (functie globala, `COMUN\programe\oproceduri_comune.prg:6059-6125`,
NEschimbata pentru aceasta cerere) incapsuleaza toata logica SAF-T 406.
**Garda `gl406`**: `UpdateExplicatieSAFTArt` (si perechile ei `Rul`/simplu) nu fac nimic daca
`gl406 = .F.` (`COMUN\programe\oinit_optiuni.prg:524`, flag global de optiuni). Adica sincronizarea
taxcode <- explicatie TVA e activa doar cand raportarea SAF-T 406 e activata pentru firma curenta -
de verificat la implementare daca `frm_facturi` are acces la acelasi `gl406`.
---
## F. Cum se deschide azi nomenclatorul de explicatie TVA
Doua rutine **globale, refolosibile din orice formular** (nu legate de contextul lui
`frm_modific2024`):
1. **`caut_explicatie_tva(tnIdJtva, tnPornire, tlDesktop, tlTipEx, tnCotaTva)`**
(`COMUN\programe\ocautare.prg:3174-3238`) - deschide dialogul de cautare pe
`select id_jtva_coloana, denumire, cota_tva from vjtva_coloane`, filtrat implicit pe
`id_jtva_coloana > 0` (cand `tlTipEx` e gol) si, daca `tnCotaTva > 0`, suplimentar pe
`cota_tva = tnCotaTva` (`:3218-3220`, filtru adaugat tot in runda asta). Returneaza obiectul
`loCauta` din `cauta_alfa` (are `.id_jtva_coloana`, `.denumire`, `.cota_tva`).
2. **`GetTaxCodeIdPart(an, luna, data_act, id_jtva, id_part, n50, n100, neexigibil)`**
(`COMUN\programe\oproceduri_comune.prg:6059-6125`) - returneaza `taxcode`-ul SAF-T.
Apelul concret cu filtrul de cota (`ArticoleNotaEditor.CautaExplicatieTva`,
`ofacturare_editare.prg:1142-1147`):
```
PROCEDURE CautaExplicatieTva
LOCAL lnCotaFiltru
lnCotaFiltru = Round((Nvl(tvd.proc_tvav, 0) - 1) * 100, 2)
RETURN caut_explicatie_tva(Nvl(tvd.id_jtva_coloana, -1), , , , m.lnCotaFiltru)
ENDPROC
```
Ambele functii sunt `.prg`-uri globale incarcate `ADDITIVE` la pornirea aplicatiei (deci apelabile
si din `frm_facturi`) - **nu cer nicio extragere de cod**, doar apel direct.
---
## G. `frm_facturi` deja afiseaza explicatia TVA - doar needitabil
Grila `grid_detalii` din `frm_facturi` are deja doua coloane needitabile pentru asta
(`ofacturare_comun.vc2:1679-1687`):
```
Column24.ControlSource = "jtva_coloana", ;
Column24.Name = "cExplicatieTVA", ;
Column24.ReadOnly = .T., ;
...
Column25.ControlSource = "jtva_coloana_ex", ;
Column25.Name = "cExplicatieTVAEx", ;
Column25.ReadOnly = .T., ;
```
cu header-ele "Explicatie TVA" / "Explicatie TVA 2" (`:1920-1946`). `jtva_coloana`/`jtva_coloana_ex`
sunt (dupa tiparul din restul fisierului, ex. `:2107-2109`) denumirile derivate din
`jtva_coloane`/`jtva_coloane_explicatii`, nu ID-uri brute - afisare, nu editare. Nu exista alt
traseu in `frm_facturi` unde utilizatorul alege interactiv o explicatie TVA (nu la adaugare de
linie - acest formular nu are adaugare de linii, e formular de listare/editare metadate, nu de
compunere a facturii).
Concluzie G: campul exista deja ca **read-only** in grid; nu trebuie adaugata coloana in grid,
doar expunerea editabila in dialogul de modificare (punctul C).
---
## H. Riscul - taxcode NU e neutru daca se copiaza tot sablonul
**Rezultat cheie, marcat apasat:** sablonul din E (runda 5, `frm_modific2024`) **nu e neutru
valoric**. La alegerea unei explicatii TVA, pe langa `taxcode`, se rescrie explicit **`proc_tvav`**
(cota TVA a liniei) si se cheama **`calculeaza_valori_articol()`**, care recalculeaza valoarea
liniei si bara de totaluri (`docs\raport_runda5_omodificari.md`, linia 41 din tabelul de apeluri
`ActualizeazaBaraTotaluri`, si citatul de la punctul E de mai sus). Adica in acel context,
"explicatie TVA" **schimba** valorile documentului daca explicatia aleasa are alta cota decat cea
curenta - filtrul de cota din `caut_explicatie_tva` (punctul F) doar restrange lista, nu blocheaza
o alegere cu cota diferita daca userul o cauta explicit sau daca linia are `proc_tvav` gol/0
(caz in care filtrul nu se aplica deloc, `ocautare.prg:3218`: `If ... tnCotaTva > 0`).
Pentru `taxcode` insusi, in fluxul actual: `taxcode` **nu e citit nicaieri in calculul
totalurilor VFP** - cautare in `COMUN\programe\ofacturare_comun.prg` si in schema campurilor din
`ofacturare_comun.vc2` (punctul D) nu arata `taxcode` folosit in nicio expresie de `valctva`,
`valtva`, `total_cu_tva` sau similar; acelea se calculeaza din `proc_tvav`/`pret`/`cantitate`,
coloane distincte. `taxcode` e strict o eticheta de raportare SAF-T/e-Factura, stocata pe linie.
**Deci verdictul depinde strict de ce se implementeaza:**
- **Daca se reface `pack_facturare.modifica_explicatie_articol` sa scrie DOAR `id_jtva_coloana`
(nou parametru) + `taxcode` derivat, FARA sa atinga `proc_tvav`** - si daca dialogul de
`frm_facturi` foloseste `caut_explicatie_tva` cu filtrul de cota calculat din
`poRec.proc_tvav` curent (ca sa nu se poata alege o explicatie de alta cota) - atunci
operatia ramane neutra valoric, exact ce cere utilizatorul. Aceasta e o **varianta redusa**
a sablonului din E, nu o copiere 1:1.
- **Daca se copiaza sablonul din E exact cum e** (inclusiv rescrierea `proc_tvav` +
recalcul) - operatia **NU mai e neutra**: ar schimba cota TVA si valorile unei facturi
**deja emise**, fara mecanismul de recalcul de totaluri/note/rulaje care exista in
`frm_modific2024` (acolo editarea e pe cursoare locale, salvata printr-un flux dedicat de
recalcul; `frm_facturi` nu are asa ceva - e un UPDATE punctual pe o singura linie, deja
emisa). Risc fiscal si de coerenta (SAF-T/e-Factura ar raporta un `taxcode` care nu mai
corespunde cu `proc_tvav`/totalurile deja emise, sau invers, daca doar taxcode se
sincronizeaza fara proc_tvav).
**Recomandare de proiectare** (schita, fara cod aplicat): varianta redusa de mai sus -
dialog `frm_modifica_articol_factura` capata un al treilea camp (combo pe explicatie TVA,
`caut_explicatie_tva` filtrat pe cota curenta a liniei, ca in `CautaExplicatieTva`), la alegere
se recalculeaza doar `taxcode` local (`GetTaxCodeIdPart`, acelasi apel ca `UpdateExplicatieSAFTArt`
dar fara `REPLACE proc_tvav`), iar la salvare `pack_facturare.modifica_explicatie_articol` capata
un parametru nou `id_jtva_coloana` (schimbare PL/SQL, in afara acestui repo VFP - de coordonat
separat) pe langa `explicatie`/`taxcode` existente. Cod nou estimat: mic pe partea VFP (~30-40
linii: un combo nou + un handler de alegere + un parametru in apelul SQL), dar cere **schimbare de
pachet Oracle** (`pack_facturare.modifica_explicatie_articol`) care nu exista in acest working
copy si trebuie facuta/aprobata separat in `DATABASE`.
---
## Rezumat surse
| ce | fisier:linie |
|---|---|
| clasa `frm_facturi` | `COMUN\clase\ofacturare_comun.vc2:1168` |
| butonul `But_modifica2` | `COMUN\clase\ofacturare_comun.vc2:1436-1444` |
| dispatcher `buton.Click` | `COMUN\clase\_cmd_base.vc2:41-71` |
| `do_modifica_explicatie` | `COMUN\clase\ofacturare_comun.vc2:4642-4659` |
| dialog `frm_modifica_articol_factura` | `COMUN\clase\ofacturare_comun.vc2:5129-5253` |
| salvare (`inainte_de_do_termin`) | `COMUN\clase\ofacturare_comun.vc2:5225-5233` |
| grid `cExplicatieTVA`/`cExplicatieTVAEx` (read-only) | `COMUN\clase\ofacturare_comun.vc2:1679-1687`, `:1920-1946` |
| sablon sincronizare (runda 5) | `COMUN\programe\ofacturare_editare.prg:1100-1147`, `COMUN\clase\omodificari.vc2:15049-15074` |
| `caut_explicatie_tva` | `COMUN\programe\ocautare.prg:3174-3238` |
| `GetTaxCodeIdPart` | `COMUN\programe\oproceduri_comune.prg:6059-6125` |
| `gl406` | `COMUN\programe\oinit_optiuni.prg:524` |
| procedura Oracle (citata din cercetare anterioara) | `docs\cercetare\rec_cale_vanzari_detalii.md:195-199` |

View File

@@ -0,0 +1,236 @@
# Cercetare: eticheta care duce la editarea facturii emise
Sarcina: gasirea locului (locurilor) unde apare textul care duce la editarea facturii emise
(formularul `frm_modific2024`, `COMUN\clase\omodificari.vc2`), pentru a evalua propunerea
utilizatorului de a-l redenumi in **"editare factura (note, rulaje, articole)"**.
Concluzie scurta: **nu exista un bar de meniu `.mnx` clasic** cu acest text. Punctul real de
intrare e un **popup dinamic construit din `xmenu(...)` intr-un fisier `.vc2`** (deci
write-back-abil prin `txt2vcx.ps1`, nu editare manuala in IDE cum s-a presupus initial in brief).
Vezi punctul C pentru corectia asta.
---
## A. Toate locurile unde apare eticheta / unde s-ar putea afla
Cautari facute (rezultate zero pentru textul cerut in `.mnx`/`.mn2`):
- `grep -in "modific\|editeaz\|editare" Meniuri\vanzare1.mn2 ... vanzare5.mn2` -> **zero hit-uri**.
- `grep -ln "modific\|editeaz\|editare" Meniuri\*.mn2 COMUN\meniuri\*.mn2` -> doar
`Meniuri\model.mn2` si `Meniuri\roafacturare.mn2`, ambele **irelevante**:
- `Meniuri\roafacturare.mn2:50` — `DEFINE BAR 3 OF Ajutor PROMPT "\<Jurnal modificari"` (jurnal
de audit al aplicatiei, nu editare factura).
- `Meniuri\model.mn2:14` — `DEFINE BAR 2 OF Shortcut PROMPT "\<Modificare raport"` (editare
raport definit de utilizator, nu factura).
- Niciun `.mnx`/`.mn2` din proiect sau din `COMUN\meniuri\` nu contine referinta la
`frm_modific2024` sau `ofacturare_editare` (verificat cu grep pe toate cele 43 de fisiere unde
apare `frm_modific2024` — niciunul e `.mn2`).
**Locul real** e un buton de toolbar generic + un popup `xmenu()` construit in cod, in
`COMUN\clase\ofacturare_comun.vc2`, pe formularul `frm_facturi` (lista de facturi emise):
1. **Butonul de toolbar** — clasa `but_modifica`, definita generic in
`COMUN\clase\cmd_butoane.vc2:182-197`:
```
DEFINE CLASS but_modifica AS buton OF "_cmd_base.vcx"
caction = inainte_de_do_modifica
...
ToolTipText = "Modificare (CTRL+M)"
```
Acest buton e adaugat **direct in clasa de baza a tuturor formularelor**,
`COMUN\clase\_frm_base.vc2:1103` (`ADD OBJECT 'But_modifica1' AS but_modifica ...`), deci apare
pe **toate** formularele-lista din suita ROA (parteneri, personal, stocuri, note contabile,
facturi etc.), nu doar pe cel de facturi. Textul lui (`"Modificare (CTRL+M)"`) e generic si
hardcodat, nu vine din `Locale\` (vezi punctul C).
2. **Popup-ul specific facturii** — `frm_facturi` (definit in `ofacturare.vcx`/`ofacturare_comun.vc2`)
suprascrie handler-ul butonului cu propriul sau meniu contextual:
```
COMUN\clase\ofacturare_comun.vc2:4928-4937
PROCEDURE inainte_de_do_modifica
Local lnOptiune
lnOptiune = xmenu('Modificare \<date factura;Editare \<factura (articole, cantitati, preturi)')
Do Case
Case lnOptiune = 1
This.do_modifica()
Case lnOptiune = 2
This.do_editare_factura()
Endcase
ENDPROC
```
Textul exact de azi al liniei (verificat byte cu byte, ASCII curat, fara diacritice corupte):
`xmenu('Modificare \<date factura;Editare \<factura (articole, cantitati, preturi)')` —
`COMUN\clase\ofacturare_comun.vc2:4930`.
Deci utilizatorul care apasa butonul "Modificare (CTRL+M)" pe lista de facturi vede un
mini-meniu cu **doua optiuni**:
- "Modificare date factura" (accelerator D) → `This.do_modifica()` — ruta generica
`afisjurcom.do_modifica` din `comun.vc2:2222-2572` (aceeasi metoda partajata de foarte multe
tipuri de documente din suita, cu ramuri Do Case pentru fiecare).
- "Editare factura (articole, cantitati, preturi)" (accelerator F) →
`This.do_editare_factura()` — **asta e ruta directa catre `frm_modific2024`**.
## B. Lantul real: eticheta -> comanda -> `frm_modific2024`
```
frm_facturi (lista facturi emise)
-> but_modifica1 (mostenit din _frm_base, ToolTipText "Modificare (CTRL+M)")
-> click -> inainte_de_do_modifica() [ofacturare_comun.vc2:4928]
-> xmenu('Modificare \<date factura;Editare \<factura (articole, cantitati, preturi)')
[ofacturare_comun.vc2:4930]
-> optiunea 2 -> This.do_editare_factura() [ofacturare_comun.vc2:3715]
-> seteaza tipul, verifica drepturi (This.lactiv3), Reccount('crsfacturi')...
-> Omodif = Createobject([frm_modific2024], lnIdSet) [ofacturare_comun.vc2:3796]
-> Omodif.Show()
```
Confirmare independenta a clasei tinta prin `vfp_symbols.ps1 -Where "ofacturare_comun.vc2:4935"`:
`frm_facturi.inainte_de_do_modifica [method] ofacturare_comun.vc2:4928-4937`.
**Alte cai catre acelasi formular**, gasite prin `grep -rn frm_modific2024` (43 fisiere), toate
insa in alt context, nu din meniul principal de facturare al utilizatorului final:
- `COMUN\clase\comun.vc2:2439` — `afisjurcom.do_modifica` (metoda generica, ramura
`IF gnAn >= ... ELSE Createobject([frm_modific])`) — e ruta **veche/generica** de "nota
contabila", partajata cu toate produsele ROA (nu specifica facturii).
- `COMUN\clase\anaf_efactura.vc2:13087` — deschidere `frm_modific2024` din fluxul e-Factura ANAF
(verificare/validare, nu din butonul de lista).
- `COMUN\ferestre\frm_import_note_facturi_clienti.sc2:898` si
`COMUN\ferestre\frm_initializare_facturi_balanta.sc2:1806` — fluxuri de import/initializare,
nu editarea unei facturi deja emise, deci **nu intra in scopul cerut**.
Pentru cererea utilizatorului ("editarea facturii emise"), **singurul loc relevant si cu text
apropiat de ce isi doreste** este `ofacturare_comun.vc2:4930` (optiunea 2 din popup).
## C. Cum se schimba textul in practica
- **Nu e in `.mnx`/`.mn2`.** Nu exista un `DEFINE BAR ... PROMPT` cu acest text nicaieri in
proiect. Deci nu e nevoie de editare manuala in IDE-ul de meniuri.
- **E literal, hardcodat intr-o metoda `.vc2`**: `COMUN\clase\ofacturare_comun.vc2:4930`, in
interiorul clasei `frm_facturi`, metoda `inainte_de_do_modifica`. Fisierul `.vc2` **este**
write-back-abil prin `txt2vcx.ps1` (regula generala din `CLAUDE.md`: doar `.vc2`/`.sc2` accepta
scriere inapoi in binar). Deci schimbarea propusa de utilizator **se poate face prin editare
text + `txt2vcx.ps1`**, nu necesita interventie manuala in IDE.
- Text propus de utilizator ca inlocuitor pentru optiunea 2: in loc de
`"Editare \<factura (articole, cantitati, preturi)"` ar deveni ceva de forma
`"Editare \<factura (note, rulaje, articole)"` — pastrand acceleratorul `\<F`. (Nu am facut
modificarea — doar raportez unde s-ar face, conform interdictiilor primite.)
- **`ToolTipText = "Modificare (CTRL+M)"`** din `cmd_butoane.vc2:194` **NU trebuie schimbat** — e
folosit de zeci de formulare din toata suita ROA (am numarat >100 instantieri `but_modifica` in
`.vc2`/`.sc2`, majoritatea in `onomenclatoare.vc2`, `onom_sal.vc2`, `oparteneri.vc2` etc.,
niciunul specific facturii). Un text de forma "editare factura..." pus acolo ar minti pe toate
celelalte ecrane (parteneri, personal, stocuri...).
- **Nu exista mecanism `.mnx` compilat separat de regenerat** pentru aceasta schimbare — nefiind
vorba de un `.mnx`, nu se aplica `git_sync.ps1`/recompilare de meniu; e nevoie doar de rebuild-ul
normal al proiectului dupa modificarea `.vcx` (Project > Build din VFP IDE), conform fluxului
standard descris in `CLAUDE.md`.
## C-bis. Localizare (`Locale\`)
- Directorul `Locale\` **exista dar e complet gol** in aceasta copie de lucru
(`find D:\ROA\ROAFACTURARE\Locale -type f` → 0 fisiere). Nu exista tabele DBF de traducere
incarcate aici.
- Nu exista niciun apel `Traduc(...)`/`goApp.Traduc(...)` in jurul `ToolTipText`-ului `but_modifica`
sau al liniei `xmenu(...)` de mai sus — am cautat in `cmd_butoane.vc2` si `_frm_base.vc2`
(`grep -n "Traduc\b"` → zero hit-uri in ambele). Singura potrivire pentru "traduc" in tot
proiectul e in `comun.vc2:2157-2158`, cod comentat (`*!* IF THIS.filtru_traducere()#""`),
nefolosit si nelegat de acest buton.
- **Concluzie**: textul e o constanta Romana hardcodata direct in `.vc2`, nu vine din nicio sursa
de traducere. Nu exista alte limbi de verificat pentru acest text.
## D. Captions reale ale paginilor din `frm_modific2024`
Premisa utilizatorului ("trei pagini: Note, Rulaje si Articole") **nu se potriveste exact cu
Caption-urile din formular**. Am verificat cu:
```
vfp_symbols.ps1 -Find frm_modific2024 -> class frm_modific2024 omodificari.vc2:6375-16836
awk 'NR==6375,NR==16836' omodificari.vc2 | grep -n "AS _pageframe\|PAGE.\.Caption"
```
Rezultat: **exista un singur `PageFrame`** in formular, `pgfArticole`
(`omodificari.vc2:8714-8732`, adica linia absoluta ≈ `6375+2341` in indexare relativa), cu
**3 pagini**, niciuna captionata "Note":
```
COMUN\clase\omodificari.vc2:8725 PAGE1.Caption = "Rulaje materii prime, materiale, marfuri, obiecte inventar (303)"
COMUN\clase\omodificari.vc2:8728 PAGE2.Caption = "Rulaje obiecte inventar in folosinta (8039)"
COMUN\clase\omodificari.vc2:8731 PAGE3.Caption = "Articole factura"
```
Nu exista nicio pagina/tab captionat literal **"Note"**. Zona de "note" (liniile notei contabile —
coloane precum "Explicatie C", "Cont", "Cota TVA" etc., verificate prin
`grep -n "Caption = \"" ` in intervalul clasei) e **corpul principal al formularului** (grid-ul de
baza `grid1`), nu o pagina din pageframe. Deci structura reala e:
- corp principal (nepaginat) = liniile notei contabile ("Note" e denumirea informala folosita in
discutii, nu un Caption efectiv);
- `pgfArticole.PAGE1` + `PAGE2` = doua variante de "Rulaje" (materiale 303 / obiecte inventar 8039);
- `pgfArticole.PAGE3` = "Articole factura".
Titlul formularului insusi e generic: `Lb_titlu_alb_b121.Caption = "MODIFICARE"`
(`omodificari.vc2:6934`, in Init-ul clasei `frm_modific2024`), nu mentioneaza nici el "factura" sau
"note/rulaje/articole".
**Recomandare pentru textul de meniu**: formularea "note, rulaje, articole" e conceptual corecta
(reflecta cele 3 zone reale), dar nu e un citat al Caption-urilor efective de pe ecran — daca se
doreste consistenta stricta cu ce vede utilizatorul, ar trebui fie sa se ajusteze si Caption-urile
paginilor (schimbare separata, in afara scopului acestei cercetari), fie sa se accepte ca eticheta
de meniu descrie zonele functional, nu litera Caption-urilor.
## E. Pagina Articole e conditionata
**Da, e strict conditionata** — nu apare intotdeauna. Dovezi:
```
COMUN\clase\omodificari.vc2:14927 This.lAreArticoleVanzari = .F.
COMUN\clase\omodificari.vc2:14928 IF "OFACTURARE_EDITARE" $ Upper(Set("Procedure")) AND Reccount('tact') > 0
COMUN\clase\omodificari.vc2:14929 IncarcaVanzareDinNota('tact')
COMUN\clase\omodificari.vc2:14930 IF Reccount('tvanz') = 1
COMUN\clase\omodificari.vc2:14931 This.lAreArticoleVanzari = .T.
COMUN\clase\omodificari.vc2:14932 This.nIdVanzare = tvanz.id_vanzare
COMUN\clase\omodificari.vc2:14933 This.nTipVanzare = tvanz.tip
COMUN\clase\omodificari.vc2:14934 This.lArticoleReadOnly = EsteInEFactura(Nvl(tvanz.id_fact,0))
COMUN\clase\omodificari.vc2:14935 ENDIF
COMUN\clase\omodificari.vc2:14936 ENDIF
...
COMUN\clase\omodificari.vc2:14941 IF This.lAreArticoleVanzari
COMUN\clase\omodificari.vc2:... This.pgfArticole.PageCount = 3
COMUN\clase\omodificari.vc2:... ELSE
COMUN\clase\omodificari.vc2:14958 This.pgfArticole.PageCount = 2
COMUN\clase\omodificari.vc2:14959 This.but_nouR.Enabled = .F.
COMUN\clase\omodificari.vc2:14960 ENDIF
```
Conditia exacta pentru ca pagina "Articole factura" sa fie vizibila:
1. formularul a fost deschis din contextul de editare factura (`"OFACTURARE_EDITARE" $
Upper(Set("Procedure"))` — adica din lantul `frm_facturi -> do_editare_factura`, nu din ruta
generica `do_modifica`);
2. nota are cel putin o linie de tip articol (`Reccount('tact') > 0`);
3. exista **exact o** vanzare (factura) legata de nota (`Reccount('tvanz') = 1`).
Daca oricare din conditii lipseste, `pgfArticole.PageCount = 2` — pagina "Articole factura" **nu
apare deloc**, ramanand doar cele doua pagini de "Rulaje". In plus, chiar cand pagina apare, poate
fi **doar-citire** (`This.lArticoleReadOnly = EsteInEFactura(...)`) daca factura a fost deja
trimisa in e-Factura ANAF (`omodificari.vc2:14934`, plus dezactivari vizibile in
`omodificari.vc2:14943-14947`: `but_nouR.Enabled`, `txtDiscountArt.ReadOnly`,
`lblArticoleReadOnly.Visible`, `cmdSincronizeazaArticole.Enabled`).
**Implicatie pentru textul de meniu**: un text care promite mereu "articole" (ex. "editare factura
(note, rulaje, articole)") poate **minti in unele cazuri** — cand nota nu are exact o vanzare
asociata sau nu are linii de tip articol, utilizatorul nu vede pagina de articole deloc, desi
eticheta i-a promis-o. E o observatie de continut, nu un blocaj — decizia de a accepta acest risc
(eticheta descrie "ce se poate edita in general", nu "ce va vedea de fiecare data") apartine
utilizatorului/echipei.
## Sumar raspunsuri la intrebarile din brief
| Intrebare | Raspuns |
|---|---|
| Exista eticheta `.mnx` literala? | **NU** — cautare completa in toate `.mn2`-urile proiectului si `COMUN\meniuri\`, zero hit-uri pe "editare factura" sau variante |
| Unde e reala eticheta? | `COMUN\clase\ofacturare_comun.vc2:4930`, in `xmenu()`, clasa `frm_facturi`, metoda `inainte_de_do_modifica` |
| Textul de azi | `Editare \<factura (articole, cantitati, preturi)` |
| Vine din `Locale\`? | Nu — `Locale\` e goala, nu exista apel `Traduc()` in jurul acestui text |
| Se schimba in `.mnx` sau in cod? | In cod (`.vc2`), write-back prin `txt2vcx.ps1` — **nu** necesita editare manuala in IDE |
| Paginile se numesc Note/Rulaje/Articole? | Nu literal — un singur pageframe cu 3 pagini: "Rulaje materii prime..." / "Rulaje obiecte inventar..." / "Articole factura"; zona de note e corpul principal, nepaginat |
| Pagina Articole e mereu prezenta? | Nu — conditionata de 3 conditii (`omodificari.vc2:14927-14936`), poate lipsi sau fi doar-citire |
Nimic din NESTABILIT — toate afirmatiile au dovada `fisier:linie` verificata direct in text.

View File

@@ -0,0 +1,255 @@
# Cercetare R6 — nomenclatoare in gridul Articole (frm_modific2024)
Zona: `COMUN\clase\omodificari.vc2` (formularul `frm_modific2024`), gridul
`pgfArticole.PAGE3.grdArticoleFactura`. Dispecerul comun e in
`COMUN\programe\ofacturare_editare.prg`, clasa `ArticoleNotaEditor`.
## A. Inventarul coloanelor gridului Articole
Bloc `ADD OBJECT 'pgfArticole.PAGE3.grdArticoleFactura'` incepe la
`omodificari.vc2:12330`; lista completa a coloanelor (`ControlSource` + `Name`) e la
`omodificari.vc2:12349-12471`.
| Coloana (Name) | ControlSource | Linie ControlSource | Legata de nomenclator? |
|---|---|---|---|
| cDenumireArt (Column1) | `tvd.denumire` | `omodificari.vc2:12349` | DA — articol |
| cCodmatArt (Column2) | `tvd.codmat` | `omodificari.vc2:12356` | nu |
| cSerieArt (Column3) | `tvd.serie` | `omodificari.vc2:12363` | nu |
| cLotArt (Column4) | `tvd.lot` | `omodificari.vc2:12370` | nu |
| cCantitateArt (Column5) | `tvd.cantitate` | `omodificari.vc2:12377` | nu |
| cPretArt (Column6) | `tvd.pret` | `omodificari.vc2:12386` | nu |
| cPretAchizitieArt (Column7) | `tvd.pret_achizitie` | `omodificari.vc2:12395` | nu |
| cPretCuTvaArt (Column8) | `tvd.pret_cu_tva` | `omodificari.vc2:12404` | nu (checkbox propriu) |
| cProcTvavArt (Column9) | `tvd.proc_tvav` | `omodificari.vc2:12412` | nu |
| cDiscountUnitarArt (Column10) | `tvd.discount_unitar` | `omodificari.vc2:12421` | nu |
| **cGestiuneArt (Column11)** | `tvd.nume_gestiune` | `omodificari.vc2:12430` | **DA — gestiune** |
| **cValutaArt (Column12)** | `tvd.nume_val` | `omodificari.vc2:12437` | **DA — valuta** |
| cExplicatieArt (Column13) | `tvd.explicatie` | `omodificari.vc2:12444` | nu (text liber) |
| **cTaxcodeArt (Column14)** | `tvd.taxcode` | `omodificari.vc2:12451` | **DA — cod TVA SAF-T** |
| cValoareArt (Column15) | `tvd.valoare` | `omodificari.vc2:12458` | nu (calculat) |
| **cExplicatieTvaArt (Column16)** | `Iif(Seek(Nvl(tvd.id_jtva_coloana,0),'crsJtvaTemp','id_jtva'),crsJtvaTemp.denumire,'')` | `omodificari.vc2:12467` | **DA — explicatie TVA / id_jtva_coloana** |
Cele 5 coloane legate de un nomenclator sunt exact cele acceptate de dispecer —
vezi `ArticoleNotaEditor::AreNomenclator` (`ofacturare_editare.prg:978-981`):
```
PROCEDURE AreNomenclator
LPARAMETERS tcControl
RETURN Inlist(This.NumeCamp(m.tcControl), 'denumire', 'nume_gestiune', 'nume_val', 'taxcode', 'id_jtva_coloana')
ENDPROC
```
Observatie importanta pentru cauza de la punctul D: **`cTaxcodeArt` si `cExplicatieTvaArt` au
Column.ReadOnly = .F.** (`omodificari.vc2:12379` / linia `Column14.ReadOnly = .F.` la
`omodificari.vc2` in blocul 12451-12457, respectiv `Column16.ReadOnly = .F.` in blocul
12467-12471), dar **controlul `Text1` din interiorul lor are `ReadOnly = .T.`** — vezi
`omodificari.vc2:12759` (cTaxcodeArt.Text1) si `omodificari.vc2:12594` (cExplicatieTvaArt.Text1).
Diferenta reala dintre ele nu e ReadOnly-ul (identic), ci **daca `ControlSource` e camp direct
sau expresie** — vezi punctul D.
## B. Ce evenimente exista azi pe fiecare coloana cu nomenclator
| Coloana | InteractiveChange | DblClick | Buton lateral "Modificare" (`But_modificaR`) |
|---|---|---|---|
| cDenumireArt | DA — `omodificari.vc2:16710-16714` | **nu exista** | DA (comun, vezi E) |
| cGestiuneArt | DA — `omodificari.vc2:16739-16743` | **nu exista** | DA (comun) |
| cValutaArt | DA — `omodificari.vc2:16826-16830` | **nu exista** | DA (comun) |
| cTaxcodeArt | DA — `omodificari.vc2:16815-16819` | **nu exista** | DA (comun) |
| cExplicatieTvaArt | DA — `omodificari.vc2:16728-16732` (exista, dar nu se declanseaza — vezi D) | **nu exista** | DA (comun) |
Corpul e identic pentru toate cele 5 (exemplu, taxcode):
```
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cTaxcodeArt.Text1.InteractiveChange
Local loEditor
loEditor = Createobject('ArticoleNotaEditor', Thisform)
loEditor.ModificaNomenclator(Thisform.pccontrol)
ENDPROC
```
Confirmare ca `DblClick` nu exista deloc pe `grdArticoleFactura` sau pe coloanele lui: cautare
`InteractiveChange|DblClick` in tot `omodificari.vc2` — singurele `DblClick` din fisier sunt pe
alt grid, `grdRequest` (`omodificari.vc2:390` si `omodificari.vc2:408`, vezi F).
## C. Dispecerul comun — `ArticoleNotaEditor::ModificaNomenclator`
Definit in `COMUN\programe\ofacturare_editare.prg:1069-1137`, clasa `ArticoleNotaEditor`
(`ofacturare_editare.prg:947-1165`).
Semnatura: `PROCEDURE ModificaNomenclator LPARAMETERS tcControl` — primeste un string de forma
`alias.camp` (ex. `tvd.taxcode`, `tvd.id_jtva_coloana`).
Garda de intrare (`ofacturare_editare.prg:1073-1076`):
```
lcCamp = This.NumeCamp(m.tcControl)
IF !This.Editabil() OR Reccount('tvd') = 0 OR !This.AreNomenclator(m.lcCamp) OR Nvl(tvd.id_vanzare_set, 0) <> 0
RETURN .F.
ENDIF
```
`Do Case` de deschidere a nomenclatorului (`ofacturare_editare.prg:1078-1089`):
```
DO CASE
CASE m.lcCamp == 'denumire'
loCauta = caut_articol()
CASE m.lcCamp == 'nume_gestiune'
loCauta = caut_gestiune()
CASE m.lcCamp == 'nume_val'
loCauta = caut_valuta()
CASE m.lcCamp == 'id_jtva_coloana'
loCauta = This.CautaExplicatieTva()
OTHERWISE
loCauta = This.CautaCodTva()
ENDCASE
```
Discrimineaza dupa **numele campului** extras din `tcControl` (nu dupa numele coloanei din grid,
nu dupa un parametru separat) — `NumeCamp` (`ofacturare_editare.prg:966-974`) taie tot ce e
inainte de primul `.`. `taxcode` cade pe ramura `OTHERWISE` (`CautaCodTva`,
`ofacturare_editare.prg:1152-1158`).
Dupa alegere, al doilea `Do Case` (`ofacturare_editare.prg:1096-1132`) scrie valorile alese
inapoi in `tvd`, cu ramura dedicata pentru `id_jtva_coloana`
(`ofacturare_editare.prg:1124-1129`, recalculeaza `proc_tvav` si cheama
`This.oForm.UpdateExplicatieSAFTArt()` + `calculeaza_valori_articol()`).
## D. De ce "explicatie TVA" nu se declanseaza pe InteractiveChange
**Nu e o garda care blocheaza si nu e o ramura lipsa in dispecer** — `id_jtva_coloana` e in
`AreNomenclator` (`ofacturare_editare.prg:980`) si are ramura proprie in `Do Case`
(`ofacturare_editare.prg:1085-1086`). Handlerul `InteractiveChange` exista si e corect
(`omodificari.vc2:16728-16732`).
**Cauza e ca evenimentul `InteractiveChange` pur si simplu nu se produce niciodata din tastare
pe aceasta coloana**, pentru ca `ControlSource`-ul ei e o expresie, nu un camp direct:
```
Column16.ControlSource = "Iif(Seek(Nvl(tvd.id_jtva_coloana,0),'crsJtvaTemp','id_jtva'),crsJtvaTemp.denumire,'')"
```
(`omodificari.vc2:12467`)
versus `cTaxcodeArt`:
```
Column14.ControlSource = "tvd.taxcode"
```
(`omodificari.vc2:12451`)
In VFP, un `Textbox` intr-o coloana de grid cu `ReadOnly = .T.` (asa cum sunt ambele,
`omodificari.vc2:12759` si `omodificari.vc2:12594`) nu accepta editare directa de text, dar cand
`Column.ControlSource` e un **camp real dintr-un cursor navigabil** ("bound field"), gridul
VFP declanseaza cautarea incrementala nativa la tastare (type-ahead / seek pe recordset): tasta
apasata muta recordul curent la prima potrivire, ceea ce schimba valoarea afisata legata de acel
camp — iar aceasta schimbare de `Value` e cea care declanseaza `InteractiveChange`. Cand
`ControlSource` e o **expresie** (ca la `cExplicatieTvaArt`, care afiseaza denumirea din
`crsJtvaTemp` via `Seek`, nu campul `tvd.id_jtva_coloana` in sine), gridul nu are un camp
indexabil pe care sa faca seek — mecanismul de cautare incrementala nu se activeaza, deci
`Value`-ul textboxului nu se schimba niciodata din tastare, deci `InteractiveChange` nu are cum
sa se declanseze prin tastare pe aceasta coloana.
Chiar comentariul din cod confirma asta, la `GotFocus`-ul aceleiasi coloane
(`omodificari.vc2:16723`):
```
*!* coloana afiseaza denumirea din crsJtvaTemp, deci ControlSource e expresie - campul editat se da explicit
```
adica autorul stia deja ca `ControlSource` e o expresie si de-aia a scris explicit campul real
(`tvd.id_jtva_coloana`) in `Thisform.pccontrol` la `GotFocus`, in loc sa se bazeze pe
`This.ControlSource` (cum fac celelalte coloane). Explicatia asta e chiar mecanismul care face ca
butonul lateral sa functioneze — vezi E.
Dublu-click nu se declanseaza pe nicio coloana pentru ca **pur si simplu nu exista niciun handler
`DblClick`** pe `grdArticoleFactura` (vezi B) — nu e o problema specifica de "explicatie TVA", e
valabil identic si pentru `taxcode`, doar ca la `taxcode` utilizatorul are alternativa tastarii,
care functioneaza (camp direct).
## E. Cum ajunge butonul lateral "Modificare" sa deschida explicatie TVA
Handler: `PROCEDURE But_modificaR.Click`, `omodificari.vc2:15319-15326`:
```
PROCEDURE But_modificaR.Click
Local lcRul, loEditor
IF thisform.pgfArticole.ActivePage = 3
loEditor = Createobject('ArticoleNotaEditor', Thisform)
loEditor.ModificaNomenclator(thisform.pccontrol)
RETURN
ENDIF
...
ENDPROC
```
Cheia e ca butonul **nu citeste `ControlSource`-ul coloanei curente** — foloseste direct
`Thisform.pccontrol`, o proprietate a formularului scrisa de fiecare `GotFocus` al coloanei
active, inainte ca utilizatorul sa apuce sa tasteze ceva. Pentru `cExplicatieTvaArt`, `GotFocus`
scrie explicit campul real (`omodificari.vc2:16722-16726`):
```
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cExplicatieTvaArt.Text1.GotFocus
*!* coloana afiseaza denumirea din crsJtvaTemp, deci ControlSource e expresie - campul editat se da explicit
Thisform.pccontrol = 'tvd.id_jtva_coloana'
Thisform.pncolumnorder = This.Parent.ColumnOrder
ENDPROC
```
Pentru celelalte 4 coloane, `GotFocus` foloseste `Alltrim(This.ControlSource)` direct (ex.
`cTaxcodeArt.Text1.GotFocus`, `omodificari.vc2:16810-16813`), ceea ce functioneaza pentru ele
pentru ca acolo `ControlSource` chiar e campul real.
Deci calea functioneaza pentru ca **`GotFocus` — care se declanseaza la orice intrare in celula,
prin click simplu, tab sau navigare cu sagetile, indiferent de tipul de `ControlSource` — a
populat deja `Thisform.pccontrol` corect inainte ca butonul sa fie apasat.** Butonul nu depinde
deloc de `InteractiveChange`; de-aia merge la explicatie TVA desi tastarea nu merge.
## F. Cost estimat pentru DblClick pe fiecare coloana cu nomenclator
**Exista deja un sablon direct aplicabil**, chiar in cadrul modelului de "coloana Text1 in grid cu
InteractiveChange care deschide ceva" — `grdRequest`, alt grid din acelasi formular:
```
PROCEDURE grdRequest.cLabelItem.Text1.DblClick
Thisform.lEditMode = .T.
ENDPROC
```
(`omodificari.vc2:390-392`)
```
PROCEDURE grdRequest.cValoareDefaultText.Text1.DblClick
If !Empty(Nvl(crsRequest.fis_lista, ''))
Thisform.Selectfromlookup()
Endif
ENDPROC
```
(`omodificari.vc2:408-412`)
Confirmare pentru VFP: intr-adevar, evenimentul se pune pe **controlul din interiorul coloanei**
(`Column.Text1.DblClick`), exact cum arata cele doua exemple de mai sus si exact cum sunt puse
deja `InteractiveChange` si `GotFocus` pe `grdArticoleFactura` (`Column.Text1.InteractiveChange`,
`Column.Text1.GotFocus`) — nu exista niciun `DblClick` pus pe nivelul `Column` insusi in tot
fisierul. Modelul `grdRequest` e potrivit ca forma (unde se pune evenimentul), dar nu ca
continut — acolo `DblClick` face lucruri specifice acelui grid (comuta un mod de editare / arata
un lookup din alta zona), nu apeleaza un dispecer de nomenclator.
Sablonul de continut potrivit e chiar `InteractiveChange`-urile deja existente pe
`grdArticoleFactura` (`omodificari.vc2:16710-16714`, `16728-16732`, `16739-16743`,
`16815-16819`, `16826-16830`) — toate identice, 3 linii:
```
Local loEditor
loEditor = Createobject('ArticoleNotaEditor', Thisform)
loEditor.ModificaNomenclator(Thisform.pccontrol)
```
Costul e mic si uniform: adaugarea a 5 metode noi `DblClick` (una per coloana cu nomenclator),
fiecare cu acelasi corp de 3 linii de mai sus. Pentru ca toate cele 5 `GotFocus` seteaza deja
`Thisform.pccontrol` corect — fie din `ControlSource` (denumire, gestiune, valuta, taxcode), fie
explicit (explicatie TVA, `omodificari.vc2:16724`) — folosirea lui `Thisform.pccontrol` in
`DblClick` functioneaza uniform pe toate cele 5, inclusiv pe explicatie TVA, fara sa depinda de
mecanismul de seek care lipseste la coloana cu `ControlSource` expresie. Practic acelasi apel pe
care il face deja `But_modificaR.Click` (`omodificari.vc2:15319-15326`, vezi E).
Nu e nevoie de nicio schimbare in dispecer (`ModificaNomenclator`) sau in `AreNomenclator` — ele
deja accepta toate cele 5 campuri.

View File

@@ -0,0 +1,229 @@
# R6: zecimale la Pret si aspect gri al campurilor editabile, grid Articole (frm_modific2024)
Cercetare, fara modificari de cod. Zona: `COMUN\clase\omodificari.vc2`, clasa `frm_modific2024`
(liniile 6375-16838), grid-ul de articole al facturii editate: `pgfArticole.PAGE3.grdArticoleFactura`
(nu vechiul `Grid1`/`Column1..44` — acela e alt grid, RecordSource `"tACT"`, folosit pentru notele
contabile, nu pentru articolele facturii; grid-ul de articole are 16 coloane numite, ADD OBJECT la
`omodificari.vc2:12330`).
## Partea 1 — zecimalele la pret
### A. Coloanele monetare din grid si masca lor
RecordSource al grid-ului = cursorul `tvd` (creat in `COMUN\programe\ofacturare_editare.prg:278`,
populat in `IncarcaArticoleFactura`, `ofacturare_editare.prg:294-328`, din view-ul Oracle
`VVANZARI_ARTICOLE`).
| Coloana (Header) | Name | ControlSource | Format/InputMask | Linie |
|---|---|---|---|---|
| Pret | cPretArt | `tvd.pret` | `get_mask(12,gnPPRET)` | `omodificari.vc2:12391` |
| Pret achizitie | cPretAchizitieArt | `tvd.pret_achizitie` | `get_mask(12,gnPPRET)` | `omodificari.vc2:12400` |
| Pret cu TVA | cPretCuTvaArt | `tvd.pret_cu_tva` | **fara Format/InputMask** | `omodificari.vc2:12404-12409` |
| Discount unitar | cDiscountUnitarArt | `tvd.discount_unitar` | `get_mask(12,gnPPRET)` | `omodificari.vc2:12426` |
| Cantitate | cCantitateArt | `tvd.cantitate` | `get_mask(12,gnPCANT)` | `omodificari.vc2:12382` |
| Valoare | cValoareArt | `tvd.valoare` | `get_mask(14,gnPa)` | `omodificari.vc2:12463` |
| TVA % | cProcTvavArt | `tvd.proc_tvav` | `"9.99"` (fix, 2 zecimale) | `omodificari.vc2:12417` |
Structura cursorului `tvd` (`ofacturare_editare.prg:278-281`):
```
pret N(14,4) NULL, pret_cu_tva N(14,4) NULL, proc_tvav N(6,4) NULL, discount_unitar N(14,4) NULL,
..., pret_achizitie N(14,4) NULL, ..., cantitate N(12,3) NULL (nu e in acest citat, vezi mai jos)
```
Precizia campurilor din cursor e **hardcodata** (4 zecimale la pret/pret_cu_tva/discount_unitar/
pret_achizitie, 3 la cantitate — vezi linia completa `ofacturare_editare.prg:278`), independent de
`gnPPret`/`gnPCant`. Asta e doar plafonul de stocare; ce se AFISEAZA il decide InputMask-ul de pe
coloana (cand exista).
### B. De unde vin cele 3 zecimale afisate la "Pret"
**Nu dintr-un bug de masca lipsa.** Coloana "Pret" (`cPretArt`, `tvd.pret`) ARE InputMask legat de
o variabila globala: `get_mask(12,gnPPRET)`, `omodificari.vc2:12391`. Daca `gnPPRET` = 3 in mediul
curent, InputMask-ul produce exact 3 zecimale — comportamentul e "controlat de o variabila globala",
exact ce cere utilizatorul. **Problema e ca variabila legata e cea gresita**: `gnPPRET`, nu
`gnPPretV` — vezi punctul D.
### C. Ce este `gnPPret` si care e sablonul consacrat
`gnPPret` e o variabila PUBLIC incarcata din firma (parametru `OPTIUNI.PPRET`), documentata deja in
cercetarea anterioara `docs\cercetare\rec_fact008_pret_achizitie_zecimale.md:28`:
> `OPTIUNI.PPRET` (precizia pretului de achizitie) = **3** (in mediul de productie investigat acolo)
N-am gasit in acest proiect linia exacta unde `gnPPret` e citit din `OPTIUNI` si atribuit (cautare
in `COMUN\programe\oinit_optiuni.prg` — gaseste `gnPA`, `gnPC`, `gnPCurs`, `gnPPretV`, `gnZ`, dar
nu `gnPPret`; probabil incarcat generic, printr-un mecanism care nu foloseste literal sirul
`"gnPPret ="`) — **NESTABILIT exact unde**, dar valoarea si semantica (precizie **achizitie**) sunt
confirmate atat de raportul anterior cat si de tiparul de folosire de mai jos.
Exista o familie de variabile de precizie, cu roluri distincte, confirmate prin comentarii si prin
folosire consecventa in cod:
- `gnPPret` — precizie **pret de achizitie** (cost). Folosire: `ofacturare_stoc.prg:364`
`Alltrim(Str(poArticol.pret_achizitie,18,Max(gnPPret,4)))`; masti pe `pret_achizitie` in
`ointroduceri.vc2:16390,19638` (`get_mask(12,gnPPret)`, coloana NIR).
- `gnPPretV` — precizie **pret de vanzare**, comentata explicit
`COMUN\programe\oinit_optiuni.prg:515`: `Store 0 To gnPPretV && nr. de zecimale pret vanzare`.
Folosire masiva in `ofacturare_comun.prg` (peste 40 de locuri: `pretftva`, `pretctva`,
`pretv_orig`, `discountftva` etc. — toate rotunjite/formatate cu `gnPPretV`) si, esential, la
**scrierea efectiva a pretului de vanzare al liniei de factura**:
`ofacturare_stoc.prg:367` — `Alltrim(Str(poArticol.Pret,18,Iif(poDate.in_valuta=1,gnPVal,gnPPretV)))`
(in functia care insereaza randul in `VANZARI_DETALII`, aceeasi coloana `PRET` pe care o incarca
`tvd.pret`).
- `gnPCant` — precizie cantitate (`get_mask(12,gnPCANT)`, `omodificari.vc2:12382`).
- `gnPc` (`gnPC`) — precizie de calcul/valoare (`get_mask(14,gnPa)`/`gnPc` la coloana Valoare).
- `gnPa` (`gnPA`) — precizie de afisare generica (folosit la `cValoareArt`).
Sablonul consacrat de constructie a mastii: `get_mask(nrCaractere, nrZecimale)`
(`COMUN\programe\proceduri_comune.prg:1090-1110`) — construieste un sir `"999 999.999"` cu atatea
`9` dupa punct cate zecimale i se dau; **nu exista logica speciala in `get_mask`**, deci diferenta
de comportament vine strict din CE variabila i se paseaza la fiecare coloana.
### D. Modelul corect exista deja, in acelasi fisier de clase — `ofacturare.vc2`
`COMUN\clase\ofacturare.vc2:3489-3498`, grid de rulaje/stoc, doua coloane invecinate:
```
Column5.ControlSource = "pret", ;
Column5.InputMask = (get_mask(14,gnPPret)), ; && cost / achizitie
Column5.Name = "cPret", ;
...
Column6.ControlSource = "pretv", ;
Column6.InputMask = (get_mask(14,gnPPretV)), ; && vanzare
Column6.Name = "cPretv", ;
```
Acelasi tipar apare si in `COMUN\clase\ocomenzi.vc2:1174`
(`This._grdrow2.cPret.InputMask = get_mask(14,gnPPretV)` — pret de vanzare pe comanda) si in
`ointroduceri.vc2:10071,16390,19638` (`get_mask(...,gnPPret)` — toate pe campuri de **achizitie**,
in formularele de NIR).
**Regula confirmata in tot codul suitei: campul care reprezinta pretul de ACHIZITIE foloseste
`gnPPret`; campul care reprezinta pretul de VANZARE al liniei foloseste `gnPPretV`.** Chiar scrierea
in Oracle a coloanei `VANZARI_DETALII.PRET` (aceeasi coloana din care se umple `tvd.pret`) respecta
aceasta regula: `ofacturare_stoc.prg:367` foloseste `gnPPretV`, nu `gnPPret`.
### Concluzie Partea 1 — defectul
`omodificari.vc2:12391` — `Column6.InputMask = (get_mask(12,gnPPRET))` pentru **`cPretArt`**
(`tvd.pret`, coloana "Pret" = pretul de VANZARE al liniei) foloseste variabila gresita din familie:
**`gnPPRET`** (precizie achizitie) in loc de **`gnPPretV`** (precizie vanzare, cea corecta pentru
acest camp). Coloana vecina, `cPretAchizitieArt` (`omodificari.vc2:12400`, `tvd.pret_achizitie`),
foloseste corect `gnPPRET` — e evident ca la scrierea acestei sectiuni de grid s-a copiat masca de
la achizitie si pe coloana de vanzare de langa ea.
Daca in mediul reclamat `OPTIUNI.PPRET` (achizitie) = 3 si `OPTIUNI.PPRETV` (vanzare) e alta
valoare (tipic 2), exact asta explica simptomul: coloana "Pret" (vanzare) afiseaza 3 zecimale
pentru ca "imprumuta" precizia de achizitie, nu pe cea de vanzare.
Coloana "Pret cu TVA" (`cPretCuTvaArt`) nu are deloc InputMask (`omodificari.vc2:12404-12409`) — nu
e sursa celor "3 zecimale" reclamate la "Pret", dar e un defect inrudit: afiseaza precizia bruta a
campului din cursor (4 zecimale, `pret_cu_tva N(14,4)`), necontrolata de nicio variabila globala.
**Schita de interventie** (fara aplicare): `Column6.InputMask = (get_mask(12,gnPPRET))` ->
`Column6.InputMask = (get_mask(12,gnPPretV))` la `omodificari.vc2:12391`, dupa modelul din
`ofacturare.vc2:3497` / `ocomenzi.vc2:1174`.
## Partea 2 — fundalul gri al campurilor editabile
### E-F. Sursa reala a griului si divergenta Column.ReadOnly vs Text1.ReadOnly
Confirmat: aproape toate coloanele au `DynamicForeColor` (nu `BackColor`) la nivel de coloana —
un singur `IIF` pentru culoarea textului dupa `tvd.sters`/`id_vanzare_set`, identic pe toate cele
16 coloane (ex. `omodificari.vc2:12350`). **Niciun `DynamicBackColor`, niciun `Enabled=.F.`** pe
coloane. Culoarea de fundal reala vine din proprietatea `BackColor` fixa a controlului `Text1`
inclus in fiecare coloana (`ADD OBJECT '...Text1' AS textbox`), si **nu coincide cu
`Column.ReadOnly`**:
| Camp | Column.ReadOnly | Column linie | Text1.ReadOnly | Text1.BackColor | Text1 linii |
|---|---|---|---|---|---|
| Denumire (cDenumireArt) | `.F.` (editabil) | 12354 | **`.T.`** | **225,225,225 (gri)** | 12527-12536 |
| Cod material (cCodmatArt) | `.T.` | 12361 | `.T.` | 225,225,225 | 12506-12515 (consistent) |
| Serie (cSerieArt) | `.F.` (editabil) | 12368 | **`.T.`** | **225,225,225 (gri)** | 12734-12743 |
| Lot (cLotArt) | `.F.` (editabil) | 12375 | **`.T.`** | **225,225,225 (gri)** | 12631-12640 |
| Cantitate (cCantitateArt) | `.F.` | 12384 | `.F.` | 255,255,255 (alb) | 12485-12494 (consistent) |
| Pret (cPretArt) | `.F.` | 12393 | `.F.` | 255,255,255 (alb) | 12673-12682 (consistent) |
| Pret achizitie (cPretAchizitieArt) | `.F.` | 12402 | `.F.` | 255,255,255 (alb) | 12652-12661 (consistent) |
| TVA % (cProcTvavArt) | `.F.` (editabil) | 12419 | **`.T.`** | **225,225,225 (gri)** | 12713-12722 |
| Discount unitar (cDiscountUnitarArt) | `.T.` | 12428 | `.T.` | 225,225,225 | 12548-12557 (consistent) |
| Gestiune (cGestiuneArt) | `.F.` (editabil) | 12435 | **`.T.`** | **225,225,225 (gri)** | 12610-12619 |
| Valuta (cValutaArt) | `.F.` (editabil) | 12442 | **`.T.`** | **225,225,225 (gri)** | 12797-12806 |
| Explicatie (cExplicatieArt) | `.F.` (editabil) | 12449 | **`.T.`** | **225,225,225 (gri)** | 12569-12578 |
| Taxcode (cTaxcodeArt) | `.F.` (editabil) | 12456 | **`.T.`** | **225,225,225 (gri)** | 12755-12764 |
| Valoare (cValoareArt) | `.T.` | 12465 | `.T.` | 225,225,225 | 12776-12785 (consistent) |
| Explicatie TVA (cExplicatieTvaArt) | `.F.` (editabil) | 12472 | **`.T.`** | **225,225,225 (gri)** | 12589-12598 |
**9 din 16 coloane sunt declarate editabile la nivel de `Column.ReadOnly=.F.` dar controlul `Text1`
inclus e `ReadOnly=.T.` cu fundal gri (225,225,225).** In VFP, cand o coloana de grid contine un
control custom (nu editorul implicit), editabilitatea reala vine din controlul inclus, nu din
`Column.ReadOnly` — deci aceste 9 campuri **nu accepta tastare directa in celula**, indiferent ce
spune `Column.ReadOnly`. Asta explica de ce aratau gri: griul e corect pentru "nu se editeaza prin
tastare directa".
Pentru unele din ele exista insa un mecanism real de editare, prin dialog, nu prin tastare:
`cGestiuneArt.Text1.GotFocus` (`omodificari.vc2:12734`... de fapt liniile de cod sunt la
`16734-16744`) seteaza `Thisform.pccontrol` si `InteractiveChange` deschide un editor:
```
PROCEDURE pgfArticole.PAGE3.grdArticoleFactura.cGestiuneArt.Text1.InteractiveChange (16739)
loEditor = Createobject('ArticoleNotaEditor', Thisform)
loEditor.ModificaNomenclator(Thisform.pccontrol)
```
Acelasi tipar la `cExplicatieTvaArt` (`16722-16732`). Pentru `cSerieArt`, `cLotArt`, `cExplicatieArt`
exista doar un handler `.When` (`16745`, `16716`, `16804`) care blocheaza intrarea in celula cand
`Thisform.lArticoleReadOnly` sau `id_vanzare_set<>0`, dar **niciun cod care sa comute
`Text1.ReadOnly` la `.F.`** — n-am gasit nicio atribuire dinamica `.Text1.ReadOnly = ...` in tot
corpul clasei `frm_modific2024` pentru aceste coloane (cautare exhaustiva pe intervalul
6375-16838). **Concluzie verificabila pe cod**: cu configuratia actuala, `cSerieArt`, `cLotArt`,
`cExplicatieArt`, `cValutaArt`, `cTaxcodeArt`, `cDenumireArt`, `cProcTvavArt` **nu au niciun
mecanism de editare activ in acest grid** — nici tastare (Text1.ReadOnly=.T.), nici editor prin
dublu-clic/InteractiveChange (spre deosebire de cGestiuneArt/cExplicatieTvaArt, care au editor).
Daca utilizatorul le percepe ca "editabile", fie testeaza cu date/build vechi, fie editarea se
intampla prin alta cale neinventariata aici (buton `but_modificaR` — vezi mai jos, NESTABILIT unde
e definit click-ul lui).
`Thisform.but_modificaR.Enabled` se seteaza in `AfterRowColChange`
(`omodificari.vc2:16678-16682`) dupa coloana curenta si `!lArticoleReadOnly`, sugerand un buton
extern de "Modifica" — dar **n-am gasit definitia obiectului `but_modificaR` sau handler-ul lui de
`Click` in `omodificari.vc2`** (cautare `grep -n "but_modificaR"` in tot fisierul: doar cele 7
referinte de mai sus, toate `.Enabled = ...` sau `.Visible = ...`, niciun `ADD OBJECT` sau
`PROCEDURE but_modificaR...Click`). **NESTABILIT**: butonul e probabil mostenit dintr-o clasa
parinte in afara acestui fisier — nu s-a investigat mai departe (ar necesita cautare in
`_frm_base.vcx`/alte `.vc2` din `COMUN\clase`, in afara scopului "aceasta clasa").
### G. Conventia de culoare in restul aplicatiei
**Exista o conventie, si e vizibila chiar in acelasi fisier de clasa**, pe pagina "Rulaje"
(`pgfArticole.PAGE1.grdRulaje`, alt tab al aceluiasi formular):
- editabil prin tastare directa: `BackColor = 255,255,255` (alb) + `ReadOnly = .F.` — ex.
`cPret.Text1`, `omodificari.vc2:9990-9999`.
- editabil prin dialog (GotFocus seteaza `pccontrol`, InteractiveChange deschide editor):
`BackColor = 160,255,205` (**verde deschis**) + `ReadOnly = .F.` — ex. `cGest.Text1`
(`omodificari.vc2:9689-9698`) si `cDenumire.Text1` (`omodificari.vc2:9595-9604`).
- needitabil: `BackColor = 225,225,225` (gri) + `ReadOnly = .T.`.
Pe pagina "Articole" (`grdArticoleFactura`, PAGE3 — grid-ul reclamat), campul `cGestiuneArt` are
**exact acelasi mecanism de editare prin dialog** ca `cGest` de pe pagina Rulaje (acelasi tipar
GotFocus/InteractiveChange/`ArticoleNotaEditor`), dar culoarea lui e **gri + ReadOnly=.T.**
(`omodificari.vc2:12610-12619`), nu verde + ReadOnly=.F. ca omologul lui de pe Rulaje. **Asta e
dovada directa a inconsistentei**: acelasi tipar functional (editare prin editor extern) e codat
cu doua culori diferite in doua pagini ale aceluiasi formular — verde (corect, semnaleaza "se
editeaza altfel") pe Rulaje, gri (gresit, semnaleaza "nu se editeaza") pe Articole.
## Rezumatul defectelor gasite (fara aplicare)
1. **Zecimale**: `Column6.InputMask` la `cPretArt` (`omodificari.vc2:12391`) foloseste `gnPPRET`
(precizie achizitie) in loc de `gnPPretV` (precizie vanzare) — schita: schimbat argumentul.
2. **Aspect gri**: 9 coloane (`cDenumireArt`, `cSerieArt`, `cLotArt`, `cProcTvavArt`,
`cGestiuneArt`, `cValutaArt`, `cExplicatieArt`, `cTaxcodeArt`, `cExplicatieTvaArt`) au
`Column.ReadOnly=.F.` dar `Text1.ReadOnly=.T.` + `BackColor=225,225,225` — contrazic propria
lor declaratie de editabilitate. Dintre ele, doar `cGestiuneArt` si `cExplicatieTvaArt` au
mecanism real de editare (editor extern) si ar trebui colorate verde
(`160,255,205`, ca modelul de pe Rulaje) + `ReadOnly=.F.` pe Text1; restul (`cDenumireArt`,
`cSerieArt`, `cLotArt`, `cProcTvavArt`, `cValutaArt`, `cExplicatieArt`, `cTaxcodeArt`) fie n-au
deloc cale de editare activa in cod (raman corect needitabile — atunci `Column.ReadOnly` ar
trebui pus `.T.` ca sa nu mai contrazica aspectul), fie editarea lor se face pe o cale
neidentificata in aceasta cercetare (butonul `but_modificaR`, NESTABILIT unde e definit).

View File

@@ -0,0 +1,53 @@
# Regresie dupa modificarea `omodificari.vc2` (validare cantitate + garda S4b)
Context: s-au schimbat doua puncte in `COMUN\clase\omodificari.vc2` (write-back deja facut si
verificat inainte de aceasta rulare):
1. `:14386` — validarea de cantitate `Nvl(cantitate,0) <= 0` -> `= 0` (negativul e legitim: retur,
storno, linii de discount).
2. `:14438` — blocul de sincronizare S4b de la salvare a primit garda
`AND Used('tvd') AND m.lnLiniiActive > 0`.
Scopul acestei rulari: confirmarea ca suita de regresie ramane la baseline dupa aceste doua
schimbari.
## Rezultate
Fiecare suita a fost rulata individual (o singura lansare de `vfp9.exe -A -T` per apel de tool),
cu `.FXP`/`.fxp` sterse inainte de fiecare rulare si mtime-ul logului verificat imediat dupa,
inainte de a citi vreo cifra.
| suita | baseline | obtinut | mtime log | exit code | verdict |
|---|---|---|---|---|---|
| `test_adauga_linie_articol` | 20 PASS / 0 FAIL | 20 PASS / 0 FAIL | 2026-08-13 03:05:52 | 0 | OK, la baseline |
| `test_adauga_linie_valuta` | 16 PASS / 0 FAIL | 16 PASS / 0 FAIL | 2026-08-13 03:06:02 | 0 | OK, la baseline |
| `test_verdict_act_rul` | 26 PASS / 0 FAIL | 26 PASS / 0 FAIL | 2026-08-13 03:06:12 | 0 | OK, la baseline |
| `test_incarca_vanzare_din_nota` | 5 PASS / 0 FAIL (`done`) | 5 PASS / 0 FAIL (`done`) | 2026-08-13 03:06:25 | 0 | OK, la baseline |
| `test_efactura_readonly` | 23 PASS / 0 FAIL | 23 PASS / 0 FAIL | 2026-08-13 03:06:35 | 0 | OK, la baseline |
| `test_s4b_sincronizare` | 42 PASS / 0 FAIL | 42 PASS / 0 FAIL | 2026-08-13 03:06:44 | 0 | OK, la baseline |
| `test_s4b_dialog` | 35 PASS / 0 FAIL | 35 PASS / 0 FAIL | 2026-08-13 03:06:54 | 0 | OK, la baseline |
| `test_s7_rotunjire` | 6 PASS / 0 FAIL | 6 PASS / 0 FAIL | 2026-08-13 03:07:06 | 0 | OK, la baseline |
Deja rulate anterior de sesiunea principala (nu au fost reluate in aceasta misiune):
| suita | baseline | obtinut |
|---|---|---|
| `test_s5_validari_articole` | 35/0 | 35/0 (la baseline) |
| `test_page3_articole` | 14/2 | 14/2 (la baseline) |
## Suite care au cerut rerulare
Niciuna. Toate cele 8 suite rulate in aceasta misiune au pornit curat, au scris logul cu mtime
proaspat imediat dupa lansare si au iesit cu `exit=0` la prima incercare — nu a fost nevoie de
nicio garda de timp (`timeout`) declansata si nicio rerulare.
## Procese ramase
Zero procese `vfp9.exe` ramase dupa terminarea rularilor (verificat cu
`Get-Process vfp9 -ErrorAction SilentlyContinue`, rezultat gol).
## Concluzie
Toate cele 8 suite din lista primita plus cele 2 deja rulate de sesiunea principala (10 suite in
total) raporteaza cifre identice cu baseline-ul. Nu s-a observat nicio regresie in urma celor doua
modificari din `omodificari.vc2`.

View File

@@ -0,0 +1,168 @@
# S8 — matricea pe tipuri de sursa, cele 4 documente
Test: `COMUN\utile\Teste\editare_factura\test_s8_matrice_surse.prg`, log:
`COMUN\utile\Teste\editare_factura\test_s8_matrice_surse_log.txt` (rescris la fiecare rulare -
cifrele de mai jos sunt citite din log **imediat dupa fiecare rulare**, inainte de a activa
urmatorul document, si verificate independent prin `sqlplus`).
Matricea completa (4 documente) a fost rulata, cate unul pe rand, activat prin `gaCazuriActive` in
`test_s8_matrice_surse.prg`. Codul (`EditeazaDinRoafacturare`/`EditeazaDinRegistruJurnal`/
`VerificaDupaEditare`/`CalculeazaTotaluriS4b`) e neschimbat intre rulari - parametrizarea prin
`id_vanzare` a functionat neschimbata pe toate cele 4 tipuri.
## Sinteza cifrelor `REZULTAT` (autoritare, din log)
| Document | Tip sursa | REZULTAT | FAIL-uri | Cauza FAIL-urilor |
|---|---|---|---|---|
| 1048 | lista de preturi (tip 1) | **44 PASS / 0 FAIL** | - | - |
| 1055 | factura din contract (tip 2) | **44 PASS / 0 FAIL** | - | - |
| 1052 | aviz (tip 22) | **26 PASS / 1 FAIL** | 1 | garda `ReferinteDocumenteNota` blocheaza corect intrarea ROAFACTURARE (constatare, nu defect de test - vezi mai jos) |
| 1054 | factura din aviz (tip 4) | **42 PASS / 2 FAIL** | 2 | `Reccount(trul)=0` pe ambele intrari - normal pentru tip 4 (vezi mai jos), nu defect |
Niciun `EROARE` in niciunul din cele 4 loguri.
## Document 1048 — lista de preturi (tip 1)
Deja raportat integral in versiunea anterioara a acestui document (ambele intrari au reusit complet,
0 anomalii). Stare finala neschimbata de rularile ulterioare pe celelalte documente: `cod=1140918`,
`id_fact=8009658` (neschimbat), totaluri `250.01/52.50/302.51` (neschimbate), 1 linie activa.
## Document 1055 — factura din contract (tip 2)
Ambele intrari au reusit complet (`COMMIT`), fara nicio anomalie in lantul de scriere.
| | inainte | dupa ROAFACTURARE | dupa registrul jurnal |
|---|---|---|---|
| `cod` | 1140911 | 1140919 | **1140920** |
| `id_fact` | 8009680 | 8009680 | 8009680 (neschimbat) |
| totaluri | 200.00 / 42.00 / 242.00 | neschimbate | neschimbate |
| linii active | 2 | 2 | 2 |
`vact_tot`: cod 1140911 si 1140919 (notele vechi) - toate randurile `STERS=1`; cod 1140920 (nota
curenta) - toate randurile `STERS=0`.
**Constatare (nu defect)**: verdictul S4b e **divergent** pe tot parcursul - `ACT=242`, `RUL=121`
(exact jumatate), neschimbat de cele doua editari (`121 -> 121` pe ambele treceri). Verdictul e
etichetat explicit "informativ, nu e eroare" de catre `frm_modific2024` insusi (decizia din S4b:
formularul arata divergenta, nu o blocheaza). Asertiile testului nu presupun `ACT=RUL` - verifica
doar ca fiecare total ramane **concordant fata de inainte de editare**, ceea ce s-a confirmat.
## Document 1052 — aviz (tip 22)
**Nu e factura** - constatare confirmata, exact cum a semnalat briefingul.
### Intrarea ROAFACTURARE (`do_editare_factura`): BLOCATA de garda, corect
```
FAIL ... document fara referinte / netrimis in eFactura [.T.]
```
`ReferinteDocumenteNota(2026, 8, 1140908)` a intors adevarat - verificat direct in Oracle:
`ACT.id_factc = 8009677` (id_fact-ul avizului 1052) exista pe cod=1140910, care e nota lui **1054**
("factura din aviz", emisa din acest aviz). Garda functioneaza exact cum trebuie: **blocheaza
editarea unui document sursa care are deja o factura emisa din el** - nicio scriere nu s-a produs
(verificat: `cod` a ramas `1140908` neschimbat pana la intrarea urmatoare). `EsteInEFactura` nu a
contribuit (`anaf_efactura` nu are randuri pentru `id_fact=8009677`).
Aceasta e o `FAIL` de asertie **asteptata si corecta** - testul a presupus (mostenit din modelul
S5, gandit pentru facturi) ca documentul e editabil; pentru un aviz cu factura deja emisa din el,
nu e. Marcata ca atare, nu ca defect.
### Intrarea registru jurnal ROACONT (`do_modifica`): A REUSIT COMPLET, fara aceeasi garda
```
PASS ... blocul ScrieArticoleFacturaEditate s-a executat pe aceasta cale (garda satisfacuta) [.T.]
PASS ... lantul complet a reusit (COMMIT) [lnSucces=1]
```
**Constatare reala, de raportat lui Marius**: `afisjurcom.do_modifica` (`comun.vc2:2222-2569`) **nu
are garda `ReferinteDocumenteNota`/`EsteInEFactura`** in secventa reprodusa (confirmat deja indirect
de `test_s5_al_doilea_intrare.prg`, dar niciodata exercitata pana acum pe un document care CHIAR are
o referinta reala). Rezultat: editarea prin registrul jurnal a trecut pana la `COMMIT` pe un
document (1052) pe care intrarea ROAFACTURARE l-a blocat explicit din acelasi motiv.
Pe aceasta rulare **fara** consecinta vizibila (editarea nu a modificat nicio linie - doar a
realocat `cod`-ul si a refacut nota; documentul 1054, care il refera, a fost verificat neschimbat:
`cod=1140910`, `id_fact=8009679`, totaluri `252.07/52.93/305.00`, toate identice cu inainte).
Legatura structurala ramane valida pentru ca trece prin `id_fact`/`id_vanzare`, niciodata prin `cod`
(S6, deja inchis). Dar daca editarea prin ROACONT ar fi inclus si o modificare de continut pe un
document cu referinte reale, nimic nu ar fi oprit-o - asimetria intre cele doua puncte de intrare e
reala, nu doar teoretica. **Nu s-a atins `comun.vc2`/`omodificari.vc2` pentru a o corecta** (livrare
inchisa) - se raporteaza pentru decizia lui Marius.
| | inainte | dupa ROAFACTURARE | dupa registrul jurnal |
|---|---|---|---|
| `cod` | 1140908 | *(blocat, nescris)* | **1140921** |
| `id_fact` | 8009677 | - | 8009677 (neschimbat) |
| totaluri | 271.17 / 56.94 / 328.11 | - | neschimbate |
| linii active | 2 | - | 2 |
`vact_tot`: cod 1140908 (nota veche) - toate randurile `STERS=1`; cod 1140921 (nota curenta) -
toate randurile `STERS=0`. S4b: `ACT=RUL=328.11`, verdict sincronizat, neschimbat de editare.
## Document 1054 — factura din aviz (tip 4)
Ambele intrari au reusit complet (`COMMIT`), singurele 2 `FAIL` din aceasta rulare sunt pe
`Reccount(trul)>0`.
**Stabilit inainte de editare, nu ghicit**: interogare pe toate cele 6 documente `tip=4` din baza
(`145, 665, 668, 697, 974, 1054`) - **toate** au exact 2 randuri `ACT` si **0** randuri `RUL`,
indiferent de `total_cu_tva`. Tiparul e 100% consistent pe populatia completa de documente tip 4,
nu doar pe 1054 - **normal pentru tip 4**, nu o particularitate a acestui document. Explicatia
structurala plauzibila: miscarea de stoc s-a inregistrat deja la emiterea avizului sursa; factura
emisa din aviz nu mai genereaza randuri `RUL` proprii (ar dubla miscarea), doar nota contabila
(`ACT`). Cele doua `FAIL` (`Reccount(trul)>0` cerut de asertia generica, `Reccount(trul)=0` gasit)
sunt **asteptate si corecte pentru acest tip de document** - verificarea "rulaje refacute" nu e
concludenta pentru tip 4 (nu exista rulaje de refacut), nu semnaleaza un defect.
Restul verificarilor (nota veche/noua, `id_fact`, totaluri denormalizate, S4b ACT concordant,
stoc) au trecut integral pe ambele intrari.
| | inainte | dupa ROAFACTURARE | dupa registrul jurnal |
|---|---|---|---|
| `cod` | 1140910 | 1140922 | **1140923** |
| `id_fact` | 8009679 | 8009679 | 8009679 (neschimbat) |
| totaluri | 252.07 / 52.93 / 305.00 | neschimbate | neschimbate |
| linii active | 1 | 1 | 1 |
`vact_tot`: cod 1140910 si 1140922 (notele vechi) - toate randurile `STERS=1`; cod 1140923 (nota
curenta) - toate randurile `STERS=0`. S4b: `ACT=305`, `RUL=0`, verdict divergent (informativ),
neschimbat de editare (`305->305`, `0->0`).
## Verdict explicit pe cele 8 verificari cerute de plan (`plan_06_editare_factura.md:286-288`)
| # | Verificare | Verdict pe matrice |
|---|---|---|
| 1 | nota veche `STERS=1` | **PASS** pe toate cele 7 scrieri reusite (1048x2, 1055x2, 1052x1 - ROACONT, 1054x2). N/A pe 1052/ROAFACTURARE (blocat inainte de scriere, nu s-a creat nicio nota noua). |
| 2 | nota noua corecta (exista, activa, acelasi `id_fact`) | **PASS** pe toate cele 7 scrieri reusite. |
| 3 | `id_fact` neschimbat | **PASS** pe toate cele 7 - 8009658, 8009680, 8009677, 8009679 identice inainte/dupa. |
| 4 | `VANZARI`/`VANZARI_DETALII` sincronizate | **PASS** pe toate cele 7 - numar de linii active neschimbat, totaluri coerente (`ftva+tva=ctva`). |
| 5 | totalurile denormalizate corecte | **PASS** pe toate cele 7 - identice cu inainte de editare (reeditare fara modificari de continut). |
| 6 | rulajele refacute | **PASS** pe 1048 (x2), 1055 (x2), 1052 (x1, ROACONT). **FAIL asteptat** pe 1054 (x2) - `RUL=0` e normal pentru tip 4 (confirmat pe toate cele 6 documente tip 4 din baza), verificarea nu e concludenta pentru acest tip. |
| 7 | totalurile de control S4b concordante | **PASS** pe toate cele 7 - `Total ACT` si `Total RUL` raman fiecare **neschimbate fata de inainte de editare**, indiferent daca verdictul absolut e sincronizat (1048, 1052) sau divergent (1055, 1054 - divergenta insasi e preexistenta editarii, nu cauzata de ea). |
| 8 | verificarile de stoc de la emitere NU s-au declansat | **PASS** pe toate cele 7 - `gcMockUltimMesaj` ramas gol dupa fiecare lant de scriere; structural, `verifica_stoc` (`oscrie_in_fisiere.prg:92`) nu se poate declansa pentru ca ambele puncte de intrare trec `tlModificare=.T.`. |
**Al treilea caz obligatoriu** (document care nu e factura, deschis din registru jurnal, fara
pagina de articole) - deja acoperit separat, per handoff (`docs\handoff_punct6_10082026_pm.md:113`).
## Constatare de raportat separat (nu de reparat aici)
> **INCHISA — decizia lui Marius, 12.08.2026: asimetria se accepta, nu se repara.** Nu se atinge nici
> `comun.vc2`, nici `omodificari.vc2`. Sectiunea de mai jos ramane ca inventar al constatarii, **nu mai
> e o intrebare deschisa** — nu o redeschide. Motivele, in `docs\progres.md`.
**Asimetria de garda intre punctele de intrare** (sectiunea document 1052 de mai sus): intrarea
ROAFACTURARE (`ofacturare_comun.vc2`, `do_editare_factura`) verifica `ReferinteDocumenteNota`/
`EsteInEFactura` inainte de a permite editarea; intrarea registru jurnal ROACONT
(`comun.vc2`, `afisjurcom.do_modifica`) nu are aceasta garda in secventa reprodusa si a scris pana
la capat pe acelasi document pe care ROAFACTURARE l-a blocat. Nicio consecinta vizibila pe aceasta
rulare (editare fara modificari de continut, documentul care refera - 1054 - verificat neschimbat),
dar protectia lipseste structural pe calea ROACONT. `omodificari.vc2`/`comun.vc2` **nu au fost
atinse** (livrari inchise) - decizia (adaugarea gardei si pe ROACONT, sau acceptarea asimetriei) ii
revine lui Marius.
## Ramas de facut
Toate cele 4 documente din matrice au fost editate de doua ori si verificate. Nimic ramas pe
felia S8 in sine. Documentele consumate (coduri realocate, note vechi sterse ireversibil):
1048 (-> 1140918), 1052 (-> 1140921), 1054 (-> 1140923), 1055 (-> 1140920).

View File

@@ -0,0 +1,143 @@
# Cercetare: rulaje (RUL) pe factura emisă din aviz (tip = 4) — `test_s8_matrice_surse.prg:308`
## Verdict
**Asserția e greșită pentru tip = 4.** Tabela `RUL` reține exclusiv mișcări pe conturi de stoc
(`371` în datele verificate); o factură emisă din aviz nu atinge niciodată contul de stoc — ea doar
transformă creanța provizorie (`418`) în creanță fermă (`4111`) și TVA neexigibilă în TVA colectată
(`4428`→`4428`) — pentru că marfa a ieșit deja din gestiune la avizul-sursă (tip 22), care e cel ce
scrie rândurile de `RUL`. `Reccount(trul) = 0` pe un document de tip 4 e starea corectă, nu un semn
că editarea a distrus rulaje.
---
## Ce am verificat la sursă (cod + date Oracle)
### 1. De unde se umple `trul`
`IncarcaCursoareModificareNota` (`COMUN\programe\ofacturare_editare.prg:35-148`) încarcă `trul`
direct din view-ul `vrul_tot`, filtrat pe `an`/`luna`/`cod`, fără nicio sinteză sau completare:
```
COMUN\programe\ofacturare_editare.prg:76
lcSql = [select * from vrul_tot where ] + lcConditieSters + [ an = ] + Transform(m.tnAn) + [ and luna = ] + Transform(m.tnLuna) + [ and cod = ] + Alltrim(Str(m.tnCod)) + [ order by id_rul]
```
`Reccount(trul)` e deci exact numărul de rânduri deja existente în `RUL` (server) pentru acel `cod`
— nu ceva calculat sau garantat nenul de vreo regulă de business în client.
### 2. Cine scrie rulajele — și diferența pe tip de document
Scrierea efectivă e server-side, în Oracle, `PACK_CONTAFIN.SCRIE_IN_RUL` (`COMUN\docs\PACK_CONTAFIN.pck:1896-2019`):
face `INSERT INTO RUL (...) SELECT ... FROM RUL_TEMP` — deci **scrie exact ce a fost pus în
`RUL_TEMP` de partea VFP**, fără vreo ramură condiționată de tipul documentului. Tot ce contează e
dacă `RUL_TEMP` (populat din `trul`, deci din `vrul_tot` deja existent) are rânduri.
Pe partea VFP, calea de editare a facturii (cea exercitată de test, prin `frm_facturi.do_editare_factura`,
`COMUN\clase\ofacturare_comun.vc2:3799-3821`) copiază necondiționat `trul` înapoi în `RUL_TEMP` și
apelează `OSCRIE_IN_FISIERE(0,.T.,.T.)` — al treilea parametru (echivalentul lui `llRul` din editorul
generic) e **hardcodat `.T.`**, nu depinde de starea inițială:
```
COMUN\clase\ofacturare_comun.vc2:3811-3821
If Used('rul_temp')
Use In rul_temp
Endif
Select * From trul Into Cursor RUL_TEMP Readwrite
Replace All id_util With gnIdUtil, sters With 0
...
lnSucces = OSCRIE_IN_FISIERE(0,.T.,.T.)
```
Deci: dacă `trul` a fost gol la încărcare (pasul 1), rămâne gol și la scriere — codul **conservă**
starea, nu o corectează și nu o strică. (Contrast: editorul generic de jurnal contabil,
`afisjurcom.do_modifica`, `COMUN\clase\comun.vc2:2370-2482`, are un flag explicit `llRul` setat doar
dacă `Reccount(lcCursor)>0` la încărcarea inițială din `vrul_tot` — dar calea de factură nu folosește
acest flag deloc, e mereu `.T.`, ceea ce confirmă din nou că absența rândurilor din `RUL` nu e
tratată ca eroare de nicio ramură a codului.)
Originea rândurilor din `RUL` e deci strict la **crearea/finalizarea notei**, nu la editare —
`PACK_CONTAFIN.finalizeaza_modificare_nota` (apelat și la creare, și la editare) doar re-scrie ce
primește; nu am găsit nicio ramură condiționată de `tip`/`ntip` care să decidă "acest tip de
document trebuie să aibă rulaje" — decizia e implicită, dată de **ce conturi apar în notă**, nu de
tipul documentului.
### 3. Rolul lui `IN_STOC` în `ActualizeazaVerdictActRul`
`COMUN\clase\omodificari.vc2:13045-13146`. Verdictul ACT/RUL are trei stări, toate **informative,
niciodată eroare**:
```
COMUN\clase\omodificari.vc2:13129-13139
DO CASE
CASE !m.llVerdictSigur
lcVerdict = 'Verdict ACT/RUL: nu se aplica (informativ, date de import)'
CASE Abs(This.nTotalActRon - This.nTotalRulRon) <= 0.02
lcVerdict = 'Verdict ACT/RUL: sincronizat (informativ)'
OTHERWISE
lcVerdict = 'Verdict ACT/RUL: divergent (informativ, nu e eroare)'
ENDCASE
```
`in_stoc` corectează doar **suma** comparată (exclude valoarea liniilor nestocate din articolele
facturii, convertită în RON, din totalul RUL — `tvd` unde `Nvl(in_stoc,1) = 0`, linia 13111), nu
decide dacă documentul "trebuie" să aibă rulaje. Nu există în această metodă (verificat integral,
liniile 13045-13146, nu doar grep) nicio ramură specifică pentru `RUL = 0` pe tip 4 — cazul cade pur
și simplu în "divergent (informativ, nu e eroare)" dacă `nTotalActRon <> 0` și `nTotalRulRon = 0`, ceea
ce confirmă că design-ul acceptă explicit acest caz ca non-eroare.
### 4. Verificare pe date, read-only, Oracle `MARIUSM_AUTO`/`ROA_CENTRAL`
Conectat cu succes (`sqlplus.exe`, `SET TRANSACTION READ ONLY` ... `ROLLBACK`, fișier `.sql` ASCII,
fără pipe). Cod-urile curente (verificate din `VANZARI`, nu presupuse):
| id_vanzare | tip | cod (curent) | sters | RUL (cnt activ) | ACT (cnt activ) |
|---|---|---|---|---|---|
| 1048 | 1 | 1140918 | 0 | 1 | 5 |
| 1052 | 22 (aviz) | 1140921 | 0 | 4 | 12 |
| 1054 | 4 (factură din aviz) | **1140923** | 0 | **0** | 2 |
| 1055 | 2 | 1140920 | 0 | 1 | 7 |
`id_vanzare = 1054` are `cod` curent `1140923` (confirmă realocarea din test:
`1140910 -> 1140922 -> 1140923`, citită direct din `VANZARI`, nu presupusă).
Detaliu decisiv — conturile efective din `ACT`/`RUL` pentru perechea aviz→factură (avizul 1052 e cel
mai probabil sursă a facturii 1054, pe baza secvenței de cod-uri și a fluxului tip 22→tip 4):
```
ACT pe avizul 1052 (tip 22): scd/scc includ 607/371, 371/378, 378/371, 4428/371, 371/4428, 418/704, 418/4428
RUL pe avizul 1052 (tip 22): 4 rânduri, toate CONT = 371 (cont de stoc)
ACT pe factura 1054 (tip 4): scd/scc = 4111/418 (305 lei) și 4428/4428 (52.93 lei) -- NICIUN cont 371
RUL pe factura 1054 (tip 4): 0 rânduri
```
Avizul e cel care mișcă efectiv contul de stoc (`371`) și de aceea are rânduri în `RUL` (care
urmărește exclusiv cantități pe conturi de stoc — vezi și `cant`/`cante` din `trul`, folosite ca
atare în `ActualizeazaVerdictActRul:13102`). Factura emisă din acel aviz nu mai atinge `371` deloc —
transformă doar creanța provizorie (`418`) în creanță fermă (`4111`) și TVA neexigibilă (`4428`) în
TVA colectată (`4428`). N-are, structural, ce rând de stoc să scrie.
Zero tranzacții deschise la final (`ROLLBACK` executat, `SET TRANSACTION READ ONLY` respectat pe tot
scriptul).
## Ce am dedus (nu verificat direct, dar consistent cu toate dovezile de mai sus)
- Regula generală: `RUL` se scrie doar pentru documentele/notele care conțin mișcare pe cont de
stoc (aviz, NIR, bonuri de consum etc.); facturile "de închidere" (emise din aviz, sau orice
document care doar transformă o creanță provizorie într-una fermă) nu vor avea niciodată rânduri
în `RUL`, indiferent câte documente de acest fel verifici — nu e o particularitate a lui
`id_vanzare = 1054`, ci a tipului de operațiune contabilă.
- Aserția de la `test_s8_matrice_surse.prg:308` (`Reccount(trul) > 0` obligatoriu după editare) e
probabil corectă ca test general "rulajele nu trebuie distruse de editare" pentru documentele care
AU avut rulaje înainte de editare, dar e o presupunere greșită aplicată universal — pentru tip 4
(și, prin extensie, orice tip de document care nu atinge cont de stoc) condiția trebuie relaxată
la "dacă documentul avea rulaje înainte, tot le are și după" sau pur și simplu exclusă pentru
tip = 4.
## Fișiere atinse
Niciunul (misiune read-only). Fișiere citite: `COMUN\programe\ofacturare_editare.prg`,
`COMUN\clase\omodificari.vc2`, `COMUN\clase\comun.vc2`, `COMUN\clase\ofacturare_comun.vc2`,
`COMUN\docs\PACK_CONTAFIN.pck`. Interogări Oracle read-only pe `MARIUSM_AUTO`@`ROA_CENTRAL`, cu
`ROLLBACK` la final.