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