Alaguraj0361 353281e248 Refactor and update Chennora theme to Shivasakthi branding
- Removed obsolete XML views related to product templates and configuration settings.
- Deleted shop page layout and snippets XML files to streamline theme structure.
- Added new logo and slider images for Shivasakthi theme.
- Updated SCSS styles for improved footer design and added pre-footer food slider.
- Modified layout XML to incorporate new logo and enhanced footer with social links and contact information.
- Removed premium auto-sliding gallery section from pages XML.
- Updated site metadata and canonical links in HTML files to reflect Shivasakthi branding.
- Adjusted Docker Compose configuration for new database and volume names.
2026-06-09 13:04:44 +05:30

483 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { PosStore } from "@point_of_sale/app/store/pos_store";
const ESC = 0x1b;
const GS = 0x1d;
const CR = 0x0d;
const LF = 0x0a;
const DEFAULT_COLUMNS = 42;
const END_FEED_LINES = 8;
function columnsFromConfig(config) {
const value = Number.parseInt(config?.qz_paper_width || DEFAULT_COLUMNS, 10);
return Number.isFinite(value) ? value : DEFAULT_COLUMNS;
}
function cleanText(value) {
return String(value ?? "")
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[“”]/g, '"')
.replace(/[]/g, "'")
.replace(/[–—]/g, "-")
.replace(/\u00a0/g, " ")
.replace(/[^\x09\x0a\x0d\x20-\x7e]/g, "")
.replace(/[ \t]+/g, " ")
.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 wrapText(text, width) {
const clean = cleanText(text);
if (clean.length <= width) {
return [clean];
}
const wrapped = [];
let rest = clean;
while (rest.length > width) {
let splitAt = rest.lastIndexOf(" ", width);
if (splitAt <= 0) {
splitAt = width;
}
wrapped.push(rest.slice(0, splitAt).trimEnd());
rest = rest.slice(splitAt).trimStart();
}
if (rest) {
wrapped.push(rest);
}
return wrapped;
}
function bytesToBase64(bytes) {
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.slice(i, i + chunkSize));
}
return btoa(binary);
}
class EscPosBuilder {
constructor(columns) {
this.columns = columns;
this.bytes = [];
}
raw(...values) {
this.bytes.push(...values);
}
text(value) {
for (const char of cleanText(value)) {
this.bytes.push(char.charCodeAt(0) & 0xff);
}
}
line(value = "") {
this.text(value);
this.bytes.push(CR, LF);
}
lines(values) {
for (const value of values) {
this.line(value);
}
}
init() {
this.raw(ESC, 0x40);
}
align(value) {
const modes = { left: 0, center: 1, right: 2 };
this.raw(ESC, 0x61, modes[value] ?? 0);
}
bold(enabled) {
this.raw(ESC, 0x45, enabled ? 1 : 0);
}
size(value) {
const sizes = { normal: 0x00, doubleHeight: 0x01, doubleWidth: 0x10, double: 0x11 };
this.raw(GS, 0x21, sizes[value] ?? 0x00);
}
feed(lines = 1) {
this.raw(ESC, 0x64, Math.max(0, Math.min(8, lines)));
}
cut(mode = "partial") {
this.raw(GS, 0x56, mode === "full" ? 0x41 : 0x42, 0x00);
}
separator(char = "-") {
this.line(char.repeat(this.columns));
}
}
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) {
const width = builder.columns;
const rightText = cleanText(right);
const leftWidth = rightText ? width - rightText.length - 1 : width;
const wrapped = wrapText(left, Math.max(8, leftWidth - indent));
if (rightText) {
builder.line(leftRight(`${" ".repeat(indent)}${wrapped[0]}`, rightText, width));
for (const extra of wrapped.slice(1)) {
builder.line(`${" ".repeat(indent)}${extra}`);
}
} else {
for (const line of wrapped) {
builder.line(`${" ".repeat(indent)}${line}`);
}
}
}
function addOrderLines(builder, order, currency) {
builder.bold(true);
builder.line(leftRight("ITEM", "TOTAL", builder.columns));
builder.bold(false);
builder.separator("-");
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 addTaxes(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));
}
}
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);
builder.init();
builder.raw(...(await getLogoRasterBytes(pos, columns)));
builder.align("center");
builder.bold(true);
builder.size("doubleHeight");
builder.line(company.name || "Shivasakthi");
builder.size("normal");
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.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));
}
if (order?.customer_count) {
builder.line(leftRight("Guests", 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));
}
builder.separator("=");
addOrderLines(builder, order, currency);
builder.separator("=");
builder.line(leftRight("Subtotal", 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));
}
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.bold(false);
addPaymentLines(builder, receiptData, currency);
const change = receiptData.change || order?.get_change?.() || 0;
if (change > 0) {
builder.line(leftRight("Change", formatMoney(change, currency), columns));
}
addTaxes(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("THANK YOU");
builder.bold(false);
builder.line("Please visit again");
builder.line("");
builder.line("Powered by Dine360");
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, {
async printReceipt() {
if (this.pos.config.use_qz_printer && this.pos.config.qz_billing_printer_name) {
try {
if (!window.qz) {
console.error("QZ Tray library not loaded.");
return false;
}
if (!qz.websocket.isActive()) {
await qz.websocket.connect({ retries: 2, delay: 1 });
}
const config = qz.configs.create(this.pos.config.qz_billing_printer_name, {
encoding: "CP437",
copies: 1,
spool: { size: 1 },
});
const payload = await buildEscPosReceipt(this.currentOrder, this.pos);
await qz.print(config, [{
type: "raw",
format: "command",
flavor: "base64",
data: payload,
}]);
if (this.currentOrder) {
this.currentOrder._printed = true;
}
return true;
} catch (err) {
console.error("QZ Tray Print Error:", err);
this.env.services.popup.add(ErrorPopup, {
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.",
});
return false;
}
}
return super.printReceipt(...arguments);
},
});
async function buildEscPosKitchenTicket(order, pos, changes) {
const config = pos?.config || {};
const columns = columnsFromConfig(config);
const builder = new EscPosBuilder(columns);
builder.init();
builder.align("center");
builder.bold(true);
builder.size("doubleHeight");
builder.line("KITCHEN ORDER");
builder.size("normal");
builder.line("");
builder.align("left");
if (order.name) builder.line(`Order: ${order.name}`);
if (order.table?.name) builder.line(`Table: ${order.table.name}`);
if (order.customer_count) builder.line(`Guests: ${order.customer_count}`);
builder.line(`Time: ${new Date().toLocaleTimeString()}`);
builder.separator("=");
for (const category of Object.values(changes.categories || {})) {
if (category.name) {
builder.bold(true);
builder.line(category.name.toUpperCase());
builder.bold(false);
}
for (const line of category.orderlines || []) {
const qty = line.quantity;
const qtyText = Number.isInteger(qty) ? String(qty) : qty.toFixed(2).replace(/\.?0+$/, "");
addWrappedLine(builder, `${qtyText} x ${line.name}`);
if (line.note) {
addWrappedLine(builder, `Note: ${line.note}`, "", 2);
}
}
builder.line("");
}
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(PosStore.prototype, {
async sendOrderToPrinter(order) {
if (this.config.use_qz_printer && this.config.qz_kitchen_printer_name) {
const changes = order.computeChanges();
if (Object.keys(changes.categories).length > 0) {
try {
if (!window.qz) {
console.error("QZ Tray library not loaded.");
return false;
}
if (!qz.websocket.isActive()) {
await qz.websocket.connect({ retries: 2, delay: 1 });
}
const config = qz.configs.create(this.config.qz_kitchen_printer_name, {
encoding: "CP437",
copies: 1,
spool: { size: 1 },
});
const payload = await buildEscPosKitchenTicket(order, this, changes);
await qz.print(config, [{
type: "raw",
format: "command",
flavor: "base64",
data: payload,
}]);
order.saved_quantity = order.get_orderlines().reduce((acc, line) => acc + line.get_quantity(), 0);
return true;
} catch (err) {
console.error("QZ Tray Kitchen Print Error:", err);
// Fallback or show error
}
}
return true;
}
return super.sendOrderToPrinter(...arguments);
},
});