Files
roafacturare/docs/cercetare/zi_curs_validare.md
2026-09-09 22:19:22 +03:00

15 KiB
Raw Permalink Blame History

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