369 lines
16 KiB
Markdown
369 lines
16 KiB
Markdown
# Patch propus (v3) — arhiva 10 zile + fisier anual BNR, link alternativ cursbnr.ro
|
|
|
|
Tinta: `COMUN\clase\onom_curs.vc2`, clasa `actualizare_curs_bnr`. Investigatie sursa:
|
|
`docs\raport_curs_bnr_politici_preturi.md`. Diff unificat aplicabil: `docs\patch_curs_bnr_arhiva.diff`.
|
|
Inlocuieste integral v1 si v2 (nu e delta) — aplicat pe fisierul original, neschimbat intre timp.
|
|
|
|
**Scop restrans de Marius, valabil din v2**: se lucreaza DOAR pe ramura `nTip = 1`, cazul de dupa
|
|
ora 13. `nTip = 2` si `nTip = 3` raman neatinse.
|
|
|
|
Doua verificari live, facute de Marius, folosite ca atare (nereverificate aici):
|
|
- `https://curs.bnr.ro/nbrfxrates10days.xml` -> HTTP 200, un `<OrigCurrency>`, mai multe
|
|
`<Cube date="YYYY-MM-DD">` descrescator, pana la 10 zile lucratoare (v2).
|
|
- `https://curs.bnr.ro/files/xml/years/nbrfxrates2026.xml` -> HTTP 200, ~245 KB, structura
|
|
IDENTICA (un `<OrigCurrency>`, apoi `<Cube date="...">` de la 2026-01-05 pana azi, ordine
|
|
crescatoare). Acelasi cod de extragere a unui `<Cube>` merge neschimbat.
|
|
|
|
## 1. Proprietati noi: `cLinkArhiva` si `cLinkAn`
|
|
|
|
Bloc afectat: definitia clasei (lista `*p:`, `PropValue`, `_memberdata`), `onom_curs.vc2:16-42`.
|
|
|
|
**VECHI** (`onom_curs.vc2:16-18`, in `*<DefinedPropArrayMethod>`):
|
|
```
|
|
*p: ccursor
|
|
*p: clink
|
|
*p: ctempfile
|
|
```
|
|
**NOU:**
|
|
```
|
|
*p: ccursor
|
|
*p: clink
|
|
*p: clinkan
|
|
*p: clinkarhiva
|
|
*p: ctempfile
|
|
```
|
|
|
|
**VECHI** (`onom_curs.vc2:26-29`, in `*<PropValue>`):
|
|
```
|
|
ccursor = ccrsbnr
|
|
clink = https://curs.bnr.ro/nbrfxrates.xml
|
|
ctempfile = C:\temp\temp.xml
|
|
```
|
|
**NOU:**
|
|
```
|
|
ccursor = ccrsbnr
|
|
clink = https://curs.bnr.ro/nbrfxrates.xml
|
|
clinkan = https://curs.bnr.ro/files/xml/years/nbrfxrates%.xml
|
|
clinkarhiva = https://curs.bnr.ro/nbrfxrates10days.xml
|
|
ctempfile = C:\temp\temp.xml
|
|
```
|
|
`cLinkAn` tine sablonul cu `%` in locul anului; se compune la rulare cu
|
|
`Strtran(This.cLinkAn,"%",Alltrim(Str(lnAn)))`.
|
|
|
|
**VECHI** (`onom_curs.vc2:37-40`, in `_memberdata`):
|
|
```
|
|
<memberdata name="ctempfile" display="cTempFile"/>
|
|
<memberdata name="clink" display="cLink"/>
|
|
<memberdata name="ccursor" display="cCursor"/>
|
|
```
|
|
**NOU:**
|
|
```
|
|
<memberdata name="ctempfile" display="cTempFile"/>
|
|
<memberdata name="clink" display="cLink"/>
|
|
<memberdata name="clinkan" display="cLinkAn"/>
|
|
<memberdata name="clinkarhiva" display="cLinkArhiva"/>
|
|
<memberdata name="ccursor" display="cCursor"/>
|
|
```
|
|
|
|
## 2. `citeste_curs_bnr` (`onom_curs.vc2:100-136`) — parametru `tnSursa` (0/1/2) + cautare inapoi + reincercare an anterior
|
|
|
|
Parametrul `tlArhiva` (logic, din v2) devine **`tnSursa`** (numeric): `0` = fisierul zilei (`cLink`,
|
|
comportamentul actual, apelul existent `This.citeste_curs_bnr()` fara argumente ramane identic —
|
|
parametrul netransmis e `.F.` logic, convertit explicit la `0` cu `Vartype`), `1` = arhiva 10 zile
|
|
(`cLinkArhiva`), `2` = fisierul anual (`cLinkAn`, cu anul `Year(This.dData - 1)`). Cod de retur
|
|
neschimbat fata de v2: `0` = succes, `1` = eroare HTTP/serviciu indisponibil, `2` = nicio zi din
|
|
fereastra cautata nu exista in raspuns.
|
|
|
|
Cautarea inapoi zi cu zi (max 10 incercari) din v2 e neschimbata pentru `tnSursa=1` si e refolosita
|
|
identic pentru `tnSursa=2`. Cazul special pentru `tnSursa=2`: daca in timpul cautarii data candidata
|
|
trece in anul precedent fata de anul fisierului deja descarcat (caz real: o factura din primele zile
|
|
ale lui ianuarie are nevoie de publicarea din 31 decembrie anul trecut), se face **o singura
|
|
reincercare** — un al doilea request HTTP pe fisierul anual anterior — si cautarea continua in noul
|
|
raspuns. Nu se reincearca a doua oara peste alt an (`llRetrasAnAnterior` blocheaza a doua trecere).
|
|
|
|
**VECHI** (`onom_curs.vc2:100-136`, integral):
|
|
```
|
|
PROCEDURE citeste_curs_bnr
|
|
Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
|
|
Local lcData, lcFisier, lcServer
|
|
|
|
If Empty(This.cCursor)
|
|
This.cCursor = [ccrsbnr]
|
|
Endif
|
|
creeaza_backup_cursoare(This.cCursor) && modificare 03.06.2013
|
|
If Used(This.cCursor)
|
|
Use In (This.cCursor)
|
|
Endif
|
|
|
|
lcServer = This.cLink
|
|
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
|
|
|
|
lcFisier = Strextract(lcFisier,[</OrigCurrency>],[</Body>])
|
|
lcData = Strextract(lcFisier,[date="],["])
|
|
lcFisier= Strtran(lcFisier,[date="]+lcData+["],[])
|
|
lcFisier= Strtran(Strtran(lcFisier,[">],[" curs="]),[</Rate>],[" />])
|
|
Xmltocursor(lcFisier,This.cCursor)
|
|
This.dData = Ttod(Ctot(lcData+[T000000]))+1
|
|
*!* modificare 03.06.2013
|
|
sterge_backup_cursoare(This.cCursor)
|
|
Else
|
|
repune_backup_cursoare(This.cCursor)
|
|
Endif
|
|
*!* modificare 03.06.2013 ^
|
|
Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
|
|
|
|
ENDPROC
|
|
```
|
|
|
|
**NOU (v4, cu reparatia `Try/Catch` de la sectiunea 7):**
|
|
```
|
|
PROCEDURE citeste_curs_bnr
|
|
Lparameters tnSursa
|
|
Local loHTTP As 'winHTTP.winHTTPrequest.5.1'
|
|
Local lcData, lcFisier, lcServer, lcCubeCautat, ldCandidat, lnIncercari, lnAnCautat, llRetrasAnAnterior, lnStatus
|
|
|
|
If Vartype(tnSursa) <> "N"
|
|
tnSursa = 0
|
|
Endif
|
|
|
|
If Empty(This.cCursor)
|
|
This.cCursor = [ccrsbnr]
|
|
Endif
|
|
creeaza_backup_cursoare(This.cCursor) && modificare 03.06.2013
|
|
If Used(This.cCursor)
|
|
Use In (This.cCursor)
|
|
Endif
|
|
|
|
Do Case
|
|
Case tnSursa = 1
|
|
lcServer = This.cLinkArhiva
|
|
Case tnSursa = 2
|
|
lnAnCautat = Year(This.dData - 1)
|
|
lcServer = Strtran(This.cLinkAn,"%",Alltrim(Str(lnAnCautat)))
|
|
Otherwise
|
|
lcServer = This.cLink
|
|
Endcase
|
|
loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
|
|
loHTTP.Open('GET', lcServer, .F.)
|
|
loHTTP.SetTimeouts(5000,5000,5000,15000)
|
|
loHTTP.setRequestHeader("Content-Type", "application/xml;")
|
|
poLog.Log(m.lcServer)
|
|
Try
|
|
loHTTP.Send()
|
|
lnStatus = loHTTP.Status
|
|
Catch
|
|
lnStatus = 0
|
|
Endtry
|
|
|
|
If lnStatus = 200
|
|
lcFisier = loHTTP.Responsebody
|
|
|
|
lcFisier = Strextract(lcFisier,[</OrigCurrency>],[</Body>])
|
|
If tnSursa = 0
|
|
lcData = Strextract(lcFisier,[date="],["])
|
|
lcFisier= Strtran(lcFisier,[date="]+lcData+["],[])
|
|
Else
|
|
lcCubeCautat = []
|
|
ldCandidat = This.dData - 1
|
|
lnIncercari = 0
|
|
llRetrasAnAnterior = .F.
|
|
Do While Empty(lcCubeCautat) And lnIncercari < 10
|
|
If tnSursa = 2 And Year(ldCandidat) < lnAnCautat And !llRetrasAnAnterior
|
|
llRetrasAnAnterior = .T.
|
|
lnAnCautat = lnAnCautat - 1
|
|
lcServer = Strtran(This.cLinkAn,"%",Alltrim(Str(lnAnCautat)))
|
|
loHTTP = Createobject('winHTTP.winHTTPrequest.5.1')
|
|
loHTTP.Open('GET', lcServer, .F.)
|
|
loHTTP.SetTimeouts(5000,5000,5000,15000)
|
|
loHTTP.setRequestHeader("Content-Type", "application/xml;")
|
|
poLog.Log(m.lcServer)
|
|
Try
|
|
loHTTP.Send()
|
|
lnStatus = loHTTP.Status
|
|
Catch
|
|
lnStatus = 0
|
|
Endtry
|
|
If lnStatus <> 200
|
|
repune_backup_cursoare(This.cCursor)
|
|
Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
|
|
Return 1
|
|
Endif
|
|
lcFisier = Strextract(loHTTP.Responsebody,[</OrigCurrency>],[</Body>])
|
|
Endif
|
|
lcData = Str(Year(ldCandidat),4) + "-" + Padl(Alltrim(Str(Month(ldCandidat))),2,"0") + "-" + Padl(Alltrim(Str(Day(ldCandidat))),2,"0")
|
|
lcCubeCautat = Strextract(lcFisier,[<Cube date="]+lcData+[">],[</Cube>])
|
|
ldCandidat = ldCandidat - 1
|
|
lnIncercari = lnIncercari + 1
|
|
Enddo
|
|
If Empty(lcCubeCautat)
|
|
repune_backup_cursoare(This.cCursor)
|
|
Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
|
|
Return 2
|
|
Endif
|
|
lcFisier = [<Cube>] + lcCubeCautat + [</Cube>]
|
|
Endif
|
|
lcFisier= Strtran(Strtran(lcFisier,[">],[" curs="]),[</Rate>],[" />])
|
|
Xmltocursor(lcFisier,This.cCursor)
|
|
If tnSursa = 0
|
|
This.dData = Ttod(Ctot(lcData+[T000000]))+1
|
|
Endif
|
|
*!* modificare 03.06.2013
|
|
sterge_backup_cursoare(This.cCursor)
|
|
Else
|
|
repune_backup_cursoare(This.cCursor)
|
|
Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
|
|
Return 1
|
|
Endif
|
|
*!* modificare 03.06.2013 ^
|
|
Release loUpdate,loIp,loUrl,lcData,lnSize,lcFisier,lcData
|
|
|
|
Return 0
|
|
ENDPROC
|
|
```
|
|
|
|
Explicatie tehnica pe scurt (nu intra in cod, doar aici):
|
|
- `tnSursa=0` (apel normal, nemodificat functional) merge exact pe ramura veche, pastreaza
|
|
`This.dData = publishingDate+1`.
|
|
- `tnSursa=1` — identic v2: cauta cel mai recent `Cube` cu data `<= This.dData - 1`, mergand inapoi
|
|
zi cu zi, max 10 incercari.
|
|
- `tnSursa=2` — acelasi mers inapoi zi cu zi, dar pe fisierul anual al anului `Year(This.dData-1)`;
|
|
daca data candidata trece in anul precedent fara sa fi gasit inca un `Cube`, face un singur request
|
|
suplimentar pe fisierul anului precedent si continua cautarea acolo. Daca acel request esueaza
|
|
(`Status<>200`), intoarce direct `1` (indisponibilitate), nu `2`.
|
|
- `This.dData` NU se rescrie pentru `tnSursa=1` sau `2` — ramane ziua ceruta, exact ce asteapta
|
|
`scrie_curs_bnr`.
|
|
|
|
## 3. `initializeaza` (`onom_curs.vc2:152-202`) — ramura `nTip=1`, cazul "prea tarziu azi": cascada + link alternativ
|
|
|
|
Se atinge STRICT ramura de la `onom_curs.vc2:170-173`. `nTip=2` si `nTip=3` raman neatinse.
|
|
|
|
**VECHI** (`onom_curs.vc2:170-173`):
|
|
```
|
|
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") && din wwutils.prg
|
|
Endif
|
|
```
|
|
|
|
**NOU:**
|
|
```
|
|
Case This.dData < Ttod(ltDataOra) Or This.dData = Ttod(ltDataOra) And Hour(ltDataOra) >= 13
|
|
lnStareBnr = This.citeste_curs_bnr(1)
|
|
If lnStareBnr = 2
|
|
lnStareBnr = This.citeste_curs_bnr(2)
|
|
Endif
|
|
Do Case
|
|
Case lnStareBnr = 0
|
|
&& curs preluat din arhiva sau fisierul anual BNR
|
|
Case lnStareBnr = 2
|
|
amessagebox("Nu exista cursul BNR pentru data specificata!",48,"Atentie")
|
|
Otherwise
|
|
If amessagebox("Nu poate fi preluat automat cursul BNR. Doriti sa accesati pagina cursbnr.ro?",4+32,"Confirmare") = 6
|
|
goUrl("https://www.cursbnr.ro/")
|
|
Endif
|
|
Endcase
|
|
```
|
|
|
|
Cascada, exact cum a cerut Marius:
|
|
1. incearca arhiva 10 zile (`citeste_curs_bnr(1)`);
|
|
2. daca intoarce `2` (data lipsa din raspuns), incearca fisierul anual (`citeste_curs_bnr(2)`);
|
|
3. daca si acela intoarce `2` -> `"Nu exista cursul BNR pentru data specificata!"`;
|
|
4. daca oricare din cele doua intoarce `1` (eroare HTTP/timeout) -> mesajul de indisponibilitate,
|
|
cu oferta de a deschide **`https://www.cursbnr.ro/`** (nu mai `http://www.bnr.ro`, doar in aceasta
|
|
ramura — celelalte doua aparitii ale `goUrl("http://www.bnr.ro")` din fisier, ramura de dinainte
|
|
de ora 13 si `Otherwise`-ul cu `nTip` necunoscut, raman neschimbate).
|
|
|
|
`lnStareBnr` nu e declarata explicit — la fel ca `lcSql`, `lnSucces`, `ltDataOra` deja folosite in
|
|
aceeasi metoda fara declarare explicita (scop implicit PRIVATE al VFP); pastrat pentru consistenta
|
|
stilistica.
|
|
|
|
## Raspuns despre `nTip`
|
|
|
|
Neschimbat fata de v2: se acopera doar `nTip=1`; `nTip=2` si `nTip=3` raman neatinse, prin decizie
|
|
explicita a lui Marius.
|
|
|
|
## 4. Ce acopera
|
|
|
|
| Scenariu | Rezultat |
|
|
|---|---|
|
|
| Azi, dupa ora 13, Oracle nu are cursul zilei | arhiva 10 zile gaseste ziua anterioara (`lnStareBnr=0`), fara mesaj |
|
|
| Weekend (luni, dupa ora 13, vinerea nu era salvata local) | arhiva gaseste `Cube`-ul de vineri prin mersul inapoi zi cu zi |
|
|
| Sarbatoare legala (o zi lucratoare inconjurata de nelucratoare) | arhiva merge inapoi pana gaseste ultima zi publicata, fara lista de sarbatori in cod |
|
|
| Data mai veche de 10 zile lucratoare, dar in anul curent sau anul trecut | arhiva 10 zile esueaza cu `2`, se incearca fisierul anual (`cLinkAn`), care acopera restul anului |
|
|
| Factura pe inceput de ianuarie (ex. 2 ianuarie), cursul cerut e din 31 decembrie anul trecut | fisierul anual porneste pe anul curerii (`Year(This.dData-1)`), cautarea trece granita anului, se face o singura reincercare pe fisierul anului precedent |
|
|
| Data mai veche decat acopera si arhiva, si fisierul anual (inclusiv anul precedent, dupa reincercare) | `"Nu exista cursul BNR pentru data specificata!"` |
|
|
| BNR picat / timeout la oricare din cele doua incercari (arhiva sau an) | mesajul de indisponibilitate, cu oferta de a deschide `https://www.cursbnr.ro/` |
|
|
|
|
## 5. Riscuri deschise
|
|
|
|
1. **Al doilea request HTTP inline in bucla, doar pentru `tnSursa=2` la trecerea de an** — cost
|
|
suplimentar minor (o cerere HTTP in plus, o singura data, doar cand data ceruta cade in primele
|
|
zile ale unui an); restul cautarii ramane pe sirul deja descarcat in memorie.
|
|
2. **Confirmat prin fetch real**: structura `nbrfxrates2026.xml` e identica per-Cube cu celelalte
|
|
doua fisiere BNR, doar cu mai multe elemente `<Cube date="...">`, in ordine crescatoare — extractia
|
|
printr-un singur `Cube` inainte de `Xmltocursor` ramane corecta si aici.
|
|
3. **`repune_backup_cursoare` pe cursor deja inchis — risc preexistent, nu introdus acum** (aceeasi
|
|
observatie ca in v1/v2): `citeste_curs_bnr` inchide `This.cCursor` inainte de orice apel HTTP;
|
|
`repune_backup_cursoare` (`oproceduri_comune.prg:6358-6375`) face `Select` pe un alias deja inchis
|
|
in acel moment — comportament mostenit, nu nou, si in afara scope-ului acestui patch.
|
|
4. **URL-uri BNR pe host `curs.bnr.ro`**, confirmate live acum (`nbrfxrates10days.xml`,
|
|
`nbrfxrates2026.xml`); se pot schimba fara preaviz — de reverificat manual daca reapare mesajul de
|
|
indisponibilitate des dupa aplicare.
|
|
5. **Fisierul anual creste an de an** (~245 KB la mijlocul lui 2026) — cautarea foloseste tot
|
|
`Strextract` pe sirul deja incarcat in memorie, fara reincarcare per incercare; cost acceptabil
|
|
pentru o operatie declansata manual din UI, nu intr-o bucla de facturare in masa.
|
|
|
|
## 6. Cum se probeaza (fara sa ruleze acum)
|
|
|
|
Nu am rulat nimic din ce urmeaza — se lasa pentru proba manuala dupa aprobare si aplicare:
|
|
|
|
1. Aplica diff-ul (`docs\patch_curs_bnr_arhiva.diff`) pe `COMUN\clase\onom_curs.vc2` prin fluxul
|
|
text->binar existent (skill `roa-vfp-text-edit`, `txt2vcx.ps1` cu `-ProjectRoot D:\ROA\ROAFACTURARE`).
|
|
2. Caz "azi dupa ora 13": instantiaza `actualizare_curs_bnr` cu `nTip=1`, seteaza data sistemului
|
|
sau `ltDataOra` simulat dupa ora 13, cere `citeste_curs(Ttod(get_ora()))` pentru o zi pentru care
|
|
Oracle nu are inca rand — verifica in `poLog` ca al doilea link apelat e
|
|
`https://curs.bnr.ro/nbrfxrates10days.xml`.
|
|
3. Caz "mai vechi de 10 zile, in acelasi an": alege o data acoperita doar de fisierul anual —
|
|
verifica in `poLog` ca dupa arhiva urmeaza apelul catre `https://curs.bnr.ro/files/xml/years/nbrfxrates<an>.xml`.
|
|
4. Caz trecere de an: cere cursul pentru 2 ianuarie (anul curent), cand 31 decembrie anul trecut nu
|
|
e salvat local — verifica cele doua request-uri succesive catre fisierul anual curent si cel
|
|
anterior, si ca `This.dData` ramane 2 ianuarie (nu 31 decembrie).
|
|
5. Verifica in `This.cCursor` dupa apel: `Reccount()>0`, campurile `Currency`/`Curs`/`multiplier`
|
|
populate identic cu un fetch normal.
|
|
6. Verifica in Oracle, dupa `scrie_curs_bnr`, ca randul scris in `CURS` are `data` = ziua ceruta.
|
|
7. Caz negativ, mai vechi decat orice arhiva: seteaza `This.dData` cu cativa ani in urma — asteapta
|
|
mesajul "Nu exista cursul BNR pentru data specificata!".
|
|
8. Caz eroare retea: schimba temporar `cLinkArhiva` (sau `cLinkAn`) la un URL invalid — asteapta
|
|
mesajul de indisponibilitate si confirma linkul nou `https://www.cursbnr.ro/` in loc de
|
|
`http://www.bnr.ro`, doar pe aceasta ramura.
|
|
|
|
## 7. Reparatie (v4) — `loHTTP.Send()` neprotejat (host nerezolvabil)
|
|
|
|
Defect gasit de testul headless (Cazul 6, `docs\test_curs_bnr_headless.md`): `loHTTP.Send()` nu era
|
|
in `Try/Catch`. La host complet nerezolvabil (DNS esuat, eroare OLE 1429), executia sarea peste
|
|
blocul `If loHTTP.Status = 200 ... Else ... Return 1 ... Endif`, intra in bucla de cautare `Cube`
|
|
cu `lcFisier` nedefinit si iesea gresit pe `Return 2` ("data lipsa" in loc de "BNR picat").
|
|
|
|
Reparatie, pe ambele aparitii ale `loHTTP.Send()` din metoda (fisierul zilei/arhiva/an, si
|
|
reincercarea pe anul precedent):
|
|
```
|
|
Try
|
|
loHTTP.Send()
|
|
lnStatus = loHTTP.Status
|
|
Catch
|
|
lnStatus = 0
|
|
Endtry
|
|
```
|
|
`Status` se citeste o singura data, in interiorul lui `Try` — citit dupa, a doua exceptie OLE
|
|
("The data necessary...") ar fi picat imediat. `lnStatus` inlocuieste toate referintele la
|
|
`loHTTP.Status` din metoda si e adaugat la lista `Local` existenta. `Catch` gol, fara logare noua;
|
|
pe al doilea `Send()` comportamentul la esec ramane neschimbat (`repune_backup_cursoare` +
|
|
`Return 1`). Cod complet, integrat in blocul "NOU" de la sectiunea 2.
|