From 53a16f9f6f9b11c224b3ff5f337f8a7fe4dbfc38 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:17:57 +0200 Subject: [PATCH] fix(accounting): Banana I&E export uses the 'Category' column (not 'ContraAccount') MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real Banana Income & Expense files name the category column 'Category', not 'ContraAccount' (which the doc listed but is a double-entry concept) — so the income/expense account never landed and Banana warned 'ContraAccount column not found'. Use 'Category'. VatCode stays (it only warns on a non-VAT-enabled file; amounts are gross). Test updated. --- .../__tests__/services/ledgerService.test.js | 6 +++--- backend/src/services/ledgerService.js | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/__tests__/services/ledgerService.test.js b/backend/__tests__/services/ledgerService.test.js index edd84dd0..5ad53bcd 100644 --- a/backend/__tests__/services/ledgerService.test.js +++ b/backend/__tests__/services/ledgerService.test.js @@ -210,13 +210,13 @@ describe('exportPostings', () => { it('banana_ie format is Income & Expense columns, tab-separated .txt', async () => { const { content, filename, contentType } = await ledgerService.exportPostings({ ...period, format: 'banana_ie' }); const [header, row] = content.trim().split('\r\n'); - expect(header).toBe('Date\tDoc\tDescription\tIncome\tExpenses\tContraAccount\tVatCode'); + expect(header).toBe('Date\tDoc\tDescription\tIncome\tExpenses\tCategory\tVatCode'); // The mock period holds one revenue posting (gross 108.10) → Income filled, - // Expenses empty, ContraAccount = the revenue account. + // Expenses empty, Category = the revenue account. const cells = row.split('\t'); expect(cells[3]).toBe('108.10'); // Income expect(cells[4]).toBe(''); // Expenses - expect(cells[5]).not.toBe(''); // ContraAccount (revenue account) + expect(cells[5]).not.toBe(''); // Category (revenue account) expect(filename).toMatch(/_banana_ie\.txt$/); expect(contentType).toMatch(/text\/plain/); }); diff --git a/backend/src/services/ledgerService.js b/backend/src/services/ledgerService.js index 2d589ff0..5e4dfa05 100644 --- a/backend/src/services/ledgerService.js +++ b/backend/src/services/ledgerService.js @@ -429,19 +429,21 @@ async function exportPostings({ from, to, currency, format = 'generic' } = {}) { } else if (fmt === 'banana_ie') { // Banana Income & Expense (cash-book / Einnahmen-Ausgaben) import — for a // file that is NOT double-entry. Columns: Date, Doc, Description, Income, - // Expenses, ContraAccount (the income/expense account — banana.ch doc 9946), - // VatCode. Revenue → gross in Income + the revenue account; cost → gross in - // Expenses + the expense account. Amount is gross; VatCode expands the VAT. - // Same TAB-separated .txt shape as double-entry. - headers = ['Date', 'Doc', 'Description', 'Income', 'Expenses', 'ContraAccount', 'VatCode']; + // Expenses, Category (the income/expense category account), VatCode. NOTE: + // the category column's NameXml in an I&E file is "Category" (verified + // against a real Banana file) — NOT the double-entry "ContraAccount". + // Revenue → gross in Income + the revenue account; cost → gross in Expenses + // + the expense account. VatCode warns harmlessly on a non-VAT file. Same + // TAB-separated .txt shape as double-entry. + headers = ['Date', 'Doc', 'Description', 'Income', 'Expenses', 'Category', 'VatCode']; rowOf = (p) => { const isRevenue = p.source === 'revenue'; const amount = minorToDecimal(p.grossMinor); - // Contra = the P&L account: revenue account (credit side) for income, + // Category = the P&L account: revenue account (credit side) for income, // expense account (debit side) for costs. - const contra = isRevenue ? p.creditAccount : p.debitAccount; + const category = isRevenue ? p.creditAccount : p.debitAccount; return [dateOnly(p.date), p.docNumber, p.description, - isRevenue ? amount : '', isRevenue ? '' : amount, contra, p.vatCode]; + isRevenue ? amount : '', isRevenue ? '' : amount, category, p.vatCode]; }; } else if (fmt === 'bexio') { // bexio manual-entry import.