From 04a78f99db6b67c006887009138e1edbb648dbf8 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Thu, 17 Sep 2026 19:54:37 +0300 Subject: [PATCH] sync SVN r18174 --- AGENTS.md | 14 +- docs/raport_curs_bnr_politici_preturi.md | 230 +++++++++++++++++++++++ 2 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 docs/raport_curs_bnr_politici_preturi.md diff --git a/AGENTS.md b/AGENTS.md index 75cf9ae..5d6c642 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,14 +21,8 @@ skill-ului. - final de sesiune sau de lucrare / "salveaza ce ai invatat" / actualizezi documentatia sau fluxurile -> skill `roa-actualizare-doc` -## Stil de interactiune +## Stil de interactiune si lucru -Cand ai o decizie, o ambiguitate sau mai multe abordari valide, prezinta optiuni cu trade-off-uri -si o recomandare clara, apoi intreaba — nu actiona unilateral. Pune recomandarea prima, marcat-o. -Intreaba o singura data, asteapta raspunsul; nu repeta intrebari la care nu s-a raspuns si nu intra -in loop de decizie. Actioneaza direct doar cand scopul e clar si schimbarea e mica; pentru orice -netrivial, propune un plan si intreaba inainte de a edita. Daca eziti intre doua variante -rezonabile, alege una si mergi — poti corecta. Separa "a raspunde" de "a executa": o intrebare sau -o parere primeste un raspuns, nu o implementare. Fii concis: fara preambul, fara sa povestesti ce -urmeaza sa faci. Extinderea completa (interactiune + cautare, distilata din promptul Claude Code): -`COMUN\docs\stil_interactiune_cautare.md`. +OBLIGATORIU, inainte de orice raspuns sau editare, si in sesiunea principala si in orice +subagent: citeste `COMUN\docs\stil_interactiune_cautare.md` si respecta-l (interactiune, +livrarea muncii, corectii, cautare). Nu il rezuma din memorie. diff --git a/docs/raport_curs_bnr_politici_preturi.md b/docs/raport_curs_bnr_politici_preturi.md new file mode 100644 index 0000000..82a075c --- /dev/null +++ b/docs/raport_curs_bnr_politici_preturi.md @@ -0,0 +1,230 @@ +# 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 !')` + (`: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.