forked from alaguraj/odoo-testing-addons
Implement Kitchen Display System (KDS) with real-time order line status updates for POS and a backend Kanban view.
This commit is contained in:
parent
4db79c5f79
commit
f87c69b3aa
@ -22,7 +22,14 @@
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'dine360_kds/static/src/css/kds_style.css',
|
||||
'dine360_kds/static/src/js/kds_backend.js',
|
||||
],
|
||||
'point_of_sale.assets_prod': [
|
||||
'dine360_kds/static/src/css/pos_kds.css',
|
||||
'dine360_kds/static/src/js/pos_kds.js',
|
||||
# 'dine360_kds/static/src/xml/pos_kds.xml', # Temporarily disabled
|
||||
],
|
||||
|
||||
},
|
||||
'installable': True,
|
||||
'application': True,
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
from . import pos_order_line
|
||||
from . import product
|
||||
from . import pos_session
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = 'pos.order.line'
|
||||
@ -28,19 +31,73 @@ class PosOrderLine(models.Model):
|
||||
else:
|
||||
line.cooking_time = 0
|
||||
|
||||
def _notify_pos(self):
|
||||
"""Send notification to POS when order line status changes"""
|
||||
_logger.info("=== _notify_pos called for %s lines ===" % len(self))
|
||||
for line in self:
|
||||
_logger.info(f"Processing line {line.id}, order: {line.order_id.name}, config: {line.order_id.config_id}")
|
||||
if line.order_id.config_id:
|
||||
channel_name = "pos_config_Channel_%s" % line.order_id.config_id.id
|
||||
payload = {
|
||||
'line_id': line.id,
|
||||
'order_id': line.order_id.id,
|
||||
'status': line.preparation_status,
|
||||
'status_label': dict(self._fields['preparation_status']._description_selection(self.env)).get(line.preparation_status),
|
||||
'order_uid': line.order_id.pos_reference,
|
||||
'product_id': line.product_id.id,
|
||||
'qty': line.qty,
|
||||
}
|
||||
_logger.info(f"KDS NOTIFICATION: Sending update for Line {line.id} Status {line.preparation_status} to {channel_name}")
|
||||
self.env['bus.bus']._sendone(channel_name, 'kds_update', payload)
|
||||
else:
|
||||
_logger.warning(f"Line {line.id} has no config_id - cannot send notification")
|
||||
|
||||
def _notify_kds(self):
|
||||
"""Send notification to KDS backend when new order lines are created"""
|
||||
_logger.info("=== _notify_kds called for %s lines ===" % len(self))
|
||||
for line in self:
|
||||
# Only notify for kitchen items
|
||||
if line.product_id.is_kitchen_item and line.product_id.name != 'Water':
|
||||
# Send to global KDS channel
|
||||
kds_channel = "kds_channel"
|
||||
payload = {
|
||||
'line_id': line.id,
|
||||
'order_id': line.order_id.id,
|
||||
'product_name': line.product_id.name,
|
||||
'qty': line.qty,
|
||||
'table_name': line.table_id.name if line.table_id else '',
|
||||
'floor_name': line.floor_id.name if line.floor_id else '',
|
||||
'customer_note': line.customer_note or '',
|
||||
'preparation_status': line.preparation_status,
|
||||
'create_date': line.create_date.isoformat() if line.create_date else '',
|
||||
}
|
||||
_logger.info(f"KDS BACKEND NOTIFICATION: New order line {line.id} for {line.product_id.name}")
|
||||
self.env['bus.bus']._sendone(kds_channel, 'kds_new_order', payload)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Override create to send notifications to KDS when new orders are added"""
|
||||
lines = super(PosOrderLine, self).create(vals_list)
|
||||
# Send notification to KDS backend for real-time updates
|
||||
lines._notify_kds()
|
||||
return lines
|
||||
|
||||
def action_start_preparing(self):
|
||||
self.write({
|
||||
'preparation_status': 'preparing',
|
||||
'preparation_time_start': fields.Datetime.now()
|
||||
})
|
||||
self._notify_pos()
|
||||
|
||||
def action_mark_ready(self):
|
||||
self.write({
|
||||
'preparation_status': 'ready',
|
||||
'preparation_time_end': fields.Datetime.now()
|
||||
})
|
||||
self._notify_pos()
|
||||
|
||||
def action_mark_served(self):
|
||||
self.write({
|
||||
'preparation_status': 'served'
|
||||
})
|
||||
self._notify_pos()
|
||||
|
||||
9
addons/dine360_kds/models/pos_session.py
Normal file
9
addons/dine360_kds/models/pos_session.py
Normal file
@ -0,0 +1,9 @@
|
||||
from odoo import models
|
||||
|
||||
class PosSession(models.Model):
|
||||
_inherit = 'pos.session'
|
||||
|
||||
def _loader_params_pos_order_line(self):
|
||||
params = super()._loader_params_pos_order_line()
|
||||
params['search_params']['fields'].extend(['preparation_status', 'preparation_time_start', 'preparation_time_end'])
|
||||
return params
|
||||
29
addons/dine360_kds/static/src/css/pos_kds.css
Normal file
29
addons/dine360_kds/static/src/css/pos_kds.css
Normal file
@ -0,0 +1,29 @@
|
||||
.pos .badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pos .badge-waiting {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.pos .badge-preparing {
|
||||
background-color: #5bc0de;
|
||||
}
|
||||
|
||||
.pos .badge-ready {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
|
||||
.pos .badge-served {
|
||||
background-color: #777;
|
||||
}
|
||||
|
||||
.pos .badge-cancelled {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
79
addons/dine360_kds/static/src/js/kds_backend.js
Normal file
79
addons/dine360_kds/static/src/js/kds_backend.js
Normal file
@ -0,0 +1,79 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { onWillUnmount } from "@odoo/owl";
|
||||
|
||||
export class KdsKanbanController extends KanbanController {
|
||||
setup() {
|
||||
super.setup();
|
||||
console.log("[KDS Controller] Setup");
|
||||
|
||||
// Direct access to services to avoid useService potential conflicts
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.notification = this.env.services.notification;
|
||||
|
||||
const kdsChannel = "kds_channel";
|
||||
|
||||
if (this.busService) {
|
||||
console.log(`[KDS Controller] Subscribing to channel: ${kdsChannel}`);
|
||||
this.busService.addChannel(kdsChannel);
|
||||
|
||||
const handler = this._onKdsNotification.bind(this);
|
||||
this.busService.addEventListener("notification", handler);
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this.busService) {
|
||||
this.busService.removeEventListener("notification", handler);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error("[KDS Controller] Bus service not found!");
|
||||
}
|
||||
}
|
||||
|
||||
_onKdsNotification(event) {
|
||||
// console.log("[KDS Controller] Notification received:", event);
|
||||
|
||||
const notifications = event.detail || [];
|
||||
let shouldReload = false;
|
||||
|
||||
for (const notif of notifications) {
|
||||
// console.log("[KDS Controller] Processing notification:", notif);
|
||||
|
||||
if (notif.type === "kds_new_order") {
|
||||
console.log("[KDS Controller] New order notification:", notif.payload);
|
||||
|
||||
// Show notification to user
|
||||
if (this.notification) {
|
||||
const payload = notif.payload;
|
||||
this.notification.add(
|
||||
`New Order: ${payload.qty}x ${payload.product_name} - ${payload.table_name}`,
|
||||
{
|
||||
title: "Kitchen Display",
|
||||
type: "info",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
shouldReload = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldReload) {
|
||||
// Reload the view to show the new order
|
||||
console.log("[KDS Controller] Reloading view...");
|
||||
this.model.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const kdsKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: KdsKanbanController,
|
||||
};
|
||||
|
||||
registry.category("views").add("kds_kanban", kdsKanbanView);
|
||||
|
||||
console.log("[KDS Backend] kds_backend.js loaded (JS Class mode - manual services)");
|
||||
121
addons/dine360_kds/static/src/js/pos_kds.js
Normal file
121
addons/dine360_kds/static/src/js/pos_kds.js
Normal file
@ -0,0 +1,121 @@
|
||||
/** @odoo-module */
|
||||
|
||||
// Immediate console log to verify file is loaded
|
||||
console.log("==============================================");
|
||||
console.log("[KDS] pos_kds.js FILE IS LOADING!");
|
||||
console.log("==============================================");
|
||||
|
||||
import { Orderline } from "@point_of_sale/app/store/models";
|
||||
import { PosStore } from "@point_of_sale/app/store/pos_store";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
console.log("[KDS] Imports successful");
|
||||
|
||||
// Patch Orderline model
|
||||
patch(Orderline.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.preparation_status = this.preparation_status || 'waiting';
|
||||
},
|
||||
|
||||
init_from_JSON(json) {
|
||||
super.init_from_JSON(...arguments);
|
||||
if (json.preparation_status) {
|
||||
this.preparation_status = json.preparation_status;
|
||||
}
|
||||
},
|
||||
|
||||
export_as_JSON() {
|
||||
const json = super.export_as_JSON(...arguments);
|
||||
json.preparation_status = this.preparation_status;
|
||||
return json;
|
||||
},
|
||||
|
||||
set_preparation_status(status) {
|
||||
this.preparation_status = status;
|
||||
},
|
||||
|
||||
get_preparation_status() {
|
||||
return this.preparation_status;
|
||||
},
|
||||
|
||||
get_preparation_status_label() {
|
||||
const labels = {
|
||||
'waiting': 'Waiting',
|
||||
'preparing': 'Preparing',
|
||||
'ready': 'Ready',
|
||||
'served': 'Served',
|
||||
'cancelled': 'Cancelled'
|
||||
};
|
||||
return labels[this.preparation_status] || this.preparation_status;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[KDS] Orderline patched successfully");
|
||||
|
||||
// Patch PosStore
|
||||
patch(PosStore.prototype, {
|
||||
async _processData(loadedData) {
|
||||
console.log("[KDS] _processData called!");
|
||||
await super._processData(...arguments);
|
||||
|
||||
const channel = `pos_config_Channel_${this.config.id}`;
|
||||
console.log(`[KDS] Setting up channel: ${channel}`);
|
||||
|
||||
try {
|
||||
const busService = this.env.services.bus_service;
|
||||
busService.addChannel(channel);
|
||||
console.log("[KDS] Channel added successfully");
|
||||
|
||||
busService.addEventListener("notification", (event) => {
|
||||
console.log("[KDS] *** NOTIFICATION RECEIVED ***", event);
|
||||
const notifications = event.detail || [];
|
||||
|
||||
for (const notif of notifications) {
|
||||
if (notif.type === 'kds_update') {
|
||||
console.log("[KDS] *** KDS UPDATE ***", notif.payload);
|
||||
this._handleKdsUpdate(notif.payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[KDS] Listener registered successfully");
|
||||
} catch (error) {
|
||||
console.error("[KDS] ERROR:", error);
|
||||
}
|
||||
},
|
||||
|
||||
_handleKdsUpdate(payload) {
|
||||
console.log("[KDS] Handling update:", payload);
|
||||
|
||||
try {
|
||||
const { order_uid, product_id, status, status_label, line_id, order_id } = payload;
|
||||
const orders = this.get_order_list();
|
||||
|
||||
let order = orders.find(o => o.server_id === order_id);
|
||||
if (!order) {
|
||||
order = orders.find(o => o.name === order_uid || o.pos_reference === order_uid);
|
||||
}
|
||||
|
||||
if (order) {
|
||||
let line = order.get_orderlines().find(l => l.server_id === line_id);
|
||||
if (!line) {
|
||||
line = order.get_orderlines().find(l => l.product.id === product_id);
|
||||
}
|
||||
|
||||
if (line) {
|
||||
line.set_preparation_status(status);
|
||||
this.env.services.notification.add(
|
||||
`${line.product.display_name} is ${status_label}`,
|
||||
{ title: "Kitchen Update", type: "info" }
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[KDS] Error in handler:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[KDS] PosStore patched successfully");
|
||||
console.log("[KDS] MODULE FULLY LOADED!");
|
||||
12
addons/dine360_kds/static/src/xml/pos_kds.xml
Normal file
12
addons/dine360_kds/static/src/xml/pos_kds.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="dine360_kds.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="inside">
|
||||
<li class="info" t-if="props.line.get_preparation_status() and props.line.get_preparation_status() !== 'waiting'">
|
||||
<span t-attf-class="badge badge-{{props.line.get_preparation_status()}}">
|
||||
<t t-esc="props.line.get_preparation_status_label()"/>
|
||||
</span>
|
||||
</li>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@ -4,7 +4,7 @@
|
||||
<field name="name">pos.order.line.kds.kanban</field>
|
||||
<field name="model">pos.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="preparation_status" create="false" class="o_kanban_small_column o_kanban_project_tasks" sample="1">
|
||||
<kanban js_class="kds_kanban" default_group_by="preparation_status" create="false" class="o_kanban_small_column o_kanban_project_tasks" sample="1">
|
||||
<field name="preparation_status"/>
|
||||
<field name="color"/>
|
||||
<field name="product_id"/>
|
||||
@ -123,9 +123,9 @@
|
||||
<field name="name">Kitchen Display System</field>
|
||||
<field name="res_model">pos.order.line</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="domain">[('product_id.is_kitchen_item', '=', True)]</field>
|
||||
<field name="domain">[('product_id.is_kitchen_item', '=', True), ('product_id.name', '!=', 'Water'), ('order_id.session_id.state', '!=', 'closed'), ('product_id.pos_categ_ids.name', '!=', 'Drinks')]</field>
|
||||
<field name="search_view_id" ref="view_pos_order_line_kds_search"/>
|
||||
<field name="context">{'search_default_in_progress': 1, 'search_default_group_status': 1}</field>
|
||||
<field name="context">{'search_default_in_progress': 1, 'search_default_group_status': 1, 'search_default_today': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Welcome to the Kitchen!
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user