fix(accounting): emit ISO dates in exports (Postgres returns Date objects)

The Date column imported empty into Banana because dateOnly() did
String(d).slice(0,10) — on Postgres the date columns come back as JS Date
objects, so that yields "Thu Jan 15" instead of "2026-01-15", which Banana
rejects. (SQLite returns strings, so the tests never caught it — the
pg-date-serialisation trap.)

- ledgerService.dateOnly + taxReportService CSV now format Date objects to
  yyyy-mm-dd via local calendar parts (DATE columns are local-midnight).
- Regression test added with a real Date object (the existing tests all used
  string dates).
This commit is contained in:
Luca
2026-06-15 23:01:20 +02:00
parent 445d6d7b6d
commit 0c0fb29770
3 changed files with 39 additions and 2 deletions
@@ -221,6 +221,20 @@ describe('exportPostings', () => {
expect(contentType).toMatch(/text\/plain/); expect(contentType).toMatch(/text\/plain/);
}); });
it('formats a Postgres Date object as yyyy-mm-dd (not "Thu Jan ...")', async () => {
// PG returns DATE columns as JS Date objects (SQLite returns strings); the
// export must still emit an ISO date, or Banana rejects it and the Date
// column imports empty.
invoiceRows = [{
id: 1, invoice_number: 'R-2026-0001', issue_date: new Date(2026, 0, 10),
vat_rate: 8.1, net_amount_minor: 10000, vat_amount_minor: 810, total_amount_minor: 10810,
customer_company_name: 'ACME',
}];
const { content } = await ledgerService.exportPostings({ ...period, format: 'banana' });
const dateCell = content.split('\r\n')[1].split('\t')[0];
expect(dateCell).toBe('2026-01-10');
});
it('bexio format includes tax_code + currency', async () => { it('bexio format includes tax_code + currency', async () => {
const { content } = await ledgerService.exportPostings({ ...period, format: 'bexio' }); const { content } = await ledgerService.exportPostings({ ...period, format: 'bexio' });
const header = content.split('\r\n')[0]; const header = content.split('\r\n')[0];
+14 -1
View File
@@ -390,7 +390,20 @@ function csvEscape(cell) {
return `"${s.replace(/"/g, '""')}"`; return `"${s.replace(/"/g, '""')}"`;
} }
function minorToDecimal(m) { return ((Number(m) || 0) / 100).toFixed(2); } function minorToDecimal(m) { return ((Number(m) || 0) / 100).toFixed(2); }
function dateOnly(d) { return String(d || '').slice(0, 10); } // yyyy-mm-dd, robust to Postgres returning DATE/TIMESTAMP columns as JS Date
// objects (SQLite returns strings). String(dateObj).slice(0,10) yields
// "Thu Jan 15", which Banana / accounting tools reject — so format the calendar
// parts explicitly. Uses local parts (DATE columns come back at local midnight).
function dateOnly(d) {
if (!d) return '';
if (d instanceof Date) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
return String(d).slice(0, 10);
}
const EXPORT_FORMATS = ['generic', 'banana', 'banana_ie', 'bexio']; const EXPORT_FORMATS = ['generic', 'banana', 'banana_ie', 'bexio'];
+11 -1
View File
@@ -978,6 +978,16 @@ async function renderTaxReportCsv({ from, to, currency, locale } = {}) {
const minorToDotDecimal = (m) => ((Number(m) || 0) / 100).toFixed(2); const minorToDotDecimal = (m) => ((Number(m) || 0) / 100).toFixed(2);
// yyyy-mm-dd, robust to Postgres returning dates as JS Date objects (SQLite
// returns strings) — raw String(dateObj) is "Thu Jan 15", not an ISO date.
const isoDate = (d) => {
if (!d) return '';
if (d instanceof Date) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
return String(d).slice(0, 10);
};
const typeLabelKey = (type) => ( const typeLabelKey = (type) => (
type === 'outgoing' ? 'tax_type_outgoing' type === 'outgoing' ? 'tax_type_outgoing'
: type === 'incoming' ? 'tax_type_incoming' : type === 'incoming' ? 'tax_type_incoming'
@@ -1013,7 +1023,7 @@ async function renderTaxReportCsv({ from, to, currency, locale } = {}) {
lines.push([ lines.push([
i + 1, i + 1,
t(useLocale, typeLabelKey(row.type)), t(useLocale, typeLabelKey(row.type)),
row.date, isoDate(row.date),
reference, reference,
row.party, row.party,
row.eventName, row.eventName,