Accounting: A/P-clearing payments, check return address + MICR gaps, dashboard fixes

Bill/vendor payments:
- Bill "Pay" and bank-register vendor debits now clear Accounts Payable
  (Dr A/P / Cr Bank) instead of re-debiting the expense, which had
  double-counted expenses in the P&L and never cleared A/P. The expense
  account is kept as a display-only category on the payment.

Checks:
- Bill-payment checks now print the return address (payer name/address)
  from the company check layout (was hardcoded blank).
- Per-segment MICR gap control (check# / routing / account) in both check
  generators, wired to Check Setup (bill payments) and the Check Layout
  editor (Print Checks). New columns: check_settings.micr_gap_1/2 and
  check_layouts/company_check_layouts.micr_gap_1/2.

Accounting Dashboard / Financial Overview:
- Date-range selector (presets + custom) drives the charts, top expenses,
  and recent transactions.
- PDF title renamed to "Financial Overview" and shows the period.
- Fixed amounts rendering as "$ 5 0 0. 0 0": the U+2212 minus sign forced
  jsPDF into a UTF-16 fallback; replaced with an ASCII hyphen (also in the
  report PDF out-of-balance line).

DB migrations applied to the project: repost_gl_on_line_item_change,
budget_actuals_accrual_owner_income, add_micr_gap_controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 00:35:30 -04:00
parent 5bf2a5887e
commit c3a0682e57
9 changed files with 228 additions and 67 deletions
+17 -6
View File
@@ -51,6 +51,10 @@ export type CheckPrintOptions = {
offsetY?: number;
/** Extra vertical shift for the MICR line only (fine-tune for check stock) */
micrOffsetY?: number;
/** Spaces between MICR check# and routing segments (default 1) */
micrGap1?: number;
/** Spaces between MICR routing and account segments (default 1) */
micrGap2?: number;
};
// ── Constants ────────────────────────────────────────────────────────────────
@@ -71,12 +75,15 @@ const CHECK_H = SECTION_H; // check occupies the full top section
// Format matches reference check: C000000305C A263191387A 1100034740184C
// C{check#}C A{routing}A {account}C
function buildMicr(routing: string, account: string, checkNum: number): string {
function buildMicr(routing: string, account: string, checkNum: number, gap1 = 1, gap2 = 1): string {
const r = (routing ?? "").replace(/\D/g, "").slice(0, 9);
const a = (account ?? "").replace(/\D/g, "");
const c = String(checkNum).padStart(9, "0");
if (!r && !a) return "";
return `C${c}C A${r}A ${a}C`;
const g1 = " ".repeat(Math.max(0, Math.round(gap1)));
const g2 = " ".repeat(Math.max(0, Math.round(gap2)));
// C{check#}C {gap1} A{routing}A {gap2} {account}C
return `C${c}C${g1}A${r}A${g2}${a}C`;
}
// ── Fill a line to width with a pad character (for written-amount security) ─
@@ -103,7 +110,7 @@ function hline(doc: jsPDF, x1: number, y: number, x2: number, w = 0.006, gray =
// ── Check face ───────────────────────────────────────────────────────────────
function drawCheck(doc: jsPDF, originY: number, c: CheckData, ox = 0, micrOy = 0) {
function drawCheck(doc: jsPDF, originY: number, c: CheckData, ox = 0, micrOy = 0, micrGap1 = 1, micrGap2 = 1) {
const y = (dy: number) => originY + dy;
const x = (dx: number) => dx + ox; // Apply global X offset to every horizontal coordinate
@@ -270,7 +277,9 @@ function drawCheck(doc: jsPDF, originY: number, c: CheckData, ox = 0, micrOy = 0
const micr = buildMicr(
(c.routingNumber ?? "").replace(/\D/g, ""),
(c.accountNumber ?? "").replace(/\D/g, ""),
c.checkNumber
c.checkNumber,
micrGap1,
micrGap2
);
if (micr) {
@@ -351,19 +360,21 @@ export function generateCheckPDF(checks: CheckData[], opts: CheckPrintOptions):
const ox = opts.offsetX ?? 0;
const oy = opts.offsetY ?? 0;
const micrOy = opts.micrOffsetY ?? 0;
const g1 = opts.micrGap1 ?? 1;
const g2 = opts.micrGap2 ?? 1;
checks.forEach((c, idx) => {
if (idx > 0) doc.addPage();
if (opts.style === "voucher") {
drawCheck(doc, 0 + oy, c, ox, micrOy);
drawCheck(doc, 0 + oy, c, ox, micrOy, g1, g2);
drawStub(doc, SECTION_H + oy, c, ox);
drawStub(doc, SECTION_H * 2 + oy, c, ox);
} else {
let posY = 0;
if (opts.position === "middle") posY = PH / 2 - CHECK_H / 2;
if (opts.position === "bottom") posY = PH - CHECK_H;
drawCheck(doc, posY + oy, c, ox, micrOy);
drawCheck(doc, posY + oy, c, ox, micrOy, g1, g2);
}
});