forked from alaguraj/odoo-testing-addons
Implement online order management with KDS integration and a custom POS navbar.
This commit is contained in:
parent
015f703026
commit
d58a1fd30f
@ -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):
|
||||
|
||||
@ -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."
|
||||
)
|
||||
|
||||
@ -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).")
|
||||
|
||||
1
addons/dine360_online_orders/__init__.py
Normal file
1
addons/dine360_online_orders/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
31
addons/dine360_online_orders/__manifest__.py
Normal file
31
addons/dine360_online_orders/__manifest__.py
Normal file
@ -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',
|
||||
}
|
||||
2
addons/dine360_online_orders/models/__init__.py
Normal file
2
addons/dine360_online_orders/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import pos_order
|
||||
from . import sale_order
|
||||
119
addons/dine360_online_orders/models/pos_order.py
Normal file
119
addons/dine360_online_orders/models/pos_order.py
Normal file
@ -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
|
||||
62
addons/dine360_online_orders/models/sale_order.py
Normal file
62
addons/dine360_online_orders/models/sale_order.py
Normal file
@ -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
|
||||
)
|
||||
@ -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
|
||||
|
BIN
addons/dine360_online_orders/static/description/icon.png
Normal file
BIN
addons/dine360_online_orders/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
320
addons/dine360_online_orders/static/src/css/online_orders.css
Normal file
320
addons/dine360_online_orders/static/src/css/online_orders.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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!");
|
||||
@ -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!");
|
||||
@ -0,0 +1,247 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<!-- Online Orders Screen -->
|
||||
<t t-name="dine360_online_orders.OnlineOrdersScreen" owl="1">
|
||||
<div class="online-orders-screen screen d-flex flex-column h-100">
|
||||
<!-- Header -->
|
||||
<div class="online-orders-header d-flex align-items-center justify-content-between px-4 py-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<button class="btn btn-light btn-back" t-on-click="back">
|
||||
<i class="fa fa-arrow-left me-2"/>Back
|
||||
</button>
|
||||
<h2 class="mb-0 fw-bold online-orders-title">
|
||||
<i class="fa fa-shopping-cart me-2"/>
|
||||
Online Orders
|
||||
<span class="badge bg-danger ms-2 order-count-badge" t-if="orderCount > 0">
|
||||
<t t-esc="orderCount"/>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-refresh" t-on-click="loadOnlineOrders">
|
||||
<i class="fa fa-refresh me-1"/>Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="online-orders-body flex-grow-1 d-flex overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<div t-if="state.loading" class="d-flex align-items-center justify-content-center w-100">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;"/>
|
||||
<p class="text-muted">Loading online orders...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div t-elif="state.orders.length === 0" class="d-flex align-items-center justify-content-center w-100">
|
||||
<div class="text-center empty-state">
|
||||
<i class="fa fa-inbox empty-icon mb-3"/>
|
||||
<h3 class="text-muted">No Pending Orders</h3>
|
||||
<p class="text-muted">New website orders will appear here automatically</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders List -->
|
||||
<t t-else="">
|
||||
<!-- Left Panel: Order Cards -->
|
||||
<div class="online-orders-list p-3">
|
||||
<t t-foreach="state.orders" t-as="order" t-key="order.id">
|
||||
<div t-attf-class="order-card mb-3 p-3 #{state.selectedOrder and state.selectedOrder.id === order.id ? 'selected' : ''}"
|
||||
t-on-click="() => this.selectOrder(order)">
|
||||
<!-- Order Header -->
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<span class="order-ref fw-bold">
|
||||
<i class="fa fa-receipt me-1"/>
|
||||
<t t-esc="order.name"/>
|
||||
</span>
|
||||
<span class="badge bg-info ms-2">ONLINE</span>
|
||||
</div>
|
||||
<span class="order-total fw-bold">
|
||||
<t t-esc="formatCurrency(order.amount_total)"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Customer Info -->
|
||||
<div class="order-customer mb-2">
|
||||
<i class="fa fa-user me-1 text-muted"/>
|
||||
<span t-esc="order.partner_name"/>
|
||||
<t t-if="order.partner_phone">
|
||||
<span class="ms-2 text-muted">
|
||||
<i class="fa fa-phone me-1"/>
|
||||
<t t-esc="order.partner_phone"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Items Summary -->
|
||||
<div class="order-items-summary text-muted small">
|
||||
<t t-esc="order.lines.length"/> items
|
||||
<t t-if="order.sale_order_name">
|
||||
<span class="ms-2">
|
||||
<i class="fa fa-link me-1"/>
|
||||
<t t-esc="order.sale_order_name"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="order-date text-muted small mt-1">
|
||||
<i class="fa fa-clock-o me-1"/>
|
||||
<t t-esc="formatDate(order.date_order)"/>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="order-actions d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success btn-sm flex-grow-1 btn-confirm-order"
|
||||
t-on-click.stop="() => this.confirmOrder(order.id)"
|
||||
t-att-disabled="state.confirmingId === order.id">
|
||||
<t t-if="state.confirmingId === order.id">
|
||||
<span class="spinner-border spinner-border-sm me-1"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-check me-1"/>
|
||||
</t>
|
||||
Confirm
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm flex-grow-1 btn-reject-order"
|
||||
t-on-click.stop="() => this.rejectOrder(order.id)">
|
||||
<i class="fa fa-times me-1"/>Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Order Detail -->
|
||||
<div class="online-order-detail flex-grow-1 p-3">
|
||||
<t t-if="state.selectedOrder">
|
||||
<div class="detail-card p-4">
|
||||
<h4 class="mb-3 fw-bold">
|
||||
<i class="fa fa-file-text me-2"/>
|
||||
Order Details: <t t-esc="state.selectedOrder.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Customer Detail -->
|
||||
<div class="detail-section mb-4">
|
||||
<h6 class="text-muted text-uppercase mb-2">Customer</h6>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="customer-avatar">
|
||||
<i class="fa fa-user"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold" t-esc="state.selectedOrder.partner_name"/>
|
||||
<div class="text-muted small" t-if="state.selectedOrder.partner_phone">
|
||||
<i class="fa fa-phone me-1"/>
|
||||
<t t-esc="state.selectedOrder.partner_phone"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note -->
|
||||
<div class="detail-section mb-4" t-if="state.selectedOrder.note">
|
||||
<h6 class="text-muted text-uppercase mb-2">Note</h6>
|
||||
<div class="note-box p-2 rounded">
|
||||
<t t-esc="state.selectedOrder.note"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Lines -->
|
||||
<div class="detail-section mb-4">
|
||||
<h6 class="text-muted text-uppercase mb-2">Items</h6>
|
||||
<table class="table order-lines-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th class="text-center">Qty</th>
|
||||
<th class="text-end">Price</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="state.selectedOrder.lines" t-as="line" t-key="line.id">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span t-attf-class="kitchen-badge #{line.is_kitchen_item ? 'active' : ''}">
|
||||
<i t-attf-class="fa #{line.is_kitchen_item ? 'fa-fire' : 'fa-circle-o'}"/>
|
||||
</span>
|
||||
<span t-esc="line.product_name"/>
|
||||
</div>
|
||||
<div class="text-muted small mt-1" t-if="line.customer_note">
|
||||
<i class="fa fa-comment-o me-1"/>
|
||||
<t t-esc="line.customer_note"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center fw-bold">
|
||||
<t t-esc="line.qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-esc="formatCurrency(line.price_unit)"/>
|
||||
</td>
|
||||
<td class="text-end fw-bold">
|
||||
<t t-esc="formatCurrency(line.price_subtotal_incl)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="3" class="text-end fw-bold">Total</td>
|
||||
<td class="text-end fw-bold order-total-amount">
|
||||
<t t-esc="formatCurrency(state.selectedOrder.amount_total)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Detail Actions -->
|
||||
<div class="detail-actions d-flex gap-3">
|
||||
<button class="btn btn-success btn-lg flex-grow-1 btn-confirm-detail"
|
||||
t-on-click="() => this.confirmOrder(state.selectedOrder.id)"
|
||||
t-att-disabled="state.confirmingId === state.selectedOrder.id">
|
||||
<t t-if="state.confirmingId === state.selectedOrder.id">
|
||||
<span class="spinner-border spinner-border-sm me-2"/>
|
||||
Confirming...
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-check-circle me-2"/>
|
||||
Confirm & Send to Kitchen
|
||||
</t>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-lg btn-reject-detail"
|
||||
t-on-click="() => this.rejectOrder(state.selectedOrder.id)">
|
||||
<i class="fa fa-times-circle me-1"/>Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="d-flex align-items-center justify-content-center h-100 text-center">
|
||||
<div>
|
||||
<i class="fa fa-hand-pointer-o" style="font-size: 4rem; color: #ccc;"/>
|
||||
<p class="mt-3 text-muted">Select an order to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Navbar button for Online Orders -->
|
||||
<t t-name="dine360_online_orders.NavbarButton" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('status-buttons')]" position="before">
|
||||
<button class="online-orders-nav-btn btn d-flex align-items-center gap-2 h-100 px-3 border-0"
|
||||
t-on-click="onClickOnlineOrders">
|
||||
<i class="fa fa-shopping-cart"/>
|
||||
<span t-if="!ui.isSmall">Online Orders</span>
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
13
addons/dine360_online_orders/views/kds_override_views.xml
Normal file
13
addons/dine360_online_orders/views/kds_override_views.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Override KDS Dashboard Action to exclude pending online orders -->
|
||||
<record id="dine360_kds.action_kds_dashboard" model="ir.actions.act_window">
|
||||
<field name="domain">[
|
||||
('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')
|
||||
]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
68
addons/dine360_online_orders/views/pos_order_views.xml
Normal file
68
addons/dine360_online_orders/views/pos_order_views.xml
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Tree View for Online Orders -->
|
||||
<record id="view_pos_order_online_tree" model="ir.ui.view">
|
||||
<field name="name">pos.order.online.tree</field>
|
||||
<field name="model">pos.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Online Orders" decoration-warning="online_order_status=='pending'" decoration-success="online_order_status=='confirmed'" decoration-danger="online_order_status=='rejected'">
|
||||
<field name="pos_reference"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date_order"/>
|
||||
<field name="amount_total"/>
|
||||
<field name="online_order_status" widget="badge"
|
||||
decoration-warning="online_order_status=='pending'"
|
||||
decoration-success="online_order_status=='confirmed'"
|
||||
decoration-danger="online_order_status=='rejected'"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="config_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View extension -->
|
||||
<record id="view_pos_order_online_form" model="ir.ui.view">
|
||||
<field name="name">pos.order.online.form.inherit</field>
|
||||
<field name="model">pos.order</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='state']" position="after">
|
||||
<field name="is_online_order" invisible="not is_online_order"/>
|
||||
<field name="online_order_status" invisible="not is_online_order" widget="badge"
|
||||
decoration-warning="online_order_status=='pending'"
|
||||
decoration-success="online_order_status=='confirmed'"
|
||||
decoration-danger="online_order_status=='rejected'"/>
|
||||
<field name="sale_order_id" invisible="not is_online_order"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Online Orders -->
|
||||
<record id="action_online_orders" model="ir.actions.act_window">
|
||||
<field name="name">Online Orders</field>
|
||||
<field name="res_model">pos.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('is_online_order', '=', True)]</field>
|
||||
<field name="context">{'search_default_pending': 1}</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_pos_order_online_search" model="ir.ui.view">
|
||||
<field name="name">pos.order.online.search</field>
|
||||
<field name="model">pos.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Online Orders">
|
||||
<field name="pos_reference"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
<filter name="pending" string="Pending" domain="[('online_order_status', '=', 'pending')]"/>
|
||||
<filter name="confirmed" string="Confirmed" domain="[('online_order_status', '=', 'confirmed')]"/>
|
||||
<filter name="rejected" string="Rejected" domain="[('online_order_status', '=', 'rejected')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_status" string="Status" context="{'group_by': 'online_order_status'}"/>
|
||||
<filter name="group_date" string="Date" context="{'group_by': 'date_order'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
addons/dine360_pos_navbar/__init__.py
Normal file
2
addons/dine360_pos_navbar/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
18
addons/dine360_pos_navbar/__manifest__.py
Normal file
18
addons/dine360_pos_navbar/__manifest__.py
Normal file
@ -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',
|
||||
}
|
||||
5
addons/dine360_pos_navbar/static/src/css/pos_navbar.css
Normal file
5
addons/dine360_pos_navbar/static/src/css/pos_navbar.css
Normal file
@ -0,0 +1,5 @@
|
||||
/* Placeholder for custom navbar styling */
|
||||
.pos .pos-topheader {
|
||||
/* Custom style placeholder */
|
||||
background: inherit;
|
||||
}
|
||||
9
addons/dine360_pos_navbar/static/src/js/pos_navbar.js
Normal file
9
addons/dine360_pos_navbar/static/src/js/pos_navbar.js
Normal file
@ -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
|
||||
});
|
||||
4
addons/dine360_pos_navbar/static/src/xml/pos_navbar.xml
Normal file
4
addons/dine360_pos_navbar/static/src/xml/pos_navbar.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<!-- Placeholder for custom navbar styling/elements -->
|
||||
</templates>
|
||||
114
tmp_pos/pos_restaurant/views/pos_restaurant_views.xml
Normal file
114
tmp_pos/pos_restaurant/views/pos_restaurant_views.xml
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<!-- RESTAURANTS FLOORS -->
|
||||
|
||||
<record id="view_restaurant_floor_form" model="ir.ui.view">
|
||||
<field name="name">Restaurant Floors</field>
|
||||
<field name="model">restaurant.floor</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Restaurant Floor">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<group col="4">
|
||||
<field name="name" />
|
||||
<field name="pos_config_ids" widget="many2many_tags"/>
|
||||
<field name="background_color" groups="base.group_no_one" />
|
||||
</group>
|
||||
<field name="table_ids">
|
||||
<tree string='Tables'>
|
||||
<field name="name" />
|
||||
<field name="seats" />
|
||||
<field name="shape" />
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_restaurant_floor_tree" model="ir.ui.view">
|
||||
<field name="name">Restaurant Floors</field>
|
||||
<field name="model">restaurant.floor</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Restaurant Floors">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name" />
|
||||
<field name="pos_config_ids" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_restaurant_floor_search" model="ir.ui.view">
|
||||
<field name="name">restaurant.floor.search</field>
|
||||
<field name="model">restaurant.floor</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<filter string="Archived" name="active" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_restaurant_floor_kanban" model="ir.ui.view">
|
||||
<field name="name">restaurant.floor.kanban</field>
|
||||
<field name="model">restaurant.floor</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="pos_config_ids" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div><strong>Floor Name: </strong><t t-esc="record.name.value"/></div>
|
||||
<div><strong>Point of Sales: </strong><t t-esc="record.pos_config_ids.value"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_restaurant_floor_form" model="ir.actions.act_window">
|
||||
<field name="name">Floor Plans</field>
|
||||
<field name="res_model">restaurant.floor</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add a new restaurant floor
|
||||
</p><p>
|
||||
A restaurant floor represents the place where customers are served, this is where you can
|
||||
define and position the tables.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_restaurant_table_form" model="ir.ui.view">
|
||||
<field name="name">Restaurant Table</field>
|
||||
<field name="model">restaurant.table</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Restaurant Table">
|
||||
<sheet>
|
||||
<group col="2">
|
||||
<field name="name" />
|
||||
<field name="seats" />
|
||||
</group>
|
||||
<group col="4" string="Appearance" groups="base.group_no_one">
|
||||
<field name="shape" />
|
||||
<field name="color" />
|
||||
<field name="position_h" />
|
||||
<field name="position_v" />
|
||||
<field name="width" />
|
||||
<field name="height" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_restaurant_floor_all"
|
||||
parent="point_of_sale.menu_point_config_product"
|
||||
action="action_restaurant_floor_form"
|
||||
sequence="10"
|
||||
groups="point_of_sale.group_pos_user"/>
|
||||
</odoo>
|
||||
49
tmp_pos/pos_restaurant/views/res_config_settings_views.xml
Normal file
49
tmp_pos/pos_restaurant/views/res_config_settings_views.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.pos_restaurant</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div id="warning_text_pos_restaurant" position="replace"/>
|
||||
<block id="restaurant_section" position="inside">
|
||||
<setting id="floor_and_table_map" string="Floors & Tables Map" help="Design floors and assign orders to tables" invisible="is_kiosk_mode or not pos_module_pos_restaurant">
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<label string="Floors" for="pos_floor_ids" class="o_light_label me-2"/>
|
||||
<field name="pos_floor_ids" widget="many2many_tags" readonly="pos_has_active_session" />
|
||||
</div>
|
||||
<div>
|
||||
<button name="%(pos_restaurant.action_restaurant_floor_form)d" icon="oi-arrow-right" type="action" string="Floors" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Early Receipt Printing" help="Allow to print receipt before payment" id="iface_printbill" invisible="not pos_module_pos_restaurant or is_kiosk_mode">
|
||||
<field name="pos_iface_printbill"/>
|
||||
</setting>
|
||||
<setting help="Split total or order lines" id="iface_splitbill" invisible="not pos_module_pos_restaurant or is_kiosk_mode">
|
||||
<field name="pos_iface_splitbill" string="Allow Bill Splitting"/>
|
||||
</setting>
|
||||
<setting help="Online reservation for restaurant" invisible="not pos_module_pos_restaurant or is_kiosk_mode">
|
||||
<field name="pos_module_pos_restaurant_appointment" string="Table Booking" widget="upgrade_boolean" />
|
||||
<div class="content-group" id="pos_table_booking" invisible="not pos_module_pos_restaurant_appointment">
|
||||
<div class="text-warning mt16 mb4">
|
||||
Save this page and come back here to set up the feature.
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
<div id="tip_product" position="after">
|
||||
<div invisible="not pos_module_pos_restaurant or not pos_iface_tipproduct">
|
||||
<field name="pos_set_tip_after_payment" class="oe_inline"/>
|
||||
<label class="fw-normal" for="pos_set_tip_after_payment" string="Add tip after payment"/>
|
||||
</div>
|
||||
</div>
|
||||
<block id="pos_interface_section" position="inside">
|
||||
<setting string="Internal Notes" help="Add internal notes on order lines for the kitchen" id="iface_orderline_notes">
|
||||
<field name="pos_iface_orderline_notes"/>
|
||||
</setting>
|
||||
</block>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
x
Reference in New Issue
Block a user