diff --git a/src/components/CheckLayoutEditor.tsx b/src/components/CheckLayoutEditor.tsx index 54ad83b..88c05b3 100644 --- a/src/components/CheckLayoutEditor.tsx +++ b/src/components/CheckLayoutEditor.tsx @@ -53,6 +53,8 @@ const DEFAULTS: CheckLayout = { font_family: "helvetica", field_positions: {}, logo_url: "", + micr_gap_1: 3, + micr_gap_2: 3, }; const FIELD_ORDER: CheckFieldKey[] = [ @@ -107,6 +109,8 @@ export default function CheckLayoutEditor({ associationId, associationName, mode font_family: (data as any).font_family || "helvetica", field_positions: ((data as any).field_positions as CheckLayout["field_positions"]) || {}, logo_url: (data as any).logo_url || "", + micr_gap_1: Number((data as any).micr_gap_1 ?? 3), + micr_gap_2: Number((data as any).micr_gap_2 ?? 3), }); } else { setLayoutId(null); @@ -178,6 +182,8 @@ export default function CheckLayoutEditor({ associationId, associationName, mode font_family: (data as any).font_family || "helvetica", field_positions: ((data as any).field_positions as CheckLayout["field_positions"]) || {}, logo_url: (data as any).logo_url || "", + micr_gap_1: Number((data as any).micr_gap_1 ?? 3), + micr_gap_2: Number((data as any).micr_gap_2 ?? 3), }); toast({ title: "Settings copied", @@ -255,6 +261,8 @@ export default function CheckLayoutEditor({ associationId, associationName, mode font_family: layout.font_family || "helvetica", field_positions: (layout.field_positions || {}) as any, logo_url: layout.logo_url || null, + micr_gap_1: Number(layout.micr_gap_1 ?? 3), + micr_gap_2: Number(layout.micr_gap_2 ?? 3), }; const payload: any = isCompany ? basePayload @@ -412,6 +420,17 @@ export default function CheckLayoutEditor({ associationId, associationName, mode update("offset_y", Number(e.target.value))} /> +
+ + update("micr_gap_1", Math.max(0, parseInt(e.target.value) || 0))} /> +

Spaces between segments in the MICR line.

+
+
+ + update("micr_gap_2", Math.max(0, parseInt(e.target.value) || 0))} /> +
diff --git a/src/pages/accounting/AccountingBankingPage.tsx b/src/pages/accounting/AccountingBankingPage.tsx index 074d5a1..7233530 100644 --- a/src/pages/accounting/AccountingBankingPage.tsx +++ b/src/pages/accounting/AccountingBankingPage.tsx @@ -256,9 +256,14 @@ export default function AccountingBankingPage() { const description = [partyName, coaName, memo].filter(Boolean).join(" · "); const category = coaName; + // A vendor payment (debit) clears Accounts Payable — the expense was already + // recognized when the bill was entered (accrual). Leaving coa_account_id null + // with the vendor set makes post_transaction_gl post Dr A/P / Cr Bank; the + // chosen expense account is retained as the display `category` only. Customer + // deposits (credits) clear A/R via customer_id, so they need no change here. const payload: any = { account_id, date, description, amount, type, category, reference: reference || null, - coa_account_id: coa_account_id || null, + coa_account_id: type === "debit" ? null : (coa_account_id || null), vendor_id: vendor_id || null, customer_id: customer_id || null, }; diff --git a/src/pages/accounting/AccountingBillsPage.tsx b/src/pages/accounting/AccountingBillsPage.tsx index 36b9f95..291c1a9 100644 --- a/src/pages/accounting/AccountingBillsPage.tsx +++ b/src/pages/accounting/AccountingBillsPage.tsx @@ -329,10 +329,21 @@ export default function AccountingBillsPage() { const { data: billItems } = await accounting.from("bill_items").select("description,amount").eq("bill_id", payBill?.id ?? ""); const vendorAddress = payBill?.vendors?.address ?? undefined; + // Return-address (payer) block comes from the company check layout the user + // configured in Settings → Check Layout. Without this, checks print no + // return address. + const { data: layout } = await supabase + .from("company_check_layouts") + .select("payer_name, payer_address, show_payer_block") + .maybeSingle(); + const payerName = (layout?.payer_name || "").trim(); + const showPayer = layout?.show_payer_block !== false; + const returnAddress = showPayer ? (layout?.payer_address || undefined) : undefined; + const dataUrl = generateCheckPDF( [{ - companyName: associationName ?? "Company", - companyAddress: undefined, + companyName: (payerName || associationName) ?? "Company", + companyAddress: returnAddress, bankName: cs?.bank_name ?? bankAccount?.name ?? undefined, bankAddress: cs?.bank_address ?? undefined, routingNumber: cs?.routing_number ?? undefined, @@ -355,6 +366,8 @@ export default function AccountingBillsPage() { offsetX: (cs as any)?.offset_x ?? 0, offsetY: (cs as any)?.offset_y ?? 0, micrOffsetY: (cs as any)?.micr_offset_y ?? 0, + micrGap1: (cs as any)?.micr_gap_1 ?? 1, + micrGap2: (cs as any)?.micr_gap_2 ?? 1, } ); const w = window.open(""); @@ -372,16 +385,24 @@ export default function AccountingBillsPage() { const vendorName = payBill.vendors?.name ?? "Vendor"; const refLabel = payReference || payMethod.toUpperCase(); - // 1) Bank ledger transaction — debit = money OUT of the bank (payment to vendor) - // Pull the primary expense account from the first bill item so the COA balance updates - const { data: billItems } = await accounting - .from("bill_items") - .select("account_id") - .eq("bill_id", payBill.id) - .not("account_id", "is", null) - .limit(1); - const primaryCoa = billItems?.[0]?.account_id ?? null; + // Expense category name(s) from the bill — shown on the payment line in the + // transaction journal for visibility (display only; does not affect the GL). + const { data: payItems } = await accounting + .from("bill_items").select("account_id").eq("bill_id", payBill.id).not("account_id", "is", null); + const payAcctIds = Array.from(new Set((payItems ?? []).map((i: any) => i.account_id))); + let categoryLabel = "Bill Payment"; + if (payAcctIds.length) { + const { data: payAccs } = await accounting.from("accounts").select("name").in("id", payAcctIds); + const names = (payAccs ?? []).map((a: any) => a.name).filter(Boolean); + if (names.length) categoryLabel = names.join(", "); + } + // 1) Bank ledger transaction — debit = money OUT of the bank (payment to vendor). + // A bill payment must clear Accounts Payable, NOT re-hit the expense account: + // the expense was already recognized when the bill was entered (accrual). + // coa_account_id stays null with vendor_id set, so post_transaction_gl posts + // Dr Accounts Payable / Cr Bank (clears the payable, no second P&L hit). The + // expense category is stored in `category` for the journal view only. await accounting.from("transactions").insert({ company_id: cid, account_id: payAccountId, @@ -389,10 +410,10 @@ export default function AccountingBillsPage() { type: "debit", amount: payAmount, description: `Bill Payment · ${vendorName} · Bill ${payBill.number}`, - category: "Bill Payment", + category: categoryLabel, reference: refLabel, - coa_account_id: primaryCoa, // links to expense account → updates COA balance - vendor_id: payBill.vendor_id ?? null, // link to vendor for reporting + coa_account_id: null, // → posts against Accounts Payable (via vendor) + vendor_id: payBill.vendor_id ?? null, // required so the GL clears A/P }); // 2) Bank balance auto-updated by DB trigger trg_sync_account_balance diff --git a/src/pages/accounting/AccountingCheckSetupPage.tsx b/src/pages/accounting/AccountingCheckSetupPage.tsx index a5eaa8c..0d7529a 100644 --- a/src/pages/accounting/AccountingCheckSetupPage.tsx +++ b/src/pages/accounting/AccountingCheckSetupPage.tsx @@ -35,6 +35,8 @@ export default function AccountingCheckSetupPage() { const [offsetX, setOffsetX] = useState(0); const [offsetY, setOffsetY] = useState(0); const [micrOffsetY, setMicrOffsetY] = useState(0); + const [micrGap1, setMicrGap1] = useState(1); + const [micrGap2, setMicrGap2] = useState(1); const { data: settings } = useQuery({ queryKey: ["check-settings", cid], @@ -67,6 +69,8 @@ export default function AccountingCheckSetupPage() { setOffsetX(Number(s.offset_x ?? 0)); setOffsetY(Number(s.offset_y ?? 0)); setMicrOffsetY(Number(s.micr_offset_y ?? 0)); + setMicrGap1(Number(s.micr_gap_1 ?? 1)); + setMicrGap2(Number(s.micr_gap_2 ?? 1)); }, [settings]); const routingValid = /^\d{9}$/.test(routingNumber.replace(/\D/g, "")); @@ -90,6 +94,8 @@ export default function AccountingCheckSetupPage() { offset_x: offsetX, offset_y: offsetY, micr_offset_y: micrOffsetY, + micr_gap_1: micrGap1, + micr_gap_2: micrGap2, }, { onConflict: "company_id" }); setSaving(false); if (error) return toast.error(error.message); @@ -130,7 +136,7 @@ export default function AccountingCheckSetupPage() { style: defaultStyle as any, position: defaultPosition as any, fontSize: fontSize as any, - offsetX, offsetY, micrOffsetY, + offsetX, offsetY, micrOffsetY, micrGap1, micrGap2, }); const w = window.open(""); if (w) w.document.write(``); @@ -141,7 +147,7 @@ export default function AccountingCheckSetupPage() { style: defaultStyle as any, position: defaultPosition as any, fontSize: fontSize as any, - offsetX, offsetY, micrOffsetY, + offsetX, offsetY, micrOffsetY, micrGap1, micrGap2, }); const w = window.open(""); if (w) w.document.write(``); @@ -345,6 +351,13 @@ export default function AccountingCheckSetupPage() {

Run the alignment test first, measure the offset on blank paper, then adjust here and preview again.

+
+

MICR gaps (spaces between segments)

+
+ + +
+
@@ -368,6 +381,35 @@ export default function AccountingCheckSetupPage() { ); } +function GapControl({ label, value, onChange }: { + label: string; value: number; onChange: (v: number) => void; +}) { + const adj = (d: number) => onChange(Math.max(0, Math.round(value + d))); + return ( +
+

{label}

+
+ + onChange(Math.max(0, parseInt(e.target.value) || 0))} + className="h-7 text-center font-mono text-xs" + /> + +
+
+ ); +} + function OffsetControl({ label, hint, value, onChange }: { label: string; hint: string; value: number; onChange: (v: number) => void; }) { diff --git a/src/pages/accounting/AccountingDashboardPage.tsx b/src/pages/accounting/AccountingDashboardPage.tsx index 093573d..80b92cc 100644 --- a/src/pages/accounting/AccountingDashboardPage.tsx +++ b/src/pages/accounting/AccountingDashboardPage.tsx @@ -6,8 +6,30 @@ import { useCompanyId } from "./lib/useCompanyId"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { money, fmtDate } from "./lib/format"; import { generateDashboardPdf } from "./lib/dashboardPdf"; + +type RangePreset = "this-month" | "this-quarter" | "ytd" | "last-6-months" | "custom"; + +function presetRange(p: RangePreset): { from: string; to: string } { + const now = new Date(); + const y = now.getFullYear(); + const iso = (d: Date) => d.toISOString().slice(0, 10); + const today = iso(now); + switch (p) { + case "this-month": return { from: iso(new Date(y, now.getMonth(), 1)), to: today }; + case "this-quarter": return { from: iso(new Date(y, Math.floor(now.getMonth() / 3) * 3, 1)), to: today }; + case "ytd": return { from: `${y}-01-01`, to: today }; + case "custom": return { from: `${y}-01-01`, to: today }; + case "last-6-months": + default: { + const s = new Date(y, now.getMonth() - 5, 1); + return { from: iso(s), to: today }; + } + } +} import { Receipt, FileText, Landmark, ArrowUpRight, ArrowDownRight, Loader2, FileDown, } from "lucide-react"; @@ -31,15 +53,19 @@ export default function AccountingDashboardPage({ association }: { association?: const logoUrl = (assocMeta as any)?.logo_url || null; const [exporting, setExporting] = useState(false); + // Date range for the charts + recent transactions (receivables/payables/cash + // remain current snapshots — they are point-in-time by nature). + const [preset, setPreset] = useState("last-6-months"); + const [range, setRange] = useState(() => presetRange("last-6-months")); + const from = range.from; + const to = range.to; + const applyPreset = (p: RangePreset) => { setPreset(p); if (p !== "custom") setRange(presetRange(p)); }; + const rangeLabel = `${fmtDate(from)} – ${fmtDate(to)}`; + const { data } = useQuery({ - queryKey: ["dashboard", cid], + queryKey: ["dashboard", cid, from, to], enabled: !!cid, queryFn: async () => { - const sixMonthsAgo = new Date(); - sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5); - sixMonthsAgo.setDate(1); - const sinceIso = sixMonthsAgo.toISOString().slice(0, 10); - const [inv, bills, tx, acc] = await Promise.all([ accounting.from("invoices").select("total,status,issue_date").eq("company_id", cid), accounting.from("bills").select("total,status,issue_date").eq("company_id", cid), @@ -47,8 +73,10 @@ export default function AccountingDashboardPage({ association }: { association?: .from("transactions") .select("amount,type,date,description,category,reference") .eq("company_id", cid) + .gte("date", from) + .lte("date", to) .order("date", { ascending: false }) - .limit(10), + .limit(50), accounting.from("accounts").select("balance,is_bank").eq("company_id", cid), ]); @@ -78,28 +106,32 @@ export default function AccountingDashboardPage({ association }: { association?: } } - // 6 month income/expense + // Income/expense by month across the selected range const months: { key: string; label: string; income: number; expense: number }[] = []; - const now = new Date(); - for (let i = 5; i >= 0; i--) { - const d = new Date(now.getFullYear(), now.getMonth() - i, 1); + const cursor = new Date(`${from}T00:00:00`); + cursor.setDate(1); + const lastMonth = new Date(`${to}T00:00:00`); + lastMonth.setDate(1); + while (cursor <= lastMonth && months.length < 36) { months.push({ - key: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`, - label: d.toLocaleString("en-US", { month: "short" }), + key: `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}`, + label: cursor.toLocaleString("en-US", { month: "short", year: "2-digit" }), income: 0, expense: 0, }); + cursor.setMonth(cursor.getMonth() + 1); } const bucket = (date: string) => date.slice(0, 7); + const inWin = (d?: string | null) => !!d && d >= from && d <= to; for (const i of invoices) { - if (i.issue_date && i.issue_date >= sinceIso) { - const m = months.find((x) => x.key === bucket(i.issue_date)); + if (inWin(i.issue_date)) { + const m = months.find((x) => x.key === bucket(i.issue_date!)); if (m) m.income += Number(i.total); } } for (const b of billsArr) { - if (b.issue_date && b.issue_date >= sinceIso) { - const m = months.find((x) => x.key === bucket(b.issue_date)); + if (inWin(b.issue_date)) { + const m = months.find((x) => x.key === bucket(b.issue_date!)); if (m) m.expense += Number(b.total); } } @@ -124,7 +156,7 @@ export default function AccountingDashboardPage({ association }: { association?: counts, months, topExpenses, - recent: txAll, + recent: txAll.slice(0, 15), }; }, }); @@ -175,25 +207,46 @@ export default function AccountingDashboardPage({ association }: { association?:

Dashboard

-

{associationName}

+

{associationName} · {rangeLabel}

+
+
+ + {preset === "custom" && ( +
+ setRange((r) => ({ ...r, from: e.target.value }))} /> + to + setRange((r) => ({ ...r, to: e.target.value }))} /> +
+ )} +
-
{/* Cash flow summary */} diff --git a/src/pages/accounting/lib/checkPdf.ts b/src/pages/accounting/lib/checkPdf.ts index 1773e20..23299c1 100644 --- a/src/pages/accounting/lib/checkPdf.ts +++ b/src/pages/accounting/lib/checkPdf.ts @@ -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); } }); diff --git a/src/pages/accounting/lib/dashboardPdf.ts b/src/pages/accounting/lib/dashboardPdf.ts index 4207c67..1fa01d6 100644 --- a/src/pages/accounting/lib/dashboardPdf.ts +++ b/src/pages/accounting/lib/dashboardPdf.ts @@ -109,6 +109,7 @@ export async function generateDashboardPdf(opts: { logoUrl?: string | null; currency?: string; data: DashboardPdfData; + rangeLabel?: string; }) { const { companyName, data } = opts; const currency = opts.currency || "USD"; @@ -120,8 +121,11 @@ export async function generateDashboardPdf(opts: { const logo = await loadBrandedLogo(opts.logoUrl); let y = drawBrandedHeader(doc, { logo, - title: "Accounting Dashboard", - metaLines: [{ label: "Properties:", value: companyName }], + title: "Financial Overview", + metaLines: [ + { label: "Properties:", value: companyName }, + ...(opts.rangeLabel ? [{ label: "Period:", value: opts.rangeLabel }] : []), + ], }); // ── Summary cards ── @@ -194,7 +198,7 @@ export async function generateDashboardPdf(opts: { t.description ?? "", t.category ?? "—", t.reference ?? "—", - `${t.type === "credit" ? "+" : "−"}${money(Math.abs(Number(t.amount || 0)), currency)}`, + `${t.type === "credit" ? "+" : "-"}${money(Math.abs(Number(t.amount || 0)), currency)}`, ]), styles: { font: "helvetica", fontSize: 8, textColor: TEXT, lineColor: [225, 228, 232], lineWidth: 0.1 }, headStyles: { fillColor: [237, 239, 242], textColor: TEXT, fontStyle: "bold" }, diff --git a/src/pages/accounting/lib/reportPdf.ts b/src/pages/accounting/lib/reportPdf.ts index 7a4e60b..a93dd72 100644 --- a/src/pages/accounting/lib/reportPdf.ts +++ b/src/pages/accounting/lib/reportPdf.ts @@ -236,7 +236,7 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js doc.setFont("helvetica", "bold"); doc.setFontSize(9.5); doc.setTextColor(...txt); const msg = ok ? "Balance Sheet is balanced" - : `Balance Sheet is OUT OF BALANCE by ${fmtAmount(report.outOfBalanceAmount ?? 0)} (Assets − Liabilities − Equity)`; + : `Balance Sheet is OUT OF BALANCE by ${fmtAmount(report.outOfBalanceAmount ?? 0)} (Assets - Liabilities - Equity)`; doc.text(msg, ML + 8, y + 14); y += 28; } diff --git a/src/utils/checkPdfGenerator.ts b/src/utils/checkPdfGenerator.ts index f9ced13..841ea02 100644 --- a/src/utils/checkPdfGenerator.ts +++ b/src/utils/checkPdfGenerator.ts @@ -93,6 +93,10 @@ export interface CheckLayout { field_positions?: Partial> | null; /** New: optional logo image URL printed on the check. */ logo_url?: string | null; + /** Spaces between MICR check# and routing segments (default 3). */ + micr_gap_1?: number | null; + /** Spaces between MICR routing and account segments (default 3). */ + micr_gap_2?: number | null; } /* ------------------------------------------------------------------ */ @@ -200,14 +204,16 @@ function registerMicrFont(doc: jsPDF) { doc.addFont("MICRCHECK.ttf", "MICRCHECK", "normal"); } -export function formatMicrLine(routing: string, account: string, checkNumber: string): string { +export function formatMicrLine(routing: string, account: string, checkNumber: string, gap1 = 3, gap2 = 3): string { const r = (routing || "").replace(/\D/g, ""); const a = (account || "").replace(/\D/g, ""); const c = (checkNumber || "").replace(/\D/g, ""); // Pad check number with leading zeros to 9 digits const paddedCheck = c.padStart(9, "0"); - // Order: check number (on-us), space, transit/routing, space, account - return `${MICR_ON_US}${paddedCheck}${MICR_ON_US} ${MICR_TRANSIT}${r}${MICR_TRANSIT} ${a}${MICR_ON_US}`; + const g1 = " ".repeat(Math.max(0, Math.round(gap1))); + const g2 = " ".repeat(Math.max(0, Math.round(gap2))); + // Order: check number (on-us), gap1, transit/routing, gap2, account + return `${MICR_ON_US}${paddedCheck}${MICR_ON_US}${g1}${MICR_TRANSIT}${r}${MICR_TRANSIT}${g2}${a}${MICR_ON_US}`; } export function amountToWords(amount: number): string { @@ -462,7 +468,7 @@ function drawCheckSection( if (includeMicr && c.bank_routing_number && c.bank_account_number) { const p = px("micr"); if (p.visible !== false) { - const micr = formatMicrLine(c.bank_routing_number, c.bank_account_number, c.check_number || ""); + const micr = formatMicrLine(c.bank_routing_number, c.bank_account_number, c.check_number || "", layout.micr_gap_1 ?? 3, layout.micr_gap_2 ?? 3); doc.setFont("MICRCHECK", "normal").setFontSize(p.font_size || 12); doc.text(micr, p.X, p.Y); }