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/);
|
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];
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user