duly / web/lib/csv.js · raw
// The export an accountant opens. Plain columns, plain words, one row per invoice.import { formatAmount } from './money.js';export const STATUS_WORDS = {  open: 'Not paid yet',  overdue: 'Overdue',  partial: 'Part paid',  paid: 'Paid',  partially_refunded: 'Paid, part refunded',  refunded: 'Refunded',  cancelled: 'Cancelled',};const COLUMNS = [  'Invoice number', 'Client', 'Issued', 'Due', 'Status', 'Currency',  'Invoice total', 'Tax included', 'Discount given', 'Received', 'Date received',  'Set aside', 'Kept in balance', 'Refunded', 'Extra paid by client',  'Shown to client in', 'Exchange rate', 'Total in that currency',  'Paid from', 'Transaction link',];/** A cell starting with these can run as a formula in a spreadsheet. */function cell(v) {  let s = v === null || v === undefined ? '' : String(v);  if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;  return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;}const day = (ms) => (Number(ms) > 0 ? new Date(Number(ms)).toISOString().slice(0, 10) : '');const plain = (u) => formatAmount(u, { min: 2 }).replace(/,/g, '');export function invoiceRow(inv, doc, txLink) {  const applied = BigInt(inv.applied);  const setAside = BigInt(inv.set_aside);  const excess = BigInt(inv.excess);  const paid = BigInt(inv.paid);  const refunded = BigInt(inv.refunded);  const amount = BigInt(inv.amount);  const discount = inv.closed_at_ms !== '0' && applied < amount ? amount - applied : 0n;  const fx = doc?.fx ? Number(doc.fx) : null;  const first = inv.payments?.[0];  return [    `INV-${String(inv.number).padStart(4, '0')}`,    doc?.to?.name ?? '',    day(inv.issued_at_ms),    day(inv.due_at_ms),    STATUS_WORDS[inv.status] ?? inv.status,    'USDC',    plain(amount),    doc?.taxAmount ?? '',    discount > 0n ? plain(discount) : '0.00',    plain(paid),    day(inv.closed_at_ms !== '0' ? inv.closed_at_ms : first?.at_ms),    plain(setAside),    plain(paid - setAside - refunded),    plain(refunded),    plain(excess),    doc?.ccy && doc.ccy !== 'USD' ? doc.ccy : '',    fx ? String(fx) : '',    fx ? (Number(amount) / 1e6 * fx).toFixed(2) : '',    first?.refund_to ?? '',    txLink ?? '',  ];}export function toCsv(rows) {  const lines = [COLUMNS, ...rows].map((r) => r.map(cell).join(','));  // The byte order mark makes Excel read the file as UTF-8.  return `${lines.join('\r\n')}\r\n`;}