Skip to main content

Proposals & Invoices · Article 5.9

Factuurnummering — opeenvolgend per jaar per type, atomisch, geen hiaten

Elke factuur krijgt een opeenvolgend nummer van een atomische teller die is begrensd per freelancer per jaar per documenttype. Geen hiaten toegestaan conform EU-BTW-Richtlijn Art. 226(2); afgedwongen door een rij-niveau databasevergrendeling zodat gelijktijdige bewerkingen niet kunnen botsen.

Factuurnummering lijkt triviaal totdat u bedenkt: twee facturen die op exact hetzelfde moment worden verwerkt mogen niet botsen op een nummer; verwijderde facturen kunnen hun nummer niet vrijgeven voor hergebruik; een jaargrens moet schoon resetten; hiaten in de reeks zijn rode vlaggen in audits. Het tellerontwerp lost al deze problemen op in één mechanisme — geïmplementeerd in apps/proposals/models.py:DocumentCounter en overal gebruikt waar een document wordt genummerd.

Why this works this way

Waarom geen hiaten zijn toegestaan onder EU-recht. EU-BTW-Richtlijn Art. 226(2) vereist dat factuurnummers "gebaseerd zijn op één of meer reeksen, die de factuur uniek identificeren". Nationale implementaties (§14 UStG in DE, BTW Art. 35a in NL) vereisen expliciet een ononderbroken reeks zonder hiaten. Accountants beschouwen reeksgaten als een signaal dat facturen zijn uitgerekt en vervolgens achtergehouden (een gebruikelijk ontduikingspatroon: een factuur printen voor contante betaling, hem vernietigen, niemand ziet het gat). De auditstandaard: elk nummer van 0001 opwaarts in een bepaald jaar moet in uw administratie aanwezig zijn.

Het tellerontwerp (apps/proposals/models.py:DocumentCounter):

``python class DocumentCounter(BaseModel): freelancer = ForeignKey(User) # bereik: per freelancer document_type = CharField() # bereik: per type (PROPOSAL, DEPOSIT, FINAL, …) year = IntegerField() # bereik: per jaar next_number = IntegerField(default=1) # de waarde class Meta: unique_together = [('freelancer', 'document_type', 'year')] ``

Het atomische ophoogpatroon:

``python with transaction.atomic(): counter, created = DocumentCounter.objects.select_for_update().get_or_create( freelancer=proposal.freelancer, document_type='PROPOSAL', year=2026, defaults={'next_number': 1} ) number = counter.next_number counter.next_number += 1 counter.save() ``

select_for_update() geeft een SELECT … FOR UPDATE SQL-query uit die een rij-niveau vergrendeling neemt op de tellerrij voor de duur van de transactie. Gelijktijdige transacties voor dezelfde tellerrij blokkeren op de vergrendeling; zodra de eerste transactie commit, ziet de tweede next_number = 2 en gaat verder. Geen botsing mogelijk.

Waarom dit belangrijk is voor addenda en storno's. Wanneer een addendum een Storno (STR-) genereert en een nieuwe INV (revisie 2), krijgt elk zijn eigen opeenvolgend nummer van zijn eigen teller:

- STR-2026-0001 (eerste storno van het jaar) - INV-2026-0067 (volgende beschikbare INV in uw jaar, anders dan eventuele eerdere INV's)

De nieuwe INV is revisie=2 van dezelfde logische factuur maar krijgt een nieuw nummer. De originele INV (revisie 1) behoudt zijn oorspronkelijke nummer; beide worden bewaard (het origineel is gemarkeerd als vervangen, nooit verwijderd). Zo toont de INV-reeks geen gat — elke uitgereichte INV blijft aanwezig, slechts met een vervangen-vlag voor vervangen exemplaren.

Reset op jaargrens. Op 1 januari verandert het jaar-veld; nieuwe facturen zoeken (freelancer, type, 2027) op — die rij bestaat nog niet, dus get_or_create maakt hem aan met next_number=1. De tellerrij van vorig jaar blijft ongewijzigd (niet verwijderen; nuttig voor audit). De eerste factuur van het nieuwe jaar is INV-2027-0001.

Geen hergebruik, geen hiaat-opvulling. Als een factuur wordt nietig verklaard (Storno), blijft zijn nummer in de reeks — de Storno is een nieuw document dat ernaar verwijst; het geeft het originele nummer niet vrij. Als een factuur per ongeluk is uitgerekt (bijv. testgegevens in productie — mag nooit gebeuren, maar), neemt het nog steeds zijn slot in. Het audittraject is bewaard.

Onafhankelijkheid per documenttype. Uw DEP-teller is onafhankelijk van uw INV-teller. U kunt dus hebben:

- DEP-2026-0042 + INV-2026-0042 (een project waarbij DEP en INV toevallig op hetzelfde serienummer uitkwamen — toeval, niet afgedwongen). - DEP-2026-0099 + INV-2026-0017 (DEP wordt vaker verstuurd als u veel kleine klanten heeft met voorschotten maar weinig projectvoltooiingen in een jaar).

Dit is opzettelijk — verschillende documenttypen dienen verschillende juridische gebeurtenistiming, dus hun reeksen zijn onafhankelijk.

Wat gebeurt er als de tellerrij niet gesynchroniseerd raakt? Uiterst zeldzaam (zou een database-integriteitsfout vereisen); als het toch gebeurt, hergebruikt de volgende factuur een bestaand nummer → IntegrityError bij invoeging vanwege de unieke beperking op (freelancer, type, jaar, nummer) op het documentmodel. Clozo vangt dit op en probeert het volgende beschikbare nummer. Zichtbaar effect voor de eindgebruiker: een extra ~50ms bij de zeldzame conflicterende opslag.

Aangepaste nummerprefixes per bedrijf. Momenteel niet configureerbaar — elke Clozo-gebruiker heeft dezelfde INV- / DEP- / STR- / DCR- / CRN- / BON- / PRO- / SOV- prefixes. Sommige legacy-boekhoudingsystemen geven de voorkeur aan nummeriek of bedrijfsspecifieke prefixes (bijv. "RE-2026-0042" voor een Duitse "Rechnung"). Op de roadmap; voor nu zijn de gestandaardiseerde prefixes EU-erkend en worden ze geaccepteerd door elke belastingautoriteit waartegen we hebben getest.

Troubleshooting

Keep reading