Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0032-invoice-backdating-to-previous-month.md
T
Stefan c741535aec
Playwright Tests / test (push) Canceled after 0s
feat: Implement mfr truncation fix and enhance invoice handling
- Added a new SQL script to address the "String or binary data would be truncated" error in mfr sync by widening the Note fields in service objects and companies to NVARCHAR(MAX).
- Updated the table types and associated stored procedures to accommodate the changes, ensuring compatibility with existing data.
- Queued a re-sync for previously rejected rows due to truncation issues.
- Created application logins for fuchs_fds and fuchs_web with placeholder passwords for initial setup.
- Introduced functions for invoice backdating and invoice date retrieval to streamline invoice processing.
- Developed a password generation function with customizable parameters for length and character types.
- Added a random number generation function to support password creation.
2026-09-30 16:56:08 +02:00

4.0 KiB
Raw Blame History

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-09-30
Fuchs_Database/dbo/Functions/fds__fn_invoiceBackdate.sql
Fuchs_Database/dbo/Stored Procedures/fds__setInvoiceFinal.sql
Fuchs/code/InvoiceDates.cs
Fuchs/Services/ERechnungMapper.cs
Fuchs/Services/InvoiceService.cs
Fuchs/Controllers/IntranetController.InvoiceDraft.cs
Fuchs/Controllers/IntranetController.Requests.cs
Fuchs/js/intranet/modules/fis.inv_shared.js

0032 — Invoices may be backdated to the last working day of the previous month (admins only)

Context

Work done at the end of a month is often invoiced in the first days of the next month. The customer wants those invoices dated to the previous month. The invoice date was always the moment of finalisation (fds__setInvoiceFinal: DateFinalized = GETUTCDATE()), and the invoice number is drawn from the year of that date (fds__newInvoiceId(Year(...))).

Two invoice dates also existed side by side: the PDF printed DateCreated (draft creation) as "Rechnungsdatum", while the embedded ZUGFeRD/XRechnung issue date (BT-2) used DateFinalized — a visible date and an e-invoice date that could disagree.

Decision

  • One invoice date. PDF "Rechnungsdatum" and e-invoice issue date both come from InvoiceDates.IssueDate: DateFinalized (converted from the stored UTC to the German calendar day), falling back to DateCreated for drafts.
  • Backdating option. When finalising, a user with fds_inv > 2 may date the invoice to the last working day (Mon–Fri) of the previous month — a fixed date, not a free choice. Public holidays are not excluded (possible later extension).
  • Only while no invoice exists after that date. Invoice numbers must ascend with invoice dates, so the option is offered and accepted only while no invoice has been finalized after the backdate day (a 2 h margin covers the UTC storage).
  • Single source of truth: [dbo].[fds__fn_invoiceBackdate](@authuser) returns the allowed date or NULL (authorisation + eligibility). inv/dpreview returns it so the preview dialog shows the per-invoice checkbox; req/sconf passes @backdate = 1, and fds__setInvoiceFinal re-evaluates the function and THROWs (60001) when it is no longer allowed — nothing is finalised then.
  • Serialized finalisation. fds__setInvoiceFinal runs check + numbering UPDATE in one transaction under an exclusive, transaction-owned app lock (sp_getapplock, resource fds__setInvoiceFinal) taken by every finalisation, so a concurrent normal finalisation cannot slip between the eligibility check and the backdated write and break the date/number order.
  • req/sconf maps only SQL error 60001 to the "no longer possible, create without backdating" message. A 0-row result (invoice already final/sent, e.g. a double submit) is reported as "already finalised" — never as a backdate rejection, which would invite a duplicate invoice.
  • A backdated invoice is stored with DateFinalized = <day> 12:00 (UTC, stays on the same German day), its number comes from that day's year, and DueDate is recomputed from the payment term anchored on that day.

Consequences

  • The PDF of every newly finalised invoice now shows the finalisation date instead of the draft creation date (intended: it now matches the e-invoice and the number sequence).
  • In January the option yields a December date and therefore a number from the previous year's range.
  • Lists, reports, DATEV and reminders follow the same invoice date — see ADR 0035.
  • Telemetry: fuchs.invoices.backdated (outcome ok / rejected / already_final / error); successful backdating logs EventId InvoiceBackdated.

Alternatives considered

  • Free date picker for admins — rejected: breaks the date/number order too easily; the need is exactly "previous month's last working day".
  • Global admin switch "backdate everything until the first invoice of the month" — rejected by the customer in favour of an explicit per-invoice checkbox.