Merge pull request #636 from Luca-Timo/feat/accounting-inbound-invoices

feat(accounting): incoming-invoice workflow v2 + VAT/financial settings consolidation
This commit is contained in:
Paul Nothaft
2026-06-18 21:23:51 +02:00
committed by GitHub
30 changed files with 1434 additions and 288 deletions
@@ -35,6 +35,14 @@ export interface InboundDocument {
markupPercent: number | null;
markupFlatMinor: number | null;
billedInvoiceId: number | null;
/** Client a rebill/passthrough is attached to (migration 132). */
customerAccountId: number | null;
customerName: string | null;
customerEmail: string | null;
/** ISO-2 supplier country — auto-defaults the tax treatment (reclaim list). */
supplierCountry: string | null;
/** Free-text categorisation note. */
note: string | null;
supplierPaid: boolean;
supplierPaidAt: string | null;
supplierPaymentMethod: PaymentMethod | null;
@@ -79,6 +87,20 @@ export interface InvoiceExpensePayload {
markupFlatMinor?: number | null;
}
/** One customer with categorised-but-unbilled rebill/passthrough docs. */
export interface PendingRebillSummary {
customerAccountId: number;
companyName: string | null;
displayName: string | null;
firstName: string | null;
lastName: string | null;
email: string | null;
isPassive: boolean;
billingCadence: string | null;
itemCount: number;
openAmountMinor: number;
}
export interface ExpenseCategory { id: number; name: string; color: string | null; is_seed: boolean; display_order: number; }
export interface Paginated<T> { items: T[]; pagination: { page: number; pageSize: number; total: number; totalPages: number }; }
@@ -92,6 +114,8 @@ export interface AccountingSettings {
/** ISO-2 countries whose input VAT can be reclaimed (drives cost
* tax-treatment + the report's VAT-payable). */
accounting_vat_reclaim_countries: string[];
/** Output VAT code stamped onto NEW invoices/quotes ('' = none). */
accounting_default_output_vat_code: string;
}
export interface CategorizePayload {
@@ -148,6 +172,10 @@ export const accountingService = {
async updateInbound(id: number, fields: Partial<InboundDocument>): Promise<InboundDocument> { const { data } = await api.patch(`/admin/expenses/inbound/${id}`, fields); return data.document; },
async categorizeInbound(id: number, payload: CategorizePayload): Promise<InboundDocument> { const { data } = await api.post(`/admin/expenses/inbound/${id}/categorize`, payload); return data.document; },
async rebillInbound(id: number, payload: CategorizePayload): Promise<{ document: InboundDocument; invoiceId: number }> { const { data } = await api.post(`/admin/expenses/inbound/${id}/rebill`, payload); return data; },
/** Per-event customers with pending (categorised, unbilled) re-bills. */
async listPendingRebills(): Promise<PendingRebillSummary[]> { const { data } = await api.get('/admin/expenses/inbound/pending-summary'); return data.items; },
/** Bundle one customer's pending re-bills into a single invoice. */
async billPendingRebills(customerAccountId: number): Promise<{ invoiceId: number; count: number }> { const { data } = await api.post('/admin/expenses/inbound/bill-pending', { customerAccountId }); return data; },
async markInboundPaid(id: number, payload: { paid: boolean; paidAt?: string; paymentMethod?: PaymentMethod; paymentReference?: string }): Promise<InboundDocument> { const { data } = await api.post(`/admin/expenses/inbound/${id}/supplier-payment`, payload); return data.document; },
async getInboundFileBlob(id: number): Promise<Blob> { const { data } = await api.get(`/admin/expenses/inbound/${id}/file`, { responseType: 'blob' }); return data; },
async getInboundPageBlob(id: number, page: number): Promise<Blob> { const { data } = await api.get(`/admin/expenses/inbound/${id}/page/${page}`, { responseType: 'blob' }); return data; },
@@ -186,6 +214,8 @@ export const accountingService = {
accounting_vat_registered: data.accounting_vat_registered === true,
accounting_vat_reclaim_countries: Array.isArray(data.accounting_vat_reclaim_countries)
? data.accounting_vat_reclaim_countries : [],
accounting_default_output_vat_code: typeof data.accounting_default_output_vat_code === 'string'
? data.accounting_default_output_vat_code : '',
};
},
async updateSettings(payload: Partial<AccountingSettings>): Promise<{ updated: string[] }> {