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 integrations (load after components)
|
||||||
'pos_bluetooth_thermal_printer/static/src/js/pos_session_integration.js',
|
'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_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',
|
'pos_bluetooth_thermal_printer/static/src/js/pos_navbar_extension.js',
|
||||||
|
|
||||||
# Templates
|
# 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);
|
console.log('[BluetoothPrint] Element classes:', el?.className);
|
||||||
|
|
||||||
// Determine print context
|
// 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 pos = this.env?.services?.pos;
|
||||||
const currentPrintContext = pos?.hardwareProxy?.currentPrintContext;
|
const currentPrintContext = pos?.hardwareProxy?.currentPrintContext;
|
||||||
const order = this._currentPrintOrder || currentPrintContext?.order || pos?.getOrder();
|
const order = this._currentPrintOrder || currentPrintContext?.order || pos?.getOrder();
|
||||||
|
|
||||||
let printType = currentPrintContext?.printType;
|
let printType = currentPrintContext?.printType;
|
||||||
if (!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) {
|
if (this._currentPrintBasic) {
|
||||||
// basic_receipt flag set in print() props → table checker
|
|
||||||
printType = 'CHECKER';
|
printType = 'CHECKER';
|
||||||
} else if (el && el.querySelector && el.querySelector('.new-changes, .preset-name, .o-employee-name, .order-ref-prefix')) {
|
} 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';
|
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') {
|
if (printType === 'CHECKER') {
|
||||||
this._currentPrintBasic = true;
|
this._currentPrintBasic = true;
|
||||||
}
|
}
|
||||||
|
console.log(`[BluetoothPrint] printHtml context - printType: ${printType}, orderState: ${order?.state}`);
|
||||||
// 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}`);
|
|
||||||
|
|
||||||
// Check if a Bluetooth printer is configured (from localStorage)
|
// Check if a Bluetooth printer is configured (from localStorage)
|
||||||
const storage = new BluetoothPrinterStorage();
|
const storage = new BluetoothPrinterStorage();
|
||||||
@ -275,7 +250,6 @@ patch(PosPrinterService.prototype, {
|
|||||||
} finally {
|
} finally {
|
||||||
this._currentPrintOrder = null;
|
this._currentPrintOrder = null;
|
||||||
this._currentPrintBasic = false;
|
this._currentPrintBasic = false;
|
||||||
this._isCashPayment = false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -437,10 +411,10 @@ patch(PosPrinterService.prototype, {
|
|||||||
* @param {boolean} isCashPayment - True if context is cash payment print
|
* @param {boolean} isCashPayment - True if context is cash payment print
|
||||||
* @returns {Uint8Array}
|
* @returns {Uint8Array}
|
||||||
*/
|
*/
|
||||||
_applyCashDrawerSecurityGate(escposData, isCashPayment) {
|
_applyCashDrawerSecurityGate(escposData) {
|
||||||
if (!escposData) return 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 initialLen = escposData.length;
|
||||||
const cleanData = EscPosGenerator.stripCashDrawerCommands(escposData);
|
const cleanData = EscPosGenerator.stripCashDrawerCommands(escposData);
|
||||||
const strippedCount = Math.floor((initialLen - cleanData.length) / 5);
|
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.`);
|
console.log(`[BluetoothPrint] Stripped ${strippedCount} ESC p command(s) from print stream.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalData = cleanData;
|
return 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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -529,7 +482,7 @@ patch(PosPrinterService.prototype, {
|
|||||||
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data');
|
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data');
|
||||||
|
|
||||||
// Apply cash drawer security gate
|
// Apply cash drawer security gate
|
||||||
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
|
escposData = this._applyCashDrawerSecurityGate(escposData);
|
||||||
|
|
||||||
// Send data to printer
|
// Send data to printer
|
||||||
console.log('[BluetoothPrint] Sending graphics 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');
|
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of text data');
|
||||||
|
|
||||||
// Apply cash drawer security gate
|
// Apply cash drawer security gate
|
||||||
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
|
escposData = this._applyCashDrawerSecurityGate(escposData);
|
||||||
|
|
||||||
// Send data to printer
|
// Send data to printer
|
||||||
console.log('[BluetoothPrint] Sending text 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');
|
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of ESC/POS data');
|
||||||
|
|
||||||
// Apply cash drawer security gate
|
// Apply cash drawer security gate
|
||||||
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
|
escposData = this._applyCashDrawerSecurityGate(escposData);
|
||||||
|
|
||||||
// Send data to printer
|
// Send data to printer
|
||||||
console.log('[BluetoothPrint] Sending to printer...');
|
console.log('[BluetoothPrint] Sending to printer...');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user