Files
roafacturare/docs/raport_curs_bnr_politici_preturi.md
2026-09-17 19:54:37 +03:00

14 KiB

Raport — mesajul "Nu poate fi preluat cursul BNR" la facturare pe politici de preturi

Investigatie READ-ONLY. Sursa: D:\ROA\ROAFACTURARE + D:\ROA\ROAFACTURARE\COMUN. Nicio editare, nicio rulare, niciun write-back.

1. Textul exact al mesajului si locatia lui

Gasit in COMUN\clase\onom_curs.vc2, clasa actualizare_curs_bnr, PROCEDURE initializeaza:

  • onom_curs.vc2:171 — textul exact reclamat de utilizator:
    If amessagebox("Nu poate fi preluat automat cursul BNR. Doriti sa accesati pagina BNR?",4+32,"Confirmare") = 6
        goUrl("http://www.bnr.ro")		&&	din wwutils.prg
    Endif
    
  • Variante inrudite, in aceeasi metoda:
    • onom_curs.vc2:175 si :181 si :190 — amessagebox("Nu exista cursul BNR pentru data specificata!",48,"Atentie")
    • onom_curs.vc2:193 — amessagebox("Doriti sa accesati pagina BNR?",4+32,"Confirmare") (ramura Otherwise, nTip necunoscut)

Nu exista alt loc in .prg/.vc2/.sc2 (in ROAFACTURARE sau COMUN) cu text asemanator ("cursul BNR", "nbrfxrates", "curs BNR") legat de un mesaj de eroare — un singur punct de emitere, in actualizare_curs_bnr::initializeaza.

2. Lantul de apel complet, de la facturare pana la mesaj

Punct de intrare din meniul de facturare pe lista/politica de preturi:

  1. COMUN\programe\oproceduri_facturare.prg:114-116 —
    Procedure facturare_lista_de_preturi
        Do politica.mpr
    Endproc
    
    Apelat din butonul Page2.Cw1.do_actiune (confirmat deja in docs\plan_11_integrare_politici_preturi.md:16-18).
  2. Meniuri\politica.mn2:12,37,43,46 — meniul politica.mpr cheama factureaza(1) (lei), factureaza(5)/(6) (invoice), factureaza(10) (factura fiscala in valuta), factureaza(9) (retur valuta) etc.
  3. COMUN\programe\ofacturare.prg:103 — Procedure factureaza(tnTip, toFactura, toSursa). Pentru tipurile de lista de preturi (1,5,7,10,22,23,29 — vezi comentariul din cod, ofacturare.prg:140-149) populeaza articolele prin SQL server-side care trimite poDate.zi_curs: ofacturare.prg:266-308 (dupa cercetarea deja existenta docs\cercetare\s4d_zi_curs_reactiv.md:144-146):
    Case Inlist(tnTip, 1,22,5,29,7,10,23) -> cursor_preturi(?poDate.zi_curs, ...)
    
  4. Oracle, pack_facturare.cursor_preturi cheama necondiționat pack_facturare.verifica_cursuri_valute(V_DATA_CURS, V_ID_UTIL) (D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:2153), care arunca RAISE_APPLICATION_ERROR(-20005, 'Nu este setat cursul din data de ... pentru <valuta>!') (:16268-16272) daca vreo valuta din listele de preturi ale utilizatorului nu are curs valabil la data ceruta (exclude doar moneda nationala).
  5. VFP prinde eroarea Oracle: COMUN\programe\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
    
  6. COMUN\programe\oproceduri_curs.prg:8-42 — Procedure vizualizeaza_curs(tdDataCurs) — deschide ecranul de administrare a cursurilor: loFrmCurs = Createobject("frm_curs",tdDataCurs) (:34), loFrmCurs.Show(1) (:36, modal). frm_curs (clasa de cautare/lista) NU declanseaza singura preluarea BNR — utilizatorul apasa "Nou" (But_nou1) ca sa adauge cursul lipsa, ceea ce deschide frm_curs_nou.
  7. COMUN\clase\onom_curs.vc2:1305-1326 — frm_curs_nou::Init leaga schimbarea datei/valutei inregistrarii noi la fetch automat:
    onom_curs.vc2:1323-1325
    Bindevent(poRec,"data",Thisform,"actualizeaza_curs_bnr",1)
    Bindevent(poRec,"iso_valuta",Thisform,"actualizeaza_curs_bnr",1)
    Thisform.oactualizare = Createobject("actualizare_curs_bnr",gnTipActualizareCurs)
    
  8. COMUN\clase\onom_curs.vc2:1209-1226 — frm_curs_nou::actualizeaza_curs_bnr (declansat la fiecare schimbare a datei/valutei din formular):
    onom_curs.vc2:1211
    This.oactualizare.citeste_curs(poRec.Data,poRec.iso_valuta)
    
  9. COMUN\clase\onom_curs.vc2:46-62 — actualizare_curs_bnr::citeste_curs(tdZiua,tcIsoValuta) — :50-52: If !Used(This.cCursor) Or tdZiua <> This.dData Then This.initializeaza(tdZiua).
  10. COMUN\clase\onom_curs.vc2:152-202 — actualizare_curs_bnr::initializeaza(tdZiua) — interogheaza Oracle (pack_curs.citeste_cotatii_curs, :158-159); daca Reccount(This.cCursor)=0 (nimic salvat local pentru data ceruta), intra pe Do Case descris la punctul 3 mai jos, unde se afla mesajul de la punctul 1.

A doua cale de intrare, directa, manuala, separata de fluxul de facturare de mai sus, spre acelasi mesaj: Clase\ofundal_facturare.vc2:929-931 (fereastra principala de facturare, tab Page1, buton Cw7):

PROCEDURE Page1.Cw7.do_actiune
    DO vizualizeaza_curs IN oproceduri_curs.prg WITH Ttod(get_ora())
ENDPROC

Trimite explicit data curenta (Ttod(get_ora())), deci un utilizator care apasa acest buton la ora 19 si cursul zilei nu exista inca in Oracle intra pe acelasi drum (pasii 6-10 de mai sus).

3. Cum se decide data pentru care se cere cursul

Doua surse posibile pentru tdZiua/This.dData, ambele data curenta a sistemului, nu data documentului si nu "data - 1":

  • Ruta automata (eroare -20005): poDate.zi_curs, care e populat necondiționat cu data curenta la initializarea antetului de factura — COMUN\programe\ofacturare_comun.prg:247 (Init) si :496 (Reset): .zi_curs = ldData, unde ldData = Ttod(get_ora()) ajustat doar la luna/anul de facturare deschisa (docs\cercetare\zi_curs_validare.md:198-204, citat ca sursa deja verificata). Nu e data documentului (dataact) decat daca operatorul a sincronizat manual campul (acelasi punct, §4b).
  • Ruta manuala (buton Cw7): Ttod(get_ora()) — literal ora curenta a sistemului, transformata in data (ofundal_facturare.vc2:930).

Decizia efectiva "azi sau nu" si "inainte sau dupa ora 13" se ia in actualizare_curs_bnr::initializeaza, onom_curs.vc2:164-176:

onom_curs.vc2:164-176
ltDataOra = get_Ora()
Do Case
    Case This.nTip = 1
        Do Case
            Case (This.dData = Ttod(ltDataOra) And Hour(ltDataOra) <13) Or (This.dData = Ttod(ltDataOra) + 1 And Hour(ltDataOra) >= 13)
                This.citeste_curs_bnr()
            Case This.dData < Ttod(ltDataOra) Or This.dData = Ttod(ltDataOra) And Hour(ltDataOra) >= 13
                If amessagebox("Nu poate fi preluat automat cursul BNR. Doriti sa accesati pagina BNR?",4+32,"Confirmare") = 6
                    goUrl("http://www.bnr.ro")
                Endif
            Otherwise
                amessagebox("Nu exista cursul BNR pentru data specificata!",48,"Atentie")
        Endcase

ltDataOra = ora curenta a sistemului (get_Ora()). Pentru scenariul raportat (data ceruta = azi, ora curenta = 19:00): This.dData = Ttod(ltDataOra) si Hour(ltDataOra) >= 13 -> intra direct pe a doua ramura (linia 170), care nu incearca deloc This.citeste_curs_bnr() — afiseaza direct mesajul de la linia 171. Prima ramura (fetch live, linia 168) se executa doar cand se cere ziua curenta inainte de ora 13, sau ziua urmatoare dupa ora 13 — niciodata pentru "azi, dupa ora 13".

4. Fallback la curs anterior — nu exista

In toata actualizare_curs_bnr::initializeaza (onom_curs.vc2:152-202) nu exista nicio ramura care sa citeasca "ultimul curs disponibil" sau "ziua lucratoare anterioara" cand data ceruta nu are curs local si e prea tarziu sa se mai incerce live-ul. Cele trei ramuri posibile pentru nTip=1 (liniile 166-176) sunt exhaustiv: (a) incearca live fetch, (b) cere confirmare pentru deschidere manuala a bnr.ro, (c) "Nu exista cursul BNR pentru data specificata!". Niciuna nu cade inapoi pe un rand data2 >= data anterioara din tabela locala. Confirmat si de cercetarea independenta anterioara (docs\cercetare\s4d_zi_curs_reactiv.md:160-171): reteta Oracle de sub verifica_cursuri_valute verifica strict daca exista curs care sa acopere data ceruta — daca nu, arunca -20005, fara fallback pe partea de baza de date.

Exista o a treia "sursa alternativa" definita in cod, actualizare_curs_bnr::citeste_curs_altesurse (onom_curs.vc2:64-98, nTip=2), care foloseste clasa curs (INFOVALUTAR, nu BNR direct) — dar se activeaza doar cand This.nTip = 2 sau = 3 (onom_curs.vc2:177-191). Valoarea lui nTip vine din parametrul gnTipActualizareCurs transmis la creare (onom_curs.vc2:1325, :1699). Nu am gasit nicio declarare/atribuire a variabilei gnTipActualizareCurs in tot codul text-cache din ROAFACTURARE sau COMUN (cautare exhaustiva, case-insensitive) — deci nu se poate confirma din cod ce valoare are efectiv la rulare; actualizare_curs_bnr::Init (onom_curs.vc2:144-146) o trateaza ca Iif(Empty(tnTip),1,tnTip), deci daca variabila e goala/nedefinita comportamentul cade pe nTip=1 (ramura fara alte surse, cea din mesajul raportat).

5. Sursa BNR: cum se apeleaza, timeout, tratarea erorii

actualizare_curs_bnr::citeste_curs_bnr — onom_curs.vc2:100-136:

onom_curs.vc2:100-118
PROCEDURE citeste_curs_bnr
    Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
    ...
    lcServer = This.cLink                      && "https://curs.bnr.ro/nbrfxrates.xml" (onom_curs.vc2:28)
    loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
    loHTTP.Open('GET', lcServer, .F.)
    loHTTP.setRequestHeader("Content-Type", "application/xml;")
    poLog.Log(m.lcServer)
    loHTTP.Send()

    If loHTTP.Status = 200
        lcFisier = loHTTP.Responsebody
        ...
        Xmltocursor(lcFisier,This.cCursor)
        This.dData = Ttod(Ctot(lcData+[T000000]))+1
        sterge_backup_cursoare(This.cCursor)
    Else
        repune_backup_cursoare(This.cCursor)
    Endif
  • Componenta: WinHTTP.WinHTTPRequest.5.1 (COM), cerere sincrona (Open(..., .F.) = async=False).
  • URL fix: https://curs.bnr.ro/nbrfxrates.xml (onom_curs.vc2:28, proprietatea cLink).
  • Niciun timeout explicit setat pe loHTTP (nu apare SetTimeouts/.Timeout in metoda) — foloseste timeout-ul implicit WinHTTP.
  • Tratare eroare: un singur test, If loHTTP.Status = 200 — orice alt rezultat (server jos, DNS, timeout, 404, XML gol, 500) cade pe Else -> repune_backup_cursoare(...) (:131), fara mesaj propriu si fara sa arunce exceptie — pur si simplu This.cCursor ramane fara date noi (Reccount=0).
  • Important: aceasta metoda (citeste_curs_bnr) e cea care ar face fetch-ul live, dar in scenariul raportat (azi, ora 19) nu e apelata deloc — vezi punctul 3, ramura (b) sare peste ea. Deci mesajul "Nu poate fi preluat automat cursul BNR" nu vine dintr-un fetch HTTP esuat in acest scenariu — vine dintr-o decizie de orar (Hour(ltDataOra) >= 13) care presupune ca preluarea trebuia sa se fi intamplat deja (manual sau altfel) inainte de ora 13, si nu mai incearca live dupa aceea.
  • Consecinta directa la intrebarea 5: mesajul de eroare NU distinge "serviciu indisponibil" de "data lipsa din raspuns", pentru simplul motiv ca in cazul raportat nici macar nu se ajunge sa se apeleze serviciul — testul e pur temporal (Hour>=13), nu bazat pe rezultatul unei incercari HTTP. Pe ramura care CHIAR apeleaza HTTP (citeste_curs_bnr, cazul "azi, inainte de ora 13" sau "maine, dupa ora 13"), un eventual esec HTTP (status <>200) e complet tacut fata de operator (fara amessagebox) — duce doar la Reccount=0, ceea ce las document fara curs, fara mesaj dedicat de "server indisponibil".

6. Stocare si import

  • View Oracle citit de UI: gcS.vcurs (schema curenta), coloane confirmate in oproceduri_curs.prg:20-21: id_curs, id_valuta, data, data2, nume_val, curs, multiplicator, id_valuta_iso, iso_valuta, curs_bnr.
  • Scriere: actualizare_curs_bnr::scrie_curs_bnr (onom_curs.vc2:204-225) apeleaza pack_curs.scrie_cotatii_curs(?pdData,?pcSirCursuri,?gnIdUtil) — daca ziua e vineri (Dow(pdData,2)=6), scrie si pentru sambata/duminica urmatoare (:213-216, curs valabil pe weekend). Aceasta scriere se intampla doar dupa un fetch reusit (citeste_curs_bnr cu status 200), apelata din initializeaza la linia 198 (If Reccount(This.cCursor)>0 Then This.scrie_curs_bnr()).
  • Citire cotatii curente pentru o data: pack_curs.citeste_cotatii_curs(?pdData) (onom_curs.vc2:158).
  • Nu exista, in codul text-cache din ROAFACTURARE/COMUN, niciun job/scheduler/task separat care sa apeleze automat citeste_curs_bnr/scrie_curs_bnr fara interventia unui utilizator care deschide un formular de curs (frm_curs_nou/frm_curs_nou_multiplu) sau declanseaza fluxul de eroare -20005. Singurele doua puncte de instantiere a clasei actualizare_curs_bnr in tot codul (onom_curs.vc2:1325, :1699) sunt ambele in formulare UI, niciuna intr-un .prg de pornire/batch/scheduler. Nu am gasit un al doilea produs/proces (in acest repo) care scrie in aceeasi tabela CURS — nu pot confirma sau infirma existenta unui import extern din alt sistem (ex. ROAPRETURI sau alt exe din suita), care nu are cache text generat si nu a fost cautat aici.

Rezumat lant, pe scurt

facturare_lista_de_preturi (oproceduri_facturare.prg:114) -> politica.mpr -> factureaza(tnTip) (ofacturare.prg:103) -> SQL cursor_preturi(?poDate.zi_curs) -> Oracle pack_facturare.verifica_cursuri_valute -> ORA-20005 daca lipseste cursul zilei curente -> VFP ofacturare.prg:313-317 -> vizualizeaza_curs(poDate.zi_curs) (oproceduri_curs.prg:8) -> frm_curs/frm_curs_nou (onom_curs.vc2) -> actualizare_curs_bnr.citeste_curs (:46) -> initializeaza (:152) -> la ora >=13 pentru data curenta, ramura care afiseaza direct "Nu poate fi preluat automat cursul BNR. Doriti sa accesati pagina BNR?" (:171), fara sa mai incerce fetch live si fara fallback pe ultimul curs disponibil.