|
|
|
@@ -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>
|
|
|
|
|