feat: implement centralized cash drawer security via HardwareProxy patching to support Bluetooth printer triggers and restrict unauthorized drawer openings.
This commit is contained in:
parent
5f92333cdf
commit
85692e2fec
@ -64,6 +64,7 @@ Requirements:
|
||||
# POS integrations (load after components)
|
||||
'pos_bluetooth_thermal_printer/static/src/js/pos_session_integration.js',
|
||||
'pos_bluetooth_thermal_printer/static/src/js/pos_receipt_printer.js',
|
||||
'pos_bluetooth_thermal_printer/static/src/js/pos_hardware_proxy_patch.js',
|
||||
'pos_bluetooth_thermal_printer/static/src/js/pos_navbar_extension.js',
|
||||
|
||||
# Templates
|
||||
|
||||
125
static/src/js/pos_hardware_proxy_patch.js
Normal file
125
static/src/js/pos_hardware_proxy_patch.js
Normal file
@ -0,0 +1,125 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { HardwareProxy } from "@point_of_sale/app/services/hardware_proxy_service";
|
||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||
import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { getBluetoothPrintingServices } from "./pos_receipt_printer";
|
||||
|
||||
// Patch OrderPaymentValidation to track validation context
|
||||
patch(OrderPaymentValidation.prototype, {
|
||||
async finalizeValidation() {
|
||||
if (this.pos.hardwareProxy) {
|
||||
this.pos.hardwareProxy.currentValidationContext = {
|
||||
isValidating: true,
|
||||
order: this.order
|
||||
};
|
||||
}
|
||||
try {
|
||||
return await super.finalizeValidation(...arguments);
|
||||
} finally {
|
||||
if (this.pos.hardwareProxy) {
|
||||
this.pos.hardwareProxy.currentValidationContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Patch PosStore to track print context
|
||||
patch(PosStore.prototype, {
|
||||
async printReceipt(options = {}) {
|
||||
const printType = options.basic ? 'CHECKER' : (options.printBillActionTriggered ? 'BILL' : 'PAYMENT');
|
||||
const order = options.order || this.getOrder();
|
||||
|
||||
if (this.hardwareProxy) {
|
||||
this.hardwareProxy.currentPrintContext = {
|
||||
printType,
|
||||
order
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await super.printReceipt(...arguments);
|
||||
} finally {
|
||||
if (this.hardwareProxy) {
|
||||
this.hardwareProxy.currentPrintContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Patch HardwareProxy to restrict cashbox opening AND trigger Bluetooth printer's cashbox
|
||||
patch(HardwareProxy.prototype, {
|
||||
async openCashbox(action = false) {
|
||||
// === CASH DRAWER SECURITY GATE ===
|
||||
//
|
||||
// The physical cash drawer should only open for:
|
||||
// (A) Manual actions: Cash In/Out, Opening/Closing control, etc.
|
||||
// → identified by the `action` argument being truthy.
|
||||
// (B) Payment screen manual "Open Cashbox" button (no action arg, no context).
|
||||
// → passed through as a legitimate manual trigger.
|
||||
// (C) Order finalisation when the order is paid with cash.
|
||||
// → identified by currentValidationContext being active.
|
||||
//
|
||||
// Blocked when a print context is active and the print is not a cash payment:
|
||||
// → CHECKER (table checker / basic receipt)
|
||||
// → BILL (proforma invoice / temporary receipt)
|
||||
// → PAYMENT for non-cash methods
|
||||
|
||||
let allowOpen = false;
|
||||
|
||||
// (A) Manual action always allowed (Cash In/Out, Opening/Closing control)
|
||||
if (action) {
|
||||
console.log(`[CashDrawer] Manual action "${action}" — allowing.`);
|
||||
allowOpen = true;
|
||||
}
|
||||
// (C) Order validation in progress — only allow if paid with cash
|
||||
else if (this.currentValidationContext) {
|
||||
const order = this.currentValidationContext.order;
|
||||
if (order && order.isPaidWithCash()) {
|
||||
console.log("[CashDrawer] Allowing — validation context: order is paid with cash.");
|
||||
allowOpen = true;
|
||||
} else {
|
||||
console.log("[CashDrawer] Blocking — validation context: order is not paid with cash.");
|
||||
}
|
||||
}
|
||||
// Print context active → block unless it is a cash payment print
|
||||
else if (this.currentPrintContext) {
|
||||
const { printType, order } = this.currentPrintContext;
|
||||
if (printType === 'PAYMENT' && order && order.isPaidWithCash()) {
|
||||
console.log("[CashDrawer] Allowing — print context: cash payment receipt.");
|
||||
allowOpen = true;
|
||||
} else {
|
||||
console.log(`[CashDrawer] Blocking — print context: printType=${printType}, cash=${order?.isPaidWithCash()}`);
|
||||
}
|
||||
}
|
||||
// (B) No context at all — manual button (e.g. payment screen Open Cashbox button)
|
||||
else {
|
||||
console.log("[CashDrawer] No context — allowing as manual trigger.");
|
||||
allowOpen = true;
|
||||
}
|
||||
|
||||
if (allowOpen) {
|
||||
// Trigger Bluetooth printer's cashbox if enabled and connected
|
||||
const pos = this.pos || this.env?.services?.pos;
|
||||
if (pos && pos.config?.bluetooth_printer_enabled) {
|
||||
try {
|
||||
const services = getBluetoothPrintingServices(this.env?.services?.notification);
|
||||
const { bluetoothManager } = services;
|
||||
if (bluetoothManager && bluetoothManager.getConnectionStatus() === 'connected') {
|
||||
console.log('[BluetoothPrint] Sending cash drawer pulse command via Bluetooth.');
|
||||
const pulseCmds = new Uint8Array([
|
||||
0x1B, 0x3D, 0x01, // ESC = 1 (Select printer)
|
||||
0x1B, 0x70, 0x00, 0x19, 0x19, // ESC p 0 25 25 (pulse pin 2)
|
||||
0x1B, 0x70, 0x01, 0x19, 0x19 // ESC p 1 25 25 (pulse pin 5)
|
||||
]);
|
||||
await bluetoothManager.sendData(pulseCmds, false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BluetoothPrint] Failed to send cash drawer pulse via Bluetooth:', error);
|
||||
}
|
||||
}
|
||||
return super.openCashbox(...arguments);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -108,38 +108,16 @@ patch(PosPrinterService.prototype, {
|
||||
console.log('[BluetoothPrint] Element classes:', el?.className);
|
||||
|
||||
// Determine print context
|
||||
// Priority: hardware proxy context (set by pos_cash_drawer_cash_only) > local flags > HTML heuristics
|
||||
const pos = this.env?.services?.pos;
|
||||
const currentPrintContext = pos?.hardwareProxy?.currentPrintContext;
|
||||
const order = this._currentPrintOrder || currentPrintContext?.order || pos?.getOrder();
|
||||
|
||||
let printType = currentPrintContext?.printType;
|
||||
if (!printType) {
|
||||
// Fallback heuristics when hardware proxy context is not yet set
|
||||
// (can happen if bluetooth module loads before cash_drawer module, or context race)
|
||||
if (this._currentPrintBasic) {
|
||||
// basic_receipt flag set in print() props → table checker
|
||||
printType = 'CHECKER';
|
||||
} else if (el && el.querySelector && el.querySelector('.new-changes, .preset-name, .o-employee-name, .order-ref-prefix')) {
|
||||
// HTML contains table-checker specific elements
|
||||
printType = 'CHECKER';
|
||||
} else if (el && el.querySelector && el.querySelector('.proforma-invoice, .bill-receipt, .pre-payment')) {
|
||||
// HTML contains proforma/bill specific elements
|
||||
printType = 'BILL';
|
||||
} else {
|
||||
// Cannot determine from HTML — check order state.
|
||||
// An order with state 'open' (not yet paid) printed is a BILL/proforma.
|
||||
// An order with state 'paid' is a PAYMENT receipt.
|
||||
const orderState = order?.state;
|
||||
if (orderState && orderState !== 'paid' && orderState !== 'invoiced') {
|
||||
// Order not yet paid — this must be a bill/proforma print
|
||||
printType = 'BILL';
|
||||
} else if (orderState === 'paid' || orderState === 'invoiced') {
|
||||
printType = 'PAYMENT';
|
||||
} else {
|
||||
// Unknown state — fail safe: treat as BILL (no drawer)
|
||||
printType = 'BILL';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,10 +127,7 @@ patch(PosPrinterService.prototype, {
|
||||
if (printType === 'CHECKER') {
|
||||
this._currentPrintBasic = true;
|
||||
}
|
||||
|
||||
// Cash drawer should only open for actual cash payment receipts
|
||||
this._isCashPayment = printType === 'PAYMENT' && order && (typeof order.isPaidWithCash === 'function' ? order.isPaidWithCash() : false);
|
||||
console.log(`[BluetoothPrint] printHtml context - printType: ${printType}, orderState: ${order?.state}, isCashPayment: ${this._isCashPayment}`);
|
||||
console.log(`[BluetoothPrint] printHtml context - printType: ${printType}, orderState: ${order?.state}`);
|
||||
|
||||
// Check if a Bluetooth printer is configured (from localStorage)
|
||||
const storage = new BluetoothPrinterStorage();
|
||||
@ -275,7 +250,6 @@ patch(PosPrinterService.prototype, {
|
||||
} finally {
|
||||
this._currentPrintOrder = null;
|
||||
this._currentPrintBasic = false;
|
||||
this._isCashPayment = false;
|
||||
}
|
||||
},
|
||||
|
||||
@ -437,10 +411,10 @@ patch(PosPrinterService.prototype, {
|
||||
* @param {boolean} isCashPayment - True if context is cash payment print
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
_applyCashDrawerSecurityGate(escposData, isCashPayment) {
|
||||
_applyCashDrawerSecurityGate(escposData) {
|
||||
if (!escposData) return escposData;
|
||||
|
||||
// 1. Strip any existing cash drawer pulse commands (0x1B 0x70) to prevent unauthorized triggers
|
||||
// Strip any existing cash drawer pulse commands (0x1B 0x70) to prevent unauthorized triggers
|
||||
const initialLen = escposData.length;
|
||||
const cleanData = EscPosGenerator.stripCashDrawerCommands(escposData);
|
||||
const strippedCount = Math.floor((initialLen - cleanData.length) / 5);
|
||||
@ -448,28 +422,7 @@ patch(PosPrinterService.prototype, {
|
||||
console.log(`[BluetoothPrint] Stripped ${strippedCount} ESC p command(s) from print stream.`);
|
||||
}
|
||||
|
||||
let finalData = cleanData;
|
||||
|
||||
// 2. If it is a cash payment print context, inject the cash drawer pulse command
|
||||
if (isCashPayment) {
|
||||
console.log('[BluetoothPrint] Cash payment context: Appending cash drawer pulse commands to ESC/POS stream.');
|
||||
// Standard ESC/POS cashbox open commands (pin 2 and pin 5)
|
||||
const pulseCmds = new Uint8Array([
|
||||
0x1B, 0x3D, 0x01, // ESC = 1 (Select printer)
|
||||
0x1B, 0x70, 0x00, 0x19, 0x19, // ESC p 0 25 25 (pulse pin 2)
|
||||
0x1B, 0x70, 0x01, 0x19, 0x19 // ESC p 1 25 25 (pulse pin 5)
|
||||
]);
|
||||
|
||||
// Combine pulse commands with the clean receipt data
|
||||
const combined = new Uint8Array(pulseCmds.length + finalData.length);
|
||||
combined.set(pulseCmds, 0);
|
||||
combined.set(finalData, pulseCmds.length);
|
||||
finalData = combined;
|
||||
} else {
|
||||
console.log('[BluetoothPrint] Non-cash print context: Cash drawer pulse blocked.');
|
||||
}
|
||||
|
||||
return finalData;
|
||||
return cleanData;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -529,7 +482,7 @@ patch(PosPrinterService.prototype, {
|
||||
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data');
|
||||
|
||||
// Apply cash drawer security gate
|
||||
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
|
||||
escposData = this._applyCashDrawerSecurityGate(escposData);
|
||||
|
||||
// Send data to printer
|
||||
console.log('[BluetoothPrint] Sending graphics to printer...');
|
||||
@ -596,7 +549,7 @@ patch(PosPrinterService.prototype, {
|
||||
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of text data');
|
||||
|
||||
// Apply cash drawer security gate
|
||||
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
|
||||
escposData = this._applyCashDrawerSecurityGate(escposData);
|
||||
|
||||
// Send data to printer
|
||||
console.log('[BluetoothPrint] Sending text to printer...');
|
||||
@ -639,7 +592,7 @@ patch(PosPrinterService.prototype, {
|
||||
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of ESC/POS data');
|
||||
|
||||
// Apply cash drawer security gate
|
||||
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
|
||||
escposData = this._applyCashDrawerSecurityGate(escposData);
|
||||
|
||||
// Send data to printer
|
||||
console.log('[BluetoothPrint] Sending to printer...');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user