Skip to main content

Proposals & Invoices · Article 5.9

Numeracja faktur — sekwencyjna per rok per typ, atomowa, bez luk

Każda faktura otrzymuje numer sekwencyjny z atomowego licznika z zakresem per freelancer per rok per typ dokumentu. Żadnych luk niedopuszczalnych zgodnie z Art. 226(2) Dyrektywy VAT UE; egzekwowane przez blokadę bazy danych na poziomie wiersza, żeby równoczesne operacje nie mogły kolidować.

Numeracja faktur wydaje się trywialna, dopóki nie rozważysz: dwie faktury w locie w tym samym momencie nie mogą kolidować na numerze; usunięte faktury nie mogą zwolnić swojego numeru do ponownego użycia; granica roku musi resetować się czysto; luki w sekwencji są czerwonymi flagami w audytach. Projekt licznika rozwiązuje to wszystko w jednym mechanizmie — zaimplementowanym w apps/proposals/models.py:DocumentCounter i używanym wszędzie tam, gdzie dokument otrzymuje numer.

Why this works this way

Dlaczego zgodnie z prawem UE nie są dozwolone luki. Art. 226(2) Dyrektywy VAT UE wymaga, żeby numery faktur były "oparte na jednej lub kilku seriach, jednoznacznie identyfikujących fakturę". Krajowe implementacje (§14 UStG w DE, BTW Art. 35a w NL, Art. 106e ust. 1 pkt 2 Ustawy o VAT w PL) wyraźnie wymagają ciągłej sekwencji bez luk. Audytorzy traktują luki w sekwencji jako sygnał, że faktury zostały wystawione, a następnie ukryte (powszechny wzorzec unikania podatków: wydrukuj fakturę za gotówkę, zniszcz ją, nikt nie zauważy luki). Standard audytu: każdy numer od 0001 wzwyż w danym roku musi istnieć w Twoich aktach.

Projekt licznika (apps/proposals/models.py:DocumentCounter):

``python class DocumentCounter(BaseModel): freelancer = ForeignKey(User) # zakres: per freelancer document_type = CharField() # zakres: per typ (PROPOSAL, DEPOSIT, FINAL, …) year = IntegerField() # zakres: per rok next_number = IntegerField(default=1) # wartość class Meta: unique_together = [('freelancer', 'document_type', 'year')] ``

Wzorzec atomowego przyrostu:

``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() wydaje zapytanie SQL SELECT … FOR UPDATE, które zajmuje blokadę na poziomie wiersza dla wiersza licznika przez czas trwania transakcji. Równoczesne transakcje dla tego samego wiersza licznika blokują się na blokadzie; gdy pierwsza transakcja zatwierdzi, druga widzi next_number = 2 i kontynuuje. Kolizja niemożliwa.

Dlaczego ma to znaczenie dla aneksów i storno. Gdy aneks generuje Storno (STR-) i nowy INV (wersja 2), każdy otrzymuje własny numer sekwencyjny z własnego licznika:

- STR-2026-0001 (pierwsze storno roku) - INV-2026-0067 (następny dostępny INV w Twoim roku, różny od wcześniejszych INV)

Nowy INV to revision=2 tej samej logicznej faktury, ale otrzymuje świeży numer. Oryginalny INV (wersja 1) zachowuje swój oryginalny numer; oba są zachowane (oryginał jest oznaczony jako zastąpiony, nigdy usunięty). W ten sposób sekwencja INV nie pokazuje luki — każdy wystawiony INV pozostaje, tylko z flagą zastąpiony dla tych, które zostały zastąpione.

Reset granicy roku. 1 stycznia pole year zmienia się; nowe faktury wyszukują (freelancer, typ, 2027) — ten wiersz jeszcze nie istnieje, więc get_or_create tworzy go z next_number=1. Wiersz licznika z poprzedniego roku pozostaje bez zmian (nie usuwaj; przydatny dla audytu). Pierwsza faktura nowego roku to INV-2027-0001.

Brak ponownego użycia, brak wypełniania luk. Jeśli faktura jest unieważniona (Storno), jej numer pozostaje w sekwencji — Storno to nowy dokument się do niej odwołujący; nie zwalnia oryginalnego numeru. Jeśli faktura jest błędnie wystawiona (np. dane testowe w produkcji — nie powinno się zdarzyć, ale), nadal zajmuje swoje miejsce. Ścieżka audytu jest zachowana.

Niezależność per typ dokumentu. Twój licznik DEP jest niezależny od licznika INV. Więc możesz mieć:

- DEP-2026-0042 + INV-2026-0042 (projekt, gdzie DEP i INV przypadkowo trafiły na ten sam numer seryjny — zbieg okoliczności, nie wymuszone). - DEP-2026-0099 + INV-2026-0017 (DEP wyzwala się częściej, jeśli masz wielu małych klientów z zaliczkami, ale nieliczne zakończenia projektów w roku).

To jest celowe — różne typy dokumentów obsługują różny czas zdarzeń prawnych, więc ich sekwencje są niezwiązane.

Co się dzieje, gdy wiersz licznika desynchronizuje się? Niezwykle rzadkie (wymagałoby błędu integralności bazy danych); jeśli tak się dzieje, następna faktura używa ponownie istniejącego numeru → IntegrityError przy wstawieniu ze względu na unikalne ograniczenie na (freelancer, typ, rok, numer) w modelu dokumentu. Clozo to przechwytuje i ponawia z następnym dostępnym numerem. Wpływ widoczny dla użytkownika końcowego: dodatkowe ~50ms przy rzadkim kolidującym zapisie.

Niestandardowe prefiksy numeracji per firma. Obecnie nie są konfigurowalne — każdy użytkownik Clozo ma te same prefiksy INV- / DEP- / STR- / DCR- / CRN- / REC- / PRO- / AGR-. Niektóre starsze systemy księgowe preferują prefiksy tylko numeryczne lub specyficzne dla firmy (np. „RE-2026-0042" dla niemieckiego „Rechnung" lub „FV-2026-0042" dla polskiej „Faktury VAT"). W planie; na razie standardowe prefiksy są uznawane w UE i akceptowane przez każdy urząd skarbowy, z którym przeprowadziliśmy testy.

Troubleshooting

Keep reading