Files
comun/docs/cercetare/valuta_si_curs.md
Marius Mutu 6de489950c sync SVN r18016: editare articole in factura de vanzare, precizie pret achizitie
ROAFACTURARE 2.11.15 (r18015): programe/ofacturare_editare.prg, programe/ofacturare_stoc.prg.
docs: conventia de encoding .sc2/.vc2 corectata la cp1250; cercetarile trans-proiect mutate
din ROAFACTURARE in docs/cercetare (r18014); actualizari reguli_lucru, oracle_export,
flux-editare-vfp-text, scripturi-migrare-db, flux-modificare-stergere-nota-jurnal, todos.
.gitignore: watchdog_out si PNG-urile din rularile headless (r18008).
Text FoxBin2Prg regenerat: clase/comun.vc2, ferestre/frm_import_note_facturi_clienti.sc2,
ferestre/frm_initializare_facturi_balanta.sc2.

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

18 KiB

Cercetare: poDate.in_valuta, Clb_zi_curs si cele doua concepte de valuta

Investigatie read-only (fara editari, fara git_sync.ps1/txt2vcx.ps1, fara commit). Sursa: cache text .vc2/.prg din COMUN\clase\ofacturare.vc2, COMUN\programe\ofacturare.prg, COMUN\programe\ofacturare_comun.prg, COMUN\programe\ofacturare_stoc.prg, COMUN\programe\oproceduri_curs.prg, plus pachetul Oracle D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\03\ff_2026_03_23_04_COMUN_PACK_FACTURARE.sql (copie pe disc, alta numerotare decat baza). Reutilizeaza si confirma cercetari anterioare din docs\cercetare\rec_cale_vanzari_detalii.md si docs\cercetare\rec_consumatori_vanzari.md.


1. poDate.in_valuta — unde se seteaza si din ce

Concluzie: in_valuta e determinat o singura data, la Init, exclusiv din tipul documentului (tnTip) plus o lista fixa de id_set — niciodata din alegerea manuala a unei valute in antet si niciodata resetat ulterior. E o proprietate a tipului de factura (Invoice / credit note / retur valuta / factura fiscala valuta pe contract), nu a faptului ca articolele au preturi in valuta.

Dovezi:

  • Valoare implicita 0: COMUN\programe\ofacturare_comun.prg:165 (in_valuta = 0, in blocul de proprietati al obiectului poDate).
  • Singurul loc care il seteaza pe 1, in Procedure Init:
    ofacturare_comun.prg:248-250
    If Inlist(tnIdSet,223,225,226,227,271) OR INLIST(m.tnTip, 5,6,7,9,10,52)
        .in_valuta = 1
    Endif
    
  • Legenda tipurilor (comentariu din frm_date_factura.Init), ofacturare.vc2:9571-9582: 1=lista preturi, 2=contract, 3=comanda, 4=aviz, 5=Invoice lista preturi, 6=Invoice contract, 7=credit note, 8=retur lei, 9=retur valuta, 48=avize valoric, 52=Factura fiscala valuta contract. Tipul 10 nu are comentariu explicit in acest bloc (adaugat separat, v2.0.56, ca variatie de tip 1/5).
  • Nicio alta cale de scriere pe in_valuta in .vc2/.prg din COMUN — verificat cu grep (\.in_valuta\s*=\s*[01]) pe intreg ofacturare.vc2 si ofacturare.prg: restul potrivirilor sunt toate citiri (If poDate.in_valuta = 1 ...), nu atribuiri. ofacturare_stoc.prg:549 (poDate.in_valuta = in_valuta) e o cale alternativa (facturare "din stoc") care primeste parametrul in_valuta deja calculat in amonte, cu aceeasi semantica.
  • Consecinta directa, confirmata de cod: alegerea manuala a unei valute pentru document (control ct_clb_valuta) nu poate schimba in_valuta — controlul insusi e eliminat din formular cand in_valuta = 0 (ofacturare.vc2:9725-9728), deci userul nu are cum sa-l "aleaga" pe o factura care nu e deja de tip valuta.

2. Campul „Data curs valutar" (Clb_zi_curs) — unde e definit si cand e eliminat azi

Concluzie: campul e eliminat doar pentru facturi de retur (tip 8 sau 9), niciodata pe baza de in_valuta. Da, azi campul apare si pe facturile in lei (in_valuta=0) de orice alt tip — comportament confirmat explicit prin simetrie de cod: imediat dupa blocul de retur, exista un bloc separat care elimina ct_clb_valuta cand in_valuta=0, dar echivalentul lipseste pentru clb_zi_curs.

Dovezi (identificarea claselor facuta cu vfp_symbols.ps1 -Where):

Clasa Interval Clb_zi_curs definit Eliminat azi?
frm_date_aviz ofacturare.vc2:6566-7618 :6727-6740 (caption "Data cursului valutar") Niciodata — zero RemoveObject/conditionare pe in_valuta in toata clasa (verificat exhaustiv)
frm_date_aviz_lucrare :7620-8201 :7787-7801 Niciodata; in plus, validare necondiționata: :8076 Case Empty(Nvl(poDate.zi_curs,{})) fara nicio garda pe in_valuta (spre deosebire de frm_date_factura, vezi mai jos)
frm_date_factura :8482-9869 :8701-8714 (caption "Data curs valutar") Doar pentru tip 8/9, in Init:
ofacturare.vc2:9717-9722  (frm_date_factura.Init)
*!*	modificare v 2.0.56
If Inlist(poDate.tip, 8, 9)
    lnHeight = lnHeight - .clb_zi_curs.Height
    laPozitii(.clb_zi_curs.TabIndex, 2) = 1
    .RemoveObject('clb_zi_curs')
Endif
*!*	modificare v 2.0.56	^

ofacturare.vc2:9725-9728  (imediat dupa, in ACEEASI metoda)
If poDate.in_valuta = 0
    lnHeight = lnHeight - .ct_clb_valuta.Height
    laPozitii(.ct_clb_valuta.TabIndex, 2) = 1
    .RemoveObject('ct_clb_valuta')

Cele doua blocuri sunt adiacente si scrise dupa acelasi tipar (inaltime, laPozitii, RemoveObject) — dovada ca autorul original a tratat ct_clb_valuta (selectorul de valuta) ca dependent de in_valuta, dar nu a facut acelasi lucru pentru clb_zi_curs. Validarea de completare e de asemenea asimetrica: ofacturare.vc2:9484 (Case poDate.in_valuta = 1 And Empty(Nvl(poDate.zi_curs,{})) And ...) cere data curs doar cand in_valuta=1, in timp ce frm_date_aviz_lucrare (:8076) o cere mereu — inconsistenta intre cele doua forme, de retinut daca regula noua trebuie aplicata uniform.

Nota: frm_facturare_articole2 (clasa separata, :15741-19355) are propriul label "Curs valutar" (control diferit, :16285-16299), tratat la punctul 3/5 — nu e acelasi camp cu Clb_zi_curs din antet, dar citeste aceeasi poDate.zi_curs.


3. Cele doua concepte, in cod — confirmate distinct

Concluzie: da, distinctia exista clar in cod, pe doua axe independente:

  • (a) valuta de document: poDate.in_valuta / poDate.Curs / poDate.multiplicator / poDate.id_valuta — un singur curs, al documentului intreg, folosit cand tot documentul e emis in valuta.
  • (b) articol cu pret in valuta pe document in lei: poArticol.tip_valuta = 1 (proprietate a politicii de pret a articolului, independenta de poDate.in_valuta), cu preturile brute in valuta pastrate in poArticol.pretftva_val/pretctva_val/pretd + id_valuta_d, convertite in lei client-side, in VFP, folosind cursul zilei poDate.zi_curs.

Dovezi — conversia in lei pentru cazul (b):

ofacturare.vc2:1989   poArticol.pretftva = Round(poArticol.pretftva_val * poArticol.Curs / poArticol.multiplicator, gnPPretV)
ofacturare.vc2:2953   poArticol.pretftva = Round(poArticol.pretftva_val * poArticol.Curs / poArticol.multiplicator, gnPPretV)
ofacturare.vc2:2017   poArticol.pretctva = Round(poArticol.pretctva_val * poArticol.Curs / poArticol.multiplicator, gnPPretV)

poArticol.Curs/multiplicator (nu e alt curs decat cel al zilei documentului) provin din cursorul crscursuri, incarcat explicit cand documentul e in lei — exact pentru articolele cu preturi in valuta:

ofacturare.prg:407-418  (identic si la :941-947)
If poDate.in_valuta = 1
    Select Distinct Curs, nume_val, id_valuta, multiplicator From (lcCursor) ... Where tip_valuta = 1 ... Into Cursor crscursuri
    Select crscursuri
    poDate.Curs = Curs
    poDate.multiplicator = multiplicator
Else
    citeste_cursuri_zi(poDate.zi_curs)
    If Reccount('crscursuri') = 0
        Use In crscursuri
    Endif
Endif

citeste_cursuri_zi (COMUN\programe\oproceduri_curs.prg:113-124) construieste crscursuri cu toate cursurile valabile la tdDataCurs (= poDate.zi_curs):

oproceduri_curs.prg:117-118
lcSql = [select nume_val,curs,id_valuta,multiplicator from ] + gcS + [.vcurs where data <= ... and data2 >= ...]

Cursorul e apoi afisat direct pe grila de selectie a articolelor, cu eticheta construita din poDate.zi_curs, indiferent de in_valuta:

ofacturare.vc2:15097-15098 si 19004-19005 (frm_facturare_articole2)
If !Empty(Nvl(poDate.zi_curs, {}))
    Thisform.lb_cursuri.Caption = "Curs valutar " + Iif(!Inlist(poDate.tip, 8, 9), "(" + Dtoc(poDate.zi_curs) + ")", "")

Grila/eticheta apar doar daca crscursuri are randuri (ofacturare.vc2:15092/19000), ceea ce se intampla si pe un document in lei, daca exista macar un articol cu politica in valuta.

VANZARI vs VANZARI_DETALII — ce se stocheaza:

  • VANZARI (document): coloane proprii CURS/ID_VALUTA/MULTIPLICATOR — cursul/valuta doar ale documentului, populate din poDate.Curs/id_valuta/multiplicator (relevante cand in_valuta=1).
  • VANZARI_DETALII (linie): nu are coloana CURS — confirmat din all_tab_columns (docs\cercetare\rec_cale_vanzari_detalii.md:171-175) si din SQL-ul live al pachetului, care reface cursul liniei prin JOIN, nu dintr-o coloana proprie:
    PACK_FACTURARE.sql:3773-3778 (identic la :4034-4036, :6393-6396, :6571-6573)
    LEFT JOIN VANZARI_CURSURI B1 ON A1.ID_VANZARE = B1.ID_VANZARE AND A1.ID_VALUTA = B1.ID_VALUTA
    
    Linia stocheaza insa PRET (pretul efectiv, in lei, folosit pe factura) si PRETD + ID_VALUTAD (pretul brut in valuta si valuta originii, pastrate ca referinta/audit) — vezi INSERT-ul din adauga_articol_factura (docs\cercetare\rec_cale_vanzari_detalii.md:59-65, coloanele PRET, PRETD, ID_VALUTAD, ID_VALUTA alaturi). Deci da, se pastreaza si urma valutei originale pe linie, dar valoarea de facturare efectiva e mereu in lei pe VANZARI_DETALII.PRET.

La ce se foloseste VANZARI_CURSURI: e un instantaneu al cursurilor pentru orice valuta straina aparuta printre liniile documentului (nu doar valuta proprie a documentului), scris o singura data la emitere, indiferent de in_valuta:

PACK_FACTURARE.sql:14491-14501
PROCEDURE scrie_cursuri(V_ID_VANZARE IN NUMBER) IS
BEGIN
  INSERT INTO VANZARI_CURSURI (ID_VANZARE, ID_VALUTA, CURS, MULTIPLICATOR)
    SELECT DISTINCT V_ID_VANZARE, ID_VALUTA, CURS, MULTIPLICATOR
      FROM VANZARI_DETALII_TEMP
     WHERE ID_VALUTA <> pack_facturare.nid_moneda_nationala;
END scrie_cursuri;

Rulat necondiționat la fiecare emitere (apelat din scrie_in_vanzari, vezi docs\cercetare\rec_cale_vanzari_detalii.md:98-102) — deci si pe o factura in lei cu articole in valuta se scrie cate un rand VANZARI_CURSURI pentru fiecare valuta straina aparuta pe linii, cu cursul citit la poDate.zi_curs. Tabela e folosita ulterior la reafisare/reprint/retur, ca sursa a cursului per-linie (JOIN-urile de mai sus).


4. Ce se strica daca ascundem campul cand "nu are sens"

Concluzie: nu exista risc de "factura fara curs deloc" — poDate.zi_curs primeste mereu o valoare implicita la Init (data documentului) si nu poate ramane NULL. Riscul real e altul: userul pierde controlul asupra datei de curs pentru cazul (b) — o factura in lei cu articole in valuta ar folosi mereu cursul zilei curente/implicite, fara sa poata fi corectat manual, exact in situatia in care lipseste cursul pentru acea data (eroarea Oracle 20005, punctul 5) sau cand userul vrea sa aliniaze cursul cu data unui document sursa (aviz, contract).

Consumatori confirmati ai poDate.zi_curs (grep exhaustiv poDate\.zi_curs, fisiere ofacturare.vc2, ofacturare.prg, ofacturare_stoc.prg — singurele 3 din COMUN):

Loc Ce primeste Conditionat de in_valuta?
ofacturare.vc2:6105-6107, :13994-13996, :18030-18032 parametru to_date(...) catre pack_facturare.initializeaza_date_factura(...) la fiecare finalizare de antet Nu — trimis pentru orice tip
ofacturare.prg:272,276,281,297,303 primul parametru al pack_facturare.cursor_articole_k / cursor_preturi / cursor_gestiune / cursor_lucrare — SQL-ul care aduce lista de articole/preturi afisata userului Nu — inclusiv pentru tip=1 (lista de preturi, lei)
ofacturare_stoc.prg:79,335 acelasi rol, pe calea alternativa "facturare din stoc" Nu
ofacturare.vc2:15097-15098, :19004-19005 eticheta grilei "Curs valutar (data)" din frm_facturare_articole2 Nu — apare oricand crscursuri are randuri
ofacturare.vc2:9484 validare "Nu ati completat ziua cursului valutar" Da, doar in_valuta=1 (frm_date_factura)
ofacturare.vc2:8076 aceeasi validare Nu, necondiționata (frm_date_aviz_lucrare)

Riscul concret: daca regula noua ascunde/dezactiveaza campul strict cand poDate.in_valuta=0, pe frm_date_factura (tip 1, lista de preturi) userul nu va mai putea edita zi_curs inainte de a deschide grila de articole — dar ofacturare.prg:279-282 tot va trimite acel poDate.zi_curs (neschimbat, ramas pe data initializata la Init) catre cursor_preturi, iar daca acolo Oracle ridica eroarea 20005 (curs lipsa pentru acea data/valuta), fluxul de recuperare (vizualizeaza_curs, punctul 5) va porni oricum, dar userul nu va (mai) avea camp pe antet ca sa retina/corecteze data dupa ce inchide formularul de curs. Pe frm_date_aviz_lucrare, ascunderea ar intra chiar in conflict cu validarea necondiționata de la :8076, care ar bloca finalizarea antetului cerand un camp care nu mai exista pe ecran — de reparat impreuna, nu doar ascuns.


5. Formularul de curs valutar din antet — localizare si traseu

Concluzie: nu exista niciun buton/dblclick pe Clb_zi_curs care sa deschida formularul de curs (verificat exhaustiv — zero hit pentru Clb_zi_curs.DblClick/RightClick/Click sau but_curs in ofacturare.vc2). Formularul se deschide automat, ca recuperare de eroare, dupa ce antetul (frm_date_factura/frm_date_aviz) s-a inchis deja si codul incearca sa incarce lista de articole.

Traseu:

  1. ofacturare.prg:228-235ofrmceredate = Createobject(lcObiect, toFactura) (lcObiect = frm_date_factura sau frm_date_aviz), .Show() modal; la inchidere, Release ofrmceredate (:248).
  2. ofacturare.prg:266-308 — pe baza tipului, se construieste apelul catre pack_facturare.cursor_preturi(?poDate.zi_curs, ...) (sau cursor_articole_k/cursor_gestiune/ cursor_lucrare) si se executa (:311 goExecutor.oExecute(lcSqlCursor, lcCursor)).
  3. Daca esueaza cu eroarea Oracle 20005 (curs lipsa pentru data/valuta ceruta):
    ofacturare.prg:313-317 (identic la :828-832)
    If lnSucces < 0
        AMESSAGEBOX(goExecutor.oPrelucrareEroare(), 16, "Eroare")
        If goExecutor.nEroare = 20005
            vizualizeaza_curs(poDate.zi_curs)
        ENDIF
    
  4. vizualizeaza_curs(tdDataCurs)COMUN\programe\oproceduri_curs.prg:8-42 — construieste cursorul crscurs si deschide loFrmCurs = Createobject("frm_curs", tdDataCurs) / .Show(1) (modal).
  5. Clasa frm_curs e definita in COMUN\clase\onom_curs.vc2:537 (DEFINE CLASS frm_curs AS _frmbase ...), cu formulare-satelit pentru adaugare curs: frm_curs_nou (onom_curs.vc2:1066) si frm_curs_nou_multiplu (onom_curs.vc2:1330, apelat de la onom_curs.vc2:941).
  6. A doua cale catre acelasi formular: citeste_cursuri_stoc (oproceduri_curs.prg:45-110, folosit pe fluxul de facturare din stoc) cheama vizualizeaza_curs() fara parametru de data (oproceduri_curs.prg:94) cand gaseste valute fara curs pentru ziua ceruta, in bucla Do While llVerificare (retry pana userul completeaza sau renunta).

Aceasta e exact situatia din COMUN\docs\todos.txt:45 (punctul 16): "la finalizare se verifica cursul valutar necesar pentru politicile de preturi care se factureaza [...] la revenire din formularul de curs valutar, focusul revine [...] pe TIP DOCUMENT, si la iesire din serie se regenereaza numar act" — confirmat ca fenomenul are loc dupa ce antetul (frm_date_factura) e deja inchis si eliberat (Release ofrmceredate, pasul 1), deci orice regenerare de numar/focus vizibila dupa inchiderea frm_curs tine de ecranul/starea care ramane activa in spate (nu s-a localizat mai departe — cere depanare separata, in afara scopului "doar localizare" cerut aici).


Cand are sens campul „Data curs valutar" — propunere de regula

Campul are sens de aratat/editabil pe antet exact cand exista vreo sansa ca cursul zilei sa fie folosit la facturare — nu doar cand documentul insusi e in valuta:

Arata (si activeaza) Clb_zi_curs cand poDate.tip NU e retur (!Inlist(poDate.tip,8,9)) SI (poDate.in_valuta = 1 SAU exista macar o politica de pret in valuta accesibila tipului curent de document — adica exact conditia care astazi populeaza crscursuri cu randuri, verificata deja empiric la ofacturare.vc2:15092/:19000: Used('crscursuri') And Reccount('crscursuri') > 0 dupa incarcarea listei de articole). Pe retur (8/9) ramane ascuns ca azi.

Pe frm_date_aviz/frm_date_aviz_lucrare, unde crscursuri nu e inca disponibil la momentul antetului (se incarca dupa, la fel ca la factura), regula echivalenta practicabila pe antet (nu dupa) ar fi: arata campul daca tipul documentului admite politici de pret in valuta pentru sucursala/gestiunea curenta (verificare care azi nu exista pe antet, doar mai tarziu pe grila de articole) — necesita fie mutarea verificarii mai devreme, fie acceptarea ca antetul nu poate sti cu certitudine inainte de a incarca articolele, caz in care campul ramane vizibil implicit si doar eticheta/relevanta lui se clarifica ulterior (ca azi, in frm_facturare_articole2).


Necunoscute ramase

  • Nu s-a confirmat daca exista si alte politici (CRM_POLITICI_PRET*) sau reguli de sucursala care determina inainte de deschiderea grilei de articole daca vor exista linii tip_valuta=1 — utile pentru a decide vizibilitatea campului chiar pe antet, nu doar reactiv dupa incarcarea articolelor.
  • Nu s-a urmarit complet "focusul sare pe tip document / regenerare numar act" (punctul 5/todos #16) dupa inchiderea frm_curs — s-a localizat doar traseul de deschidere, nu si ecranul/handler-ul care ramane activ in spate si cauzeaza simptomul; cere sesiune separata de depanare (posibil in poGeneratorNumere/clb_serie_act, cf. ofacturare.vc2:9735-9737, needatate aici).
  • Layout-urile .frx (neconvertite) care afiseaza poDate.Curs/cValuta pe hartie nu au fost verificate — gap deja semnalat in rec_consumatori_vanzari.md (risc posibil #3), ramane valabil.
  • Nu s-a confirmat empiric (interogare Oracle) daca exista azi in productie facturi tip=1 (lei) cu linii ID_VALUTA <> moneda_nationala in VANZARI_DETALII — dovada ar intari direct concluzia punctului 3, dar cercetarea a fost strict pe cod (fara conexiuni DB, conform mandatului).