From 7a3ca92d4c3c35623e7f2478914718b88b2bc279 Mon Sep 17 00:00:00 2001 From: kurogeek Date: Wed, 1 Jul 2026 13:37:40 +0700 Subject: [PATCH] add support printer TM-T82X-II --- posprinter/public/js/pos_override.js | 314 ++++++++++++++++++++++++++- 1 file changed, 306 insertions(+), 8 deletions(-) diff --git a/posprinter/public/js/pos_override.js b/posprinter/public/js/pos_override.js index 6c65727..1b0c278 100644 --- a/posprinter/public/js/pos_override.js +++ b/posprinter/public/js/pos_override.js @@ -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(` +
+
${__("Change")}
+
${format_currency(doc.change_amount, doc.currency)}
+
`); + } + + 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 = ` + + + + + + + + + `; + + 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); + } +} +