Files
roafacturare/docs/propunere_runda5_articole_valuta_rate.md
Marius Mutu ca3c5d7eea 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
2026-08-20 16:35:03 +03:00

295 lines
16 KiB
Markdown

# Runda 5 - editare inline pe pagina Articole, totalurile facturii din aviz, liniile de rata
Cinci semnalari din proba lui Marius, 19.08.2026. Fiecare punct: constatarea, dovada
(`fisier:linie` sau interogare pe `MARIUSM_AUTO@ROA_CENTRAL`), propunerea.
**Stare: nimic aplicat.** Toate punctele de mai jos sunt propuneri. Diff-urile se livreaza dupa
aprobare, iar write-back-ul text->binar vine dupa diff.
Cercetarile din spate: `docs\cercetare\rec_r5_editare_inline_articole.md`,
`docs\cercetare\rec_r5_linii_fara_articol_contract.md`,
`docs\cercetare\rec_r5_totaluri_factura_din_aviz.md`.
---
## 1. Editare inline pe pagina Articole: serie, lot, explicatie, procent TVA
Coloanele cerute sunt azi `ReadOnly = .T.` in `grdArticoleFactura`
(`COMUN\clase\omodificari.vc2:12368` serie, `:12375` lot, `:12449` explicatie, `:12419`
`proc_tvav`).
Trecerea pe editabil urmeaza tiparul deja folosit pe `cPretArt`: `Text1.When` refuza editarea cand
`Thisform.lArticoleReadOnly` sau `Nvl(tvd.id_vanzare_set,0) <> 0`
(`omodificari.vc2:16735-16740`), `Text1.Valid` recalculeaza unde e nevoie.
`serie`, `lot`, `explicatie` nu cer niciun recalcul - se scriu direct in cursor.
### Defect care trebuie reparat in aceeasi livrare
`ScrieArticoleFacturaEditate` (`COMUN\programe\ofacturare_editare.prg:505-517`) **nu scrie
`serie`, `lot`, `explicatie` in UPDATE-ul liniilor deja salvate** - cele trei campuri apar doar in
INSERT-ul liniilor noi (`:535-546`). Fara aceasta corectie, editarea inline ceruta s-ar pierde
tacut la salvare pe orice linie existenta. `proc_tvav` e deja in UPDATE, deci pentru el nu e
nimic de facut.
### Procentul de TVA - punctul delicat
`tvd.proc_tvav` e in forma **1.21**, nu 21 (verificat pe date: randurile 1599-1604 din
`VANZARI_DETALII` au toate `1.21`). Se pastreaza forma asta: exista precedent direct pe aceeasi
pagina, coloana `cProc_tva` de pe grila de rulaje e deja editabila liber in aceeasi forma.
Riscul real e altul: pe `tvd`, cota e corelata cu `id_jtva_coloana` (explicatia TVA) si cu
`taxcode` (SAF-T). Azi, alegerea explicatiei scrie cota **si** recoreleaza taxcode-ul
(`ofacturare_editare.prg:1099-1104` -> `UpdateExplicatieSAFTArt`, `omodificari.vc2:15049-15073`).
Daca utilizatorul tasteaza direct cota, corelarea ramane la cota veche: explicatie de 21% langa o
cota de 19%, si `taxcode` gresit in raportarea 406. Pe rulaje riscul nu exista - `trul` nu poarta
aceeasi corelare.
**DECIZIE CERUTA - A sau B:**
| | Ce face | Consecinta |
|---|---|---|
| **A** (recomandat) | La editarea manuala a cotei, `Valid` goleste `id_jtva_coloana` si `taxcode` pe randul respectiv | Explicatia TVA si Taxcode raman goale - semnal vizibil ca trebuie realeasa explicatia; dialogul se deschide deja filtrat pe noua cota |
| **B** | Doar `calculeaza_valori_articol()`, corelarea ramane neatinsa (ca pe rulaje) | Mai simplu, dar lasa tacut un taxcode SAF-T gresit |
Varianta "recoreleaza automat" a fost respinsa: pe aceeasi cota pot exista mai multe explicatii
(JC vs JV, exigibil vs neexigibil - parametrul `tlTipEx` din `caut_explicatie_tva`,
`COMUN\programe\ocautare.prg:3174`), iar alegerea automata a uneia ar fi arbitrara exact acolo unde
conteaza sa fie corecta.
**DECIZIE CERUTA - garda pe serie/lot.** Pretul de achizitie e blocat si pe liniile deja salvate
(`Nvl(tvd.id_vanzare_det,0) <> 0`, `omodificari.vc2:16721-16726`); cantitatea si pretul nu.
Propunerea merge pe garda slaba (serie/lot editabile si pe liniile salvate, blocate doar pe cele
venite din set). Daca trasabilitatea lotului pe linii deja livrate conteaza, se aliniaza la garda
pretului de achizitie.
### Nomenclatoarele pe `InteractiveChange`
Cele cinci coloane cu nomenclator - `cDenumireArt`, `cGestiuneArt`, `cValutaArt`, `cTaxcodeArt`,
`cExplicatieTvaArt` - au deja `Text1.GotFocus` care pune `Thisform.pccontrol`
(`omodificari.vc2:16705-16720`, `:16755-16765`). Lipsesc doar `ReadOnly = .F.` si
`Text1.InteractiveChange`, care cheama `ArticoleNotaEditor.ModificaNomenclator(Thisform.pccontrol)`.
Sablonul e cel de pe grila de note (`omodificari.vc2:5890-5940`): coloana editabila, `GotFocus`
pune `pccontrol`, `InteractiveChange` deschide dialogul. `But_modificaR` ramane functional si
sincron - `BeforeRowColChange`/`AfterRowColChange` nu cer nicio modificare.
Efect lateral cunoscut si acceptat in sablonul existent: primul caracter tastat ajunge in celula
inainte sa se deschida dialogul. Daca utilizatorul renunta la dialog, caracterul ramane vizibil
pana la urmatorul refresh. Se poate atenua cu un `RefreshGrid()` pe ramura de anulare din
`ModificaNomenclator` - **spune daca il vrei in aceasta livrare** sau ramane pe alta runda.
---
## 2. Totalurile goale pe factura editata din aviz
Confirmat pe date, nu dedus.
`VANZARI` id_vanzare=1054 (SSS 100037, tip=4, `IN_VALUTA=0`, `DISCOUNT=0`) are
**`TOTAL_FARA_TVA` / `TOTAL_TVA` / `TOTAL_CU_TVA` = NULL** - de aici coloanele goale din lista.
Are o singura linie activa, `VANZARI_DETALII.id_vanzare_det=1602`, cu **`ID_VALUTA = 2` (EURO)**.
Linia-sursa din aviz (`id_vanzare_det=1599`) are `id_valuta = 3` (RON). Pe linia 1602 difera fata
de aviz si `id_gestiune` (1 vs 2), `taxcode` (310344 vs 310350) si `id_jtva_coloana` (35 vs 37) -
adica exact nomenclatoarele probate in runda 4, valuta inclusa.
### Lantul cauzal
`pack_facturare.recalculeaza_totaluri_vanzari`
(`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql`):
```sql
(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
...
left join vanzari_cursuri vc on vc.id_vanzare = V_ID_VANZARE and vd.id_valuta = vc.id_valuta
```
EURO (2) <> RON (3) -> intra pe ramura de conversie -> `VANZARI_CURSURI` **n-are niciun rand**
pentru id_vanzare=1054 -> `vc.curs` NULL -> `pret_ron` NULL -> `SUM(...)` NULL -> UPDATE-ul final
scrie NULL in cele trei totaluri. Selectul a fost rulat separat pentru 1054: intoarce NULL, NULL.
Nu conteaza care view alimenteaza lista: `FACT_VFACTURI2` citeste direct `VANZARI`, deci vede
NULL-ul scris; `FACT_VFACTURI` recalculeaza live, cu acelasi tip de agregare vulnerabila la NULL,
deci iese la fel. Alegerea intre ele e un prompt runtime
(`Clase\ofundal_facturare.vc2:944-953`).
### De ce lipseste randul de curs
La emitere, `pack_facturare.scrie_cursuri` (acelasi fisier, `:14538`) insereaza in
`VANZARI_CURSURI` cate un rand pentru fiecare valuta distincta, non-nationala, din liniile
documentului. **Calea de editare nu face acest lucru**: `ModificaNomenclator`, ramura `nume_val`
(`ofacturare_editare.prg:1096-1098`), scrie `id_valuta` pe linie, iar
`ScrieArticoleFacturaEditate` (`:512`) il duce in tabela - fara sa atinga `VANZARI_CURSURI`.
Invariantul pe care se bazeaza procedura de totaluri se rupe exact aici.
`VANZARI_CURSURI` se scrie **o singura data, la emitere**, din tabela de staging
`VANZARI_DETALII_TEMP`; nu exista nicio cale care sa-l actualizeze dupa aceea. Deci: linii intr-o
valuta straina pe un document in RON sunt legitime si frecvente - **157 de documente** cu
`in_valuta = 0` au asemenea linii, iar `VANZARI_CURSURI` are randuri pentru **125 de documente in
RON** - dar numai daca alegerea s-a facut **la emitere**. Orice schimbare de valuta facuta ulterior,
din formularul de editare, produce garantat o valuta orfana, fara curs.
Amploarea, masurata: din 235 de randuri `VANZARI` cu `total_cu_tva` NULL in toata baza, **unul
singur** se potriveste acestui defect - chiar SSS 100037 / id_vanzare 1054. Restul au cauze vechi,
cunoscute (120 fara linii active, 109 cu `discount_evidentiat` NULL). Deci nu e nevoie de nicio
reparatie in masa.
Confirmata si regresia: diff-ul fata de `ofacturare_editare.prg.pre_runda_butoane.bak:501-505`
arata ca UPDATE-ul liniilor existente **nu includea `id_valuta`** inainte de runda 4 - alegerea de
valuta se pierdea silentios si nu ajungea niciodata in tabela. Simptomul apare exact de la
extinderea UPDATE-ului.
### Propunere - doua straturi
**(a) Client - repara cauza.** Alegerea valutei pe linia de articol nu poate ramane libera: fara
un rand de curs pe document, orice valuta non-nationala e orfana, iar cursul nu se poate inventa
la editare (la emitere el vine din staging, adica din pretul negociat, nu din cursul zilei).
Garda merge in `ArticoleNotaEditor.ModificaNomenclator`, ramura `nume_val`
(`ofacturare_editare.prg:1078`), langa cea care blocheaza deja liniile din seturi (`:1069`).
Doua forme, **DECIZIE CERUTA**:
| | Regula | Efect |
|---|---|---|
| **a1** (recomandat) | se pot alege doar valutele care au deja rand in `VANZARI_CURSURI` pe documentul curent, plus moneda nationala | corectarea unei linii intre valutele deja folosite pe document ramane posibila; valuta orfana devine imposibila |
| **a2** | nomenclatorul de valuta e blocat cu totul cand `Nvl(tvanz.in_valuta,0) <> 1` | mai simplu si mai strict, dar interzice si corectiile legitime pe cele 125 de documente in RON care au deja cursuri |
**Respinsa**: varianta "salvarea completeaza singura randul lipsa din `VANZARI_CURSURI` cu cursul
zilei documentului". Cursul de la emitere e cel din oferta, nu cursul BNR al zilei - completarea
automata ar scrie un curs plauzibil dar inventat, si ar face totalurile sa arate corect fara sa fie.
**Respinsa si** varianta de a restrange conditia de conversie din procedura la `lnInValuta = 1`:
ar strica exact cele 157 de documente in RON cu linii in valuta, ale caror preturi sunt chiar in
valuta si trebuie convertite.
**(b) DB - plasa de siguranta.** In `recalculeaza_totaluri_vanzari`, UPDATE-ul final nu trebuie sa
poata scrie NULL peste totaluri valide:
```sql
total_fara_tva = NVL(lnTotalFaraTVA, total_fara_tva),
total_tva = NVL(lnTotalTVA, total_tva),
total_cu_tva = NVL(lnTotalCuTVA, total_cu_tva),
```
Pastreaza valoarea veche cand recalculul iese NULL, in loc sa goleasca documentul. E doar plasa
de siguranta, nu inlocuieste (a): fara (a), totalurile ar ramane **vechi si gresite** dupa o
editare, ceea ce e la fel de rau ca goale, doar mai greu de observat.
**Reparatia datelor - un singur document.** Randul 1602 are azi EURO fara curs, iar factura 1054
are totaluri NULL. Se repara punctual: `id_valuta` inapoi pe RON pe randul 1602, apoi un apel la
`recalculeaza_totaluri_vanzari(1054)`. **Nu-l rulez fara sa-mi ceri** - e singura scriere in baza
din toata livrarea, si e pe schema ta de dezvoltare.
---
## 3. `Field ID_ARTICOL does not accept null values` la editarea facturii din contract
Liniile de rata dintr-un contract sunt **prin proiectare** linii `VANZARI_DETALII` fara
`id_articol` (`COMUN\programe\ofacturare_comun.prg:1792` - `crsfacttemp` declara
`id_articol N(20) null` -, `:1836-1840`; documentat si in
`docs\plan_13_unificare_formular_facturare.md:2275`). Baza le accepta:
`VANZARI_DETALII.ID_ARTICOL` e **nullable**, si sunt deja 20 de linii active cu NULL.
Eroarea vine din cursorul de agregare, care nu declara campul ca acceptand NULL:
```
ofacturare_editare.prg:640 CREATE CURSOR agg_tvd (id_articol N(20), ...)
ofacturare_editare.prg:651 INSERT INTO agg_tvd (...) VALUES (tvd.id_articol, ...)
```
`agg_rul` (`:617`) are aceeasi declaratie, dar nu se poate manifesta: `RUL.ID_ARTICOL` e
**NOT NULL** in Oracle si nu exista niciun rand cu NULL.
### Propunere
Liniile fara articol se **exclud din comparatia rulaje <-> articole, chiar la sursa**: ambele
`SCAN`-uri de agregare din `ConstruiestePropunereSincronizare` (`:620` peste `trul`, `:643` peste
`tvd`) primesc in plus conditia pe articol completat. Cheia comparatiei chiar este `id_articol`;
o linie fara cheie nu poate avea corespondent in rulaje prin definitie. Filtrand la sursa, NULL-ul
nu mai ajunge la `INSERT INTO agg_tvd`, deci eroarea dispare de la radacina. Declaratia `NULL` pe
ambele cursoare ramane oricum, ca igiena.
Cele doua alternative si de ce le-am respins - comportamentul VFP a fost **masurat**, nu presupus
(test izolat `vfp9 -A -T` care reproduce `CREATE CURSOR` + `UNION` + `LOCATE FOR`, in scratchpad):
- **Doar declaratia `NULL`, fara sa umblam la comparatii.** `LOCATE FOR camp = NULL` nu gaseste
niciodata nimic, nici cu memvar NULL, nici intre doua cursoare. Randul de rata ar iesi din
`DO CASE` cu `llGasitSursa = .F.` **si** `llGasitTinta = .F.`, ar cadea pe `OTHERWISE` cu 0 = 0 si
n-ar genera nicio linie in `propunere_sincronizare` - **disparitie tacuta**, fara eroare si fara
semnalare. Acelasi rezultat practic ca filtrarea propusa, dar obtinut printr-un efect secundar
nedocumentat, care s-ar schimba brusc daca cineva "repara" mai tarziu comparatiile.
- **`Nvl(...,-1)` in cele cinci `LOCATE`** (`:626`, `:647`, `:671`, `:687`, plus `:849`/`:887`/`:923`
in aplicatoare). `UNION` trateaza NULL = NULL ca acelasi rand la deduplicare, deci **toate**
ratele unui document s-ar agrega laolalta sub un singur pseudo-articol si ar iesi ca divergenta
la fiecare salvare - dialogul de sincronizare s-ar deschide degeaba pe orice factura din contract.
---
## 4. `Linia '' nu are articol asociat` la salvarea facturii din contract
Garda e la `COMUN\clase\omodificari.vc2:14449`, in `inainte_de_do_termin`:
`IF Nvl(id_articol,0) = 0` pe fiecare linie activa din `tvd`. A fost pusa pe premisa ca
`id_articol` vine mereu completat din sursa - premisa infirmata de date (cele 20 de linii de mai
sus).
Denumirea goala din mesaj e reala, nu cosmetica: "RATA 2" sta in coloana `EXPLICATIE`, iar
`tvd.denumire` vine prin join pe `NOM_ARTICOLE` in `VVANZARI_ARTICOLE`
(`ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql`), deci iese NULL pe randul fara articol.
### Propunere
Garda ramane, dar **doar pe liniile noi, nesalvate** (`id_vanzare_det = 0`): acolo lipsa
articolului chiar inseamna ca utilizatorul n-a ales nimic din nomenclator. O linie incarcata din
baza cu `id_articol` NULL e stare legitima si trece.
Regula e crisp si nu se bazeaza pe euristici de tipul "are pret de achizitie, deci e articol
real".
### Defect legat, obligatoriu de reparat odata cu asta
`ScrieArticoleFacturaEditate` scrie azi `Nvl(id_articol,0)` **si in UPDATE (`:512`), si in INSERT
(`:537`)**. `id_articol = 0` nu exista in `NOM_ARTICOLE` (verificat: count = 0), iar coloana are FK
(`FK_VANZARE_DET002`) - deci prima salvare reusita a unei facturi cu linie de rata ar cadea pe
**`ORA-02291`**. Trece la tiparul deja folosit alaturi pentru `id_gestiune`/`id_valuta`/`taxcode`:
`Iif(Isnull(id_articol),[NULL],Alltrim(Str(id_articol)))`.
Acelasi tratament pentru inca trei campuri, dupa numarul de randuri active care au azi NULL in
baza (masurat pe `VANZARI_DETALII`, `sters = 0`, 1124 randuri in total):
| Camp | Randuri cu NULL azi | De ce conteaza |
|---|---|---|
| `pret_achizitie` | **401** | o rata n-are cost de achizitie; `0` inseamna "cost efectiv zero", valoare falsa pentru orice calcul de marja facut direct din tabela |
| `discount_unitar` | **238** | NULL si 0 sunt echivalente ca sens, dar salvarea ar rescrie tacut 238 de randuri |
| `proc_tvav` | **2** | e multiplicator (1.21), nu procent - `0` nu inseamna "fara TVA", ci anuleaza orice calcul care-l inmulteste |
Restul raman cum sunt: `pret` are coloana NOT NULL si garda proprie la `:14441`, `cantitate` e
filtrata de garda de la `:14433` si oricum n-are niciun NULL in baza, `pret_cu_tva` e flag 0/1.
---
## 5. Optional, de decis separat
`VVANZARI_ARTICOLE` ar putea capata acelasi fallback pe care il are view-ul vechi
`fact_vfacturi_detalii`: `NVL2(vd.id_articol, na.denumire, vd.explicatie) as denumire`. Ar face ca
"RATA 2" sa apara si in coloana Denumire, nu doar in Explicatie. E o modificare de view, deci un
script DB separat - **nu o bag in aceasta livrare** fara sa o ceri.
---
## Ce nu se atinge
- Ordinea coloanelor din grid (`cExplicatieTvaArt` ramane ultima) - mutarea ei langa Taxcode cere
`ColumnOrder` pe toate coloanele.
- Datele din Oracle - nicio reparatie de randuri existente fara cerere explicita.
- Dialogul de sincronizare si bara de totaluri - neschimbate fata de runda 4.
## Fisiere atinse de propunere
| Fisier | Puncte | Write-back |
|---|---|---|
| `COMUN\clase\omodificari.vc2` | 1 (grid + evenimente), 4 (garda) | necesar (`txt2vcx.ps1 -AllowComun`) |
| `COMUN\programe\ofacturare_editare.prg` | 1 (UPDATE), 2a (garda de valuta), 3 (agregare), 4 (scriere NULL) | n/a |
| script DB nou, `PACK_FACTURARE` | 2b (NVL pe UPDATE-ul de totaluri) | script separat, dupa aprobare |