Implement Kitchen Display System (KDS) with real-time order line status updates for POS and a backend Kanban view.

This commit is contained in:
Alaguraj0361 2026-02-10 11:09:39 +05:30
parent 4db79c5f79
commit f87c69b3aa
9 changed files with 318 additions and 3 deletions

View File

@ -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,

View File

@ -1,2 +1,3 @@
from . import pos_order_line
from . import product
from . import pos_session

View File

@ -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()

View 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

View 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;
}

View 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)");

View 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!");

View 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>

View File

@ -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!