# 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`).