diff --git a/addons/dine360_qz_printer/static/src/js/qz_wrapper.js b/addons/dine360_qz_printer/static/src/js/qz_wrapper.js index cfbfe73..30cda58 100644 --- a/addons/dine360_qz_printer/static/src/js/qz_wrapper.js +++ b/addons/dine360_qz_printer/static/src/js/qz_wrapper.js @@ -53,6 +53,55 @@ function leftRight(left, right, width) { return `${lhs}${" ".repeat(space)}${rhs}`; } +function safeUpper(value) { + return cleanText(value).toUpperCase(); +} + +function formatPhone(value) { + const clean = cleanText(value); + const digits = clean.replace(/\D/g, ""); + const local = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits; + if (local.length === 10) { + return `${local.slice(0, 3)}-${local.slice(3, 6)}-${local.slice(6)}`; + } + return clean; +} + +function getCompanyAddress(company) { + const parts = [ + company.street, + company.street2, + [company.city, company.state_id?.[1] || company.state_id?.name, company.zip].filter(Boolean).join(" "), + ].map(cleanText).filter(Boolean); + return parts.join(", "); +} + +function formatPlacedDate(value) { + const date = value ? new Date(value) : new Date(); + if (Number.isNaN(date.getTime())) { + return cleanText(value); + } + + const weekday = date.toLocaleDateString("en-CA", { weekday: "short" }); + const month = date.toLocaleDateString("en-CA", { month: "short" }); + const day = date.getDate(); + const suffix = day % 10 === 1 && day !== 11 ? "st" : day % 10 === 2 && day !== 12 ? "nd" : day % 10 === 3 && day !== 13 ? "rd" : "th"; + const time = date.toLocaleTimeString("en-CA", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }).replace(/\s/g, ""); + return `${weekday}, ${month} ${day}${suffix}, ${time}`; +} + +function getOrderTable(order) { + return order?.getTable?.() || order?.table; +} + +function getOrderDisplayName(receiptData, order) { + return cleanText(receiptData?.name || order?.name || ""); +} + function wrapText(text, width) { const clean = cleanText(text); if (clean.length <= width) { @@ -139,6 +188,27 @@ class EscPosBuilder { separator(char = "-") { this.line(char.repeat(this.columns)); } + + qrCode(value) { + const data = cleanText(value); + if (!data) { + return; + } + const bytes = Array.from(data).map((char) => char.charCodeAt(0) & 0xff); + const storeLength = bytes.length + 3; + this.align("center"); + this.raw(GS, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00); + this.raw(GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, 0x06); + this.raw(GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, 0x31); + this.raw( + GS, 0x28, 0x6b, + storeLength & 0xff, + (storeLength >> 8) & 0xff, + 0x31, 0x50, 0x30, + ...bytes + ); + this.raw(GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30); + } } async function getLogoRasterBytes(pos, columns) { @@ -215,9 +285,8 @@ function addWrappedLine(builder, left, right = "", indent = 0) { function addOrderLines(builder, order, currency) { builder.bold(true); - builder.line(leftRight("ITEM", "TOTAL", builder.columns)); + builder.line(leftRight("Item", "Price", builder.columns)); builder.bold(false); - builder.separator("-"); for (const line of order?.get_orderlines?.() || []) { const product = line.get_product?.(); @@ -238,15 +307,11 @@ function addOrderLines(builder, order, currency) { } } -function addTaxes(builder, receiptData, currency) { +function addTaxSummary(builder, receiptData, currency) { const taxes = receiptData?.tax_details || []; if (!taxes.length) { return; } - builder.separator("-"); - builder.bold(true); - builder.line("TAX SUMMARY"); - builder.bold(false); for (const tax of taxes) { const label = tax?.tax?.name || (tax?.tax?.amount ? `${tax.tax.amount}%` : "Tax"); builder.line(leftRight(label, formatMoney(tax.amount || 0, currency), builder.columns)); @@ -271,68 +336,78 @@ async function buildEscPosReceipt(order, pos) { const company = pos?.company || {}; const receiptData = order?.export_for_printing?.() || {}; const builder = new EscPosBuilder(columns); + const table = getOrderTable(order); + const companyName = cleanText(company.name || "Chennora"); + const fulfilmentLabel = safeUpper(receiptData.fulfilment_type_label || receiptData.fulfilment_type || "DINE-IN"); + const orderName = getOrderDisplayName(receiptData, order); + const qrValue = orderName || `${companyName} ${formatMoney(order?.get_total_with_tax?.(), currency)}`; builder.init(); builder.raw(...(await getLogoRasterBytes(pos, columns))); builder.align("center"); builder.bold(true); - builder.size("doubleHeight"); - builder.line(company.name || "Chennora"); - builder.size("normal"); + builder.line(companyName); builder.bold(false); - if (company.street) builder.line(company.street); - if (company.city) builder.line(company.city); - if (company.phone) builder.line(`Tel: ${company.phone}`); - if (company.email) builder.line(company.email); - if (company.website) builder.line(company.website); - if (config.receipt_header) { - builder.line(""); - builder.lines(wrapText(config.receipt_header, columns).map((line) => center(line, columns))); + builder.line(""); + + const address = getCompanyAddress(company); + if (address) { + builder.lines(wrapText(`Address: ${address}`, columns).map((line) => center(line, columns))); + } + if (company.email) { + builder.line(center(`Email: ${company.email}`, columns)); + } + if (company.phone) { + builder.line(center(`Phone: ${formatPhone(company.phone)}`, columns)); } + builder.line(""); + builder.bold(true); + builder.line(order?._printed ? "*** Duplicate Bill (1) ***" : "*** Bill ***"); + if (orderName) { + builder.line(`Invoice ${orderName}`); + } + builder.line(fulfilmentLabel); + builder.bold(false); + builder.line(""); + builder.align("left"); - builder.separator("="); - if (receiptData.name) builder.line(leftRight("Receipt", receiptData.name, columns)); - if (receiptData.date) builder.line(leftRight("Date", receiptData.date, columns)); - if (receiptData.cashier || receiptData.headerData?.cashier) { - builder.line(leftRight("Served by", receiptData.cashier || receiptData.headerData.cashier, columns)); - } - if (order?.table?.name) { - builder.line(leftRight("Table", order.table.name, columns)); - } + builder.separator("-"); + builder.line(leftRight("Placed", formatPlacedDate(receiptData.date), columns)); + builder.separator("-"); + builder.line(leftRight("Cashier", companyName, columns)); if (order?.customer_count) { - builder.line(leftRight("Guests", order.customer_count, columns)); + builder.line(leftRight("Guests Served", order.customer_count, columns)); } - if (receiptData.order_source_label) { - builder.line(leftRight("Source", receiptData.order_source_label, columns)); - } - if (receiptData.fulfilment_type_label) { - builder.line(leftRight("Fulfilment", receiptData.fulfilment_type_label, columns)); + if (table?.name) { + builder.line(leftRight("Table", table.name, columns)); } builder.separator("="); addOrderLines(builder, order, currency); builder.separator("="); - builder.line(leftRight("Subtotal", formatMoney(order?.get_total_without_tax?.(), currency), columns)); + builder.line(leftRight("Price", formatMoney(order?.get_total_without_tax?.(), currency), columns)); const tax = order?.get_total_tax?.() || 0; if (tax) { - builder.line(leftRight("Tax", formatMoney(tax, currency), columns)); + const firstTax = receiptData?.tax_details?.[0]?.tax; + const taxLabel = firstTax?.name || (firstTax?.amount ? `HST (${firstTax.amount}%)` : "Tax"); + builder.line(leftRight(taxLabel, formatMoney(tax, currency), columns)); } if (receiptData.total_discount) { builder.line(leftRight("Discounts", formatMoney(receiptData.total_discount, currency), columns)); } builder.bold(true); - builder.size("doubleHeight"); - builder.line(leftRight("TOTAL", formatMoney(order?.get_total_with_tax?.(), currency), columns)); - builder.size("normal"); + builder.line(leftRight("Grand Total", formatMoney(order?.get_total_with_tax?.(), currency), columns)); builder.bold(false); addPaymentLines(builder, receiptData, currency); const change = receiptData.change || order?.get_change?.() || 0; if (change > 0) { + builder.bold(true); builder.line(leftRight("Change", formatMoney(change, currency), columns)); + builder.bold(false); } - addTaxes(builder, receiptData, currency); + addTaxSummary(builder, receiptData, currency); if (config.receipt_footer) { builder.separator("-"); @@ -344,11 +419,12 @@ async function buildEscPosReceipt(order, pos) { builder.separator("-"); builder.align("center"); builder.bold(true); - builder.line("THANK YOU"); + builder.line("* PAID *"); builder.bold(false); - builder.line("Please visit again"); builder.line(""); - builder.line("Powered by Dine360"); + builder.qrCode(qrValue); + builder.line(""); + builder.line("Powered by dind360"); builder.align("left"); builder.feed(END_FEED_LINES); if (config.qz_enable_cutter !== false) {