#13: formular de facturare unificat - etapa curenta

Squash al branch-ului de lucru plan13-s2.

Cod: clasa noua ofacturare_util (geometrie si utilitare de formular), inrolata in
roafacturare.prg; meniul "Facturare (nou)" plat, cu reparatia literalului peste
limita VFP de 255 de caractere care impiedica compilarea metodei.

Livrare: changelog 2.11.19, marcajul de versiune DB pentru scripturile finale din
SCRIPTURI_CLAR\2026\09, inventarul livrarii in docs/livrare_13.md si registrul de
erori deschise. Documentatia de lucru a etapei (cercetare, rapoarte, handoff-uri,
snapshot-uri de scripturi si backup-uri de rollback) a fost scoasa din arbore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkyjGyrV2S7932om4kfSiK
This commit is contained in:
2026-09-09 21:30:51 +03:00
parent 87063b06a8
commit 104c24ec20
80 changed files with 5585 additions and 27852 deletions

View File

@@ -1,262 +0,0 @@
# Cercetare: validarea zi_curs la ofacturare.vc2:8076 si impactul asupra S4d
## Verdict (5-10 randuri)
Ascunderea selectorului de zi curs NU va lasa un document fara curs si NU va cadea la salvare,
CU CONDITIA sa se respecte precedentul deja existent in cod: `poDate.zi_curs` primeste un implicit
necondiionat (data documentului) chiar in `oDateFactura.Init`/`Reset`
(`COMUN\programe\ofacturare_comun.prg:247` si `:496`), INAINTE ca formularul sa decida ce ascunde.
Nicaieri codul nu goleste `poDate.zi_curs` cand controlul e ascuns/eliminat. Riscul real de eroare
Oracle (-20005, "Nu este setat cursul...") vine NU din camp gol, ci din faptul ca verificarea
`pack_facturare.verifica_cursuri_valute` (in `cursor_preturi`) ruleaza NECONDITIONAT de `in_valuta`
al documentului curent si exclude doar moneda nationala - deci un `zi_curs` implicit (azi) care nu
are curs setat in tabela CURS pentru o valuta folosita in listele de preturi ale utilizatorului
poate pica oricum, INDIFERENT daca selectorul e vizibil sau nu. Linia :8076 NU apartine formularului
de factura, ci unui formular separat, restrans, pentru AVIZ PE LUCRARE / AVIZ PE NIR
(`frm_date_aviz_lucrare`, tipuri 27 si 30) - fara control de valuta pe el - si valideaza `zi_curs`
strict pentru ca tipul 27 are nevoie de curs pentru articolele din comanda (posibil in valuta),
INDEPENDENT de `poDate.in_valuta`. Precedentul cerut la punctul 6 exista deja, dar in alt formular:
`frm_date_factura`, tipurile 8/9 (retur), unde `clb_zi_curs` e eliminat NECONDITIONAT si documentul
se salveaza corect - motivul principal e ca SQL-ul pentru retur (`cursor_retur`) nici nu foloseste
`poDate.zi_curs`.
## 1. Ce valideaza linia :8076
Index de simboluri: `frm_date_aviz_lucrare.inainte_de_do_termin` = `ofacturare.vc2:8054-8119`
(fisier real: `COMUN\clase\ofacturare.vc2`).
Blocul complet (validare secventiala pe formular, fiecare `Case` opreste salvarea la primul fail):
```
COMUN\clase\ofacturare.vc2:8063-8079
Do Case
Case Empty(poDate.dataireg)
amessagebox("Nu ati completat data inregistrarii!",48,"Atentie")
...
Case Empty(poDate.dataact)
amessagebox("Nu ati completat data documentului!",48,"Atentie")
...
Case Empty(Nvl(poDate.id_fdoc,0))
amessagebox("Nu ati ales felul documentului!",48,"Atentie")
...
Case Empty(Nvl(poDate.zi_curs,{}))
amessagebox("Nu ati completat ziua cursului valutar!",48,"Atentie")
This.clb_zi_curs.SetFocus()
plReturn = .F.
Case Empty(poDate.nract)
...
```
Valideaza STRICT prezenta unei date in `poDate.zi_curs` (`Empty(Nvl(...,{}))`), nu existenta unui
curs in baza pentru acea data - acel test se face abia in Oracle, la momentul in care se cere
cursorul de articole (vezi punctul 4).
## 2. Cand ruleaza si pe ce tipuri de document
`inainte_de_do_termin` e apelat la evenimentul butonului "Termina" al formularului (`BUT_TERMIN1`,
vezi lista de obiecte a clasei, `ofacturare.vc2:7674-7679`) - deci la incercarea de a incheia
completarea datelor de antet, inainte de a trece la ecranul de articole.
`frm_date_aviz_lucrare` NU e formularul de factura. E instantiat DOAR pentru doua tipuri de
document, ambele AVIZ (nu FACTURA):
```
COMUN\programe\ofacturare.prg:187-226 (identic in factureaza2, :697-699)
Do Case
Case tnTip = 27
poDate.nIdTipDoc = 6 && AVIZ
Case tnTip = 30
poDate.nIdTipDoc = 6 && AVIZ
Case tnTip < 21 Or Inlist(tnTip, 45, 48, 49, 51, 52)
poDate.nIdTipDoc = 5 && FACTURA
...
Do Case
Case tnTip = 27
lcObiect = [frm_date_aviz_lucrare]
Case tnTip = 30
lcObiect = [frm_date_aviz_lucrare]
Case tnTip < 21 Or Inlist(tnTip, 45, 48, 49, 51, 52)
lcObiect = [frm_date_factura]
Otherwise
lcObiect = [frm_date_aviz]
Endcase
```
Titlul formularului confirma: `Lb_titlu_alb_b121.Caption = "AVIZ PE BAZ­ DE LUCRARE"`
(`ofacturare.vc2:7670`), schimbat in `Init` la "AVIZ PE BAZ­ DE NIR" cand `nid_tip = 30`
(`ofacturare.vc2:8154-8161`).
Nu exista o garda mai sus in lant care sa dezactiveze validarea :8076 pe vreun tip - ruleaza
identic pentru tip 27 si tip 30, necondiionat de `poDate.in_valuta`. IMPORTANT: aceasta clasa
NU are deloc control de valuta pe formular - lista de obiecte a clasei (`ofacturare.vc2:7623-7637`)
nu contine niciun `ct_clb_valuta`. Deci validarea de aici nu e legata de "documentul e in valuta",
ci de nevoia formularului AVIZ-LUCRARE de a avea o zi de curs pentru articolele comenzii care pot
fi preturite in valuta (vezi punctul 4, `cursor_lucrare`).
Concluzie: linia :8076 NU intra deloc in fluxul de facturare (FACTURA) vizat de decizia 15 - e un
formular separat, pentru un subset ingust de avize (27 = aviz pe lucrare, 30 = aviz pe NIR).
## 3. Ce se intampla daca zi_curs e gol la :8076
Mesaj de eroare blocant (nu doar avertisment): `amessagebox("Nu ati completat ziua cursului
valutar!",48,"Atentie")`, apoi `This.clb_zi_curs.SetFocus()` si `plReturn = .F.` - `Case`-ul
opreste executia `Do Case` (Otherwise nu se mai atinge), iar `inainte_de_do_termin` returneaza
`.F.`, ceea ce (conform conventiei din restul clasei) blocheaza inchiderea formularului / trecerea
la pasul urmator.
## 4. Cine mai citeste poDate.zi_curs
Cautare `zi_curs` in `.vc2`/`.prg`/`.sc2` si in sursele Oracle
(`docs\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql`):
**a) Validari de formular (camp obligatoriu):**
- `frm_date_aviz_lucrare.inainte_de_do_termin` :8076 - necondiionat (vezi punctele 1-2).
- `frm_date_factura.inainte_de_do_termin`, `ofacturare.vc2:9484`:
```
Case poDate.in_valuta = 1 And Empty(Nvl(poDate.zi_curs,{})) And Type('thisform.clb_zi_curs.visible')<>'U'
```
Aici validarea E DEJA dublu conditionata: pe `in_valuta = 1` SI pe existenta controlului
(`Type(...)<>'U'` - devine 'U' daca controlul a fost eliminat cu `RemoveObject`). Deci pe factura
in lei, sau pe orice tip unde controlul a fost eliminat, validarea nu ruleaza deloc. Comentariul
`*!* modificare v 2.0.56` de langa arata ca exact acest lucru a fost REZOLVAT anterior pentru
formularul de factura.
**b) Populare implicita / sincronizare (fara conditie de in_valuta):**
- `oDateFactura.Init`, `COMUN\programe\ofacturare_comun.prg:247`: `.zi_curs = ldData` -
necondiionat, seteaza mereu data documentului curent (ldData = azi, ajustat la luna/anul
curent de facturare) INAINTE de blocul care seteaza `.in_valuta` (linia 248-250).
- `oDateFactura.Reset`, `ofacturare_comun.prg:496`: `.zi_curs = .Data` - la fel, necondiionat.
- `frm_date_aviz.Clb_dataact.Text_simplu1.LostFocus` (:7603-7604) si
`frm_date_aviz.Clb_dataireg...LostFocus` (:7610-7611): `poDate.zi_curs = poDate.dataact`,
necondiionat (formularul aviz general nu are guard, dar si nu are RemoveObject pe zi_curs).
- `frm_date_aviz_lucrare.Clb_dataact...LostFocus` (:8186-8187) si
`...Clb_dataireg...LostFocus` (:8193-8194): idem, necondiionat - zi_curs NU e niciodata
eliminat in aceasta clasa, deci sincronizarea merge mereu.
- `frm_date_factura.Clb_dataact...LostFocus` (:9805-9808) si `...Clb_dataireg...` (:9824-9827):
ACESTEA SUNT deja conditionate: `If Type('thisform.clb_zi_curs.visible')<>'U' ... zi_curs =
dataact ... Endif` (comentariu `*!* modificare v 2.0.56`). Cand controlul e eliminat, sincronizarea
se opreste - dar valoarea RAMASA de la Init/Reset nu se sterge, ramane cea de la creare.
**c) Consum efectiv in SQL (trimis catre Oracle, cursoare de articole):**
`COMUN\programe\ofacturare.prg:266-308` (identic in `factureaza2`, :751-816) - alegerea SQL-ului de
populare a articolelor se face pe `tnTip`, NU pe `in_valuta`:
```
Case Inlist(tnTip, 48, 49) -> cursor_articole_k(?poDate.zi_curs, ...)
Case tnTip = 45 -> cursor_preturi(?poDate.zi_curs, ...)
Case Inlist(tnTip, 1,22,5,29,7,10,23) -> cursor_preturi(?poDate.zi_curs, ...)
Case Inlist(tnTip, 2,26,6,52) -> cursor_contract(?poDate.zi_curs, ...)
Case Inlist(tnTip, 3,21,25,28,42,47) -> cursor_comanda(?poDate.zi_curs, ...)
Case tnTip = 4 -> cursor_avize(...) [FARA zi_curs]
Case Inlist(tnTip, 41) -> cursor_gestiune(?poDate.zi_curs, ...)
Case tnTip = 30 -> cursor_aviz_nir(...) [FARA zi_curs]
Case tnTip = 27 -> cursor_lucrare(?poDate.zi_curs, ...)
Case Inlist(tnTip, 8,9,24) -> cursor_retur(?poDate.in_valuta, ...) [FARA zi_curs]
```
Descoperire cheie: pentru tipurile 8, 9, 24 (retur) SI 30 (aviz pe NIR), SQL-ul NU trimite deloc
`poDate.zi_curs` catre Oracle - `zi_curs` gol sau completat nu are niciun efect pentru aceste
tipuri. Acesta e motivul real pentru care eliminarea controlului la tip 8/9 e sigura (mai puternic
decat simpla existenta a unei valori implicite).
Pentru tipurile care TRIMIT zi_curs, comportamentul in Oracle e diferit:
- `cursor_preturi` (`docs\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:2138+`) apeleaza NECONDITIONAT
`pack_facturare.verifica_cursuri_valute(V_DATA_CURS, V_ID_UTIL)` (linia 2153). Aceasta procedura
(`:16247-16274`) verifica cursul pentru TOATE valutele distincte din `FACT_VPRETURI_UTILIZATOR`
ale utilizatorului curent (nu doar valuta documentului!) si arunca
`RAISE_APPLICATION_ERROR(-20005, 'Nu este setat cursul din data de ... !')` daca oricare dintre
ele nu are curs care sa acopere `V_DATA_CURS` - EXCLUDE explicit moneda nationala
(`AND A.ID_VALUTA <> pack_facturare.nid_moneda_nationala`). Deci: chiar pe un document in LEI
(`in_valuta=0`), daca utilizatorul are liste de preturi in valuta configurate si data trimisa
(implicita sau nu) nu are curs setat, apelul PICA cu -20005 - INDIFERENT de vizibilitatea
selectorului pe formular. Riscul nu vine din camp gol, ci din "camp cu o data pentru care nu
exista curs in tabela CURS".
- `cursor_articole_k` (`:3595-3701`) si `cursor_lucrare` (`:3173-3593`, foloseste `V_DATA_CURS`
pentru comenzi_elemente) - `cursor_articole_k` NU apeleaza `verifica_cursuri_valute`, doar face
`LEFT JOIN CURS ... WHERE DATA <= V_DATA_CURS AND DATA2 >= V_DATA_CURS` - daca nu gaseste, cade
silentios pe `NVL(D.CURS,0)` (pret gresit, nu eroare). `cursor_lucrare` (:3186-3218) INSA face
o verificare proprie, similara: daca articolele comenzii au valute fara curs pe `V_DATA_CURS`,
arunca acelasi `-20005`.
- Exista deja o rutina de recuperare la acest cod de eroare: `ofacturare.prg:313-317`
```
If lnSucces < 0
AMESSAGEBOX(goExecutor.oPrelucrareEroare(), 16, "Eroare")
If goExecutor.nEroare = 20005
vizualizeaza_curs(poDate.zi_curs)
ENDIF
```
Deci sistemul ANTICIPEAZA deja cazul "curs lipsa la data respectiva" si deschide un ecran de
gestiune a cursurilor - independent de validarea din formularul de date.
**d) Afisare / etichetare (fara risc):**
- `frm_facturare_articole.Init` (:15097-15098) si `frm_facturare_articole2.Init` (:19004-19005):
`If !Empty(Nvl(poDate.zi_curs,{})) Then Thisform.lb_cursuri.Caption = "Curs valutar (" +
Dtoc(poDate.zi_curs) + ")"` - deja tolereaza gol (nu afiseaza nimic), fara eroare.
- `frm_date_factura.do_cauta_valuta` (:9344-9359) - dupa alegerea valutei, muta focusul pe
`clb_zi_curs` DACA exista (`Type(...)<>'U'`), altfel pe `clb_serie_act`. Deja conditionat.
- `onom_curs.vc2` (`ck_zi_curs`, `tx_zi_curs`) - ecran DIFERIT, de administrare a cursurilor
valutare in sine (nu are legatura cu `poDate.zi_curs`; e o cautare "dupa ziua cursului" generica).
## 5. Valoarea implicita azi si de unde vine
Vine din `oDateFactura.Init`/`Reset`, necondiionat de tip sau de `in_valuta`:
- `Init` (`ofacturare_comun.prg:235-247`): `ldData = Ttod(get_ora())`, ajustat la luna/anul curent
de facturare (`gnAn`/`gnLuna`) daca `get_ora()` cade in alta luna; apoi `.zi_curs = ldData`.
- `Reset` (`ofacturare_comun.prg:486-496`): `.zi_curs = .Data` (unde `.Data` a fost deja setat tot
din `ldData`-ul curent).
Deci implicit `zi_curs` = data curenta (get_ora, ajustata la perioada de facturare deschisa), NU
`Date()` brut si nu neaparat `dataact`/`dataireg` (desi acestea pornesc de la aceeasi `ldData`).
Ulterior, cat timp controlul `clb_zi_curs` exista pe formular, orice editare a `dataact`/`dataireg`
resincronizeaza `zi_curs = dataact` prin evenimentele `LostFocus` (vezi punctul 4b). Daca formularul
ar ascunde controlul FARA sa elimine obiectul si fara sa goleasca proprietatea, campul ar ramane
la valoarea implicita de la Init/Reset (sau la ultima valoare sincronizata inainte de ascundere).
## 6. Precedent: tip cu campul ascuns care se salveaza corect
DA, exista deja, dar in `frm_date_factura` (formularul de FACTURA), nu in `frm_date_aviz_lucrare`:
```
COMUN\clase\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 ^
```
Pentru tip 8 si 9 (facturi de retur - care pot fi chiar in valuta, vezi `ofacturare_comun.prg:248`:
`INLIST(m.tnTip, 5,6,7,9,10,52) -> .in_valuta = 1`), controlul `clb_zi_curs` e eliminat COMPLET de
pe formular, necondiionat de `in_valuta`, si documentul se salveaza corect. Motivele, in ordine de
robustete:
1. Validarea din `inainte_de_do_termin` (:9484) e deja garda cu `Type(...)<>'U'`, deci se
auto-dezactiveaza cand controlul nu mai exista.
2. SQL-ul de populare articole pentru tip 8/9 e `cursor_retur(?poDate.in_valuta,...)`
(`ofacturare.prg:306-307`) - NU trimite deloc `poDate.zi_curs`, deci nu poate cauza -20005 din
cauza acestui camp.
3. Chiar daca ar fi trimis, `poDate.zi_curs` tot ar avea valoarea implicita de la Init/Reset
(punctul 5) - nimic nu-l goleste la `RemoveObject`.
Aceasta e "reteta" cerinta de punctul 6: eliminarea vizuala e sigura pentru ca (a) validarea are
deja garda pe existenta controlului, si (b) proprietatea `poDate.zi_curs` nu e niciodata golita -
ramane pe implicitul din Init/Reset.
Pentru `frm_date_aviz_lucrare` (linia :8076) NU exista un tip cu campul ascuns - `clb_zi_curs` nu e
eliminat pentru nici tip 27, nici tip 30. Motivul plauzibil: tip 27 (aviz pe lucrare) chiar
foloseste `zi_curs` in `cursor_lucrare` pentru articolele comenzii (posibil in valuta), independent
de `poDate.in_valuta` al documentului-aviz insusi - deci acolo campul NU e un candidat sigur pentru
ascundere pe baza lui `in_valuta`. Pentru tip 30, SQL-ul (`cursor_aviz_nir`) nu foloseste `zi_curs`
deloc, deci validarea de acolo e superflua dar inofensiva (campul e mereu populat implicit).
## Ramas de verificat
- Nu am gasit inca daca decizia 15 / S4d intentioneaza sa includa si `frm_date_aviz_lucrare` in
formularul unificat, sau doar `frm_date_factura`/`frm_date_aviz`. Din cod, `frm_date_aviz_lucrare`
e un formular de sine statator, fara control de valuta, folosit doar pentru tnTip 27 si 30 - daca
planul S4d nu-l tinteste explicit, linia :8076 e in afara scopului imediat.
- Nu am verificat ce se intampla in `cursor_articole_k` (tip 48/49) si `cursor_gestiune` (tip 41)
fata de `verifica_cursuri_valute` - din citire, `cursor_articole_k` nu apeleaza acea procedura
(cade silentios pe curs 0), dar nu am verificat `cursor_gestiune`.
- Nu am verificat cum decide `frm_date_factura.Init` ce alte tipuri (in afara de 8,9) ar putea fi
candidate pentru ascunderea lui `clb_zi_curs` conform deciziei 15 - doar am confirmat mecanismul
existent si conditia dubla deja implementata la validare (in_valuta + Type<>'U').