feat(accounting): bill editor VAT dropdown + GET returns vat_code snapshot
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.
This commit is contained in:
@@ -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<InvoiceQrFormat | null>(null);
|
||||
const [vatRate, setVatRate] = useState(0);
|
||||
// Migration 130 — snapshot of the chosen output VAT code (null = custom rate).
|
||||
const [vatCode, setVatCode] = useState<string | null>(null);
|
||||
const [shipping, setShipping] = useState(0);
|
||||
const [ccPdfEmail, setCcPdfEmail] = useState('');
|
||||
const [lineItems, setLineItems] = useState<EditableLineItem[]>([]);
|
||||
@@ -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 = () => {
|
||||
<LineItemsTable items={lineItems} currency={currency} showDiscount={false}
|
||||
vatRate={vatRate / 100} shippingAmount={shipping} onChange={setLineItems} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mt-4">
|
||||
<Input type="number" step="0.1" label={t('bills.field.vatRate', 'VAT rate %') as string}
|
||||
value={vatRate} onChange={(e) => setVatRate(Number(e.target.value))} />
|
||||
<VatRateSelect
|
||||
label={t('bills.field.vatRate', 'VAT rate %') as string}
|
||||
rate={vatRate}
|
||||
code={vatCode}
|
||||
onChange={(rate, code) => { setVatRate(rate); setVatCode(code); }} />
|
||||
<Input type="number" step="0.01" label={t('bills.field.shipping', 'Shipping') as string}
|
||||
value={shipping} onChange={(e) => setShipping(Number(e.target.value))} />
|
||||
<div>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user