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 0000000..187a841 Binary files /dev/null and b/addons/dine360_online_orders/static/description/icon.png differ 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. +
+
+
+
+
+
+ +
+
+ + + + + + + +