add support printer TM-T82X-II

This commit is contained in:
2026-07-01 13:43:11 +07:00
parent 70fa65b6da
commit 7a3ca92d4c
+306 -8
View File
@@ -1,13 +1,311 @@
frappe.after_ajax(() => {
console.log("overriding ====");
import html2canvas from 'html2canvas';
const original = erpnext.PointOfSale.Controller.prototype.constuctor;
frappe.router.on("change", () => {
const route = frappe.get_route();
if (route[0] === "point-of-sale") {
waitForPOS(() => {
const controller = erpnext.PointOfSale.Controller.prototype.prepare_menu;
erpnext.PointOfSale.Controller.prototype.prepare_menu = function() {
controller.call(this, ...arguments);
this.page.add_menu_item(__("Settup USB Printer"), this.setup_printer.bind(this), false, "Ctrl+P");
};
erpnext.PointOfSale.Controller.prototype.constuctor = function () {
console.log("Yoooooooooooooooooow");
erpnext.PointOfSale.Controller.prototype.setup_printer = function() {
original.call(this, ...arguments);
if (!navigator.usb) {
frappe.show_alert({
indicator: "red",
message: __("Your browser doesn't support WebUSB, please switch to Chromium-based browser"),
});
return;
}
console.log("byeeeeeeeeeeeeeeeeee");
};
try {
navigator.usb.requestDevice({ filters: [] }).then(device => {
device.open().then(() => {
if (device.configuration === null) {
device.selectConfiguration(1);
}
}).then(() => {
let interfaceNum = null, endpointNum = null;
for (const iface of device.configuration.interfaces) {
for (const alt of iface.alternates) {
for (const ep of alt.endpoints) {
if (ep.direction === 'out' && ep.type === 'bulk') {
interfaceNum = iface.interfaceNumber;
endpointNum = ep.endpointNumber;
}
}
}
}
if (endpointNum === null) throw new Error('No bulk OUT endpoint found');
device.claimInterface(0);
this.device = device;
this.device._enpointNum = endpointNum;
this.order_summary.events.get_device = () => {
return this.device;
};
frappe.show_alert({
indicator: "green",
message: __("Printer connected successfully!"),
});
this.page.add_button(__("Printer Connected"), null, { btn_class: "btn-disabled" });
}).catch(err => {
frappe.show_alert({
indicator: "red",
message: __(err),
});
});
}).catch(err => {
frappe.show_alert({
indicator: "red",
message: __(err),
});
});
} catch (e) {
frappe.show_alert({
indicator: "red",
message: __(e),
});
};
};
erpnext.PointOfSale.PastOrderSummary.prototype.attach_change_info = function(doc) {
if (doc.change_amount == 0) {
return
}
this.$payment_container.append(`
<div class="summary-row-wrapper payments">
<div>${__("Change")}</div>
<div>${format_currency(doc.change_amount, doc.currency)}</div>
</div>`);
}
erpnext.PointOfSale.PastOrderSummary.prototype.open_cash_drawer = function(doc) {
if (doc.change_amount == 0) {
return
}
const CMD = get_command();
const device = this.events.get_device();
const open_drawer_command = new Uint8Array(CMD.open_drawer);
device.transferOut(device._enpointNum, open_drawer_command);
}
const origin_load_summary_of = erpnext.PointOfSale.PastOrderSummary.prototype.load_summary_of;
erpnext.PointOfSale.PastOrderSummary.prototype.load_summary_of = function(doc, after_submission = false) {
origin_load_summary_of.apply(this, arguments);
this.attach_change_info(doc);
this.open_cash_drawer(doc);
}
erpnext.PointOfSale.PastOrderSummary.prototype.custom_print_receipt = function() {
const frm = this.events.get_frm();
frappe.call({
method: "frappe.www.printview.get_html_and_style",
args: {
doc: this.doc,
print_format: frm.pos_print_format,
letter_head: this.doc.letter_head,
lang: this.doc.language || frappe.boot.lang,
},
}).then(resp => {
let base_url = frappe.urllib.get_base_url();
let print_css = frappe.assets.bundled_asset(
"print.bundle.css",
frappe.utils.is_rtl(this.lang_code)
);
const fullHtml = `
<html>
<head>
<style>${resp.message.style}</style>
<link href="${base_url}${print_css}" rel="stylesheet">
</head>
<body>
<div class="print-format print-format-preview" style="display: flex; flex-direction: column;">
${resp.message.html}
</div>
</body>
</html>`;
const device = this.events.get_device()
const canvas = buildCanvas(fullHtml).then(canvas => {
const {out, width, height} = canvas2ByteArray(canvas, 150);
const data = buildImagePrintData(out, width, height);
device.transferOut(device._enpointNum, data);
});
}).catch(err => {
frappe.show_alert({
indicator: "red",
message: __(`Error getting print format: ${err}`),
});
});
}
const origin_print_receipt = erpnext.PointOfSale.PastOrderSummary.prototype.print_receipt;
erpnext.PointOfSale.PastOrderSummary.prototype.print_receipt = function() {
if (typeof this.events?.get_device === 'function') {
this.custom_print_receipt();
return
}
origin_print_receipt.call(this, ...arguments);
};
});
}
});
function get_command() {
const ESC = 0x1B;
const GS = 0x1D;
const BYTE_MASK = 0xFF;
const MSB_BIT = 0x80;
const CMD = {
byte_mask: BYTE_MASK,
msb_bit: MSB_BIT,
init: [ESC, 0x40],
bold_on: [ESC, 0x45, 1],
bold_off: [ESC, 0x45, 0],
center: [ESC, 0x61, 1],
left: [ESC, 0x61, 0],
dbl_height: [GS, 0x21, 0x11],
normal_size: [GS, 0x21, 0x00],
feed3: [ESC, 0x64, 3],
cut: [GS, 0x56, 0x41, 0x00],
lf: [0x0A],
open_drawer: [0x1B, 0x70, 0x00, 0x19, 0xFA],
print_image: (xL, xH, yL, yH) => [GS, 0x76, 0x30, 0x00, xL, xH, yL, yH],
};
return CMD;
}
function buildImagePrintData(out, w, h) {
const CMD = get_command();
const BITS_PER_BYTE = 8;
// Width must be multiple of BITS_PER_BYTE
const wBytes = Math.ceil(w / BITS_PER_BYTE);
const wAligned = wBytes * BITS_PER_BYTE;
const parts = [];
const add = (bytes) => parts.push(new Uint8Array(bytes));
add(CMD.init);
add(CMD.center);
// GS v 0: xL xH yL yH [data]
// xL/xH = bytes per row (little-endian), yL/yH = number of rows
const xL = wBytes & CMD.byte_mask, xH = (wBytes >> BITS_PER_BYTE) & CMD.byte_mask;
const yL = h & CMD.byte_mask, yH = (h >> BITS_PER_BYTE) & CMD.byte_mask;
add(CMD.print_image(xL, xH, yL, yH));
// Pack bits: 1=black, MSB first
const rowData = new Uint8Array(wBytes * h);
for (let y = 0; y < h; y++) {
for (let bx = 0; bx < wBytes; bx++) {
let byte = 0;
for (let bit = 0; bit < BITS_PER_BYTE; bit++) {
const x = bx * BITS_PER_BYTE + bit;
if (x < w && out[y * w + x]) byte |= (CMD.msb_bit >> bit);
}
rowData[y * wBytes + bx] = byte;
}
}
parts.push(rowData);
add(CMD.left);
add(CMD.feed3);
add(CMD.cut);
const total = parts.reduce((s, p) => s + p.length, 0);
const buf = new Uint8Array(total);
let offset = 0;
for (const p of parts) { buf.set(p, offset); offset += p.length; }
return buf;
}
function buildCanvas (fullHtml) {
return new Promise((resolve) => {
const iframe = document.createElement("iframe");
iframe.style.width = "100%";
document.body.appendChild(iframe);
iframe.onload = () => {
const body = iframe.contentDocument.body;
const content = iframe.contentDocument.querySelector(".print-format");
html2canvas(content || body, { scale: 1, useCORS: true, }).then(canvas => {
document.body.removeChild(iframe);
resolve(canvas);
}).catch(err => {
frappe.show_alert({
indicator: "red",
message: __(`Error building canvas: ${err}`),
});
});
};
iframe.contentDocument.open();
iframe.contentDocument.write(fullHtml);
iframe.contentDocument.close();
});
}
function canvas2ByteArray(canvas, threshold) {
const w = canvas.width, h = canvas.height;
const ctx = canvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, w, h);
const pixels = imgData.data;
const gray = new Float32Array(w * h);
for (let i = 0; i < gray.length; i++) {
const r = pixels[i*4], g = pixels[i*4+1], b = pixels[i*4+2];
gray[i] = 0.299*r + 0.587*g + 0.114*b;
}
const out = new Uint8Array(w * h);
for (let i = 0; i < gray.length; i++) out[i] = gray[i] < threshold ? 1 : 0;
return { out, width: w, height: h };
}
function waitForPOS(callback) {
if (
typeof erpnext !== "undefined" &&
erpnext.PointOfSale &&
erpnext.PointOfSale.Controller &&
erpnext.PointOfSale.PastOrderSummary
) {
if (typeof erpnext.PointOfSale?.Controller?.prototype?.setup_printer === 'function') {
return
}
callback();
} else {
setTimeout(() => waitForPOS(callback), 100);
}
}