add support printer TM-T82X-II
This commit is contained in:
@@ -1,13 +1,311 @@
|
|||||||
frappe.after_ajax(() => {
|
import html2canvas from 'html2canvas';
|
||||||
console.log("overriding ====");
|
|
||||||
|
|
||||||
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 () {
|
erpnext.PointOfSale.Controller.prototype.setup_printer = function() {
|
||||||
console.log("Yoooooooooooooooooow");
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user