diff --git a/POS/src/utils/printInvoice.js b/POS/src/utils/printInvoice.js index d6111d989..0f7228813 100644 --- a/POS/src/utils/printInvoice.js +++ b/POS/src/utils/printInvoice.js @@ -3,11 +3,15 @@ import { logger } from "@/utils/logger" import { getOfflineReceiptPayload } from "@/utils/offline/offlineReceiptCache" import { getOfflineInvoiceByOfflineId } from "@/utils/offline/sync" import { offlineWorker } from "@/utils/offline/workerClient" -import { printHTML as qzPrintHTML } from "@/utils/qzTray" +import { + printHTML as qzPrintHTML, + printRawCommands as qzPrintRawCommands, +} from "@/utils/qzTray" const log = logger.create("PrintInvoice") const DEFAULT_PRINT_FORMAT = "POS Next Receipt" +const printFormatMetaCache = new Map() // ============================================================================ // Shared helpers @@ -277,6 +281,40 @@ async function resolvePrintSettings(posProfile, printFormat, letterhead) { return { printFormat: DEFAULT_PRINT_FORMAT, letterhead } } +async function getPrintFormatMeta(printFormat) { + if (!printFormat) return null + if (printFormatMetaCache.has(printFormat)) { + return printFormatMetaCache.get(printFormat) + } + + try { + const meta = await call("frappe.client.get_value", { + doctype: "Print Format", + filters: { name: printFormat }, + fieldname: ["name", "raw_printing"], + }) + const normalizedMeta = meta?.message || meta + printFormatMetaCache.set(printFormat, normalizedMeta || null) + return normalizedMeta || null + } catch (err) { + log.warn(`Could not fetch Print Format metadata for ${printFormat}:`, err?.message || err) + printFormatMetaCache.set(printFormat, null) + return null + } +} + +async function isRawPrintFormat(printFormat) { + if (typeof printFormat === "string" && /esc[\s/-]*pos/i.test(printFormat)) { + return true + } + const meta = await getPrintFormatMeta(printFormat) + return Boolean(Number.parseInt(meta?.raw_printing ?? 0, 10)) +} + +function containsRawPrinterCommands(value) { + return typeof value === "string" && /[\x1b\x1d]/.test(value) +} + // ============================================================================ // Browser printing (opens /printview in a new window) // ============================================================================ @@ -290,22 +328,25 @@ export async function printInvoice(invoiceData, printFormat = null, letterhead = try { if (!invoiceData?.name) throw new Error("Invalid invoice data") - invoiceData = await hydrateLocalOnlyInvoice(invoiceData) + const printableInvoice = await hydrateLocalOnlyInvoice(invoiceData) // Pending offline / local IDs are not in ERPNext — use embedded receipt HTML. - if (isLocalOnlyInvoiceName(invoiceData.name)) { - if (invoiceData.items?.length > 0) return printInvoiceCustom(invoiceData) + if (isLocalOnlyInvoiceName(printableInvoice.name)) { + if (printableInvoice.items?.length > 0) return printInvoiceCustom(printableInvoice) throw new Error( __("This offline receipt is no longer in browser storage. Sync the invoice, then print from history."), ) } - const doctype = invoiceData.doctype || "Sales Invoice" + const doctype = printableInvoice.doctype || "Sales Invoice" const format = printFormat || DEFAULT_PRINT_FORMAT + if (await isRawPrintFormat(format)) { + return rawPrintInvoice(printableInvoice.name, format) + } const params = new URLSearchParams({ doctype, - name: invoiceData.name, + name: printableInvoice.name, format, no_letterhead: letterhead ? 0 : 1, _lang: "en", @@ -381,6 +422,10 @@ export async function silentPrintInvoice(invoiceName, printFormat = null) { } const format = printFormat || DEFAULT_PRINT_FORMAT + if (await isRawPrintFormat(format)) { + return rawPrintInvoice(invoiceName, format) + } + const result = await call("frappe.www.printview.get_html_and_style", { doc: "Sales Invoice", name: invoiceName, @@ -392,6 +437,12 @@ export async function silentPrintInvoice(invoiceName, printFormat = null) { const style = result?.style || result?.message?.style || "" if (!html) throw new Error("Failed to get print HTML from server") + if (containsRawPrinterCommands(html)) { + await qzPrintRawCommands(html) + log.info(`Raw print sent from rendered command body for ${invoiceName}`) + return true + } + const fullHTML = `
@@ -403,6 +454,26 @@ export async function silentPrintInvoice(invoiceName, printFormat = null) { return true } +/** + * Fetch server-rendered raw commands and send them directly to QZ Tray. + * The selected print format must have Raw Printing enabled in Frappe. + */ +export async function rawPrintInvoice(invoiceName, printFormat) { + const format = printFormat || DEFAULT_PRINT_FORMAT + const result = await call("frappe.www.printview.get_rendered_raw_commands", { + doc: "Sales Invoice", + name: invoiceName, + print_format: format, + }) + + const rawCommands = result?.raw_commands || result?.message?.raw_commands + if (!rawCommands) throw new Error("Failed to get raw print commands from server") + + await qzPrintRawCommands(rawCommands) + log.info(`Raw silent print sent for ${invoiceName}`) + return true +} + /** * Silent-print a full invoice dict using the same HTML as the offline receipt fallback. */ @@ -420,22 +491,22 @@ export async function silentPrintInvoiceFromDoc(invoiceData) { * internally, so no separate connection logic is needed here. */ export async function printWithSilentFallback(invoiceData, printFormat = null) { - invoiceData = await hydrateLocalOnlyInvoice(invoiceData) - const invoiceName = invoiceData?.name + const printableInvoice = await hydrateLocalOnlyInvoice(invoiceData) + const invoiceName = printableInvoice?.name if (!invoiceName) throw new Error("Invalid invoice data — missing name") if ( isLocalOnlyInvoiceName(invoiceName) && - invoiceData.items?.length > 0 + printableInvoice.items?.length > 0 ) { try { - await silentPrintInvoiceFromDoc(invoiceData) + await silentPrintInvoiceFromDoc(printableInvoice) return { method: "silent", success: true } } catch (err) { log.warn("Silent local receipt failed, falling back to browser:", err?.message || err) } try { - printInvoiceCustom(invoiceData) + printInvoiceCustom(printableInvoice) return { method: "browser", success: true } } catch (err) { log.error("Browser print for local receipt failed:", err) @@ -443,15 +514,34 @@ export async function printWithSilentFallback(invoiceData, printFormat = null) { } } + let resolvedPrintFormat = printFormat try { - await silentPrintInvoice(invoiceName, printFormat) + if (!resolvedPrintFormat) { + if (printableInvoice?.pos_profile) { + const settings = await resolvePrintSettings(printableInvoice.pos_profile, printFormat, null) + resolvedPrintFormat = settings.printFormat + } else { + const invoiceDoc = await call("pos_next.api.invoices.get_invoice", { + invoice_name: invoiceName, + }) + if (invoiceDoc?.pos_profile) { + const settings = await resolvePrintSettings(invoiceDoc.pos_profile, printFormat, null) + resolvedPrintFormat = settings.printFormat + } + } + } + + await silentPrintInvoice(invoiceName, resolvedPrintFormat) return { method: "silent", success: true } } catch (err) { log.warn("Silent print failed, falling back to browser:", err?.message || err) } try { - await printInvoiceByName(invoiceName, printFormat) + const fallbackFormat = (await isRawPrintFormat(resolvedPrintFormat)) + ? DEFAULT_PRINT_FORMAT + : printFormat + await printInvoiceByName(invoiceName, fallbackFormat) return { method: "browser", success: true } } catch (err) { log.error("Browser print fallback also failed:", err) diff --git a/POS/src/utils/qzTray.js b/POS/src/utils/qzTray.js index e9e3b3e6c..e2a1b4b48 100644 --- a/POS/src/utils/qzTray.js +++ b/POS/src/utils/qzTray.js @@ -241,7 +241,9 @@ export async function printHTML(html, printerName, options = {}) { const printer = printerName || getSavedPrinterName() if (!printer) { - throw new Error("No printer selected. Please select a printer in POS Settings.") + throw new Error( + "No printer selected. Please select a printer in POS Settings.", + ) } const config = qz.configs.create(printer, { @@ -274,3 +276,45 @@ export async function printHTML(html, printerName, options = {}) { throw err } } + +/** + * Send raw printer commands, such as ESC/POS, directly to a printer via QZ Tray. + * + * @param {string} commands - Rendered printer-native command string + * @param {string} [printerName] - Target printer. Falls back to saved printer. + * @returns {Promise