Skip to content

anaf-sync 0.5.1 — PDF-urile lipsă se refac singure

Choose a tag to compare

@github-actions github-actions released this 30 Jul 22:08

Un PDF refuzat de ANAF nu mai rămâne lipsă pentru totdeauna: se randează din
ZIP-ul deja salvat, la fiecare rulare. O sincronizare nu mai cade din cauza
unei secunde de diferență între ceasul tău și al ANAF, mesajele pe care ANAF
le listează dar nu le mai dă nu mai sunt raportate ca erori, iar lista de
facturi nu-ți mai fuge de sub cursor. /
Missing PDFs now re-render from the archived ZIPs at the end of every sync (and
on demand, via a new anaf-sync render); a one-second clock disagreement no
longer fails the whole listing; messages past ANAF's download window stop being
counted as failures; the Facturi list stops resetting its scrollbar.

Versiunea 0.5.0 a ajuns pe PyPI, dar nu a primit niciodată o pagină de release
și nici programe descărcabile: pasul care le construiește a picat pe Windows,
după publicare. Conținutul ei e inclus aici, iar 0.5.1 e versiunea completă —
dacă ai instalat deja 0.5.0 de pe PyPI, ai tot ce e descris mai jos, cu
excepția reparației din secțiunea Internal.

Îmbunătățiri

PDF-urile lipsă se refac din ZIP-urile deja salvate. Se poate întâmpla ca
descărcarea să reușească, dar randarea PDF-ului să fie refuzată de ANAF —
firewall-ul lor respinge uneori XML-ul unor facturi perfect valide
(anafpy#10). Mesajul se
arhiva atunci doar cu ZIP-ul, iar pentru că evidența nu revizitează niciodată
un mesaj deja arhivat — regula care garantează că nimic nu se descarcă de două
ori — PDF-ul lipsea definitiv.

Nu mai e cazul, și nu trebuie făcut nimic: fiecare rulare sync se încheie
încercând din nou PDF-urile restante, direct din ZIP-urile de pe disc. Nu
cere autentificare și nu se supune limitei de 60 de zile — XML-ul e deja la
tine, iar serviciul de transformare al ANAF e public. Când există restanțe,
sumarul rulării capătă o linie pdf repair: ….

Același pas există și ca o comandă de sine stătătoare, pentru o arhivă
existentă pe care vrei s-o repari pe loc:

anaf-sync render --dry-run   # arată ce PDF-uri lipsesc și s-ar putea randa
anaf-sync render             # le randează din ZIP-urile salvate

Un refuz repetat nu strică rularea și nu schimbă codul de ieșire: spre
deosebire de o descărcare pierdută, aici nu curge niciun termen — se
reîncearcă la următoarea rulare, până când ANAF acceptă documentul. Rândurile
venite din backfill sunt excluse deliberat: catalogează foldere pe care
sincronizarea nu le administrează. (Închide
#4.)

Remedieri

O secundă de diferență între ceasuri nu mai ratează toată sincronizarea.
ANAF verifică ambele capete ale intervalului cerut după ceasul lui: sfârșitul
nu poate fi în viitor, începutul nu poate depăși limita de 60 de zile. O
căutare pe fereastra completă atinge exact ambele limite, așa că orice
nepotrivire între ceasul calculatorului tău și al ANAF — într-o direcție sau
alta — respingea listarea în întregime:

endTime = 30-07-2026 15:15:28 nu poate in viitor fata de momentul
requestului = 30-07-2026 15:15:27

Nu e o situație exotică: o sincronizare programată care ia toată fereastra
trece pe ambele limite la fiecare rulare, iar un Windows al cărui serviciu de
timp nu e ținut din scurt ajunge acolo singur. Eroarea arată ca o defecțiune
la ANAF, deși e o problemă de ceas.

anafpy 0.7.2 construiește fereastra puțin în interiorul limitelor, iar dacă
diferența e mai mare, o reface după ceasul pe care ANAF îl indică chiar în
mesajul de respingere, și reia pagina o singură dată. Nu ai nimic de
configurat; actualizarea aduce versiunea nouă cu ea.

Mesajele pe care ANAF le listează, dar nu le mai dă, nu mai sunt erori.
Listarea și descărcarea își numără cele 60 de zile din puncte diferite, așa
că ANAF listează mesaje al căror termen de descărcare s-a închis deja. Orice
căutare care ajunge atât de departe atinge banda aceea de la margine — adică
mai ales prima sincronizare: la primul run al unui client, trei mesaje din 512.

Fiecare ajungea în tabela de eșecuri, cu cod de ieșire diferit de zero și un
avertisment chihlimbariu permanent în tray — peste ceva ce nicio reîncercare nu
repară și niciun operator nu poate curăța. Acum sunt consemnate în jurnal,
numărate și lăsate să treacă: fără rând în failures, fără rând în arhivă.
Nimic nu blochează o nouă încercare, deci un verdict greșit ar costa o
reîncercare, nu factura.

Contorul rămâne totuși vizibil în anaf-sync status, pentru că își merită
locul: la prima sincronizare e doar zgomot, dar la una ulterioară înseamnă că
programarea a avut o pauză destul de mare cât să se piardă facturi.

Lista de facturi nu mai sare sub cursor. Cu peste 500 de facturi,
scrollbar-ul din fereastra Facturi sărea la fiecare jumătate de secundă, până
devenea imposibil de folosit. Chiar cititul arhivei de către tray atingea
fișierele bazei de date, supraveghetorul raporta atingerea ca pe o schimbare,
iar reîncărcarea declanșată o lua de la capăt — o buclă care se autoîntreținea
și readucea lista la prima pagină de fiecare dată.

Acum evenimentele din sistemul de fișiere spun doar uită-te, nu reîncarcă:
se reîncarcă doar când datele chiar s-au schimbat. Iar reîncărcările
legitime nu mai dezorientează — lista își păstrează câte pagini erau deschise,
își regăsește rândul de sus și factura selectată, deci o sincronizare care se
încheie în timp ce citești nu-ți mai mută ecranul și nu-ți mai golește panoul
de detalii.

Internal

  • The tray suite now runs on Windows in CI, not first at release time. The
    0.5.0 bundle job failed on a watcher test: Windows refuses to unlink a file
    another handle has open, and the probe's persistent connection is exactly
    that. CI's matrix did include windows-latest, but synced it core-only, so
    importorskip dropped the whole Qt suite there — the tray's first Windows
    run was inside the release job, the one place a failure is expensive
    (0.5.0 published to PyPI and then produced no release at all). Every leg now
    syncs the tray extra.
  • On Windows, state.db cannot be deleted while the tray runs. The
    behaviour stands rather than being worked around: syncing is unaffected — WAL
    admits the concurrent writer, and only deletion hits the sharing violation
    — and dropping the handle between polls would reopen the refresh loop the
    probe exists to close. Rebuilding a lost archive there means quitting the
    tray first. Recorded in the watcher's docstring and DESIGN.md; the test
    skips on Windows, saying why.
  • The watcher decides refreshes from PRAGMA data_version, not file
    events.
    The tray's own read-only opens write WAL read-marks, so a
    filesystem watch cannot distinguish its own reads from a sync's commits. A
    single persistent mode=ro connection — the one deliberate exception to the
    tray's ephemeral-connection pattern — reads the counter, which only moves
    when another connection commits. File stats survive for identity alone, so a
    deleted or rebuilt state.db re-registers instead of hiding behind a pinned
    inode. The 60 s poll backstop inherits the same gate. CLAUDE.md and DESIGN.md
    record the exception so it does not get "corrected" away.
  • RunRecord gains expired. It is a JSON blob, so records written by
    earlier versions parse with the default — no migration, no schema change.
  • Requires anafpy>=0.7.2 — which both names the expired-download
    condition and holds the listing window inside ANAF's limits. The engine
    lists by days, never an explicit start/end, so it sits on exactly the
    path the clock margin covers and needed no change of its own.
    pip install -U anaf-sync pulls it in.

⚠️ Verificare. Repararea PDF-urilor a fost testată pe o copie a unei
arhive reale: un rând căruia i s-a șters PDF-ul a fost randat și consemnat,
patru facturi respinse de firewall au rămas în așteptare, iar două rânduri
fără ZIP pe disc au fost sărite. Tratarea mesajelor expirate are teste, dar
nu a fost încă verificată pe un răspuns real de la ANAF — dacă o
sincronizare le raportează în continuare ca eșecuri, spune. La fel și
corecția de ceas: un ceas rămas în urmă cu peste cinci minute tot
respinge listarea, pentru că partea aceea a mesajului ANAF nu indică
niciun moment după care s-ar putea corecta.

⚠️ The bundles are unsigned — no macOS notarization, no Windows
Authenticode. First run triggers the usual OS warning; use the
right-click-open workaround documented in the README. The CLI on PyPI
(pip install anaf-sync) is unaffected.

Full changelog: v0.5.0...v0.5.1