feat(accounting): snapshot vat_code on quotes/invoices + export prefers it (foundation)
First slice of the VAT-consolidation: migration 130 adds a nullable vat_code snapshot column to quotes + invoices, and the Treuhänder export now prefers the invoice's snapshotted code over the (mutable) rate→code map, so a historical invoice's VatCode never changes when codes are re-mapped. Schema-drift guarded; behaviour-neutral until the editors start writing the snapshot (next slices). Part of: VAT registry → Settings→Accounting, invoice VAT dropdown, registration/ reclaim toggle.
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Migration: snapshot the chosen VAT code on quotes + invoices.
|
||||||
|
*
|
||||||
|
* The invoice/quote editors now pick an output VAT code from the central
|
||||||
|
* vat_codes registry (Settings → Accounting) instead of free-typing a rate.
|
||||||
|
* We snapshot the CODE STRING (e.g. "UN81") on the document at create time —
|
||||||
|
* alongside the existing vat_rate — so the Treuhänder/accounting export emits
|
||||||
|
* exactly the code the document was issued with, immutably. Editing or deleting
|
||||||
|
* a vat_codes row later never changes a historical document's export code.
|
||||||
|
*
|
||||||
|
* quotes.vat_code nullable string (snapshot)
|
||||||
|
* invoices.vat_code nullable string (snapshot)
|
||||||
|
*
|
||||||
|
* Legacy rows stay null; the export falls back to the rate→code map for those.
|
||||||
|
* Idempotent: columns guarded.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.up = async function (knex) {
|
||||||
|
for (const tbl of ['quotes', 'invoices']) {
|
||||||
|
if ((await knex.schema.hasTable(tbl)) && !(await knex.schema.hasColumn(tbl, 'vat_code'))) {
|
||||||
|
await knex.schema.alterTable(tbl, (table) => {
|
||||||
|
table.string('vat_code', 16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function (knex) {
|
||||||
|
for (const tbl of ['quotes', 'invoices']) {
|
||||||
|
if ((await knex.schema.hasTable(tbl)) && (await knex.schema.hasColumn(tbl, 'vat_code'))) {
|
||||||
|
await knex.schema.alterTable(tbl, (table) => {
|
||||||
|
table.dropColumn('vat_code');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -254,6 +254,8 @@ async function buildPostings({ from, to, currency } = {}) {
|
|||||||
const postings = [];
|
const postings = [];
|
||||||
|
|
||||||
// 1) Revenue invoices → Dr Debitoren / Cr Ertrag (gross, output VAT code).
|
// 1) Revenue invoices → Dr Debitoren / Cr Ertrag (gross, output VAT code).
|
||||||
|
// Migration 130 — prefer the snapshotted vat_code; guard for pre-130 DBs.
|
||||||
|
const hasInvVatCode = await db.schema.hasColumn('invoices', 'vat_code');
|
||||||
const invoices = await db('invoices')
|
const invoices = await db('invoices')
|
||||||
.leftJoin('customer_accounts', 'invoices.customer_account_id', 'customer_accounts.id')
|
.leftJoin('customer_accounts', 'invoices.customer_account_id', 'customer_accounts.id')
|
||||||
.leftJoin('events', 'invoices.event_id', 'events.id')
|
.leftJoin('events', 'invoices.event_id', 'events.id')
|
||||||
@@ -263,6 +265,7 @@ async function buildPostings({ from, to, currency } = {}) {
|
|||||||
.orderBy('invoices.issue_date', 'asc')
|
.orderBy('invoices.issue_date', 'asc')
|
||||||
.select(
|
.select(
|
||||||
'invoices.id', 'invoices.invoice_number', 'invoices.issue_date', 'invoices.vat_rate',
|
'invoices.id', 'invoices.invoice_number', 'invoices.issue_date', 'invoices.vat_rate',
|
||||||
|
...(hasInvVatCode ? ['invoices.vat_code'] : []),
|
||||||
'invoices.net_amount_minor', 'invoices.vat_amount_minor', 'invoices.total_amount_minor',
|
'invoices.net_amount_minor', 'invoices.vat_amount_minor', 'invoices.total_amount_minor',
|
||||||
'customer_accounts.company_name as customer_company_name',
|
'customer_accounts.company_name as customer_company_name',
|
||||||
'customer_accounts.first_name as customer_first_name',
|
'customer_accounts.first_name as customer_first_name',
|
||||||
@@ -273,7 +276,9 @@ async function buildPostings({ from, to, currency } = {}) {
|
|||||||
);
|
);
|
||||||
for (const inv of invoices) {
|
for (const inv of invoices) {
|
||||||
const revAcct = cfg.settings.defaultRevenue;
|
const revAcct = cfg.settings.defaultRevenue;
|
||||||
const vatCode = cfg.outputVatMap[rateKey(inv.vat_rate)] || '';
|
// Prefer the snapshot taken at issue time; legacy rows fall back to the
|
||||||
|
// (mutable) rate→code map.
|
||||||
|
const vatCode = inv.vat_code || cfg.outputVatMap[rateKey(inv.vat_rate)] || '';
|
||||||
const label = buildCustomerLabel(inv);
|
const label = buildCustomerLabel(inv);
|
||||||
postings.push({
|
postings.push({
|
||||||
date: inv.issue_date,
|
date: inv.issue_date,
|
||||||
|
|||||||
Reference in New Issue
Block a user