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
16 KiB
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):
(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:
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 = NULLnu gaseste niciodata nimic, nici cu memvar NULL, nici intre doua cursoare. Randul de rata ar iesi dinDO CASEcullGasitSursa = .F.sillGasitTinta = .F., ar cadea peOTHERWISEcu 0 = 0 si n-ar genera nicio linie inpropunere_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 cinciLOCATE(:626,:647,:671,:687, plus:849/:887/:923in aplicatoare).UNIONtrateaza 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 (
cExplicatieTvaArtramane ultima) - mutarea ei langa Taxcode cereColumnOrderpe 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 |