Update QZ receipt print layout

This commit is contained in:
metatroncubeswdev 2026-05-07 17:06:23 -04:00
parent c94c1f06eb
commit bbc7603c5b

View File

@ -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) {