# 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-235` — `ofrmceredate = 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).