Files
roafacturare/docs/propunere_runda5_articole_valuta_rate.md
Marius Mutu ca3c5d7eea docs: runda 6 - cercetari, propuneri si conventia mediului Oracle
Cercetarile si propunerile rundelor 4-6 pe editarea facturii emise. Starea
rundei 6 si ce s-a stabilit intra in progres.md.

Nou: docs/conventii_mediu_oracle.md - pe dev/test se lucreaza numai cu
ROA_CENTRAL si schemele CONTAFIN_ORACLE si MARIUSM_AUTO; schema ACN nu se
foloseste. Scripturile de pachet sunt necalificate, deci schema tinta o
decide sirul de conectare, nu fisierul.

roafacturare.pj2 regenerat de git_sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gYCNE26G1G7UoGu2ju7aS
2026-08-20 16:35:03 +03:00

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