From f87c69b3aaccae0ed2d777735ac4723e07eabb79 Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Tue, 10 Feb 2026 11:09:39 +0530 Subject: [PATCH] Implement Kitchen Display System (KDS) with real-time order line status updates for POS and a backend Kanban view. --- addons/dine360_kds/__manifest__.py | 7 + addons/dine360_kds/models/__init__.py | 1 + addons/dine360_kds/models/pos_order_line.py | 57 +++++++++ addons/dine360_kds/models/pos_session.py | 9 ++ addons/dine360_kds/static/src/css/pos_kds.css | 29 +++++ .../dine360_kds/static/src/js/kds_backend.js | 79 ++++++++++++ addons/dine360_kds/static/src/js/pos_kds.js | 121 ++++++++++++++++++ addons/dine360_kds/static/src/xml/pos_kds.xml | 12 ++ .../views/pos_order_line_views.xml | 6 +- 9 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 addons/dine360_kds/models/pos_session.py create mode 100644 addons/dine360_kds/static/src/css/pos_kds.css create mode 100644 addons/dine360_kds/static/src/js/kds_backend.js create mode 100644 addons/dine360_kds/static/src/js/pos_kds.js create mode 100644 addons/dine360_kds/static/src/xml/pos_kds.xml diff --git a/addons/dine360_kds/__manifest__.py b/addons/dine360_kds/__manifest__.py index 541dd54..776e9b1 100644 --- a/addons/dine360_kds/__manifest__.py +++ b/addons/dine360_kds/__manifest__.py @@ -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, diff --git a/addons/dine360_kds/models/__init__.py b/addons/dine360_kds/models/__init__.py index 0c6d361..47daf9e 100644 --- a/addons/dine360_kds/models/__init__.py +++ b/addons/dine360_kds/models/__init__.py @@ -1,2 +1,3 @@ from . import pos_order_line from . import product +from . import pos_session diff --git a/addons/dine360_kds/models/pos_order_line.py b/addons/dine360_kds/models/pos_order_line.py index 3771477..8b221ee 100644 --- a/addons/dine360_kds/models/pos_order_line.py +++ b/addons/dine360_kds/models/pos_order_line.py @@ -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() diff --git a/addons/dine360_kds/models/pos_session.py b/addons/dine360_kds/models/pos_session.py new file mode 100644 index 0000000..1c89941 --- /dev/null +++ b/addons/dine360_kds/models/pos_session.py @@ -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 diff --git a/addons/dine360_kds/static/src/css/pos_kds.css b/addons/dine360_kds/static/src/css/pos_kds.css new file mode 100644 index 0000000..375c0e1 --- /dev/null +++ b/addons/dine360_kds/static/src/css/pos_kds.css @@ -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; +} \ No newline at end of file diff --git a/addons/dine360_kds/static/src/js/kds_backend.js b/addons/dine360_kds/static/src/js/kds_backend.js new file mode 100644 index 0000000..98c2947 --- /dev/null +++ b/addons/dine360_kds/static/src/js/kds_backend.js @@ -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)"); diff --git a/addons/dine360_kds/static/src/js/pos_kds.js b/addons/dine360_kds/static/src/js/pos_kds.js new file mode 100644 index 0000000..1721fe0 --- /dev/null +++ b/addons/dine360_kds/static/src/js/pos_kds.js @@ -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!"); diff --git a/addons/dine360_kds/static/src/xml/pos_kds.xml b/addons/dine360_kds/static/src/xml/pos_kds.xml new file mode 100644 index 0000000..07ac8a8 --- /dev/null +++ b/addons/dine360_kds/static/src/xml/pos_kds.xml @@ -0,0 +1,12 @@ + + + + +
  • + + + +
  • +
    +
    +
    diff --git a/addons/dine360_kds/views/pos_order_line_views.xml b/addons/dine360_kds/views/pos_order_line_views.xml index b246f31..98f9bbd 100644 --- a/addons/dine360_kds/views/pos_order_line_views.xml +++ b/addons/dine360_kds/views/pos_order_line_views.xml @@ -4,7 +4,7 @@ pos.order.line.kds.kanban pos.order.line - + @@ -123,9 +123,9 @@ Kitchen Display System pos.order.line kanban,tree,form - [('product_id.is_kitchen_item', '=', True)] + [('product_id.is_kitchen_item', '=', True), ('product_id.name', '!=', 'Water'), ('order_id.session_id.state', '!=', 'closed'), ('product_id.pos_categ_ids.name', '!=', 'Drinks')] - {'search_default_in_progress': 1, 'search_default_group_status': 1} + {'search_default_in_progress': 1, 'search_default_group_status': 1, 'search_default_today': 1}

    Welcome to the Kitchen!