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': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
'dine360_kds/static/src/css/kds_style.css',
|
'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,
|
'installable': True,
|
||||||
'application': True,
|
'application': True,
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
from . import pos_order_line
|
from . import pos_order_line
|
||||||
from . import product
|
from . import product
|
||||||
|
from . import pos_session
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class PosOrderLine(models.Model):
|
class PosOrderLine(models.Model):
|
||||||
_inherit = 'pos.order.line'
|
_inherit = 'pos.order.line'
|
||||||
@ -28,19 +31,73 @@ class PosOrderLine(models.Model):
|
|||||||
else:
|
else:
|
||||||
line.cooking_time = 0
|
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):
|
def action_start_preparing(self):
|
||||||
self.write({
|
self.write({
|
||||||
'preparation_status': 'preparing',
|
'preparation_status': 'preparing',
|
||||||
'preparation_time_start': fields.Datetime.now()
|
'preparation_time_start': fields.Datetime.now()
|
||||||
})
|
})
|
||||||
|
self._notify_pos()
|
||||||
|
|
||||||
def action_mark_ready(self):
|
def action_mark_ready(self):
|
||||||
self.write({
|
self.write({
|
||||||
'preparation_status': 'ready',
|
'preparation_status': 'ready',
|
||||||
'preparation_time_end': fields.Datetime.now()
|
'preparation_time_end': fields.Datetime.now()
|
||||||
})
|
})
|
||||||
|
self._notify_pos()
|
||||||
|
|
||||||
def action_mark_served(self):
|
def action_mark_served(self):
|
||||||
self.write({
|
self.write({
|
||||||
'preparation_status': 'served'
|
'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="name">pos.order.line.kds.kanban</field>
|
||||||
<field name="model">pos.order.line</field>
|
<field name="model">pos.order.line</field>
|
||||||
<field name="arch" type="xml">
|
<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="preparation_status"/>
|
||||||
<field name="color"/>
|
<field name="color"/>
|
||||||
<field name="product_id"/>
|
<field name="product_id"/>
|
||||||
@ -123,9 +123,9 @@
|
|||||||
<field name="name">Kitchen Display System</field>
|
<field name="name">Kitchen Display System</field>
|
||||||
<field name="res_model">pos.order.line</field>
|
<field name="res_model">pos.order.line</field>
|
||||||
<field name="view_mode">kanban,tree,form</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="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">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Welcome to the Kitchen!
|
Welcome to the Kitchen!
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user