first commit
This commit is contained in:
commit
035dfe3dab
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.pyc
|
||||
__pycache__/
|
||||
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# POS Kitchen ESC/POS Network Printer
|
||||
|
||||
This module adds support for generic network ESC/POS thermal printers in the Point of Sale Kitchen Display / Printing configurations.
|
||||
|
||||
## Features
|
||||
- Add basic raw network printing over IP using port 9100 without relying on IoT box or EPSON-only support.
|
||||
- Configurable per-printer IP address directly under Point of Sale -> Printers settings.
|
||||
- Formats standard kitchen receipt containing products, quantity, and internal notes (without prices).
|
||||
- Replaces/upgrades the POS ticket screen's existing hidden reprint functionality with highly visible 'Reprint Kitchen Checker' buttons.
|
||||
|
||||
## Setup
|
||||
- Go to POS Configuration -> Printers
|
||||
- Add a new "Network ESC/POS Printer".
|
||||
- Fill in the appropriate local IP address (e.g. `192.168.1.100`) and the POS categories the printer should handle.
|
||||
- Set your POS configuration to route kitchen printing to this new printer as usual.
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
17
__manifest__.py
Normal file
17
__manifest__.py
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
'name': 'POS Kitchen ESC/POS Network Printer',
|
||||
'version': '1.0',
|
||||
'category': 'Sales/Point of Sale',
|
||||
'summary': 'Support generic network (IP) ESC/POS printers for kitchen receipts',
|
||||
'depends': ['point_of_sale'],
|
||||
'data': [
|
||||
'views/pos_printer_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
'pos_kitchen_printer/static/src/**/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
1
models/__init__.py
Normal file
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import pos_printer
|
||||
99
models/pos_printer.py
Normal file
99
models/pos_printer.py
Normal file
@ -0,0 +1,99 @@
|
||||
import socket
|
||||
import logging
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class PosPrinter(models.Model):
|
||||
_inherit = 'pos.printer'
|
||||
|
||||
printer_type = fields.Selection(
|
||||
selection_add=[('network_escpos', 'Network ESC/POS Printer')],
|
||||
ondelete={'network_escpos': 'set default'}
|
||||
)
|
||||
network_printer_ip = fields.Char(
|
||||
string='Network Printer IP Address',
|
||||
help="IP address of the generic ESC/POS network printer.",
|
||||
default="0.0.0.0"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _load_pos_data_fields(self, config_id):
|
||||
fields = super()._load_pos_data_fields(config_id)
|
||||
if 'network_printer_ip' not in fields:
|
||||
fields.append('network_printer_ip')
|
||||
return fields
|
||||
|
||||
@api.model
|
||||
def print_network_kitchen_receipt(self, printer_id, receipt_data):
|
||||
"""
|
||||
Receives structural receipt_data from POS frontend and sends ESC/POS string to network printer.
|
||||
receipt_data expected format:
|
||||
{
|
||||
'order_name': 'Order 0001',
|
||||
'order_time': '2023-10-01 12:00:00',
|
||||
'lines': [ { 'name': 'Burger', 'qty': 1, 'note': 'No onions' } ],
|
||||
'waiter': 'Mitchell Admin',
|
||||
'table': 'T1'
|
||||
}
|
||||
"""
|
||||
printer = self.browse(printer_id)
|
||||
if not printer or not printer.network_printer_ip:
|
||||
return {'successful': False, 'message': 'Printer IP not configured'}
|
||||
|
||||
# Build basic ESC/POS commands
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
INIT = ESC + b'@'
|
||||
CUT = GS + b'V\x41\x00'
|
||||
BOLD_ON = ESC + b'E\x01'
|
||||
BOLD_OFF = ESC + b'E\x00'
|
||||
SIZE_DOUBLE = GS + b'!\x11'
|
||||
SIZE_NORMAL = GS + b'!\x00'
|
||||
ALIGN_CENTER = ESC + b'a\x01'
|
||||
ALIGN_LEFT = ESC + b'a\x00'
|
||||
NEWLINE = b'\n'
|
||||
|
||||
data = INIT
|
||||
data += ALIGN_CENTER + SIZE_DOUBLE + BOLD_ON
|
||||
data += b"KITCHEN RECEIPT" + NEWLINE
|
||||
data += SIZE_NORMAL + BOLD_OFF + NEWLINE
|
||||
|
||||
if receipt_data.get('table'):
|
||||
data += ALIGN_LEFT + SIZE_DOUBLE
|
||||
data += f"Table: {receipt_data['table']}".encode('utf-8') + NEWLINE
|
||||
data += SIZE_NORMAL
|
||||
|
||||
data += ALIGN_LEFT
|
||||
data += f"Order: {receipt_data.get('order_name', '')}".encode('utf-8') + NEWLINE
|
||||
data += f"Date: {receipt_data.get('order_time', '')}".encode('utf-8') + NEWLINE
|
||||
if receipt_data.get('waiter'):
|
||||
data += f"Waiter: {receipt_data['waiter']}".encode('utf-8') + NEWLINE
|
||||
data += b"-" * 32 + NEWLINE
|
||||
|
||||
for line in receipt_data.get('lines', []):
|
||||
qty = line.get('qty', 1)
|
||||
name = line.get('name', '')
|
||||
note = line.get('note', '')
|
||||
|
||||
data += SIZE_DOUBLE
|
||||
data += f"{qty} x {name}".encode('utf-8') + NEWLINE
|
||||
data += SIZE_NORMAL
|
||||
if note:
|
||||
data += f" NOTE: {note}".encode('utf-8') + NEWLINE
|
||||
|
||||
data += NEWLINE
|
||||
|
||||
data += b"-" * 32 + NEWLINE + NEWLINE + NEWLINE + CUT
|
||||
|
||||
# Send to printer
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(3.0)
|
||||
s.connect((printer.network_printer_ip, 9100))
|
||||
s.sendall(data)
|
||||
s.close()
|
||||
return {'successful': True}
|
||||
except Exception as e:
|
||||
_logger.error("Failed to print to network ESC/POS printer %s: %s", printer.network_printer_ip, e)
|
||||
return {'successful': False, 'message': str(e)}
|
||||
22
static/src/app/screens/ticket_screen/ticket_screen.xml
Normal file
22
static/src/app/screens/ticket_screen/ticket_screen.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<!-- Add Reprint Kitchen Button to Synced Orders (Right Pane underneath details) -->
|
||||
<t t-name="pos_kitchen_printer.TicketScreen" t-inherit="point_of_sale.TicketScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//button[contains(@class, 'edit-order-payment')]" position="after">
|
||||
<button t-if="this.pos.unwatched.printers.length"
|
||||
class="control-button btn btn-secondary btn-lg lh-lg flex-grow-1 flex-shrink-1"
|
||||
t-on-click="() => this.onClickReprintAll(_selectedSyncedOrder)">
|
||||
<i class="fa fa-cutlery me-1" /> Reprint Kitchen Checker
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Enhance the existing cutlery button for unsynced orders -->
|
||||
<xpath expr="//button[contains(@t-on-click, 'onClickReprintAll') and contains(@class, 'd-flex')]" position="replace">
|
||||
<button class="button btn btn-secondary btn-lg d-flex flex-row align-items-center justify-content-center flex-grow-1"
|
||||
t-on-click="() => this.onClickReprintAll(_selectedSyncedOrder)"
|
||||
t-if="this.pos.unwatched.printers.length and _selectedSyncedOrder.uiState.lastPrints.length">
|
||||
<i class="fa fa-cutlery me-1" aria-hidden="true"/> Reprint Kitchen Checker
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
67
static/src/app/services/pos_store.js
Normal file
67
static/src/app/services/pos_store.js
Normal file
@ -0,0 +1,67 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||
|
||||
patch(PosStore.prototype, {
|
||||
createPrinter(config) {
|
||||
if (config.printer_type === "network_escpos") {
|
||||
return {
|
||||
config: config,
|
||||
printReceipt: async (receipt, actionId) => {
|
||||
// This dummy promise ensures the printer interface is fulfilled.
|
||||
// The actual sending to backend is hooked in `printOrderChanges`.
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
return super.createPrinter(...arguments);
|
||||
},
|
||||
|
||||
async printOrderChanges(data, printer) {
|
||||
if (printer.config && printer.config.printer_type === "network_escpos") {
|
||||
const changesData = data.changes?.data || [];
|
||||
|
||||
// Build the data object to send to backend
|
||||
const lines = changesData.map(c => ({
|
||||
name: c.name,
|
||||
qty: c.qty,
|
||||
note: c.note || ''
|
||||
}));
|
||||
|
||||
const receipt_data = {
|
||||
order_name: data.name || '',
|
||||
order_time: data.time?.raw || new Date().toISOString(),
|
||||
waiter: data.cashier || '',
|
||||
table: data.table_name || '',
|
||||
lines: lines
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await this.env.services.orm.call(
|
||||
'pos.printer',
|
||||
'print_network_kitchen_receipt',
|
||||
[printer.config.id, receipt_data]
|
||||
);
|
||||
|
||||
if (!result.successful) {
|
||||
this.env.services.notification.add(
|
||||
result.message || "Network printer error",
|
||||
{ type: "danger" }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error printing to network printer", error);
|
||||
this.env.services.notification.add(
|
||||
"Error printing to network printer",
|
||||
{ type: "danger" }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super.printOrderChanges(data, printer);
|
||||
}
|
||||
});
|
||||
13
views/pos_printer_views.xml
Normal file
13
views/pos_printer_views.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_pos_printer_form_inherit_network_escpos" model="ir.ui.view">
|
||||
<field name="name">pos.printer.form.inherit.network.escpos</field>
|
||||
<field name="model">pos.printer</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_printer_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='epson_printer_ip']" position="after">
|
||||
<field name="network_printer_ip" invisible="printer_type != 'network_escpos'" required="printer_type == 'network_escpos'"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user