Rebuild the Classic template on the engine with A4/Letter and a template harness
Classic now renders through buildRenderModel, computeFrame, the template and renderCore, with S.No and HSN/SAC columns, GST slots only through checked Slot components, taxable value, reverse-charge line, cancelled tag, continuation band and Page n of N. Letter absorbs its height deficit in elastic gaps. A harness runs 15 fixtures on A4 and Letter plus an item sweep and asserts zero audit errors, repeated headers, no split blocks, rupee extraction and deterministic output. The legacy InvoiceDocument is removed.
This commit is contained in:
+46
-27
@@ -1,23 +1,23 @@
|
||||
import { pdf } from "@react-pdf/renderer";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { registerPdfFonts } from "../pdf/fonts";
|
||||
import { InvoiceDocument, type InvoicePdfProps } from "../pdf/InvoiceDocument";
|
||||
import pkg from "../../package.json";
|
||||
import { DEFAULT_PREFS, type RenderPrefsV1 } from "../pdf/model/prefs";
|
||||
import { buildRenderModel } from "../pdf/model/build";
|
||||
import { bankFromSnapshot, vendorFromSnapshot } from "../pdf/model/vendor";
|
||||
import { initBrowserRendering } from "../pdf/render/browserInit";
|
||||
import { renderInvoicePdf as renderModelPdf, type RenderInvoiceResult } from "../pdf/render/invoice";
|
||||
import { api } from "./api";
|
||||
import { imageSizeFromDataUri } from "./imageSize";
|
||||
import type { InvoicePdfProps } from "./pdfProps";
|
||||
import type { BankAccount, Invoice, Settings } from "./types";
|
||||
|
||||
function blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = String(reader.result ?? "");
|
||||
resolve(result.substring(result.indexOf(",") + 1));
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
let bin = "";
|
||||
const step = 0x8000;
|
||||
for (let i = 0; i < bytes.length; i += step) bin += String.fromCharCode(...bytes.subarray(i, i + step));
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
/** Flat props for the on-screen preview; the PDF itself is built by renderInvoicePdf. */
|
||||
export async function buildPdfProps(
|
||||
invoice: Invoice,
|
||||
settings: Settings,
|
||||
@@ -60,26 +60,44 @@ export async function buildPdfProps(
|
||||
};
|
||||
}
|
||||
|
||||
/** Render props straight to a PDF blob. */
|
||||
export async function renderPdfBlob(props: InvoicePdfProps): Promise<Blob> {
|
||||
registerPdfFonts();
|
||||
return pdf(<InvoiceDocument {...props} />).toBlob();
|
||||
/** A logo with no readable size is fitted as if it filled Classic's 170 x 54 box. */
|
||||
const UNKNOWN_LOGO_SIZE = { width: 170, height: 54 };
|
||||
|
||||
/**
|
||||
* Renders an issued invoice through the engine: model (supplier and bank from the snapshots frozen at
|
||||
* issue, live settings only as a fallback) -> frame -> template -> PDF bytes, with the layout audit
|
||||
* issues and fingerprint.
|
||||
*/
|
||||
export async function renderInvoicePdf(
|
||||
invoice: Invoice,
|
||||
settings: Settings,
|
||||
bank: BankAccount | null,
|
||||
prefs: RenderPrefsV1 = DEFAULT_PREFS,
|
||||
): Promise<RenderInvoiceResult> {
|
||||
await initBrowserRendering();
|
||||
const vendor = vendorFromSnapshot(invoice.vendorSnapshot, settings);
|
||||
const logoUri = vendor.logoPath ? await api.readAssetDataUri(vendor.logoPath) : null;
|
||||
const signatureDataUri = invoice.signaturePath ? await api.readAssetDataUri(invoice.signaturePath) : null;
|
||||
const model = buildRenderModel({
|
||||
invoice,
|
||||
vendor: settings,
|
||||
bank,
|
||||
logo: logoUri ? { dataUri: logoUri, ...(imageSizeFromDataUri(logoUri) ?? UNKNOWN_LOGO_SIZE) } : null,
|
||||
signatureDataUri,
|
||||
appVersion: pkg.version,
|
||||
});
|
||||
return renderModelPdf({ model, prefs });
|
||||
}
|
||||
|
||||
/** Render a PDF and prompt the user for a save location. */
|
||||
export async function exportPdfFromProps(
|
||||
props: InvoicePdfProps,
|
||||
fileName: string,
|
||||
): Promise<string | null> {
|
||||
const blob = await renderPdfBlob(props);
|
||||
const dataBase64 = await blobToBase64(blob);
|
||||
/** Asks for a save location and writes the bytes there; null when the dialog is cancelled. */
|
||||
export async function savePdfBytes(bytes: Uint8Array, fileName: string): Promise<string | null> {
|
||||
const suggested = fileName.replace(/[^A-Za-z0-9._-]+/g, "-") || "invoice";
|
||||
const path = await save({
|
||||
defaultPath: `${suggested}.pdf`,
|
||||
filters: [{ name: "PDF document", extensions: ["pdf"] }],
|
||||
});
|
||||
if (!path) return null;
|
||||
await api.saveBinaryFile(path, dataBase64);
|
||||
await api.saveBinaryFile(path, bytesToBase64(bytes));
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -87,7 +105,8 @@ export async function exportInvoicePdf(
|
||||
invoice: Invoice,
|
||||
settings: Settings,
|
||||
bank: BankAccount | null,
|
||||
prefs: RenderPrefsV1 = DEFAULT_PREFS,
|
||||
): Promise<string | null> {
|
||||
const props = await buildPdfProps(invoice, settings, bank);
|
||||
return exportPdfFromProps(props, invoice.number);
|
||||
const { bytes } = await renderInvoicePdf(invoice, settings, bank, prefs);
|
||||
return savePdfBytes(bytes, invoice.number);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BankAccount, InvoiceItem, Settings, TaxType } from "./types";
|
||||
|
||||
/**
|
||||
* The flat invoice props the on-screen preview (components/InvoicePreview.tsx) renders from. The PDF no
|
||||
* longer uses them: it goes invoice -> buildRenderModel -> template (see lib/pdf.tsx).
|
||||
*/
|
||||
export interface InvoicePdfProps {
|
||||
vendor: Settings;
|
||||
bank: BankAccount | null;
|
||||
logoDataUri: string | null;
|
||||
signatureDataUri: string | null;
|
||||
number: string;
|
||||
invoiceDate: string;
|
||||
dueDate: string;
|
||||
clientName: string;
|
||||
clientAddress: string;
|
||||
clientGstin: string;
|
||||
poNumber: string;
|
||||
placeOfSupplyStateCode: string;
|
||||
items: InvoiceItem[];
|
||||
subtotal: number;
|
||||
discount: number;
|
||||
taxType: TaxType;
|
||||
taxRate: number;
|
||||
cgst: number;
|
||||
sgst: number;
|
||||
igst: number;
|
||||
total: number;
|
||||
amountInWords: string;
|
||||
notes: string;
|
||||
}
|
||||
Reference in New Issue
Block a user