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:
@@ -221,6 +221,20 @@ describe('exportPostings', () => {
|
||||
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 () => {
|
||||
const { content } = await ledgerService.exportPostings({ ...period, format: 'bexio' });
|
||||
const header = content.split('\r\n')[0];
|
||||
|
||||
@@ -390,7 +390,20 @@ function csvEscape(cell) {
|
||||
return `"${s.replace(/"/g, '""')}"`;
|
||||
}
|
||||
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'];
|
||||
|
||||
|
||||
@@ -978,6 +978,16 @@ async function renderTaxReportCsv({ from, to, currency, locale } = {}) {
|
||||
|
||||
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) => (
|
||||
type === 'outgoing' ? 'tax_type_outgoing'
|
||||
: type === 'incoming' ? 'tax_type_incoming'
|
||||
@@ -1013,7 +1023,7 @@ async function renderTaxReportCsv({ from, to, currency, locale } = {}) {
|
||||
lines.push([
|
||||
i + 1,
|
||||
t(useLocale, typeLabelKey(row.type)),
|
||||
row.date,
|
||||
isoDate(row.date),
|
||||
reference,
|
||||
row.party,
|
||||
row.eventName,
|
||||
|
||||
Reference in New Issue
Block a user