forked from alaguraj/odoo-testing-addons
Print Odoo receipt HTML through QZ only
This commit is contained in:
parent
5459ce5af7
commit
ad5564aaf6
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Dine360 Order Channels',
|
'name': 'Dine360 Order Channels',
|
||||||
'version': '17.0.1.0',
|
'version': '17.0.1.1',
|
||||||
'category': 'Sales/Point of Sale',
|
'category': 'Sales/Point of Sale',
|
||||||
'summary': 'Multi-channel order intake: Phone, WhatsApp, Social, Kiosk, Online',
|
'summary': 'Multi-channel order intake: Phone, WhatsApp, Social, Kiosk, Online',
|
||||||
'description': """
|
'description': """
|
||||||
@ -25,7 +25,6 @@
|
|||||||
'dine360_order_channels/static/src/js/channel_panel.js',
|
'dine360_order_channels/static/src/js/channel_panel.js',
|
||||||
'dine360_order_channels/static/src/js/product_screen_patch.js',
|
'dine360_order_channels/static/src/js/product_screen_patch.js',
|
||||||
'dine360_order_channels/static/src/xml/channel_panel.xml',
|
'dine360_order_channels/static/src/xml/channel_panel.xml',
|
||||||
'dine360_order_channels/static/src/xml/receipt_extension.xml',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Dine360 QZ Tray Printer',
|
'name': 'Dine360 QZ Tray Printer',
|
||||||
'version': '17.0.1.3',
|
'version': '17.0.1.4',
|
||||||
'category': 'Point of Sale',
|
'category': 'Point of Sale',
|
||||||
'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.',
|
'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.',
|
||||||
'depends': ['point_of_sale'],
|
'depends': ['point_of_sale'],
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const CR = 0x0d;
|
|||||||
const LF = 0x0a;
|
const LF = 0x0a;
|
||||||
const DEFAULT_COLUMNS = 42;
|
const DEFAULT_COLUMNS = 42;
|
||||||
const END_FEED_LINES = 8;
|
const END_FEED_LINES = 8;
|
||||||
|
let qzPrintQueue = Promise.resolve();
|
||||||
|
|
||||||
function columnsFromConfig(config) {
|
function columnsFromConfig(config) {
|
||||||
const value = Number.parseInt(config?.qz_paper_width || DEFAULT_COLUMNS, 10);
|
const value = Number.parseInt(config?.qz_paper_width || DEFAULT_COLUMNS, 10);
|
||||||
@ -25,6 +26,20 @@ function paperWidthMm(config) {
|
|||||||
return String(config?.qz_paper_width || DEFAULT_COLUMNS) === "48" ? 80 : 58;
|
return String(config?.qz_paper_width || DEFAULT_COLUMNS) === "48" ? 80 : 58;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enqueueQzPrint(task) {
|
||||||
|
const next = qzPrintQueue.catch(() => undefined).then(task);
|
||||||
|
qzPrintQueue = next.catch(() => undefined);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findConfiguredPrinter(printerName) {
|
||||||
|
const name = cleanText(printerName);
|
||||||
|
if (!name) {
|
||||||
|
throw new Error("No QZ printer name is configured.");
|
||||||
|
}
|
||||||
|
return qz.printers.find(name);
|
||||||
|
}
|
||||||
|
|
||||||
function cleanText(value) {
|
function cleanText(value) {
|
||||||
return String(value ?? "")
|
return String(value ?? "")
|
||||||
.normalize("NFKD")
|
.normalize("NFKD")
|
||||||
@ -38,74 +53,10 @@ function cleanText(value) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMoney(value, currency) {
|
|
||||||
const amount = Number(value || 0).toFixed(2);
|
|
||||||
const symbol = cleanText(currency?.symbol || "$");
|
|
||||||
return currency?.position === "after" ? `${amount} ${symbol}` : `${symbol}${amount}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function center(text, width) {
|
|
||||||
const value = cleanText(text);
|
|
||||||
const padding = Math.max(0, Math.floor((width - value.length) / 2));
|
|
||||||
return `${" ".repeat(padding)}${value}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function leftRight(left, right, width) {
|
|
||||||
const lhs = cleanText(left);
|
|
||||||
const rhs = cleanText(right);
|
|
||||||
const space = Math.max(1, width - lhs.length - rhs.length);
|
|
||||||
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) {
|
function getOrderTable(order) {
|
||||||
return order?.getTable?.() || order?.table;
|
return order?.getTable?.() || order?.table;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrderDisplayName(receiptData, order) {
|
|
||||||
return cleanText(receiptData?.name || order?.name || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapText(text, width) {
|
function wrapText(text, width) {
|
||||||
const clean = cleanText(text);
|
const clean = cleanText(text);
|
||||||
if (clean.length <= width) {
|
if (clean.length <= width) {
|
||||||
@ -143,6 +94,24 @@ function absoluteUrl(value) {
|
|||||||
return new URL(value, window.location.origin).href;
|
return new URL(value, window.location.origin).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function imageToDataUrl(src) {
|
||||||
|
const url = absoluteUrl(src);
|
||||||
|
if (!url || url.startsWith("data:") || url.startsWith("blob:")) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
const response = await fetch(url, { credentials: "include" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Unable to load receipt image: ${url}`);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function collectPrintableStyles() {
|
function collectPrintableStyles() {
|
||||||
return Array.from(document.querySelectorAll("link[rel='stylesheet'], style"))
|
return Array.from(document.querySelectorAll("link[rel='stylesheet'], style"))
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
@ -154,13 +123,13 @@ function collectPrintableStyles() {
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHtmlReceipt(receiptElement, pos) {
|
async function buildHtmlReceipt(receiptElement, pos) {
|
||||||
const config = pos?.config || {};
|
const config = pos?.config || {};
|
||||||
const widthMm = paperWidthMm(config);
|
const widthMm = paperWidthMm(config);
|
||||||
const clone = receiptElement.cloneNode(true);
|
const clone = receiptElement.cloneNode(true);
|
||||||
for (const node of clone.querySelectorAll("[src]")) {
|
await Promise.all(Array.from(clone.querySelectorAll("img[src]")).map(async (node) => {
|
||||||
node.setAttribute("src", absoluteUrl(node.getAttribute("src")));
|
node.setAttribute("src", await imageToDataUrl(node.getAttribute("src")));
|
||||||
}
|
}));
|
||||||
for (const node of clone.querySelectorAll("[href]")) {
|
for (const node of clone.querySelectorAll("[href]")) {
|
||||||
node.setAttribute("href", absoluteUrl(node.getAttribute("href")));
|
node.setAttribute("href", absoluteUrl(node.getAttribute("href")));
|
||||||
}
|
}
|
||||||
@ -195,6 +164,12 @@ function buildHtmlReceipt(receiptElement, pos) {
|
|||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findReceiptElement(screen) {
|
||||||
|
return screen.el?.querySelector?.(".pos-receipt")
|
||||||
|
|| document.querySelector(".receipt-screen .pos-receipt")
|
||||||
|
|| document.querySelector(".pos-receipt");
|
||||||
|
}
|
||||||
|
|
||||||
class EscPosBuilder {
|
class EscPosBuilder {
|
||||||
constructor(columns) {
|
constructor(columns) {
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
@ -274,61 +249,6 @@ class EscPosBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLogoRasterBytes(pos, columns) {
|
|
||||||
if (pos?.config?.qz_print_logo === false || !pos?.company_logo_base64) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = await new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => resolve(img);
|
|
||||||
img.onerror = reject;
|
|
||||||
img.src = pos.company_logo_base64;
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxWidth = Math.min(columns * 8, 384);
|
|
||||||
const maxHeight = 120;
|
|
||||||
const ratio = Math.min(maxWidth / image.width, maxHeight / image.height, 1);
|
|
||||||
const width = Math.max(8, Math.floor((image.width * ratio) / 8) * 8);
|
|
||||||
const height = Math.max(1, Math.floor(image.height * ratio));
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
||||||
ctx.fillStyle = "#fff";
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
ctx.drawImage(image, 0, 0, width, height);
|
|
||||||
|
|
||||||
const pixels = ctx.getImageData(0, 0, width, height).data;
|
|
||||||
const widthBytes = Math.ceil(width / 8);
|
|
||||||
const data = [];
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let xb = 0; xb < widthBytes; xb++) {
|
|
||||||
let byte = 0;
|
|
||||||
for (let bit = 0; bit < 8; bit++) {
|
|
||||||
const x = xb * 8 + bit;
|
|
||||||
const index = (y * width + x) * 4;
|
|
||||||
const alpha = pixels[index + 3];
|
|
||||||
const luminance = pixels[index] * 0.299 + pixels[index + 1] * 0.587 + pixels[index + 2] * 0.114;
|
|
||||||
if (alpha > 32 && luminance < 170) {
|
|
||||||
byte |= 0x80 >> bit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.push(byte);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
ESC, 0x61, 0x01,
|
|
||||||
GS, 0x76, 0x30, 0x00,
|
|
||||||
widthBytes & 0xff, (widthBytes >> 8) & 0xff,
|
|
||||||
height & 0xff, (height >> 8) & 0xff,
|
|
||||||
...data,
|
|
||||||
CR, LF,
|
|
||||||
ESC, 0x61, 0x00,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWrappedLine(builder, left, right = "", indent = 0) {
|
function addWrappedLine(builder, left, right = "", indent = 0) {
|
||||||
const width = builder.columns;
|
const width = builder.columns;
|
||||||
const rightText = cleanText(right);
|
const rightText = cleanText(right);
|
||||||
@ -346,157 +266,6 @@ function addWrappedLine(builder, left, right = "", indent = 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOrderLines(builder, order, currency) {
|
|
||||||
builder.bold(true);
|
|
||||||
builder.line(leftRight("Item", "Price", builder.columns));
|
|
||||||
builder.bold(false);
|
|
||||||
|
|
||||||
for (const line of order?.get_orderlines?.() || []) {
|
|
||||||
const product = line.get_product?.();
|
|
||||||
const name = product?.display_name || product?.name || line.get_full_product_name?.() || "";
|
|
||||||
const qty = Number(line.get_quantity?.() || 0);
|
|
||||||
const qtyText = Number.isInteger(qty) ? String(qty) : qty.toFixed(2).replace(/\.?0+$/, "");
|
|
||||||
const total = formatMoney(line.get_price_with_tax?.() || 0, currency);
|
|
||||||
addWrappedLine(builder, `${qtyText} x ${name}`, total);
|
|
||||||
|
|
||||||
const unitPrice = line.get_unit_display_price?.();
|
|
||||||
if (qty !== 1 && unitPrice !== undefined) {
|
|
||||||
builder.line(` @ ${formatMoney(unitPrice, currency)} each`);
|
|
||||||
}
|
|
||||||
const note = line.get_customer_note?.();
|
|
||||||
if (note) {
|
|
||||||
addWrappedLine(builder, `Note: ${note}`, "", 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTaxSummary(builder, receiptData, currency) {
|
|
||||||
const taxes = receiptData?.tax_details || [];
|
|
||||||
if (!taxes.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPaymentLines(builder, receiptData, currency) {
|
|
||||||
const paymentLines = receiptData?.paymentlines || [];
|
|
||||||
if (!paymentLines.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
builder.separator("-");
|
|
||||||
for (const payment of paymentLines) {
|
|
||||||
builder.line(leftRight(payment.name, formatMoney(payment.amount, currency), builder.columns));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildEscPosReceipt(order, pos) {
|
|
||||||
const config = pos?.config || {};
|
|
||||||
const columns = columnsFromConfig(config);
|
|
||||||
const currency = pos?.currency;
|
|
||||||
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.line(companyName);
|
|
||||||
builder.bold(false);
|
|
||||||
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("-");
|
|
||||||
builder.line(leftRight("Placed", formatPlacedDate(receiptData.date), columns));
|
|
||||||
builder.separator("-");
|
|
||||||
builder.line(leftRight("Cashier", companyName, columns));
|
|
||||||
if (order?.customer_count) {
|
|
||||||
builder.line(leftRight("Guests Served", order.customer_count, columns));
|
|
||||||
}
|
|
||||||
if (table?.name) {
|
|
||||||
builder.line(leftRight("Table", table.name, columns));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.separator("=");
|
|
||||||
addOrderLines(builder, order, currency);
|
|
||||||
builder.separator("=");
|
|
||||||
builder.line(leftRight("Price", formatMoney(order?.get_total_without_tax?.(), currency), columns));
|
|
||||||
const tax = order?.get_total_tax?.() || 0;
|
|
||||||
if (tax) {
|
|
||||||
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.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);
|
|
||||||
}
|
|
||||||
addTaxSummary(builder, receiptData, currency);
|
|
||||||
|
|
||||||
if (config.receipt_footer) {
|
|
||||||
builder.separator("-");
|
|
||||||
builder.align("center");
|
|
||||||
builder.lines(wrapText(config.receipt_footer, columns).map((line) => center(line, columns)));
|
|
||||||
builder.align("left");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.separator("-");
|
|
||||||
builder.align("center");
|
|
||||||
builder.bold(true);
|
|
||||||
builder.line("* PAID *");
|
|
||||||
builder.bold(false);
|
|
||||||
builder.line("");
|
|
||||||
builder.qrCode(qrValue);
|
|
||||||
builder.line("");
|
|
||||||
builder.line("Powered by dind360");
|
|
||||||
builder.align("left");
|
|
||||||
builder.feed(END_FEED_LINES);
|
|
||||||
if (config.qz_enable_cutter !== false) {
|
|
||||||
builder.cut(config.qz_cut_mode || "partial");
|
|
||||||
}
|
|
||||||
builder.feed(1);
|
|
||||||
return bytesToBase64(builder.bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
patch(ReceiptScreen.prototype, {
|
patch(ReceiptScreen.prototype, {
|
||||||
async printReceipt() {
|
async printReceipt() {
|
||||||
const printerName = getBillingPrinterName(this.pos.config);
|
const printerName = getBillingPrinterName(this.pos.config);
|
||||||
@ -511,43 +280,28 @@ patch(ReceiptScreen.prototype, {
|
|||||||
await qz.websocket.connect({ retries: 2, delay: 1 });
|
await qz.websocket.connect({ retries: 2, delay: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiptElement = this.el?.querySelector?.(".pos-receipt");
|
const receiptElement = findReceiptElement(this);
|
||||||
if (receiptElement) {
|
if (!receiptElement) {
|
||||||
const widthMm = paperWidthMm(this.pos.config);
|
throw new Error("The Odoo receipt preview was not found. Billing print was stopped to avoid printing fallback data.");
|
||||||
const config = qz.configs.create(printerName, {
|
|
||||||
copies: 1,
|
|
||||||
margins: 0,
|
|
||||||
units: "mm",
|
|
||||||
size: { width: widthMm },
|
|
||||||
rasterize: true,
|
|
||||||
scaleContent: true,
|
|
||||||
spool: { size: 1 },
|
|
||||||
});
|
|
||||||
await qz.print(config, [{
|
|
||||||
type: "pixel",
|
|
||||||
format: "html",
|
|
||||||
flavor: "plain",
|
|
||||||
data: buildHtmlReceipt(receiptElement, this.pos),
|
|
||||||
}]);
|
|
||||||
|
|
||||||
if (this.currentOrder) {
|
|
||||||
this.currentOrder._printed = true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
const widthMm = paperWidthMm(this.pos.config);
|
||||||
const config = qz.configs.create(printerName, {
|
const printer = await findConfiguredPrinter(printerName);
|
||||||
encoding: "CP437",
|
const config = qz.configs.create(printer, {
|
||||||
copies: 1,
|
copies: 1,
|
||||||
|
margins: 0,
|
||||||
|
units: "mm",
|
||||||
|
size: { width: widthMm },
|
||||||
|
rasterize: true,
|
||||||
|
scaleContent: true,
|
||||||
spool: { size: 1 },
|
spool: { size: 1 },
|
||||||
});
|
});
|
||||||
const payload = await buildEscPosReceipt(this.currentOrder, this.pos);
|
const html = await buildHtmlReceipt(receiptElement, this.pos);
|
||||||
await qz.print(config, [{
|
await enqueueQzPrint(() => qz.print(config, [{
|
||||||
type: "raw",
|
type: "pixel",
|
||||||
format: "command",
|
format: "html",
|
||||||
flavor: "base64",
|
flavor: "plain",
|
||||||
data: payload,
|
data: html,
|
||||||
}]);
|
}]));
|
||||||
|
|
||||||
if (this.currentOrder) {
|
if (this.currentOrder) {
|
||||||
this.currentOrder._printed = true;
|
this.currentOrder._printed = true;
|
||||||
@ -557,7 +311,7 @@ patch(ReceiptScreen.prototype, {
|
|||||||
console.error("QZ Tray Print Error:", err);
|
console.error("QZ Tray Print Error:", err);
|
||||||
this.env.services.popup.add(ErrorPopup, {
|
this.env.services.popup.add(ErrorPopup, {
|
||||||
title: "QZ Tray Printer Error",
|
title: "QZ Tray Printer Error",
|
||||||
body: "Failed to print through QZ Tray. Check that QZ Tray is running, the printer name is correct, and the printer supports ESC/POS raw commands.",
|
body: err?.message || "Failed to print the rendered Odoo receipt through QZ Tray.",
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -639,18 +393,19 @@ patch(Order.prototype, {
|
|||||||
if (!qz.websocket.isActive()) {
|
if (!qz.websocket.isActive()) {
|
||||||
await qz.websocket.connect({ retries: 2, delay: 1 });
|
await qz.websocket.connect({ retries: 2, delay: 1 });
|
||||||
}
|
}
|
||||||
const config = qz.configs.create(printerName, {
|
const printer = await findConfiguredPrinter(printerName);
|
||||||
|
const config = qz.configs.create(printer, {
|
||||||
encoding: "CP437",
|
encoding: "CP437",
|
||||||
copies: 1,
|
copies: 1,
|
||||||
spool: { size: 1 },
|
spool: { size: 1 },
|
||||||
});
|
});
|
||||||
const payload = await buildEscPosKitchenTicket(this, this.pos, orderChange, cancelled);
|
const payload = await buildEscPosKitchenTicket(this, this.pos, orderChange, cancelled);
|
||||||
await qz.print(config, [{
|
await enqueueQzPrint(() => qz.print(config, [{
|
||||||
type: "raw",
|
type: "raw",
|
||||||
format: "command",
|
format: "command",
|
||||||
flavor: "base64",
|
flavor: "base64",
|
||||||
data: payload,
|
data: payload,
|
||||||
}]);
|
}]));
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("QZ Tray Kitchen Print Error:", err);
|
console.error("QZ Tray Kitchen Print Error:", err);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user