From 2479d87afc5b094a5323bca0b67e6404ce05a4e2 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:20:59 +0200 Subject: [PATCH] feat(accounting): bill editor VAT dropdown + GET returns vat_code snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 2 + 1b: - Bill editor: VAT-rate field → VatRateSelect dropdown (mirrors the quote editor); snapshots vatCode on create + carries it from a source quote. - getQuoteById + the invoice serializer now return vat_code, so re-editing a saved document preserves the snapshot instead of falling back to the rate→code map. Payload types (quotes + bills) carry vatCode. 72 tests pass; build green. --- backend/src/services/invoiceService.js | 2 ++ backend/src/services/quoteService.js | 2 ++ frontend/src/pages/admin/bills/BillEditorPage.tsx | 14 ++++++++++++-- frontend/src/services/bills.service.ts | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/services/invoiceService.js b/backend/src/services/invoiceService.js index 3248fad3..b25400bd 100644 --- a/backend/src/services/invoiceService.js +++ b/backend/src/services/invoiceService.js @@ -1780,6 +1780,8 @@ async function buildInvoiceRenderContext(invoice, lineItems) { totals: { netAmountMinor: invoice.net_amount_minor, vatRate: invoice.vat_rate, + // Migration 130 — VAT-code snapshot (so re-editing preserves it). + vatCode: invoice.vat_code ?? null, vatAmountMinor: invoice.vat_amount_minor, shippingAmountMinor: invoice.shipping_amount_minor, totalAmountMinor: invoice.total_amount_minor, diff --git a/backend/src/services/quoteService.js b/backend/src/services/quoteService.js index c93abffc..8c83686e 100644 --- a/backend/src/services/quoteService.js +++ b/backend/src/services/quoteService.js @@ -1562,6 +1562,8 @@ async function duplicateQuote(id, adminId) { expectedDurationHours: quote.expected_duration_hours, paymentTermTemplateId: quote.payment_term_template_id, vatRate: quote.vat_rate, + // Migration 130 — VAT-code snapshot (so re-editing preserves it). + vatCode: quote.vat_code ?? null, shippingAmountMinor: quote.shipping_amount_minor, introText: quote.intro_text, outroText: quote.outro_text, diff --git a/frontend/src/pages/admin/bills/BillEditorPage.tsx b/frontend/src/pages/admin/bills/BillEditorPage.tsx index 228f5281..1980d1e6 100644 --- a/frontend/src/pages/admin/bills/BillEditorPage.tsx +++ b/frontend/src/pages/admin/bills/BillEditorPage.tsx @@ -14,6 +14,7 @@ import { quotesService } from '../../../services/quotes.service'; import { contractsService } from '../../../services/contracts.service'; import { businessProfileService } from '../../../services/businessProfile.service'; import { CustomerPicker } from '../../../components/admin/CustomerPicker'; +import { VatRateSelect } from '../../../components/admin/VatRateSelect'; import { LineItemsTable, type EditableLineItem } from '../../../components/admin/LineItemsTable'; import { InstallmentsPanel } from '../../../components/admin/InstallmentsPanel'; import { customerAdminService } from '../../../services/customerAdmin.service'; @@ -58,6 +59,8 @@ export const BillEditorPage: React.FC = () => { // effect; new invoices start as null so they pick up profile.) const [qrFormat, setQrFormat] = useState(null); const [vatRate, setVatRate] = useState(0); + // Migration 130 — snapshot of the chosen output VAT code (null = custom rate). + const [vatCode, setVatCode] = useState(null); const [shipping, setShipping] = useState(0); const [ccPdfEmail, setCcPdfEmail] = useState(''); const [lineItems, setLineItems] = useState([]); @@ -137,6 +140,8 @@ export const BillEditorPage: React.FC = () => { // it inherits the profile default at render time. setQrFormat((inv.qrFormat as InvoiceQrFormat | null) || null); setVatRate(Number(inv.vatRate || 0)); + setVatCode(((inv as { vatCode?: string | null }).vatCode + ?? (inv as { totals?: { vatCode?: string | null } }).totals?.vatCode) ?? null); setShipping(Number(inv.shippingAmountMinor || 0) / 100); setCcPdfEmail(inv.ccPdfEmail || ''); setPaymentTermTemplateId(inv.paymentTermTemplateId ?? null); @@ -280,6 +285,7 @@ export const BillEditorPage: React.FC = () => { const { quote, lineItems: quoteLineItems } = await quotesService.get(contract.sourceQuoteId); if (quote.currency) setCurrency(quote.currency); if (quote.vatRate != null) setVatRate(Number(quote.vatRate)); + setVatCode((quote as { vatCode?: string | null }).vatCode ?? null); if (quote.shippingAmountMinor != null) { setShipping(Number(quote.shippingAmountMinor) / 100); } @@ -373,6 +379,7 @@ export const BillEditorPage: React.FC = () => { // which is the same outcome but pollutes the column. qrFormat: qrFormat || undefined, vatRate, + vatCode, shippingAmountMinor: toMinor(shipping), ccPdfEmail: ccPdfEmail || undefined, // Payment-term template id (migration 113). null = no template @@ -651,8 +658,11 @@ export const BillEditorPage: React.FC = () => {
- setVatRate(Number(e.target.value))} /> + { setVatRate(rate); setVatCode(code); }} /> setShipping(Number(e.target.value))} />
diff --git a/frontend/src/services/bills.service.ts b/frontend/src/services/bills.service.ts index daa93720..ca5f0a9c 100644 --- a/frontend/src/services/bills.service.ts +++ b/frontend/src/services/bills.service.ts @@ -180,6 +180,8 @@ export interface InvoiceCreatePayload { installmentLabel?: string; installmentTrigger?: string; vatRate?: number; + /** Migration 130 — snapshot of the chosen output VAT code (null = custom rate). */ + vatCode?: string | null; shippingAmountMinor?: number; ccPdfEmail?: string; // null = explicitly clear the per-invoice override (back to the