Releases: robert-malai/anaf-sync
Release list
anaf-sync 0.6.1 — facturile `unknown` se pot repara acum, pe toate sistemele
Versiunea 0.6.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 — a doua oară după 0.5.0, din alt motiv. Conținutul ei e inclus
mai jos, iar 0.6.1 e versiunea completă. Dacă ai instalat deja 0.6.0 de pe
PyPI, ai tot ce e descris la Îmbunătățiri; îți lipsește doar remedierea de la
Remedieri, care privește aplicația de pe Windows.
Îmbunătățiri
Facturile care apar cu unknown se pot repara. Se poate întâmpla ca
descărcarea să reușească, dar citirea XML-ului să nu: ANAF acceptă un document
pe care versiunea instalată de anafpy nu îl poate încă interpreta. Factura
se arhivează oricum — ZIP-ul semnat e scopul — dar tot ce se deduce din XML se
prăbușește în unknown: numărul, data, partenerul și valoarea din fereastra
Facturi, plus calea pe disc, care devine un folder unknown.
Până acum nu exista cale de întoarcere. Evidența nu revizitează niciodată un
mesaj deja arhivat — regula care garantează că nimic nu se descarcă de două
ori — iar după 60 de zile nici măcar sync --redownload nu mai ajunge la el.
Rândul rămânea greșit definitiv, oricât de mult ar fi învățat anafpy între
timp să citească.
Dovada nu s-a pierdut însă niciodată: XML-ul facturii stă permanent în ZIP-ul
arhivat. Exact pe asta se bazează și repararea PDF-urilor din 0.5.1, doar că
acum se aplică datelor și căii, nu randării:
anaf-sync reprocess --dry-run # arată ce s-ar corecta
anaf-sync reprocess # recitește ZIP-urile și corectează catalogul
anaf-sync reprocess --move --dry-run # și unde s-ar muta fișierele
anaf-sync reprocess --move # le mută pe calea dată de șablonFără --move se schimbă doar ce se vede în aplicație; fișierele rămân exact
unde sunt. Cu --move se recalculează și calea din șablon, se mută acolo toate
fișierele facturii și se șterge folderul unknown rămas gol. Dacă o factură
tot nu poate fi citită, comanda o numără separat, pe unreadable: atunci
merită raportată, iar o versiune mai nouă de anafpy plus încă o rulare a
aceleiași comenzi rezolvă restul.
Comanda nu cere autentificare și nu atinge rețeaua deloc, deci o arhivă veche
de ani se repară la fel de bine ca una de azi.
Un buton pentru fiecare factură, în fereastra Facturi. Panoul din dreapta
are acum „Recitește din arhivă” — aceeași operație, pentru o singură
factură. Pe rândurile cărora le lipsesc toate datele apare evidențiat, sub o
explicație a motivului: descărcarea a reușit, doar citirea nu, iar originalul
semnat e intact pe disc. Pe celelalte rânduri rămâne discret, ca să poți
re-așeza o factură după ce schimbi șablonul, fără să reorganizezi toată arhiva.
Facturile catalogate cu backfill au butonul dezactivat, cu motivul în tooltip:
sunt foldere pe care anaf-sync nu le administrează, așa că nu le rescrie
fișierele.
reprocess --move e și modul corect de a reorganiza arhiva după ce schimbi
template. Rearanjează local fișierele deja descărcate, spre deosebire de
sync --redownload, care poate aduce din nou doar ce se mai află în fereastra
de 60 de zile. Rulează întâi cu --dry-run, ca să vezi câte fișiere s-ar muta.
Remedieri
Pe Windows, aplicația nu mai rămâne blocată pe „Se sincronizează…”. Dacă
pornirea comenzii eșua — de pildă pentru că programul anaf-sync nu se mai
găsea acolo unde fusese instalat — meniul rămânea dezactivat definitiv, iar
butonul de reparare din fereastra Facturi la fel: aplicația aștepta un proces
care nu pornise niciodată, așa că nimic nu mai venea să o deblocheze. Singura
ieșire era repornirea aplicației.
Windows anunță o pornire eșuată pe loc, spre deosebire de macOS și Linux, care
o anunță o clipă mai târziu; aplicația trata cele două anunțuri în ordinea
greșită. Acum ordinea e garantată pe toate sistemele, iar un eșec de pornire
readuce meniul la loc, ca oricare altă rulare încheiată.
Internal
- What a re-projection may not invent.
message_typeandcreated_at
(data_creare, which the delay flag reads) come only from ANAF's listing,
which is long gone by the time anything is reprocessed — so the stored row
keeps them, along withcif,direction,saved_atand the message id.
Re-deriving them asNonewould silently turn "unknown delay" into "on
time".context.project_archivedis the third door onto the module's single
parse, carrying exactly those facts back in. request_idrefuses the move. It is the one path variable with no home
in any artifact, so a template referencing it errors up front rather than
renderingunknownover paths that already hold the real value. The catalog
refresh has no such dependency and still runs.- Moves are planned before they are applied. An occupied destination
refuses that message (its catalog is still refreshed); a destination whose
source is gone is recognised as a move a previous run already made; and the
file the pass re-read the message from moves last, so an interrupted run
resumes. Files land before the row does — a crash between them leaves a
stale pointer with every byte intact, where the reverse would point the row
at files that never arrived. SyncRunneris nowCliRunner, with one in-flight guard across every
subcommand the tray spawns. Two tray-spawned children would meet at the sync
lock and the second would die on it, so the UI does not offer the second
click.ARTIFACT_SUFFIXESmoves toconfig, shared bybackfilland
reprocessinstead of copied.CliRunneremitsstartedbefore it callsQProcess.start(). Windows
fails a launch insidestart(); POSIX defers the same error to the event
loop. Emitting afterwards deliveredfinishedbeforestartedon Windows
only, and the tray's two handlers then ran in the order that leaves the UI
disabled forever. This is what broke 0.6.0's release: the tray suite — which
0.5.1 had just started running on Windows in CI, precisely to stop finding
these at release time — caught it, but on the release job's leg, after the
publish step had already put 0.6.0 on PyPI.- The tray runner's tests fake at the
QProcessseam. They used to aim at
a missing executable and assert the in-flight guard still held, which is only
true where the failure is deferred — a test that encoded one platform's
timing as the contract. A fake process makes the guard's lifetime the
runner's own, and a variant that fails synchronously pins the signal order on
every platform.
⚠️ Verificare. Comandareprocessare teste peste facturi UBL reale
(recitire, mutare, mutare întreruptă și reluată, destinație ocupată, arhive
fără ZIP) și a fost rulată cap-coadă pe o arhivă de probă: factura a ieșit din
unknown, folderul gol a dispărut, catalogul s-a completat și a doua rulare
nu a mai avut ce face. Nu a fost însă rulată pe o arhivă reală de
producție — dacă--movese poartă altfel pe un volum de sute de facturi,
spune. Rulează întâi--dry-run, și ai oricând ZIP-urile semnate ca sursă de
adevăr. Blocajul de pe Windows are un test care reproduce eșecul sincron, dar
nu a fost verificat pe un Windows real — dacă meniul tot rămâne blocat
după o pornire eșuată, spune.
⚠️ 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.6.0...v0.6.1
anaf-sync 0.6.0 — facturile `unknown` se pot repara acum
Versiunea aceasta 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 — a doua oară după 0.5.0, din alt motiv. Pagina de față
e scrisă retroactiv, ca tag-ul să nu rămână fără una. Versiunea de folosit
este 0.6.1,
care aduce tot ce e mai jos plus o remediere pentru aplicația de pe Windows.
Fișierele atașate sunt doar pachetele Python, identice cu cele publicate pe
PyPI. Programele descărcabile pentru desktop nu au existat niciodată pentru
versiunea aceasta; sunt în 0.6.1.
O factură arhivată cu câmpurile goale nu mai rămâne așa pentru totdeauna:
datele se recalculează din ZIP-ul deja salvat, iar factura se poate muta din
folderul unknown pe calea dată de șablon — local, fără autentificare și
fără limita de 60 de zile. În fereastra Facturi, fiecare factură are pentru
asta un buton propriu. /
A new anaf-sync reprocess re-derives an archived message's catalog fields —
and, with --move, its path — from the ZIP already on disk, so a document
anafpy could not read at download time is repairable by a later build,
offline and unconstrained by ANAF's window. The Facturi window offers the
same repair per invoice.
Îmbunătățiri
Facturile care apar cu unknown se pot repara. Se poate întâmpla ca
descărcarea să reușească, dar citirea XML-ului să nu: ANAF acceptă un document
pe care versiunea instalată de anafpy nu îl poate încă interpreta. Factura
se arhivează oricum — ZIP-ul semnat e scopul — dar tot ce se deduce din XML se
prăbușește în unknown: numărul, data, partenerul și valoarea din fereastra
Facturi, plus calea pe disc, care devine un folder unknown.
Până acum nu exista cale de întoarcere. Evidența nu revizitează niciodată un
mesaj deja arhivat — regula care garantează că nimic nu se descarcă de două
ori — iar după 60 de zile nici măcar sync --redownload nu mai ajunge la el.
Rândul rămânea greșit definitiv, oricât de mult ar fi învățat anafpy între
timp să citească.
Dovada nu s-a pierdut însă niciodată: XML-ul facturii stă permanent în ZIP-ul
arhivat. Exact pe asta se bazează și repararea PDF-urilor din 0.5.1, doar că
acum se aplică datelor și căii, nu randării:
anaf-sync reprocess --dry-run # arată ce s-ar corecta
anaf-sync reprocess # recitește ZIP-urile și corectează catalogul
anaf-sync reprocess --move --dry-run # și unde s-ar muta fișierele
anaf-sync reprocess --move # le mută pe calea dată de șablonFără --move se schimbă doar ce se vede în aplicație; fișierele rămân exact
unde sunt. Cu --move se recalculează și calea din șablon, se mută acolo toate
fișierele facturii și se șterge folderul unknown rămas gol. Dacă o factură
tot nu poate fi citită, comanda o numără separat, pe unreadable: atunci
merită raportată, iar o versiune mai nouă de anafpy plus încă o rulare a
aceleiași comenzi rezolvă restul.
Comanda nu cere autentificare și nu atinge rețeaua deloc, deci o arhivă veche
de ani se repară la fel de bine ca una de azi.
Un buton pentru fiecare factură, în fereastra Facturi. Panoul din dreapta
are acum „Recitește din arhivă” — aceeași operație, pentru o singură
factură. Pe rândurile cărora le lipsesc toate datele apare evidențiat, sub o
explicație a motivului: descărcarea a reușit, doar citirea nu, iar originalul
semnat e intact pe disc. Pe celelalte rânduri rămâne discret, ca să poți
re-așeza o factură după ce schimbi șablonul, fără să reorganizezi toată arhiva.
Facturile catalogate cu backfill au butonul dezactivat, cu motivul în tooltip:
sunt foldere pe care anaf-sync nu le administrează, așa că nu le rescrie
fișierele.
reprocess --move e și modul corect de a reorganiza arhiva după ce schimbi
template. Rearanjează local fișierele deja descărcate, spre deosebire de
sync --redownload, care poate aduce din nou doar ce se mai află în fereastra
de 60 de zile. Rulează întâi cu --dry-run, ca să vezi câte fișiere s-ar muta.
Internal
- What a re-projection may not invent.
message_typeandcreated_at
(data_creare, which the delay flag reads) come only from ANAF's listing,
which is long gone by the time anything is reprocessed — so the stored row
keeps them, along withcif,direction,saved_atand the message id.
Re-deriving them asNonewould silently turn "unknown delay" into "on
time".context.project_archivedis the third door onto the module's single
parse, carrying exactly those facts back in. request_idrefuses the move. It is the one path variable with no home
in any artifact, so a template referencing it errors up front rather than
renderingunknownover paths that already hold the real value. The catalog
refresh has no such dependency and still runs.- Moves are planned before they are applied. An occupied destination
refuses that message (its catalog is still refreshed); a destination whose
source is gone is recognised as a move a previous run already made; and the
file the pass re-read the message from moves last, so an interrupted run
resumes. Files land before the row does — a crash between them leaves a
stale pointer with every byte intact, where the reverse would point the row
at files that never arrived. SyncRunneris nowCliRunner, with one in-flight guard across every
subcommand the tray spawns. Two tray-spawned children would meet at the sync
lock and the second would die on it, so the UI does not offer the second
click.ARTIFACT_SUFFIXESmoves toconfig, shared bybackfilland
reprocessinstead of copied.
⚠️ Verificare. Comanda are teste peste facturi UBL reale (recitire,
mutare, mutare întreruptă și reluată, destinație ocupată, arhive fără ZIP) și
a fost rulată cap-coadă pe o arhivă de probă: factura a ieșit dinunknown,
folderul gol a dispărut, catalogul s-a completat și a doua rulare nu a mai
avut ce face. Nu a fost însă rulată pe o arhivă reală de producție — dacă
--movese poartă altfel pe un volum de sute de facturi, spune. Rulează
întâi--dry-run, și ai oricând ZIP-urile semnate ca sursă de adevăr.
⚠️ 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.1...v0.6.0
anaf-sync 0.5.1 — PDF-urile lipsă se refac singure
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 salvateUn 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 includewindows-latest, but synced it core-only, so
importorskipdropped 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.dbcannot 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 persistentmode=roconnection — 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 rebuiltstate.dbre-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. RunRecordgainsexpired. 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 bydays, never an explicitstart/end, so it sits on exactly the
path the clock margin covers and needed no change of its own.
pip install -U anaf-syncpulls 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
anaf-sync 0.5.0 — PDF-urile lipsă se refac singure
Versiunea aceasta 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. Pagina de față e scrisă retroactiv, ca tag-ul să nu
rămână fără una — versiunea de folosit e
0.5.1,
apărută în aceeași zi.
Conținutul acestei versiuni e descris integral în notele lui 0.5.1: repararea
PDF-urilor lipsă din ZIP-urile deja salvate (plus comanda anaf-sync render),
fereastra de listare ținută în interiorul limitelor ANAF, ca o diferență de
ceas să nu mai rateze toată sincronizarea, mesajele expirate numărate în loc
să fie raportate ca eșecuri, și lista de facturi care nu-și mai pierde poziția
la fiecare reîncărcare. Peste toate acestea, 0.5.1 adaugă reparația care
lipsește aici — suita de teste a aplicației de desktop rulează pe Windows în
CI, nu întâi la momentul release-ului.
Fișierele atașate sunt doar pachetele Python, identice cu cele publicate pe
PyPI. Programele descărcabile pentru desktop nu au existat niciodată pentru
versiunea aceasta; sunt în 0.5.1.
Full changelog: v0.4.0...v0.5.0
anaf-sync 0.4.0 — facturile care refuzau să se citească
anafpy 0.7.0 repară citirea facturilor pe care validarea prea strictă le
golea de date, fereastra Facturi arată data încărcării în SPV și numără
întârzierea în zile lucrătoare, iar tray-ul nu mai ține terminalul ocupat. /
anafpy 0.7.0 separates reading from authoring so real invoices no longer
collapse to unknown; the Facturi window gains the SPV upload date and a
working-day delay rule; the tray frees the terminal it was launched from.
Versiunea 0.3.2 a fost pregătită, dar nu a fost niciodată publicată —
conținutul ei (ritmul programării în schedule status, versiunea corectă în
bundle-ul de macOS) este inclus aici.
Remedieri
Facturile reale nu mai ajung unknown_unknown_unknown. Cititorul din
anafpy aplica documentelor primite de la ANAF regulile de construcție ale
modelelor de emitere, așa că un inbox real de 85 de facturi se citea în
zero: fiecare se arhiva cu number, issue_date și partner_name prăbușite
în unknown și se fișa ca unknown_unknown_unknown_<id>. anafpy 0.7.0 face
din citire și emitere contracte diferite
(anafpy#9), iar toate cele
85 se citesc acum. Arhiva nu a pierdut nimic — ZIP-urile semnate erau intacte,
doar numele fișierelor și catalogul purtau unknown; un anaf-sync backfill
peste folderul respectiv le catalogează acum pe toate.
Un document care tot nu se citește nu mai tace. Până acum, avertismentul
cititorului ajungea pe stderr — pe o rulare programată, adică nicăieri. Acum
sincronizarea îl consemnează în jurnal (view_unreadable) și arhivează
documentul oricum: ZIP-ul semnat e scopul, iar a-l refuza ar fi mai rău decât
a-l fișa cu câmpuri goale. backfill numără aceste documente separat
(unreadable, distinct de not_ubl, care până acum le absorbea pe amândouă),
iar CLI-ul tipărește contorul doar când e diferit de zero — ca semnalul de
eroare care este, nu ca încă o statistică.
Îmbunătățiri
Fereastra Facturi arată data încărcării în SPV. O coloană nouă,
Încărcată, stă lângă data emiterii — redenumită Emisă, pentru că un simplu
„Data" devenise ambiguu. Semnalul de întârziere se mută de la 5 zile
calendaristice la 5 zile lucrătoare între emitere și încărcare, numărate
luni–vineri — așa cum e formulat termenul de raportare e-Factura; sărbătorile
legale nu sunt modelate, deliberat. Evidențierea chihlimbarie se mută pe
celula Încărcată — chiar faptul întârziat. O factură marcată până acum ca
întârziată poate să nu mai fie: regula s-a schimbat, nu datele.
Tray-ul eliberează terminalul. Pe Windows, anaf-sync-tray rulează acum
sub pythonw: nicio fereastră de consolă — nici din shell, nici la pornirea
automată la login — iar prompt-ul revine imediat. Pe macOS și Linux, pornit
dintr-un terminal, tray-ul se desprinde într-o sesiune proprie și îți dă
terminalul înapoi; închiderea terminalului nu mai închide tray-ul.
--foreground păstrează comportamentul atașat pentru depanare, iar
contextele fără terminal (launchd, autostart XDG) rulează atașat în
continuare, ca managerul de procese să vadă adevărul.
schedule status arată ritmul, nu doar existența programării. Până acum,
rândul schedule: confirma doar că un job e instalat — dacă îl instalaseși cu
--every 6h acum trei luni, singura cale să afli era să deschizi tu plist-ul,
unitatea systemd sau Task Scheduler. Acum ritmul e citit chiar din
planificator și tipărit lângă stare, cu expresia cron echivalentă atunci când
una se potrivește:
launchd agent ro.anaf-sync.sync: loaded — runs daily at 07:30 (cron: 30 7 * * *)
Sursa adevărului rămâne sistemul de operare; un registru propriu ar fi
însemnat încă un lucru care se poate desincroniza tăcut. Două precizări
despre expresia cron: descrie ritmul, nu punctul de pornire (un interval
instalat cu --every se numără de la momentul instalării, deci orele reale
sunt aproape sigur decalate față de grila cron) și apare doar când e exactă —
pentru --every 45m sau --every 2d nu se afișează nicio expresie, pentru că
una familiară dar greșită e mai rea decât niciuna.
Internal
- The macOS bundle derives its version from the package it bundles.
CFBundleShortVersionStringwas a hand-written literal in the PyInstaller
spec, stale the moment it was written — every.appbetween v0.1.3 and
v0.3.1 reported 0.1.3 to the Finder. The spec now imports the installed
anaf_syncversion and passes it through PyInstaller'sversion=kwarg,
so the.appand the release can no longer disagree. - The Facturi header-state key is versioned, so a column layout saved by
an older tray is not replayed onto the six-column table; the saved layout
resets once.
⚠️ 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.3.1...v0.4.0
anaf-sync 0.3.1 — o iconiță în Dock mai puțin
Pe macOS, tray-ul instalat cu pip/uv nu mai apare în Dock ca „Python”. /
A one-fix release for the macOS tray: no more Dock icon inviting anyone to
quit the thing that watches your archive.
Fix
The tray no longer shows a Dock icon on macOS when installed from PyPI.
The downloadable .app bundle has always been menu-bar-only — its Info.plist
declares LSUIElement. A pip install "anaf-sync[tray]" or
uv tool install has no bundle, and Qt's Cocoa plugin turns the bare
interpreter into a foreground application: the tray ran with a Dock icon
labelled Python 3.13, next to no window and no obvious owner. Anyone
tidying their Dock would quit it, and the tray — and with it the only visible
sign that the archive is being watched — went away silently.
The tray now asks AppKit for the accessory activation policy at startup, the
programmatic twin of LSUIElement. The menu-bar icon is unchanged, and both
windows still come to the front when you open them: an accessory application
is absent from the app-switcher, so Facturi and Setări now ask to be activated
explicitly rather than trusting Qt's raise alone.
Nothing to configure. Restart the tray after upgrading — the policy is set
when the process starts, so a tray left running from 0.3.0 keeps its Dock icon
until it does.
Linux and Windows are untouched: the code is a no-op off macOS, and it never
raises there or anywhere else. A missing Dock icon is cosmetic; a tray that
crashed trying to hide one is not.
Internal
- The macOS bundle stops lying about its version.
CFBundleShortVersionString
in the PyInstaller spec had been pinned at0.1.3since it was written, so
every.appsince then reported that number to the Finder. It now matches
the release.
⚠️ 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.3.0...v0.3.1
anaf-sync 0.3.0 — the invoices ANAF will never list again
Facturile mai vechi de 60 de zile intră în arhivă cu anaf-sync backfill, iar
anaf-sync init cere de-acum CIF-ul. / A new command for invoices past ANAF's
retention window, one changed command signature, and two archiving fixes.
What's new
anaf-sync backfill <folder> catalogs invoices already on disk. ANAF lists
a message for 60 days and never again, so an operator adopting anaf-sync has
years of invoices the API is structurally incapable of showing it — and an
archive whose database is lost cannot be rebuilt from ANAF either. Both are the
same job: read the ZIPs off disk and catalog what they say. Number, date,
partner, total and currency all reconstruct from the document itself, and the
direction (received or sent) is deduced from the CIFs inside it; invoices
between two other firms are skipped and counted. --dry-run reports what would
be cataloged and writes nothing.
The command only reads: nothing is downloaded, moved or renamed, and files
stay exactly where they are. Two fields exist only in ANAF's listing and cannot
be recovered — the message type and the date the invoice reached SPV — so the
"declared late" check has nothing to stand on for backfilled rows. For the same
reason those rows never suppress a download: an invoice cataloged this way
that is still inside the 60-day window will be fetched from ANAF anyway, under
its real message id. A duplicate costs one file; a false skip loses an invoice
for good.
Point it at the folder in [output] directory to rebuild a lost catalog —
rows already present are left untouched, since a real ANAF id beats anything
inferable from disk. Re-running is safe: the second pass updates the same rows
rather than duplicating them.
Changed — action required
anaf-sync init now takes the CIF as an argument. anaf-sync init on its
own used to write a config carrying a 12345678 placeholder — a config that
cannot sync until it is hand-edited is one you can forget to edit, and the
failure surfaces on the first scheduled run, silently. It is now
anaf-sync init 12345678, repeatable for several firms
(anaf-sync init 12345678 87654321), with the RO prefix optional and
stripped. One CIF writes cif = "...", several write cifs = [...], each with
the other form shown as a hint. A malformed CIF is rejected before anything
touches the filesystem, so a bad run leaves no half-written file.
Scripts and documentation that call bare anaf-sync init need the argument
added. Existing config files are unaffected.
Fixes
Artifact suffixes are appended instead of replacing the last dot-segment.
Path.with_suffix replaces a trailing dot-segment rather than adding one, so a
Romanian legal form arriving as … S.R.L was filed as … S.R.zip — a letter
short. Four of ten invoices in a real archive were affected, and a template
ending in {number} fared worse: FCT.1001 became FCT.zip. Nothing was ever
lost — the database stores the un-mangled base and every reader re-applied the
same operation — but the names on disk were wrong, and two invoices differing
only past the last dot would have collapsed onto one file.
Files written by earlier versions keep their truncated names; new invoices are
filed correctly from now on. Re-running sync --redownload re-files anything
still inside the 60-day window under the corrected name.
A message where no artifact could be written is now a failure, not a silent
success. Every artifact writer may decline a message, and the engine used to
record the message as archived with an empty artifact list — the run exited 0,
and because archived records are permanent, it was never retried. The default
["zip", "pdf"] hides this, since the ZIP always writes; a pdf-only archive
does not, and ANAF's rendering service answers HTTP 200 with a JSON error
payload instead of PDF bytes when it refuses a document. Past 60 days that
invoice was unrecoverable. It is now reported as a per-message failure naming
the configured artifacts, visible in anaf-sync status and retried on the next
run while the window is still open.
Docs
The README gained a section on what is saved per invoice and why the zip
should stay in artifacts: it is the signed original every other artifact
derives from, and the derivation is one-way. The PDF is what you read; the ZIP
is what you keep.
Internal
- Dependency floor raised to anafpy 0.6.1.
- The tray's private copy of the suffix logic is gone — it now calls the shared
template.artifact_path, so the two can no longer drift.
⚠️ 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.2.4...v0.3.0
anaf-sync 0.2.4 — releases that explain themselves
Maintenance release: one visible fix, and the release process behind it.
Fix
anaf-sync --version reports the version you actually installed. The
version is declared twice — in pyproject.toml and as anaf_sync.__version__
— and v0.2.3 bumped only the first, so every 0.2.3 install answered
anaf-sync 0.2.2. The two are aligned again, and a test now fails the suite
the moment they disagree, which is the only reliable way to notice: a half-done
bump changes nothing else that anyone looks at.
Release process
Every release carries written notes from now on, and the old ones got theirs
retroactively. The prose lives in release-notes/ in the repository, one
file per tag, backfilled to v0.1.0 — including the four tags (v0.1.0, v0.1.2,
v0.1.3, v0.1.4) that were published to PyPI but never got a GitHub release at
all, and v0.2.3, whose release came out with an empty body.
That gap had a single cause: nothing in the release workflow created a release.
The one job that made one was the tray bundle upload, which has no notes to
give. A v* tag now publishes to PyPI first and creates the release
second — the release is the announcement, so it should never point at a
version pip install cannot reach yet — with the sdist, the wheel, and all
three tray bundles attached to that one release. PyPI's project page links out
to the releases, so the notes have one home.
Internal
- Dependency floor raised to anafpy 0.6.0. Nothing between 0.5.2 and 0.6.0
touches the surface anaf-sync uses (auth, e-Factura, exceptions, public
services) — those releases land DUKIntegrator self-install, a reverse-charge
authoring fix, and Windows declaration signing. The one change worth naming
is 0.5.3 movingServerConfigmisconfiguration off pydantic's
ValidationErrorand ontoAnafConfigError, whichconfig.pyalready
catches. - CI reports JUnit test results to Codecov alongside coverage, per matrix
leg, so a flaky test stays attributable to an OS and a Python version. The
upload runs even on a red run — that is precisely the run whose results are
worth reading.
⚠️ 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.2.3...v0.2.4
v0.2.3
Bump version to 0.2.3 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v0.2.2
Cleanup release: no new features, smaller and more truthful internals.
- Removed the never-wired health enrichment (interval/partner plumbing); the tray's amber alert now shows the purge countdown it always actually had.
- One shared 5-day delay rule (
health.is_delayed), one message projection in the engine, shared script-resolution between scheduler and autostart. - Facturi no longer re-scans the archive on every footer refresh.
anaf-sync statusopens the archive read-only and no longer creates state as a side effect.- Removed the redundant
anaf-sync versionsubcommand — useanaf-sync --version. - Documentation synced with the shipped tray (DESIGN.md, CLAUDE.md, CONTRIBUTING.md, README.md).