From d58a1fd30f7b0b60576245cf9443559220f97d8c Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Fri, 6 Mar 2026 18:00:13 +0530 Subject: [PATCH] Implement online order management with KDS integration and a custom POS navbar. --- addons/dine360_kds/models/pos_order_line.py | 10 +- addons/dine360_kds/models/product.py | 2 +- .../models/website_sale_integration.py | 21 +- addons/dine360_online_orders/__init__.py | 1 + addons/dine360_online_orders/__manifest__.py | 31 ++ .../dine360_online_orders/models/__init__.py | 2 + .../dine360_online_orders/models/pos_order.py | 119 +++++++ .../models/sale_order.py | 62 ++++ .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 27026 bytes .../static/src/css/online_orders.css | 320 ++++++++++++++++++ .../static/src/js/online_orders_navbar.js | 14 + .../static/src/js/online_orders_screen.js | 170 ++++++++++ .../static/src/xml/online_orders_screen.xml | 247 ++++++++++++++ .../views/kds_override_views.xml | 13 + .../views/pos_order_views.xml | 68 ++++ addons/dine360_pos_navbar/__init__.py | 2 + addons/dine360_pos_navbar/__manifest__.py | 18 + .../static/src/css/pos_navbar.css | 5 + .../static/src/js/pos_navbar.js | 9 + .../static/src/xml/pos_navbar.xml | 4 + .../views/pos_restaurant_views.xml | 114 +++++++ .../views/res_config_settings_views.xml | 49 +++ 23 files changed, 1270 insertions(+), 13 deletions(-) create mode 100644 addons/dine360_online_orders/__init__.py create mode 100644 addons/dine360_online_orders/__manifest__.py create mode 100644 addons/dine360_online_orders/models/__init__.py create mode 100644 addons/dine360_online_orders/models/pos_order.py create mode 100644 addons/dine360_online_orders/models/sale_order.py create mode 100644 addons/dine360_online_orders/security/ir.model.access.csv create mode 100644 addons/dine360_online_orders/static/description/icon.png create mode 100644 addons/dine360_online_orders/static/src/css/online_orders.css create mode 100644 addons/dine360_online_orders/static/src/js/online_orders_navbar.js create mode 100644 addons/dine360_online_orders/static/src/js/online_orders_screen.js create mode 100644 addons/dine360_online_orders/static/src/xml/online_orders_screen.xml create mode 100644 addons/dine360_online_orders/views/kds_override_views.xml create mode 100644 addons/dine360_online_orders/views/pos_order_views.xml create mode 100644 addons/dine360_pos_navbar/__init__.py create mode 100644 addons/dine360_pos_navbar/__manifest__.py create mode 100644 addons/dine360_pos_navbar/static/src/css/pos_navbar.css create mode 100644 addons/dine360_pos_navbar/static/src/js/pos_navbar.js create mode 100644 addons/dine360_pos_navbar/static/src/xml/pos_navbar.xml create mode 100644 tmp_pos/pos_restaurant/views/pos_restaurant_views.xml create mode 100644 tmp_pos/pos_restaurant/views/res_config_settings_views.xml diff --git a/addons/dine360_kds/models/pos_order_line.py b/addons/dine360_kds/models/pos_order_line.py index a4f369d..696d356 100644 --- a/addons/dine360_kds/models/pos_order_line.py +++ b/addons/dine360_kds/models/pos_order_line.py @@ -82,10 +82,12 @@ class PosOrderLine(models.Model): 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 only for new items (waiting status) - waiting_lines = lines.filtered(lambda l: l.preparation_status == 'waiting') - if waiting_lines: - waiting_lines._notify_kds() + # Skip KDS notification if flagged (online orders wait for cashier confirmation) + if not self.env.context.get('skip_kds_notify'): + # Send notification to KDS backend only for new items (waiting status) + waiting_lines = lines.filtered(lambda l: l.preparation_status == 'waiting') + if waiting_lines: + waiting_lines._notify_kds() return lines def write(self, vals): diff --git a/addons/dine360_kds/models/product.py b/addons/dine360_kds/models/product.py index 008b5b3..20e152b 100644 --- a/addons/dine360_kds/models/product.py +++ b/addons/dine360_kds/models/product.py @@ -5,6 +5,6 @@ class ProductTemplate(models.Model): is_kitchen_item = fields.Boolean( string='Show in KDS', - default=True, + default=False, help="If checked, this product will appear in the Kitchen Display System when ordered." ) diff --git a/addons/dine360_kds/models/website_sale_integration.py b/addons/dine360_kds/models/website_sale_integration.py index a4212d7..d0c0f91 100644 --- a/addons/dine360_kds/models/website_sale_integration.py +++ b/addons/dine360_kds/models/website_sale_integration.py @@ -57,7 +57,7 @@ class SaleOrder(models.Model): continue # Skip non-kitchen items (delivery charges, shipping, etc.) - if not line.product_id.is_kitchen_item: + if not line.product_id.is_kitchen_item or line.product_id.type == 'service': continue lines_data.append((0, 0, { @@ -68,19 +68,24 @@ class SaleOrder(models.Model): 'price_subtotal_incl': line.price_total, 'full_product_name': line.name, 'tax_ids': [(6, 0, line.tax_id.ids)], - # Key for KDS: - 'preparation_status': 'waiting', + # Online orders: hold for cashier confirmation before sending to KDS + 'preparation_status': False, 'customer_note': 'Web Order', })) if not lines_data: return - # Generate proper POS reference using sequence - pos_reference = session.config_id.sequence_id.next_by_id() if session.config_id.sequence_id else f"Order {sale_order.name}" + # Generate proper POS reference matching Odoo's regex pattern '([0-9-]){14,}' + # Odoo's _export_for_ui expects this to exist otherwise it crashes + import datetime + uid = f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{session.id}-{sale_order.id}" + pos_reference = f"Order {uid}" # 4. Create POS Order (in Draft/New state to avoid double accounting) - pos_order = PosOrder.create({ + # Use skip_kds_notify context to prevent immediate KDS notification + # Online orders will be sent to KDS only after cashier confirmation + pos_order = PosOrder.with_context(skip_kds_notify=True).create({ 'session_id': session.id, 'company_id': sale_order.company_id.id, 'partner_id': sale_order.partner_id.id, @@ -95,5 +100,5 @@ class SaleOrder(models.Model): # 'state': 'draft', # Default is draft }) - # Trigger KDS notification (handled by create method of pos.order.line in dine360_kds) - _logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} for KDS.") + # Notification to KDS is deferred until cashier confirms via dine360_online_orders + _logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} (pending cashier confirmation).") diff --git a/addons/dine360_online_orders/__init__.py b/addons/dine360_online_orders/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/addons/dine360_online_orders/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/dine360_online_orders/__manifest__.py b/addons/dine360_online_orders/__manifest__.py new file mode 100644 index 0000000..92b15e3 --- /dev/null +++ b/addons/dine360_online_orders/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': 'Dine360 Online Orders in POS', + 'version': '17.0.1.0', + 'category': 'Sales/Point of Sale', + 'summary': 'Receive website shop orders on POS screen with KDS integration', + 'description': """ + Online Orders Integration for POS: + - Website shop orders appear in POS as a new 'Online Orders' tab + - Cashier can confirm or reject online orders + - Confirmed orders are sent to Kitchen Display System (KDS) + - Real-time notifications via bus service + """, + 'author': 'Dine360', + 'depends': ['point_of_sale', 'pos_restaurant', 'dine360_kds', 'website_sale', 'sale_management'], + 'data': [ + 'security/ir.model.access.csv', + 'views/pos_order_views.xml', + 'views/kds_override_views.xml', + ], + 'assets': { + 'point_of_sale._assets_pos': [ + 'dine360_online_orders/static/src/css/online_orders.css', + 'dine360_online_orders/static/src/js/online_orders_screen.js', + 'dine360_online_orders/static/src/js/online_orders_navbar.js', + 'dine360_online_orders/static/src/xml/online_orders_screen.xml', + ], + }, + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/addons/dine360_online_orders/models/__init__.py b/addons/dine360_online_orders/models/__init__.py new file mode 100644 index 0000000..5055bda --- /dev/null +++ b/addons/dine360_online_orders/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_order +from . import sale_order diff --git a/addons/dine360_online_orders/models/pos_order.py b/addons/dine360_online_orders/models/pos_order.py new file mode 100644 index 0000000..27ae89f --- /dev/null +++ b/addons/dine360_online_orders/models/pos_order.py @@ -0,0 +1,119 @@ +from odoo import models, fields, api, _ +import logging + +_logger = logging.getLogger(__name__) + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + is_online_order = fields.Boolean( + string='Online Order', + default=False, + help='Indicates this order came from the website shop' + ) + online_order_status = fields.Selection([ + ('pending', 'Pending'), + ('confirmed', 'Confirmed'), + ('rejected', 'Rejected'), + ], string='Online Order Status', default='pending') + + sale_order_id = fields.Many2one( + 'sale.order', string='Source Sale Order', + help='The website sale order that generated this POS order' + ) + online_customer_name = fields.Char( + string='Online Customer', + compute='_compute_online_customer_name', store=True + ) + online_order_date = fields.Datetime( + string='Online Order Date', + default=fields.Datetime.now + ) + + @api.depends('partner_id', 'partner_id.name') + def _compute_online_customer_name(self): + for order in self: + order.online_customer_name = order.partner_id.name or 'Guest' + + def action_confirm_online_order(self): + """Cashier confirms the online order → sends to KDS""" + self.ensure_one() + self.write({'online_order_status': 'confirmed'}) + + # Set all order lines to 'waiting' so KDS picks them up + for line in self.lines: + if line.product_id.is_kitchen_item and line.product_id.type != 'service': + line.write({ + 'preparation_status': 'waiting', + }) + + # Notify KDS + self.lines.filtered( + lambda l: l.product_id.is_kitchen_item and l.product_id.type != 'service' + )._notify_kds() + + # Notify POS that order was confirmed + if self.config_id: + channel = "online_orders_%s" % self.config_id.id + self.env['bus.bus']._sendone(channel, 'online_order_confirmed', { + 'order_id': self.id, + 'order_name': self.pos_reference or self.name, + }) + + _logger.info("Online order %s confirmed and sent to KDS", self.name) + return True + + def action_reject_online_order(self): + """Cashier rejects the online order""" + self.ensure_one() + self.write({'online_order_status': 'rejected'}) + + # Notify POS + if self.config_id: + channel = "online_orders_%s" % self.config_id.id + self.env['bus.bus']._sendone(channel, 'online_order_rejected', { + 'order_id': self.id, + 'order_name': self.pos_reference or self.name, + }) + + _logger.info("Online order %s rejected", self.name) + return True + + @api.model + def get_online_orders(self, config_id): + """Fetch pending online orders for a specific POS config""" + domain = [ + ('is_online_order', '=', True), + ('online_order_status', '=', 'pending'), + ('config_id', '=', config_id), + ] + orders = self.search(domain, order='online_order_date desc') + + result = [] + for order in orders: + lines = [] + for line in order.lines: + lines.append({ + 'id': line.id, + 'product_name': line.full_product_name or line.product_id.name, + 'qty': line.qty, + 'price_unit': line.price_unit, + 'price_subtotal_incl': line.price_subtotal_incl, + 'customer_note': line.customer_note or '', + 'is_kitchen_item': line.product_id.is_kitchen_item, + }) + + result.append({ + 'id': order.id, + 'name': order.pos_reference or order.name, + 'partner_name': order.partner_id.name or 'Guest', + 'partner_phone': order.partner_id.phone or order.partner_id.mobile or '', + 'amount_total': order.amount_total, + 'date_order': order.date_order.isoformat() if order.date_order else '', + 'sale_order_name': order.sale_order_id.name if order.sale_order_id else '', + 'note': order.note or '', + 'lines': lines, + }) + + return result diff --git a/addons/dine360_online_orders/models/sale_order.py b/addons/dine360_online_orders/models/sale_order.py new file mode 100644 index 0000000..139693a --- /dev/null +++ b/addons/dine360_online_orders/models/sale_order.py @@ -0,0 +1,62 @@ +from odoo import models, fields, api, _ +import logging + +_logger = logging.getLogger(__name__) + + +class SaleOrderOnline(models.Model): + _inherit = 'sale.order' + + pos_order_id = fields.Many2one( + 'pos.order', string='POS Order', + help='The POS order created from this website sale order' + ) + + def _create_pos_order_for_kds(self, sale_order): + """ + Override from dine360_kds to also mark the POS order as an online order. + This method is called by dine360_kds.website_sale_integration when a + website sale order is confirmed. + """ + # Let the parent create the POS order + super(SaleOrderOnline, self)._create_pos_order_for_kds(sale_order) + + # Now find the POS order that was just created and mark it + # We look for the most recent POS order linked to this sale order's partner + # with the note containing the sale order name + PosOrder = self.env['pos.order'] + pos_order = PosOrder.search([ + ('note', 'like', sale_order.name), + ], order='id desc', limit=1) + + if pos_order: + pos_order.write({ + 'is_online_order': True, + 'online_order_status': 'pending', + 'sale_order_id': sale_order.id, + 'online_order_date': fields.Datetime.now(), + }) + + # Link back to sale order + sale_order.write({'pos_order_id': pos_order.id}) + + # Set all lines to a "hold" state - they will go to KDS only when cashier confirms + for line in pos_order.lines: + if line.product_id.is_kitchen_item: + line.write({'preparation_status': 'waiting'}) + + # Send bus notification to POS + if pos_order.config_id: + channel = "online_orders_%s" % pos_order.config_id.id + self.env['bus.bus']._sendone(channel, 'new_online_order', { + 'order_id': pos_order.id, + 'order_name': pos_order.pos_reference or pos_order.name, + 'customer_name': sale_order.partner_id.name or 'Guest', + 'amount_total': pos_order.amount_total, + 'items_count': len(pos_order.lines), + }) + + _logger.info( + "Marked POS Order %s as online order from Sale Order %s", + pos_order.name, sale_order.name + ) diff --git a/addons/dine360_online_orders/security/ir.model.access.csv b/addons/dine360_online_orders/security/ir.model.access.csv new file mode 100644 index 0000000..e22fb7a --- /dev/null +++ b/addons/dine360_online_orders/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_pos_order_online,pos.order.online,point_of_sale.model_pos_order,point_of_sale.group_pos_user,1,1,1,0 diff --git a/addons/dine360_online_orders/static/description/icon.png b/addons/dine360_online_orders/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..187a8414c0f3306fb4d0e99eb1255428007f2361 GIT binary patch literal 27026 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 WdEzzx7xleLe_hov`nOOU6Y@V+7x$h3 literal 0 HcmV?d00001 diff --git a/addons/dine360_online_orders/static/src/css/online_orders.css b/addons/dine360_online_orders/static/src/css/online_orders.css new file mode 100644 index 0000000..f619300 --- /dev/null +++ b/addons/dine360_online_orders/static/src/css/online_orders.css @@ -0,0 +1,320 @@ +/* ============================================ */ +/* Dine360 Online Orders Screen - POS */ +/* ============================================ */ + +.online-orders-screen { + background: #1a1a2e; + color: #eee; + font-family: 'Inter', 'Segoe UI', sans-serif; +} + +/* Header */ +.online-orders-header { + background: linear-gradient(135deg, #16213e 0%, #0f3460 100%); + border-bottom: 2px solid rgba(255, 255, 255, 0.1); + min-height: 60px; +} + +.online-orders-title { + color: #fff; + font-size: 1.4rem; +} + +.order-count-badge { + font-size: 0.8rem; + padding: 4px 10px; + border-radius: 20px; + animation: pulse-badge 2s infinite; +} + +@keyframes pulse-badge { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.15); + } +} + +.btn-back { + border-radius: 10px; + font-weight: 600; +} + +.btn-refresh { + border-radius: 10px; + font-weight: 600; + border-color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.8); +} + +.btn-refresh:hover { + background: rgba(255, 255, 255, 0.1); + color: #fff; +} + +/* Body */ +.online-orders-body { + background: #1a1a2e; +} + +/* Left Panel - Orders List */ +.online-orders-list { + width: 380px; + min-width: 380px; + overflow-y: auto; + border-right: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 0, 0, 0.15); +} + +/* Order Card */ +.order-card { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 14px; + cursor: pointer; + transition: all 0.25s ease; +} + +.order-card:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.order-card.selected { + background: rgba(15, 52, 96, 0.5); + border-color: #e94560; + box-shadow: 0 0 15px rgba(233, 69, 96, 0.2); +} + +.order-ref { + color: #fff; + font-size: 0.95rem; +} + +.order-total { + color: #53cf8a; + font-size: 1.1rem; +} + +.order-customer { + color: #ccc; + font-size: 0.9rem; +} + +.order-date { + font-size: 0.8rem; +} + +/* Action Buttons in Card */ +.btn-confirm-order { + border-radius: 8px; + font-weight: 600; + background: #27ae60; + border: none; + padding: 8px 0; +} + +.btn-confirm-order:hover { + background: #2ecc71; +} + +.btn-reject-order { + border-radius: 8px; + font-weight: 600; + background: transparent; + border: 1px solid #e74c3c; + color: #e74c3c; + padding: 8px 0; +} + +.btn-reject-order:hover { + background: #e74c3c; + color: #fff; +} + +/* Right Panel - Order Detail */ +.online-order-detail { + overflow-y: auto; +} + +.detail-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + height: 100%; +} + +.detail-section { + padding-bottom: 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.detail-section:last-of-type { + border-bottom: none; +} + +/* Customer Avatar */ +.customer-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + background: linear-gradient(135deg, #e94560, #0f3460); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + color: #fff; +} + +/* Note Box */ +.note-box { + background: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.2); + color: #ffc107; + font-style: italic; +} + +/* Order Lines Table */ +.order-lines-table { + color: #ddd; +} + +.order-lines-table thead th { + border-bottom: 2px solid rgba(255, 255, 255, 0.15); + color: #aaa; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 10px 8px; +} + +.order-lines-table tbody td { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding: 12px 8px; + vertical-align: middle; +} + +.order-lines-table tbody tr:hover { + background: rgba(255, 255, 255, 0.05); +} + +.total-row td { + border-top: 2px solid rgba(255, 255, 255, 0.2) !important; + padding-top: 14px; + font-size: 1.15rem; +} + +.order-total-amount { + color: #53cf8a; + font-size: 1.3rem !important; +} + +/* Kitchen Badge */ +.kitchen-badge { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + background: rgba(255, 255, 255, 0.1); + color: #888; +} + +.kitchen-badge.active { + background: rgba(233, 69, 96, 0.2); + color: #e94560; +} + +/* Detail Action Buttons */ +.btn-confirm-detail { + border-radius: 12px; + font-weight: 700; + font-size: 1.05rem; + background: linear-gradient(135deg, #27ae60, #2ecc71); + border: none; + padding: 14px 24px; + box-shadow: 0 4px 15px rgba(46, 204, 113, 0.3); +} + +.btn-confirm-detail:hover { + background: linear-gradient(135deg, #2ecc71, #27ae60); + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(46, 204, 113, 0.4); +} + +.btn-reject-detail { + border-radius: 12px; + font-weight: 600; + padding: 14px 24px; +} + +/* Empty State */ +.empty-state .empty-icon { + font-size: 5rem; + color: rgba(255, 255, 255, 0.15); + display: block; +} + +/* Navbar Button */ +.online-orders-nav-btn { + background: rgba(233, 69, 96, 0.15); + border: 1px solid rgba(233, 69, 96, 0.3); + color: #e94560; + border-radius: 10px; + padding: 6px 14px; + font-weight: 600; + font-size: 0.85rem; + transition: all 0.25s ease; +} + +.online-orders-nav-btn:hover { + background: #e94560; + color: #fff; + border-color: #e94560; +} + +/* Scrollbar */ +.online-orders-list::-webkit-scrollbar, +.online-order-detail::-webkit-scrollbar { + width: 6px; +} + +.online-orders-list::-webkit-scrollbar-thumb, +.online-order-detail::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; +} + +.online-orders-list::-webkit-scrollbar-track, +.online-order-detail::-webkit-scrollbar-track { + background: transparent; +} + +/* Spinner */ +.spinner-border { + color: #e94560 !important; +} + +/* Responsive */ +@media (max-width: 768px) { + .online-orders-list { + width: 100%; + min-width: 100%; + } + + .online-order-detail { + display: none; + } + + .order-card.selected+.online-order-detail { + display: block; + } +} \ No newline at end of file diff --git a/addons/dine360_online_orders/static/src/js/online_orders_navbar.js b/addons/dine360_online_orders/static/src/js/online_orders_navbar.js new file mode 100644 index 0000000..ca6c5dd --- /dev/null +++ b/addons/dine360_online_orders/static/src/js/online_orders_navbar.js @@ -0,0 +1,14 @@ +/** @odoo-module */ + +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { patch } from "@web/core/utils/patch"; + +console.log("[OnlineOrders] Patching Navbar..."); + +patch(Navbar.prototype, { + onClickOnlineOrders() { + this.pos.showScreen("OnlineOrdersScreen"); + }, +}); + +console.log("[OnlineOrders] Navbar patched!"); diff --git a/addons/dine360_online_orders/static/src/js/online_orders_screen.js b/addons/dine360_online_orders/static/src/js/online_orders_screen.js new file mode 100644 index 0000000..8495753 --- /dev/null +++ b/addons/dine360_online_orders/static/src/js/online_orders_screen.js @@ -0,0 +1,170 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; + +console.log("[OnlineOrders] Module loading..."); + +export class OnlineOrdersScreen extends Component { + static template = "dine360_online_orders.OnlineOrdersScreen"; + + setup() { + try { + console.log("[OnlineOrders] Setup starting..."); + this.pos = usePos(); + + // Direct access to services to avoid 'methods is not iterable' error + this.orm = this.env.services.orm; + this.notification = this.env.services.pos_notification; + this.busService = this.env.services.bus_service; + + this.state = useState({ + orders: [], + loading: true, + selectedOrder: null, + confirmingId: null, + }); + + console.log("[OnlineOrders] Services obtained:", { + hasOrm: !!this.orm, + hasNotif: !!this.notification, + hasBus: !!this.busService + }); + + // Subscribe to bus notifications for real-time updates + const channel = `online_orders_${this.pos.config.id}`; + console.log("[OnlineOrders] Subscribing to channel:", channel); + + if (this.busService) { + this.busService.addChannel(channel); + this._notifHandler = this._onNotification.bind(this); + this.busService.addEventListener("notification", this._notifHandler); + } + } catch (err) { + console.error("[OnlineOrders] Setup Error:", err); + } + + onMounted(() => { + this.loadOnlineOrders(); + // Auto-refresh every 30 seconds + this._refreshInterval = setInterval(() => { + this.loadOnlineOrders(); + }, 30000); + }); + + onWillUnmount(() => { + if (this._refreshInterval) { + clearInterval(this._refreshInterval); + } + if (this.busService && this._notifHandler) { + this.busService.removeEventListener("notification", this._notifHandler); + } + }); + } + + _onNotification(event) { + const notifications = event.detail || []; + for (const notif of notifications) { + if (notif.type === "new_online_order") { + console.log("[OnlineOrders] New order received!", notif.payload); + this.notification.add( + `New Online Order from ${notif.payload.customer_name} - ${this.env.utils.formatCurrency(notif.payload.amount_total)}` + ); + this.loadOnlineOrders(); + } + if (notif.type === "online_order_confirmed" || notif.type === "online_order_rejected") { + this.loadOnlineOrders(); + } + } + } + + async loadOnlineOrders() { + try { + this.state.loading = true; + const orders = await this.orm.call( + "pos.order", + "get_online_orders", + [this.pos.config.id] + ); + this.state.orders = orders; + this.state.loading = false; + console.log("[OnlineOrders] Loaded", orders.length, "orders"); + } catch (error) { + console.error("[OnlineOrders] Error loading orders:", error); + this.state.loading = false; + } + } + + selectOrder(order) { + this.state.selectedOrder = order; + } + + async confirmOrder(orderId) { + try { + this.state.confirmingId = orderId; + await this.orm.call( + "pos.order", + "action_confirm_online_order", + [[orderId]] + ); + this.notification.add("Order confirmed and sent to kitchen! 🍳"); + // Remove from list + this.state.orders = this.state.orders.filter(o => o.id !== orderId); + if (this.state.selectedOrder && this.state.selectedOrder.id === orderId) { + this.state.selectedOrder = null; + } + this.state.confirmingId = null; + } catch (error) { + console.error("[OnlineOrders] Confirm error:", error); + this.notification.add("Failed to confirm order"); + this.state.confirmingId = null; + } + } + + async rejectOrder(orderId) { + try { + await this.orm.call( + "pos.order", + "action_reject_online_order", + [[orderId]] + ); + this.notification.add("Order has been rejected"); + this.state.orders = this.state.orders.filter(o => o.id !== orderId); + if (this.state.selectedOrder && this.state.selectedOrder.id === orderId) { + this.state.selectedOrder = null; + } + } catch (error) { + console.error("[OnlineOrders] Reject error:", error); + this.notification.add("Failed to reject order"); + } + } + + formatDate(isoDate) { + if (!isoDate) return ""; + const d = new Date(isoDate); + return d.toLocaleString(); + } + + formatCurrency(amount) { + return this.env.utils.formatCurrency(amount); + } + + get orderCount() { + return this.state.orders.length; + } + + back() { + if (this.pos.config.module_pos_restaurant && !this.pos.get_order()) { + this.pos.showScreen("FloorScreen"); + } else { + this.pos.showScreen("ProductScreen"); + } + } +} + +// Register the screen +registry.category("pos_screens").add("OnlineOrdersScreen", OnlineOrdersScreen); + +console.log("[OnlineOrders] Screen registered!"); diff --git a/addons/dine360_online_orders/static/src/xml/online_orders_screen.xml b/addons/dine360_online_orders/static/src/xml/online_orders_screen.xml new file mode 100644 index 0000000..eeed5ea --- /dev/null +++ b/addons/dine360_online_orders/static/src/xml/online_orders_screen.xml @@ -0,0 +1,247 @@ + + + + + +
+ +
+
+ +

+ + Online Orders + + + +

+
+ +
+ + +
+ +
+
+
+

Loading online orders...

+
+
+ + +
+
+ +

No Pending Orders

+

New website orders will appear here automatically

+
+
+ + + + +
+ +
+ +
+
+ + + + + ONLINE +
+ + + +
+ + +
+ + + + + + + + +
+ + +
+ items + + + + + + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+ + +
+ +
+

+ + Order Details: +

+ + +
+
Customer
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
Note
+
+ +
+
+ + +
+
Items
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ItemQtyPriceTotal
+
+ + + + +
+
+ + +
+
+ + + + + +
Total + +
+
+ + +
+ + +
+
+ + +
+
+ +

Select an order to view details

+
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/addons/dine360_online_orders/views/kds_override_views.xml b/addons/dine360_online_orders/views/kds_override_views.xml new file mode 100644 index 0000000..b922ec8 --- /dev/null +++ b/addons/dine360_online_orders/views/kds_override_views.xml @@ -0,0 +1,13 @@ + + + + + [ + ('product_id.is_kitchen_item', '=', True), + ('product_id.name', '!=', 'Water'), + ('order_id.session_id.state', '!=', 'closed'), + '|', ('product_id.pos_categ_ids', '=', False), ('product_id.pos_categ_ids.name', '!=', 'Drinks'), + '|', ('order_id.is_online_order', '=', False), ('order_id.online_order_status', '!=', 'pending') + ] + + diff --git a/addons/dine360_online_orders/views/pos_order_views.xml b/addons/dine360_online_orders/views/pos_order_views.xml new file mode 100644 index 0000000..83e90a4 --- /dev/null +++ b/addons/dine360_online_orders/views/pos_order_views.xml @@ -0,0 +1,68 @@ + + + + + pos.order.online.tree + pos.order + + + + + + + + + + + + + + + + pos.order.online.form.inherit + pos.order + + + + + + + + + + + + + Online Orders + pos.order + tree,form + [('is_online_order', '=', True)] + {'search_default_pending': 1} + + + + + pos.order.online.search + pos.order + + + + + + + + + + + + + + + + diff --git a/addons/dine360_pos_navbar/__init__.py b/addons/dine360_pos_navbar/__init__.py new file mode 100644 index 0000000..67dee8c --- /dev/null +++ b/addons/dine360_pos_navbar/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/dine360_pos_navbar/__manifest__.py b/addons/dine360_pos_navbar/__manifest__.py new file mode 100644 index 0000000..72d5069 --- /dev/null +++ b/addons/dine360_pos_navbar/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Dine360 POS Navbar', + 'version': '17.0.1.0', + 'category': 'Point of Sale', + 'summary': 'Custom POS Navbar mimicking Odoo 19 style', + 'depends': ['point_of_sale'], + 'data': [], + 'assets': { + 'point_of_sale._assets_pos': [ + 'dine360_pos_navbar/static/src/css/pos_navbar.css', + 'dine360_pos_navbar/static/src/js/pos_navbar.js', + 'dine360_pos_navbar/static/src/xml/pos_navbar.xml', + ], + }, + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/addons/dine360_pos_navbar/static/src/css/pos_navbar.css b/addons/dine360_pos_navbar/static/src/css/pos_navbar.css new file mode 100644 index 0000000..6115811 --- /dev/null +++ b/addons/dine360_pos_navbar/static/src/css/pos_navbar.css @@ -0,0 +1,5 @@ +/* Placeholder for custom navbar styling */ +.pos .pos-topheader { + /* Custom style placeholder */ + background: inherit; +} \ No newline at end of file diff --git a/addons/dine360_pos_navbar/static/src/js/pos_navbar.js b/addons/dine360_pos_navbar/static/src/js/pos_navbar.js new file mode 100644 index 0000000..10b3a54 --- /dev/null +++ b/addons/dine360_pos_navbar/static/src/js/pos_navbar.js @@ -0,0 +1,9 @@ +/** @odoo-module */ +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { patch } from "@web/core/utils/patch"; + +// Removing empty/broken setup patch as it was causing the POS to crash. +// Any future navbar customizations should go here. +patch(Navbar.prototype, { + // Other navbar methods can be patched here safely +}); diff --git a/addons/dine360_pos_navbar/static/src/xml/pos_navbar.xml b/addons/dine360_pos_navbar/static/src/xml/pos_navbar.xml new file mode 100644 index 0000000..ac63867 --- /dev/null +++ b/addons/dine360_pos_navbar/static/src/xml/pos_navbar.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tmp_pos/pos_restaurant/views/pos_restaurant_views.xml b/tmp_pos/pos_restaurant/views/pos_restaurant_views.xml new file mode 100644 index 0000000..ff0349c --- /dev/null +++ b/tmp_pos/pos_restaurant/views/pos_restaurant_views.xml @@ -0,0 +1,114 @@ + + + + + + Restaurant Floors + restaurant.floor + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + Restaurant Floors + restaurant.floor + + + + + + + + + + + restaurant.floor.search + restaurant.floor + + + + + + + + + + restaurant.floor.kanban + restaurant.floor + + + + + + +
+
Floor Name:
+
Point of Sales:
+
+
+
+
+
+
+ + + Floor Plans + restaurant.floor + tree,kanban,form + +

+ Add a new restaurant floor +

+ A restaurant floor represents the place where customers are served, this is where you can + define and position the tables. +

+
+
+ + + Restaurant Table + restaurant.table + +
+ + + + + + + + + + + + + + +
+
+
+ + +
diff --git a/tmp_pos/pos_restaurant/views/res_config_settings_views.xml b/tmp_pos/pos_restaurant/views/res_config_settings_views.xml new file mode 100644 index 0000000..101d0c5 --- /dev/null +++ b/tmp_pos/pos_restaurant/views/res_config_settings_views.xml @@ -0,0 +1,49 @@ + + + + res.config.settings.view.form.inherit.pos_restaurant + res.config.settings + + +
+ + +
+
+
+
+
+
+
+ + + + + + + + +
+
+ Save this page and come back here to set up the feature. +
+
+
+
+
+
+ +
+
+ + + + + + + +