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
31 KiB
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 inagg_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_contractproduce deja doua cursoare —V_CURSOR(crsarticole, prin delegare lacursor_preturi) siV_CURSOR2(crsarticole1, articole sau rate). Filtrarea se aplica curat doar pe jumatateacrsarticole; randurile de rata n-auid_articolsi 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:
-- 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 siid_articolvine 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:
id_articole 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.- 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 luiLOCATE FOR ... = NULL. Daca cineva „repara" mai tarziu comparatiile cuNvl(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:UNIONle trateaza deja ca un singur grup NULL). Varianta B evita complet acest risc, pentru ca liniile de rata nici nu ajung sa fie agregate. - Varianta B rezolva si EROAREA 1 la radacina, fara sa mai fie nevoie sa se adauge
NULLla declaratiileagg_rul/agg_tvd— daca linia cuid_articolNULL nu mai intra in bucla deSCANcare faceINSERT INTO agg_tvd, nu mai exista nicio incercare de a insera NULL intr-un camp NOT NULL. (AdaugareaNULLla 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 = 0si linia arepret_achizitie(semn ca vine dintr-un flux de articole reale), sau invers, sare garda candIsnull(id_articol) AND !Empty(explicatie)(semnul unei linii "sintetice" descrise prinexplicatie, ca ratele). - Varianta B — scoate garda complet pentru randuri cu
id_articolNULL de la incarcare (nu adaugate manual in sesiunea curenta) — presupune sa distingi intvdintre "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_ARTICOLeNULLABLE = Y, cu FKFK_VANZARE_DET002spreNOM_ARTICOLE.PK_ARTICOL;NOM_ARTICOLEn-are niciun rand cuid_articol = 0. Vezi tabelul din punctul 2. (Scriptul original deCREATE TABLEtot nu e inD:\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_DETALIIcuproc_tvavNULL — n-am interogat asta direct (doar am confirmat ca poate fi NULL,NULLABLE=Y). Pe randul de testproc_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 lapret/cantitate; daca exista, e periculos (baza * 0 = 0 in orice recalcul din afara VFP). Se poate lamuri cu unSELECT count(*) FROM vanzari_detalii WHERE sters=0 AND proc_tvav IS NULL. - De ce n-a crapat
agg_rul— REZOLVAT.RUL.ID_ARTICOLe NOT NULL in Oracle (all_tab_columns.nullable = 'N', masurat de team-lead), si exista 0 randuriRULactive cuid_articol IS NULL. Documentul de test (SSS 549) are un singur rulaj, cu articolul real4294507173— rata n-are corespondent inRUL. Decitrulnu poate aduce niciodata NULL, iar declaratiaagg_rul (id_articol N(20), ...)faraNULL(: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
UNIONsiLOCATE FOR— REZOLVAT, testat empiric. Vezi punctul 7: test izolatvfp9.exe -A -T, script pastrat inC:\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.UNIONtrateaza NULL=NULL ca echivalent la deduplicare;LOCATE FOR camp = NULLnu 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 NULLin toata baza, punctul 2 — dar nu si cate din ele sunt pe facturi cu rulaje care ar trece prinConstruiestePropunereSincronizare).