From 45c7cc80ab68d00d3dc9b84dcf5f29e5d918aef5 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:54:23 +0200 Subject: [PATCH] fix(invoices): anchor issue date + Skonto window to the actual send date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scheduled invoice's issue_date was stamped at creation, so a long- scheduled invoice printed a stale date by the time it shipped — the relative Skonto window ("pay within N working days") and the net-days due date were then counted from the authoring day, not the send day. sendInvoice now stamps issue_date = send date on the first send and re-derives the due date from it, preserving a manual due-date override. Adds resolveNetDaysForRow to read net days from the persisted snapshot. Deselecting Skonto before the scheduled send already propagates (the scheduler re-reads the row fresh and the render context honours skonto_disabled); no change needed there. --- backend/src/services/invoiceService.js | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/backend/src/services/invoiceService.js b/backend/src/services/invoiceService.js index 19b3f818..356f98c5 100644 --- a/backend/src/services/invoiceService.js +++ b/backend/src/services/invoiceService.js @@ -144,6 +144,25 @@ async function resolveNetDays(payload, trx = db) { return 30; } +/** + * Net-days for an already-persisted invoice row (no payload). Reads the + * snapshot's net_days, then the crm_payment_default_net_days setting, + * then 30. Used at send time to re-anchor the due date when the issue + * date is stamped. Mirrors resolveNetDays' tail. + */ +async function resolveNetDaysForRow(invoice) { + const snap = typeof invoice.payment_term_snapshot === 'string' + ? (() => { try { return JSON.parse(invoice.payment_term_snapshot); } catch { return null; } })() + : invoice.payment_term_snapshot; + if (snap && snap.net_days != null) { + const n = ensureInt(snap.net_days); + if (n) return n; + } + const setting = ensureInt(await getAppSetting('crm_payment_default_net_days')); + if (setting) return setting; + return 30; +} + /** * Resolve the deal_uuid for a new invoice row (migration 140). Priority: * @@ -1904,6 +1923,39 @@ async function sendInvoice(id, adminId) { invoice.language = customer.preferred_language; } + // Stamp the issue date at the moment the invoice actually goes out. + // A scheduled invoice's issue_date is provisional — set to the + // authoring day at creation — but the legal issue date is when it + // ships. Anchoring it here keeps the printed invoice date, the Skonto + // window (a relative "pay within N working days" counted from that + // date) and the net-days due date all consistent with the send date. + // Only on the first send (status 'scheduled'); 'sent' / 'overdue' + // rows are immutable legal records and keep their stamped date. + if (invoice.status === 'scheduled') { + const sendDateIso = new Date().toISOString().slice(0, 10); + const netDays = await resolveNetDaysForRow(invoice); + // Re-anchor the due date too, but only when it was machine-set: if + // the stored due_date still equals the auto formula off the OLD + // base (scheduled_send_at, else the old issue_date), the admin never + // hand-edited it and we slide it to the new issue date. A divergent + // value means a manual override (the editor's "Override due date" + // toggle) — leave it untouched. + const oldBase = invoice.scheduled_send_at + ? new Date(invoice.scheduled_send_at) + : new Date(invoice.issue_date); + const oldAutoDue = computeDueDate(oldBase, netDays).toISOString().slice(0, 10); + const storedDue = invoice.due_date + ? new Date(invoice.due_date).toISOString().slice(0, 10) + : null; + const updates = { issue_date: sendDateIso, updated_at: new Date() }; + if (storedDue && storedDue === oldAutoDue) { + updates.due_date = computeDueDate(new Date(sendDateIso), netDays).toISOString().slice(0, 10); + } + await db('invoices').where({ id }).update(updates); + invoice.issue_date = updates.issue_date; + if (updates.due_date) invoice.due_date = updates.due_date; + } + const ctx = await buildInvoiceRenderContext(invoice, lineItems); const buffer = await pdfService.renderInvoiceToBuffer(ctx);