Files
comun/docs/cercetare/valuta_si_curs.md
Marius Mutu 09ddeabb1c docs: cercetarile trans-proiect din ROAFACTURARE trec in COMUN
Paisprezece rapoarte care nu erau ale ROAFACTURARE traiau in docs\cercetare\ al
acelui proiect: valuta si curs in ofacturare, TVA calculat vs salvat plus
denormalizarea VANZARI, inventarul consumatorilor VANZARI din toata suita ROA,
integrarile de contracte/politici/nomenclator (#10, #11, #12), watchdog-ul VFP
de testare, proiectarea Oracle a scrierii din S5 si view-ul VVANZARI_ARTICOLE.

Motivul mutarii, nu doar al pastrarii: planurile #10, #11 si #12 sunt amanate,
iar indexul lor spune explicit ca se reiau din aceste rapoarte - deci sunt punct
de plecare, nu istoric. Iar tinta lor e COMUN plus ROAPRETURI/ROACONTRACTE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SN8snvkk94KuhWwoXUUey3
2026-08-11 22:31:53 +03:00

297 lines
18 KiB
Markdown

# 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).