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