Add Dine360 Dashboard Website module with custom controllers and views
- Created __init__.py and __manifest__.py for the new dine360_dashboard_website module. - Implemented custom controllers to handle website routing and dashboard rendering. - Added views for website logo and shop template to enhance the user interface. - Updated dine360_reservation and dine360_restaurant modules to include dine360_table as a dependency. - Enhanced restaurant table management with new views and actions for floors and tables. - Updated docker-compose to change the exposed port for the Odoo service.
This commit is contained in:
parent
a03985749f
commit
c82f392e15
@ -12,6 +12,7 @@
|
|||||||
'dine360_theme_shivasakthi',
|
'dine360_theme_shivasakthi',
|
||||||
'dine360_kds',
|
'dine360_kds',
|
||||||
'dine360_reservation',
|
'dine360_reservation',
|
||||||
|
'dine360_table',
|
||||||
'dine360_uber',
|
'dine360_uber',
|
||||||
'dine360_recipe',
|
'dine360_recipe',
|
||||||
'dine360_self_order',
|
'dine360_self_order',
|
||||||
|
|||||||
@ -4,13 +4,11 @@
|
|||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
'category': 'Website',
|
'category': 'Website',
|
||||||
'summary': 'Redirect login to home and show icon grid',
|
'summary': 'Redirect login to home and show icon grid',
|
||||||
'depends': ['base', 'web', 'auth_signup', 'website', 'website_sale'],
|
'depends': ['base', 'web', 'auth_signup'],
|
||||||
'data': [
|
'data': [
|
||||||
'views/home_template.xml',
|
'views/home_template.xml',
|
||||||
'views/login_templates.xml',
|
'views/login_templates.xml',
|
||||||
'views/web_title_template.xml',
|
'views/web_title_template.xml',
|
||||||
'views/website_logo.xml',
|
|
||||||
'views/shop_template.xml',
|
|
||||||
'data/branding_data.xml',
|
'data/branding_data.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
@ -23,8 +21,6 @@
|
|||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'dine360_dashboard/static/src/css/theme_variables.css',
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
'dine360_dashboard/static/src/css/login_style.css',
|
'dine360_dashboard/static/src/css/login_style.css',
|
||||||
'dine360_dashboard/static/src/css/website_style.css',
|
|
||||||
'dine360_dashboard/static/src/css/shop_style.css',
|
|
||||||
],
|
],
|
||||||
'web.assets_common': [
|
'web.assets_common': [
|
||||||
'dine360_dashboard/static/src/css/theme_variables.css',
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.web.controllers.home import Home
|
from odoo.addons.web.controllers.home import Home
|
||||||
|
|
||||||
class CustomHome(Home):
|
class CustomHome(Home):
|
||||||
@http.route('/web/login', type='http', auth="public", website=True)
|
@http.route('/web/login', type='http', auth="public")
|
||||||
def web_login(self, *args, **kw):
|
def web_login(self, *args, **kw):
|
||||||
response = super(CustomHome, self).web_login(*args, **kw)
|
response = super(CustomHome, self).web_login(*args, **kw)
|
||||||
if request.params.get('login_success') and request.session.uid:
|
if request.params.get('login_success') and request.session.uid:
|
||||||
@ -11,137 +12,109 @@ class CustomHome(Home):
|
|||||||
return request.redirect('/')
|
return request.redirect('/')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
from odoo.addons.website.controllers.main import Website
|
def render_dashboard(request):
|
||||||
|
# 1. ROLE-BASED AUTO REDIRECTION (FOR STAFF)
|
||||||
|
# Skip the dashboard/website entirely for Chefs and Waiters
|
||||||
|
user = request.env.user.sudo()
|
||||||
|
is_admin = user.has_group('base.group_system') or \
|
||||||
|
user.has_group('dine360_restaurant.group_restaurant_admin')
|
||||||
|
|
||||||
|
if not is_admin:
|
||||||
|
# 1. WAITER / CASHIER -> Priority goes to POS
|
||||||
|
if user.has_group('dine360_restaurant.group_restaurant_waiter') or \
|
||||||
|
user.has_group('dine360_restaurant.group_restaurant_cashier'):
|
||||||
|
return request.redirect('/web#action=point_of_sale.action_client_pos_menu')
|
||||||
|
|
||||||
class ImageHome(Website):
|
# 2. CHEF -> Directly to KDS
|
||||||
@http.route('/', type='http', auth='public', website=True, sitemap=True)
|
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
||||||
def index(self, **kwargs):
|
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
||||||
# -----------------------------------------------------------
|
|
||||||
# SUPER SAFE EDITOR & IFRAME DETECTION
|
# Remove sudo() to respect Odoo's standard menu group restrictions
|
||||||
# -----------------------------------------------------------
|
menus = request.env['ir.ui.menu'].search([
|
||||||
path = request.httprequest.path
|
('parent_id', '=', False)
|
||||||
params = request.params
|
], order='sequence')
|
||||||
headers = request.httprequest.headers
|
|
||||||
referer = headers.get('Referer', '')
|
# User role checks
|
||||||
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
try:
|
||||||
|
is_admin = request.env.user.has_group('base.group_system') or \
|
||||||
|
request.env.user.has_group('dine360_restaurant.group_restaurant_admin')
|
||||||
|
except Exception:
|
||||||
|
is_admin = request.env.user.has_group('base.group_system')
|
||||||
|
|
||||||
|
# User requested to hide all standard apps and POS/KDS.
|
||||||
|
# Only allow specific menus based on user request + admin tools.
|
||||||
|
allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Table Management', 'Uber Integration', 'Apps', 'Settings', 'Dine360 SaaS']
|
||||||
|
|
||||||
|
# Hide 'Website' icon from the parent level Dine360Restaurants database
|
||||||
|
if request.env.cr.dbname and 'Dine360Restaurants' in request.env.cr.dbname:
|
||||||
|
if 'Website' in allowed_menus:
|
||||||
|
allowed_menus.remove('Website')
|
||||||
|
|
||||||
|
# Hide website/online orders if website module is not installed in current db
|
||||||
|
if 'website' not in request.env:
|
||||||
|
if 'Website' in allowed_menus:
|
||||||
|
allowed_menus.remove('Website')
|
||||||
|
if 'Online Orders' in allowed_menus:
|
||||||
|
allowed_menus.remove('Online Orders')
|
||||||
|
|
||||||
|
filtered_menus = []
|
||||||
|
seen_names = set()
|
||||||
|
for menu in menus:
|
||||||
|
# Match strictly against allowed menus
|
||||||
|
if menu.name not in allowed_menus and menu.name != 'Table Reservation':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Hide "Apps" and "Settings" for non-admins
|
||||||
|
if menu.name in ['Apps', 'Settings'] and not is_admin:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# De-duplicate by name
|
||||||
|
if menu.name in seen_names:
|
||||||
|
continue
|
||||||
|
seen_names.add(menu.name)
|
||||||
|
|
||||||
|
# Dynamic Icon Override (Dine360 Branding)
|
||||||
|
icon_mapping = {
|
||||||
|
'Apps': 'dine360_dashboard,static/src/img/icons/apps.svg',
|
||||||
|
'Settings': 'dine360_dashboard,static/src/img/icons/settings.svg',
|
||||||
|
'Table Reservation': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
|
||||||
|
'Table Reservations': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
|
||||||
|
'Uber Integration': 'dine360_dashboard,static/src/img/icons/uber_integration.svg',
|
||||||
|
'Online Orders': 'dine360_dashboard,static/src/img/icons/website.svg',
|
||||||
|
'Dine360 SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg',
|
||||||
|
'SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg',
|
||||||
|
}
|
||||||
|
|
||||||
# 1. If not logged in, always show standard homepage
|
# Find the best match in the mapping
|
||||||
if not request.session.uid:
|
current_name = menu.name
|
||||||
return super(ImageHome, self).index(**kwargs)
|
for key, icon_path in icon_mapping.items():
|
||||||
|
if key.lower() in current_name.lower():
|
||||||
|
menu.web_icon = icon_path
|
||||||
|
break
|
||||||
|
|
||||||
# 2. ROLE-BASED AUTO REDIRECTION (FOR STAFF)
|
filtered_menus.append(menu)
|
||||||
# Skip the dashboard/website entirely for Chefs and Waiters
|
|
||||||
user = request.env.user.sudo()
|
|
||||||
is_admin = user.has_group('base.group_system') or \
|
|
||||||
user.has_group('dine360_restaurant.group_restaurant_admin')
|
|
||||||
|
|
||||||
if not is_admin:
|
|
||||||
# 1. WAITER / CASHIER -> Priority goes to POS
|
|
||||||
if user.has_group('dine360_restaurant.group_restaurant_waiter') or \
|
|
||||||
user.has_group('dine360_restaurant.group_restaurant_cashier'):
|
|
||||||
return request.redirect('/web#action=point_of_sale.action_client_pos_menu')
|
|
||||||
|
|
||||||
# 2. CHEF -> Directly to KDS
|
# Low Stock Alerts (Ingredients)
|
||||||
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
low_stock_products = []
|
||||||
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
try:
|
||||||
|
ProductTemplate = request.env['product.template'].sudo()
|
||||||
# 3. SUPER SAFE EDITOR & IFRAME DETECTION
|
if hasattr(ProductTemplate, 'get_low_stock_products'):
|
||||||
path = request.httprequest.path
|
low_stock_products = ProductTemplate.get_low_stock_products(limit=5)
|
||||||
params = request.params
|
except Exception:
|
||||||
headers = request.httprequest.headers
|
|
||||||
referer = headers.get('Referer', '')
|
|
||||||
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
|
||||||
|
|
||||||
# Check for ANY editor or backend signal
|
|
||||||
editor_params = ['enable_editor', 'edit', 'path', 'website_id', 'frontend_edit', 'model', 'id']
|
|
||||||
is_editor_request = any(p in params for p in editor_params)
|
|
||||||
is_from_backend = any(m in referer for m in ['/website/force', 'enable_editor'])
|
|
||||||
|
|
||||||
# if it looks like Odoo internal business, return the real website
|
|
||||||
if fetch_dest == 'iframe' or is_editor_request or is_from_backend:
|
|
||||||
return super(ImageHome, self).index(**kwargs)
|
|
||||||
|
|
||||||
if path != '/':
|
|
||||||
return super(ImageHome, self).index(**kwargs)
|
|
||||||
|
|
||||||
# Remove sudo() to respect Odoo's standard menu group restrictions
|
|
||||||
menus = request.env['ir.ui.menu'].search([
|
|
||||||
('parent_id', '=', False)
|
|
||||||
], order='sequence')
|
|
||||||
|
|
||||||
# User role checks
|
|
||||||
try:
|
|
||||||
is_admin = request.env.user.has_group('base.group_system') or \
|
|
||||||
request.env.user.has_group('dine360_restaurant.group_restaurant_admin')
|
|
||||||
is_kitchen = request.env.user.has_group('dine360_restaurant.group_restaurant_kitchen')
|
|
||||||
except Exception:
|
|
||||||
is_admin = request.env.user.has_group('base.group_system')
|
|
||||||
is_kitchen = False
|
|
||||||
|
|
||||||
# User requested to hide all standard apps and POS/KDS.
|
|
||||||
# Only allow specific menus based on user request + admin tools.
|
|
||||||
allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Uber Integration', 'Apps', 'Settings', 'Dine360 SaaS']
|
|
||||||
|
|
||||||
# Hide 'Website' icon from the parent level Dine360Restaurants database
|
|
||||||
if request.env.cr.dbname and 'Dine360Restaurants' in request.env.cr.dbname:
|
|
||||||
if 'Website' in allowed_menus:
|
|
||||||
allowed_menus.remove('Website')
|
|
||||||
|
|
||||||
filtered_menus = []
|
|
||||||
seen_names = set()
|
|
||||||
for menu in menus:
|
|
||||||
# Match strictly against allowed menus
|
|
||||||
if menu.name not in allowed_menus and menu.name != 'Table Reservation':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Hide "Apps" and "Settings" for non-admins
|
|
||||||
if menu.name in ['Apps', 'Settings'] and not is_admin:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# De-duplicate by name
|
|
||||||
if menu.name in seen_names:
|
|
||||||
continue
|
|
||||||
seen_names.add(menu.name)
|
|
||||||
|
|
||||||
# Dynamic Icon Override (Dine360 Branding)
|
|
||||||
icon_mapping = {
|
|
||||||
'Apps': 'dine360_dashboard,static/src/img/icons/apps.svg',
|
|
||||||
'Settings': 'dine360_dashboard,static/src/img/icons/settings.svg',
|
|
||||||
'Table Reservation': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
|
|
||||||
'Table Reservations': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
|
|
||||||
'Uber Integration': 'dine360_dashboard,static/src/img/icons/uber_integration.svg',
|
|
||||||
'Online Orders': 'dine360_dashboard,static/src/img/icons/website.svg',
|
|
||||||
'Dine360 SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg',
|
|
||||||
'SaaS': 'dine360_dashboard,static/src/img/icons/saas.svg',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find the best match in the mapping
|
|
||||||
current_name = menu.name
|
|
||||||
for key, icon_path in icon_mapping.items():
|
|
||||||
if key.lower() in current_name.lower():
|
|
||||||
menu.web_icon = icon_path
|
|
||||||
break
|
|
||||||
|
|
||||||
filtered_menus.append(menu)
|
|
||||||
|
|
||||||
# Low Stock Alerts (Ingredients)
|
|
||||||
low_stock_products = []
|
low_stock_products = []
|
||||||
try:
|
|
||||||
ProductTemplate = request.env['product.template'].sudo()
|
|
||||||
if hasattr(ProductTemplate, 'get_low_stock_products'):
|
|
||||||
low_stock_products = ProductTemplate.get_low_stock_products(limit=5)
|
|
||||||
except Exception:
|
|
||||||
low_stock_products = []
|
|
||||||
|
|
||||||
return request.render('dine360_dashboard.image_home_template', {
|
values = {
|
||||||
'menus': filtered_menus,
|
'menus': filtered_menus,
|
||||||
'user_id': request.env.user,
|
'user_id': request.env.user,
|
||||||
'low_stock_products': low_stock_products
|
'low_stock_products': low_stock_products
|
||||||
})
|
}
|
||||||
|
if 'website' in request.env:
|
||||||
|
values['website'] = request.env['website'].get_current_website()
|
||||||
|
|
||||||
|
return request.render('dine360_dashboard.image_home_template', values)
|
||||||
|
|
||||||
|
class ImageHomeBase(http.Controller):
|
||||||
@http.route('/home', type='http', auth="public", website=True, sitemap=True)
|
@http.route('/', type='http', auth='user', website=True)
|
||||||
def website_home(self, **kw):
|
def index(self, **kwargs):
|
||||||
# Explicit route for standard Website Homepage
|
# Render the dashboard for logged in users when website module is not installed
|
||||||
return request.render('website.homepage')
|
return render_dashboard(request)
|
||||||
|
|||||||
2
addons/dine360_dashboard_website/__init__.py
Normal file
2
addons/dine360_dashboard_website/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import controllers
|
||||||
21
addons/dine360_dashboard_website/__manifest__.py
Normal file
21
addons/dine360_dashboard_website/__manifest__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Dine360 Dashboard Website Glue',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'category': 'Website',
|
||||||
|
'summary': 'Glue module for Dine360 Dashboard Website features',
|
||||||
|
'depends': ['dine360_dashboard', 'website', 'website_sale'],
|
||||||
|
'data': [
|
||||||
|
'views/website_logo.xml',
|
||||||
|
'views/shop_template.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_frontend': [
|
||||||
|
'dine360_dashboard/static/src/css/website_style.css',
|
||||||
|
'dine360_dashboard/static/src/css/shop_style.css',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'auto_install': True,
|
||||||
|
'installable': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
2
addons/dine360_dashboard_website/controllers/__init__.py
Normal file
2
addons/dine360_dashboard_website/controllers/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import main
|
||||||
45
addons/dine360_dashboard_website/controllers/main.py
Normal file
45
addons/dine360_dashboard_website/controllers/main.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.addons.web.controllers.home import Home
|
||||||
|
from odoo.addons.website.controllers.main import Website
|
||||||
|
from odoo.addons.dine360_dashboard.controllers.main import render_dashboard
|
||||||
|
|
||||||
|
class CustomHomeWebsite(Home):
|
||||||
|
@http.route('/web/login', type='http', auth="public", website=True)
|
||||||
|
def web_login(self, *args, **kw):
|
||||||
|
response = super(CustomHomeWebsite, self).web_login(*args, **kw)
|
||||||
|
if request.params.get('login_success') and request.session.uid:
|
||||||
|
return request.redirect('/')
|
||||||
|
return response
|
||||||
|
|
||||||
|
class ImageHomeWebsite(Website):
|
||||||
|
@http.route('/', type='http', auth='public', website=True, sitemap=True)
|
||||||
|
def index(self, **kwargs):
|
||||||
|
# 1. If not logged in, show website homepage
|
||||||
|
if not request.session.uid:
|
||||||
|
return super(ImageHomeWebsite, self).index(**kwargs)
|
||||||
|
|
||||||
|
# 2. If logged in, check for editor / iframe detection
|
||||||
|
params = request.params
|
||||||
|
headers = request.httprequest.headers
|
||||||
|
referer = headers.get('Referer', '')
|
||||||
|
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
||||||
|
|
||||||
|
editor_params = ['enable_editor', 'edit', 'path', 'website_id', 'frontend_edit', 'model', 'id']
|
||||||
|
is_editor_request = any(p in params for p in editor_params)
|
||||||
|
is_from_backend = any(m in referer for m in ['/website/force', 'enable_editor'])
|
||||||
|
|
||||||
|
if fetch_dest == 'iframe' or is_editor_request or is_from_backend:
|
||||||
|
return super(ImageHomeWebsite, self).index(**kwargs)
|
||||||
|
|
||||||
|
if request.httprequest.path != '/':
|
||||||
|
return super(ImageHomeWebsite, self).index(**kwargs)
|
||||||
|
|
||||||
|
# Render the dashboard
|
||||||
|
return render_dashboard(request)
|
||||||
|
|
||||||
|
@http.route('/home', type='http', auth="public", website=True, sitemap=True)
|
||||||
|
def website_home(self, **kw):
|
||||||
|
# Explicit route for standard Website Homepage
|
||||||
|
return request.render('website.homepage')
|
||||||
10
addons/dine360_dashboard_website/views/shop_template.xml
Normal file
10
addons/dine360_dashboard_website/views/shop_template.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<odoo>
|
||||||
|
<!-- Ensure Shop Filter Sidebar is Visible -->
|
||||||
|
<template id="products_categories_inherit" inherit_id="website_sale.products_categories" name="Show Categories Sidebar">
|
||||||
|
<!-- Force display of categories sidebar -->
|
||||||
|
<!-- <xpath expr="//div[@id='products_grid_before']" position="attributes">
|
||||||
|
<attribute name="class">col-lg-3 d-block</attribute>
|
||||||
|
<attribute name="style">display: block !important; visibility: visible !important;</attribute>
|
||||||
|
</xpath> -->
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
14
addons/dine360_dashboard_website/views/website_logo.xml
Normal file
14
addons/dine360_dashboard_website/views/website_logo.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!--
|
||||||
|
Override the Shared Brand Placeholder
|
||||||
|
Using a generic selector '//span' because strict field matching failed.
|
||||||
|
This template typically contains a single span or img for the logo.
|
||||||
|
-->
|
||||||
|
<template id="custom_website_logo_placeholder" inherit_id="website.placeholder_header_brand" name="Custom Website Logo Placeholder">
|
||||||
|
<xpath expr="/*" position="replace">
|
||||||
|
<img t-att-src="'/web/image/res.company/%s/logo' % res_company.id" t-att-alt="website.name" class="img-fluid" style="max-height: 60px; width: auto; object-fit: contain;"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
- WhatsApp/SMS notification hooks
|
- WhatsApp/SMS notification hooks
|
||||||
""",
|
""",
|
||||||
'author': 'Dine360',
|
'author': 'Dine360',
|
||||||
'depends': ['base', 'website', 'pos_restaurant'],
|
'depends': ['base', 'website', 'pos_restaurant', 'dine360_table'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/reservation_sequence.xml',
|
'data/reservation_sequence.xml',
|
||||||
|
|||||||
@ -48,4 +48,22 @@
|
|||||||
</p>
|
</p>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- Inherit Standalone Table Form View from dine360_table -->
|
||||||
|
<record id="view_restaurant_table_form_inherit_dine360_table" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.table.form.inherit.dine360.table</field>
|
||||||
|
<field name="model">restaurant.table</field>
|
||||||
|
<field name="inherit_id" ref="dine360_table.view_restaurant_table_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//group[@name='basic_config']" position="after">
|
||||||
|
<group string="Reservation Settings" name="reservation_settings">
|
||||||
|
<field name="is_reservation_enabled"/>
|
||||||
|
<field name="reservation_slot_duration"/>
|
||||||
|
<field name="min_party_size"/>
|
||||||
|
<field name="max_party_size"/>
|
||||||
|
<field name="zone"/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
- Store Keeper
|
- Store Keeper
|
||||||
""",
|
""",
|
||||||
'author': 'Dine360',
|
'author': 'Dine360',
|
||||||
'depends': ['point_of_sale', 'pos_restaurant', 'purchase', 'stock', 'website_sale'],
|
'depends': ['point_of_sale', 'pos_restaurant', 'purchase', 'stock'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/restaurant_security.xml',
|
'security/restaurant_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
|||||||
@ -12,13 +12,19 @@ class Dine360SelfOrderController(http.Controller):
|
|||||||
def self_order_menu(self, **kwargs):
|
def self_order_menu(self, **kwargs):
|
||||||
"""Displays the self-order menu for a specific table/kiosk"""
|
"""Displays the self-order menu for a specific table/kiosk"""
|
||||||
table_id = kwargs.get('table_id')
|
table_id = kwargs.get('table_id')
|
||||||
table = False
|
pos_table = False
|
||||||
if table_id:
|
if table_id:
|
||||||
table = request.env['restaurant.table'].sudo().browse(int(table_id))
|
try:
|
||||||
|
pos_table_id = int(table_id)
|
||||||
|
pos_table = request.env['restaurant.table'].sudo().browse(pos_table_id)
|
||||||
|
if not pos_table.exists():
|
||||||
|
pos_table = False
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pos_table = False
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
'table': table,
|
'pos_table': pos_table,
|
||||||
'floor': table.floor_id if table else False,
|
'pos_floor': pos_table.floor_id if pos_table else False,
|
||||||
}
|
}
|
||||||
return request.render('dine360_self_order.self_order_menu_template', values)
|
return request.render('dine360_self_order.self_order_menu_template', values)
|
||||||
|
|
||||||
|
|||||||
@ -1,259 +1,252 @@
|
|||||||
/** @odoo-module */
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||||
|
|
||||||
// We'll use a standard self-invoking function style since it's a public web module
|
publicWidget.registry.Dine360SelfOrder = publicWidget.Widget.extend({
|
||||||
(function () {
|
selector: '#self_order_app',
|
||||||
'use strict';
|
events: {
|
||||||
|
'input #product_search': '_onSearchInput',
|
||||||
|
'click #view_cart_btn': '_onViewCartClick',
|
||||||
|
'click #submit_order_btn': '_onSubmitOrderClick',
|
||||||
|
'click [data-category]': '_onCategoryClick',
|
||||||
|
'click .add-to-cart-btn': '_onAddToCartClick',
|
||||||
|
'click .increase-qty': '_onIncreaseQtyClick',
|
||||||
|
'click .decrease-qty': '_onDecreaseQtyClick',
|
||||||
|
'click .service-select': '_onServiceSelectClick',
|
||||||
|
},
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
init: function () {
|
||||||
const app = document.querySelector('#self_order_app');
|
this._super.apply(this, arguments);
|
||||||
if (!app) return;
|
this.config = {};
|
||||||
|
this.state = {
|
||||||
const dataEl = document.querySelector('#self_order_data');
|
|
||||||
const config = {
|
|
||||||
tableId: dataEl.dataset.tableId,
|
|
||||||
tableName: dataEl.dataset.tableName,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
products: [],
|
products: [],
|
||||||
cart: [],
|
cart: [],
|
||||||
activeCategory: 'all',
|
activeCategory: 'all',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// --- UI Elements ---
|
start: async function () {
|
||||||
const productList = document.querySelector('#product_list');
|
await this._super.apply(this, arguments);
|
||||||
const categoryFilter = document.querySelector('#category_filter');
|
const dataEl = this.el.querySelector('#self_order_data');
|
||||||
const cartCount = document.querySelector('#cart_count');
|
if (dataEl) {
|
||||||
const cartTotal = document.querySelector('#cart_total');
|
this.config.tableId = dataEl.dataset.tableId;
|
||||||
const footerCart = document.querySelector('#footer_cart');
|
this.config.tableName = dataEl.dataset.tableName;
|
||||||
const loadingOverlay = document.querySelector('#loading_overlay');
|
}
|
||||||
const contentArea = document.querySelector('#self_order_content');
|
|
||||||
|
|
||||||
// --- Init ---
|
|
||||||
try {
|
try {
|
||||||
const products = await jsonrpc('/dine360/self_order/products', {});
|
const products = await jsonrpc('/dine360/self_order/products', {});
|
||||||
state.products = products;
|
this.state.products = products;
|
||||||
renderCategories();
|
this._renderCategories();
|
||||||
renderProducts();
|
this._renderProducts();
|
||||||
loadingOverlay.classList.add('d-none');
|
this.$('#loading_overlay').addClass('d-none');
|
||||||
contentArea.classList.remove('d-none');
|
this.$('#self_order_content').removeClass('d-none');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load products", e);
|
console.error("Failed to load products", e);
|
||||||
alert("Error connecting to server. Please try again later.");
|
alert("Error connecting to server. Please try again later.");
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// --- Functions ---
|
_renderCategories: function () {
|
||||||
function renderCategories() {
|
const categories = ['all', ...new Set(this.state.products.map(p => p.pos_categ_name))];
|
||||||
const categories = ['all', ...new Set(state.products.map(p => p.pos_categ_name))];
|
const html = categories.map(cat => `
|
||||||
categoryFilter.innerHTML = categories.map(cat => `
|
<button class="btn btn-sm ${this.state.activeCategory === cat ? 'btn-warning fw-bold shadow-sm' : 'btn-white border'} rounded-pill px-3 py-2" data-category="${cat}">
|
||||||
<button class="btn btn-sm ${state.activeCategory === cat ? 'btn-warning fw-bold shadow-sm' : 'btn-white border'} rounded-pill px-3 py-2" data-category="${cat}">
|
${cat === 'all' ? 'All Items' : cat}
|
||||||
${cat === 'all' ? 'All Items' : cat}
|
</button>
|
||||||
</button>
|
`).join('');
|
||||||
`).join('');
|
this.$('#category_filter').html(html);
|
||||||
|
},
|
||||||
|
|
||||||
categoryFilter.querySelectorAll('button').forEach(btn => {
|
_renderProducts: function () {
|
||||||
btn.onclick = () => {
|
let filtered = this.state.products;
|
||||||
state.activeCategory = btn.dataset.category;
|
if (this.state.activeCategory !== 'all') {
|
||||||
renderCategories();
|
filtered = filtered.filter(p => p.pos_categ_name === this.state.activeCategory);
|
||||||
renderProducts();
|
}
|
||||||
};
|
if (this.state.searchTerm) {
|
||||||
});
|
const term = this.state.searchTerm.toLowerCase();
|
||||||
|
filtered = filtered.filter(p => p.display_name.toLowerCase().includes(term));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProducts() {
|
const html = filtered.map(p => `
|
||||||
let filtered = state.products;
|
<div class="col-6 col-md-4 col-lg-3">
|
||||||
if (state.activeCategory !== 'all') {
|
<div class="card h-100 border-0 shadow-sm product-card transition-all" data-id="${p.id}">
|
||||||
filtered = filtered.filter(p => p.pos_categ_name === state.activeCategory);
|
<div class="position-relative">
|
||||||
}
|
<img src="${p.image_url}" class="card-img-top rounded-top-4" alt="${p.display_name}" style="height: 140px; object-fit: cover; opacity: 1;"/>
|
||||||
if (state.searchTerm) {
|
<div class="position-absolute bottom-0 end-0 p-1">
|
||||||
const term = state.searchTerm.toLowerCase();
|
<button class="btn btn-warning btn-sm rounded-circle add-to-cart-btn shadow" style="width: 32px; height: 32px; padding: 0;">
|
||||||
filtered = filtered.filter(p => p.display_name.toLowerCase().includes(term));
|
<i class="fa fa-plus"/>
|
||||||
}
|
|
||||||
|
|
||||||
productList.innerHTML = filtered.map(p => `
|
|
||||||
<div class="col-6 col-md-4 col-lg-3">
|
|
||||||
<div class="card h-100 border-0 shadow-sm product-card transition-all" data-id="${p.id}">
|
|
||||||
<div class="position-relative">
|
|
||||||
<img src="${p.image_url}" class="card-img-top rounded-top-4" alt="${p.display_name}" style="height: 140px; object-fit: cover; opacity: 1;"/>
|
|
||||||
<div class="position-absolute bottom-0 end-0 p-1">
|
|
||||||
<button class="btn btn-warning btn-sm rounded-circle add-to-cart-btn shadow" style="width: 32px; height: 32px; padding: 0;">
|
|
||||||
<i class="fa fa-plus"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-3">
|
|
||||||
<div class="fw-bold text-dark small mb-1 text-truncate">${p.display_name}</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<span class="fw-bold text-primary">$${p.list_price.toFixed(2)}</span>
|
|
||||||
<t t-if="p.is_kitchen_item">
|
|
||||||
<i class="fa fa-fire text-danger small opacity-50"/>
|
|
||||||
</t>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
productList.querySelectorAll('.add-to-cart-btn').forEach(btn => {
|
|
||||||
btn.onclick = (ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const card = btn.closest('.product-card');
|
|
||||||
const productId = parseInt(card.dataset.id);
|
|
||||||
addToCart(productId);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addToCart(productId) {
|
|
||||||
const product = state.products.find(p => p.id === productId);
|
|
||||||
const existing = state.cart.find(item => item.product_id === productId);
|
|
||||||
if (existing) {
|
|
||||||
existing.qty++;
|
|
||||||
} else {
|
|
||||||
state.cart.push({
|
|
||||||
product_id: product.id,
|
|
||||||
display_name: product.display_name,
|
|
||||||
price_unit: product.list_price,
|
|
||||||
qty: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updateCartUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCartUI() {
|
|
||||||
const count = state.cart.reduce((acc, item) => acc + item.qty, 0);
|
|
||||||
const total = state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0);
|
|
||||||
|
|
||||||
cartCount.textContent = count;
|
|
||||||
cartTotal.textContent = `$${total.toFixed(2)}`;
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
footerCart.classList.remove('d-none');
|
|
||||||
setTimeout(() => footerCart.style.transform = 'translateY(0)', 10);
|
|
||||||
} else {
|
|
||||||
footerCart.style.transform = 'translateY(100%)';
|
|
||||||
setTimeout(() => footerCart.classList.add('d-none'), 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Search ---
|
|
||||||
document.querySelector('#product_search').oninput = (ev) => {
|
|
||||||
state.searchTerm = ev.target.value;
|
|
||||||
renderProducts();
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Cart Modal ---
|
|
||||||
const cartModal = new bootstrap.Modal(document.getElementById('cart_modal'));
|
|
||||||
document.querySelector('#view_cart_btn').onclick = () => {
|
|
||||||
renderCartModal();
|
|
||||||
cartModal.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderCartModal() {
|
|
||||||
const list = document.querySelector('#cart_items_list');
|
|
||||||
const total = state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0);
|
|
||||||
|
|
||||||
list.innerHTML = state.cart.map((item, index) => `
|
|
||||||
<div class="d-flex align-items-center mb-4 cart-item">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="fw-bold mb-1">${item.display_name}</div>
|
|
||||||
<div class="text-primary fw-bold">$${(item.qty * item.price_unit).toFixed(2)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center gap-2 bg-light rounded-pill p-1 border">
|
|
||||||
<button class="btn btn-sm btn-white rounded-circle shadow-sm decrease-qty" data-index="${index}" style="width:28px; height:28px; padding:0;">-</button>
|
|
||||||
<span class="px-2 fw-bold" style="min-width: 20px; text-align:center;">${item.qty}</span>
|
|
||||||
<button class="btn btn-sm btn-white rounded-circle shadow-sm increase-qty" data-index="${index}" style="width:28px; height:28px; padding:0;">+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
document.querySelector('#checkout_subtotal').textContent = `$${total.toFixed(2)}`;
|
|
||||||
document.querySelector('#checkout_total').textContent = `$${total.toFixed(2)}`;
|
|
||||||
|
|
||||||
list.querySelectorAll('.increase-qty').forEach(btn => {
|
|
||||||
btn.onclick = () => {
|
|
||||||
state.cart[btn.dataset.index].qty++;
|
|
||||||
renderCartModal();
|
|
||||||
updateCartUI();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
list.querySelectorAll('.decrease-qty').forEach(btn => {
|
|
||||||
btn.onclick = () => {
|
|
||||||
const index = btn.dataset.index;
|
|
||||||
state.cart[index].qty--;
|
|
||||||
if (state.cart[index].qty <= 0) {
|
|
||||||
state.cart.splice(index, 1);
|
|
||||||
}
|
|
||||||
if (state.cart.length === 0) cartModal.hide();
|
|
||||||
renderCartModal();
|
|
||||||
updateCartUI();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Submit Order ---
|
|
||||||
document.querySelector('#submit_order_btn').onclick = async () => {
|
|
||||||
const btn = document.querySelector('#submit_order_btn');
|
|
||||||
const originalText = btn.innerHTML;
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"/>Sending...';
|
|
||||||
|
|
||||||
const fulfilmentType = document.querySelector('input[name="fulfilment"]:checked')?.value || 'dine_in';
|
|
||||||
|
|
||||||
const orderData = {
|
|
||||||
table_id: config.tableId,
|
|
||||||
fulfilment_type: fulfilmentType,
|
|
||||||
lines: state.cart
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await jsonrpc('/dine360/self_order/submit_order', { order_data: orderData });
|
|
||||||
if (response.success) {
|
|
||||||
cartModal.hide();
|
|
||||||
state.cart = [];
|
|
||||||
updateCartUI();
|
|
||||||
|
|
||||||
// Success View
|
|
||||||
document.querySelector('#self_order_content').innerHTML = `
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="bg-success text-white d-inline-flex align-items-center justify-content-center rounded-circle shadow-lg" style="width: 100px; height: 100px;">
|
|
||||||
<i class="fa fa-check fa-4x"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 class="fw-bold mb-3">Order Received!</h2>
|
|
||||||
<p class="text-muted mb-4 px-4">${response.message}</p>
|
|
||||||
<div class="bg-white p-4 rounded-4 shadow-sm mb-4">
|
|
||||||
<div class="small text-muted mb-1">Order Number</div>
|
|
||||||
<div class="h4 fw-bold text-dark mb-0">${response.order_name}</div>
|
|
||||||
</div>
|
|
||||||
<button onclick="window.location.reload()" class="btn btn-dark btn-lg px-5 rounded-pill shadow">
|
|
||||||
Order More Items
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
} else {
|
<div class="card-body p-3">
|
||||||
alert(response.error || "Order submission failed");
|
<div class="fw-bold text-dark small mb-1 text-truncate">${p.display_name}</div>
|
||||||
}
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
} catch (e) {
|
<span class="fw-bold text-primary">$${p.list_price.toFixed(2)}</span>
|
||||||
console.error(e);
|
${p.is_kitchen_item ? '<i class="fa fa-fire text-danger small opacity-50"/>' : ''}
|
||||||
alert("Connection lost. Please check your internet.");
|
</div>
|
||||||
} finally {
|
</div>
|
||||||
btn.disabled = false;
|
</div>
|
||||||
btn.innerHTML = originalText;
|
</div>
|
||||||
}
|
`).join('');
|
||||||
|
this.$('#product_list').html(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCategoryClick: function (ev) {
|
||||||
|
this.state.activeCategory = $(ev.currentTarget).data('category');
|
||||||
|
this._renderCategories();
|
||||||
|
this._renderProducts();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAddToCartClick: function (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const card = $(ev.currentTarget).closest('.product-card');
|
||||||
|
const productId = parseInt(card.data('id'));
|
||||||
|
const product = this.state.products.find(p => p.id === productId);
|
||||||
|
const existing = this.state.cart.find(item => item.product_id === productId);
|
||||||
|
if (existing) {
|
||||||
|
existing.qty++;
|
||||||
|
} else {
|
||||||
|
this.state.cart.push({
|
||||||
|
product_id: product.id,
|
||||||
|
display_name: product.display_name,
|
||||||
|
price_unit: product.list_price,
|
||||||
|
qty: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._updateCartUI();
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateCartUI: function () {
|
||||||
|
const count = this.state.cart.reduce((acc, item) => acc + item.qty, 0);
|
||||||
|
const total = this.state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0);
|
||||||
|
|
||||||
|
this.$('#cart_count').text(count);
|
||||||
|
this.$('#cart_total').text(`$${total.toFixed(2)}`);
|
||||||
|
|
||||||
|
const $footerCart = this.$('#footer_cart');
|
||||||
|
if (count > 0) {
|
||||||
|
$footerCart.removeClass('d-none');
|
||||||
|
setTimeout(() => $footerCart.css('transform', 'translateY(0)'), 10);
|
||||||
|
} else {
|
||||||
|
$footerCart.css('transform', 'translateY(100%)');
|
||||||
|
setTimeout(() => $footerCart.addClass('d-none'), 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSearchInput: function (ev) {
|
||||||
|
this.state.searchTerm = ev.target.value;
|
||||||
|
this._renderProducts();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onViewCartClick: function () {
|
||||||
|
this._renderCartModal();
|
||||||
|
if (!this.cartModal) {
|
||||||
|
this.cartModal = new bootstrap.Modal(this.el.querySelector('#cart_modal'));
|
||||||
|
}
|
||||||
|
this.cartModal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderCartModal: function () {
|
||||||
|
const total = this.state.cart.reduce((acc, item) => acc + (item.qty * item.price_unit), 0);
|
||||||
|
|
||||||
|
const html = this.state.cart.map((item, index) => `
|
||||||
|
<div class="d-flex align-items-center mb-4 cart-item">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="fw-bold mb-1">${item.display_name}</div>
|
||||||
|
<div class="text-primary fw-bold">$${(item.qty * item.price_unit).toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 bg-light rounded-pill p-1 border">
|
||||||
|
<button class="btn btn-sm btn-white rounded-circle shadow-sm decrease-qty" data-index="${index}" style="width:28px; height:28px; padding:0;">-</button>
|
||||||
|
<span class="px-2 fw-bold" style="min-width: 20px; text-align:center;">${item.qty}</span>
|
||||||
|
<button class="btn btn-sm btn-white rounded-circle shadow-sm increase-qty" data-index="${index}" style="width:28px; height:28px; padding:0;">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
this.$('#cart_items_list').html(html);
|
||||||
|
this.$('#checkout_subtotal').text(`$${total.toFixed(2)}`);
|
||||||
|
this.$('#checkout_total').text(`$${total.toFixed(2)}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onIncreaseQtyClick: function (ev) {
|
||||||
|
const index = $(ev.currentTarget).data('index');
|
||||||
|
this.state.cart[index].qty++;
|
||||||
|
this._renderCartModal();
|
||||||
|
this._updateCartUI();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onDecreaseQtyClick: function (ev) {
|
||||||
|
const index = $(ev.currentTarget).data('index');
|
||||||
|
this.state.cart[index].qty--;
|
||||||
|
if (this.state.cart[index].qty <= 0) {
|
||||||
|
this.state.cart.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (this.state.cart.length === 0 && this.cartModal) {
|
||||||
|
this.cartModal.hide();
|
||||||
|
}
|
||||||
|
this._renderCartModal();
|
||||||
|
this._updateCartUI();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onServiceSelectClick: function (ev) {
|
||||||
|
const $label = $(ev.currentTarget);
|
||||||
|
this.$('.service-select').removeClass('active border-warning bg-warning-light');
|
||||||
|
$label.addClass('active border-warning bg-warning-light');
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSubmitOrderClick: async function () {
|
||||||
|
const $btn = this.$('#submit_order_btn');
|
||||||
|
const originalText = $btn.html();
|
||||||
|
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
$btn.html('<span class="spinner-border spinner-border-sm me-2"/>Sending...');
|
||||||
|
|
||||||
|
const fulfilmentType = this.$('input[name="fulfilment"]:checked').val() || 'dine_in';
|
||||||
|
|
||||||
|
const orderData = {
|
||||||
|
table_id: this.config.tableId,
|
||||||
|
fulfilment_type: fulfilmentType,
|
||||||
|
lines: this.state.cart
|
||||||
};
|
};
|
||||||
|
|
||||||
// Service Type Toggle
|
try {
|
||||||
document.querySelectorAll('.service-select').forEach(label => {
|
const response = await jsonrpc('/dine360/self_order/submit_order', { order_data: orderData });
|
||||||
label.onclick = () => {
|
if (response.success) {
|
||||||
document.querySelectorAll('.service-select').forEach(l => l.classList.remove('active', 'border-warning', 'bg-warning-light'));
|
if (this.cartModal) this.cartModal.hide();
|
||||||
label.classList.add('active', 'border-warning', 'bg-warning-light');
|
this.state.cart = [];
|
||||||
};
|
this._updateCartUI();
|
||||||
});
|
|
||||||
});
|
// Success View
|
||||||
})();
|
this.$('#self_order_content').html(`
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="bg-success text-white d-inline-flex align-items-center justify-content-center rounded-circle shadow-lg" style="width: 100px; height: 100px;">
|
||||||
|
<i class="fa fa-check fa-4x"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="fw-bold mb-3">Order Received!</h2>
|
||||||
|
<p class="text-muted mb-4 px-4">${response.message}</p>
|
||||||
|
<div class="bg-white p-4 rounded-4 shadow-sm mb-4">
|
||||||
|
<div class="small text-muted mb-1">Order Number</div>
|
||||||
|
<div class="h4 fw-bold text-dark mb-0">${response.order_name}</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="window.location.reload()" class="btn btn-dark btn-lg px-5 rounded-pill shadow">
|
||||||
|
Order More Items
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
alert(response.error || "Order submission failed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Connection lost. Please check your internet.");
|
||||||
|
} finally {
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
$btn.html(originalText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicWidget.registry.Dine360SelfOrder;
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
<div id="self_order_app" class="bg-light min-vh-100 pb-5">
|
<div id="self_order_app" class="bg-light min-vh-100 pb-5">
|
||||||
<!-- Data injection for JS -->
|
<!-- Data injection for JS -->
|
||||||
<div id="self_order_data" class="d-none"
|
<div id="self_order_data" class="d-none"
|
||||||
t-att-data-table-id="table.id if table else ''"
|
t-att-data-table-id="pos_table.id if pos_table else ''"
|
||||||
t-att-data-floor-id="floor.id if floor else ''"
|
t-att-data-floor-id="pos_floor.id if pos_floor else ''"
|
||||||
t-att-data-table-name="table.display_name if table else ''"/>
|
t-att-data-table-name="pos_table.display_name if pos_table else ''"/>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top shadow-sm py-2">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top shadow-sm py-2">
|
||||||
@ -15,9 +15,9 @@
|
|||||||
<a class="navbar-brand fw-bold d-flex align-items-center" href="/dine360/menu">
|
<a class="navbar-brand fw-bold d-flex align-items-center" href="/dine360/menu">
|
||||||
<i class="fa fa-cutlery me-2 text-danger"/> Dine360
|
<i class="fa fa-cutlery me-2 text-danger"/> Dine360
|
||||||
</a>
|
</a>
|
||||||
<div t-if="table" class="text-white-50 small">
|
<div t-if="pos_table" class="text-white-50 small">
|
||||||
<span class="badge bg-danger text-white px-2 py-1 rounded-pill">
|
<span class="badge bg-danger text-white px-2 py-1 rounded-pill">
|
||||||
Table <t t-esc="table.name"/>
|
Table <t t-esc="pos_table.name"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div t-else="" class="text-white-50 small">
|
<div t-else="" class="text-white-50 small">
|
||||||
@ -85,7 +85,7 @@
|
|||||||
<div id="cart_items_list" class="p-4" style="max-height: 400px; overflow-y: auto;">
|
<div id="cart_items_list" class="p-4" style="max-height: 400px; overflow-y: auto;">
|
||||||
<!-- JS injects checkout lines here -->
|
<!-- JS injects checkout lines here -->
|
||||||
</div>
|
</div>
|
||||||
<div t-if="not table" class="p-4 bg-light border-top">
|
<div t-if="not pos_table" class="p-4 bg-light border-top">
|
||||||
<h6 class="fw-bold mb-3">Service Type</h6>
|
<h6 class="fw-bold mb-3">Service Type</h6>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<label t-attf-class="flex-fill border p-3 rounded-3 text-center cursor-pointer service-select active {{'w-100' if not website.enable_delivery_option else ''}}" data-mode="pickup">
|
<label t-attf-class="flex-fill border p-3 rounded-3 text-center cursor-pointer service-select active {{'w-100' if not website.enable_delivery_option else ''}}" data-mode="pickup">
|
||||||
|
|||||||
1
addons/dine360_table/__init__.py
Normal file
1
addons/dine360_table/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
22
addons/dine360_table/__manifest__.py
Normal file
22
addons/dine360_table/__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Dine360 Table Management',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Sales/Restaurant',
|
||||||
|
'summary': 'Dedicated Floor and Table Management System',
|
||||||
|
'description': """
|
||||||
|
Provides backend menus and views to create, configure and manage floors and tables,
|
||||||
|
independent of POS configuration, and seamlessly integrated with Dine360 modules.
|
||||||
|
""",
|
||||||
|
'author': 'Dine360',
|
||||||
|
'depends': ['pos_restaurant'],
|
||||||
|
'data': [
|
||||||
|
'views/restaurant_floor_views.xml',
|
||||||
|
'views/restaurant_table_views.xml',
|
||||||
|
'views/menu_items.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'icon': '/dine360_table/static/description/icon.png',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
BIN
addons/dine360_table/static/description/icon.png
Normal file
BIN
addons/dine360_table/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 394 KiB |
21
addons/dine360_table/views/menu_items.xml
Normal file
21
addons/dine360_table/views/menu_items.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Root Menu -->
|
||||||
|
<menuitem id="menu_dine360_table_root"
|
||||||
|
name="Table Management"
|
||||||
|
sequence="21"
|
||||||
|
web_icon="dine360_table,static/description/icon.png"/>
|
||||||
|
|
||||||
|
<!-- Submenus -->
|
||||||
|
<menuitem id="menu_dine360_floor_submenu"
|
||||||
|
name="Floors"
|
||||||
|
parent="menu_dine360_table_root"
|
||||||
|
action="action_restaurant_floor_form"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_dine360_table_submenu"
|
||||||
|
name="Tables"
|
||||||
|
parent="menu_dine360_table_root"
|
||||||
|
action="action_restaurant_table_form"
|
||||||
|
sequence="20"/>
|
||||||
|
</odoo>
|
||||||
66
addons/dine360_table/views/restaurant_floor_views.xml
Normal file
66
addons/dine360_table/views/restaurant_floor_views.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_restaurant_floor_tree" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.floor.tree.dine360</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>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_restaurant_floor_form" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.floor.form.dine360</field>
|
||||||
|
<field name="model">restaurant.floor</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Restaurant Floor">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name" class="oe_edit_only"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="e.g. Ground Floor, Garden, VIP..."/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Configurations">
|
||||||
|
<field name="pos_config_ids" widget="many2many_tags" placeholder="Link to Point of Sale Configs..."/>
|
||||||
|
<field name="background_color" widget="color" placeholder="e.g. rgb(220, 220, 220)"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Tables on Floor" name="tables">
|
||||||
|
<field name="table_ids" context="{'default_floor_id': active_id}">
|
||||||
|
<tree string="Tables" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="seats"/>
|
||||||
|
<field name="shape"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Floor Window Action -->
|
||||||
|
<record id="action_restaurant_floor_form" model="ir.actions.act_window">
|
||||||
|
<field name="name">Floors</field>
|
||||||
|
<field name="res_model">restaurant.floor</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="view_id" ref="view_restaurant_floor_tree"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new floor plan!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Floors represent different physical sections of your restaurant (e.g., Main Hall, Terrace, Bar).
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
63
addons/dine360_table/views/restaurant_table_views.xml
Normal file
63
addons/dine360_table/views/restaurant_table_views.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_restaurant_table_tree" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.table.tree.dine360</field>
|
||||||
|
<field name="model">restaurant.table</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Restaurant Tables">
|
||||||
|
<field name="floor_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="seats"/>
|
||||||
|
<field name="shape"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_restaurant_table_form" model="ir.ui.view">
|
||||||
|
<field name="name">restaurant.table.form.dine360</field>
|
||||||
|
<field name="model">restaurant.table</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Restaurant Table">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name" class="oe_edit_only"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="e.g. Table 1, T2..."/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Basic Configuration" name="basic_config">
|
||||||
|
<field name="floor_id" options="{'no_create': True}"/>
|
||||||
|
<field name="seats"/>
|
||||||
|
<field name="shape"/>
|
||||||
|
</group>
|
||||||
|
<group string="POS Position & Size" name="pos_layout">
|
||||||
|
<field name="position_h"/>
|
||||||
|
<field name="position_v"/>
|
||||||
|
<field name="width"/>
|
||||||
|
<field name="height"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Table Window Action -->
|
||||||
|
<record id="action_restaurant_table_form" model="ir.actions.act_window">
|
||||||
|
<field name="name">Tables</field>
|
||||||
|
<field name="res_model">restaurant.table</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="view_id" ref="view_restaurant_table_tree"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new table!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Tables are placed on floor plans. You can configure their shape, seat capacity, and custom positioning for the Point of Sale system.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -16,7 +16,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
- "10002:8069"
|
- "10006:8069"
|
||||||
environment:
|
environment:
|
||||||
HOST: db
|
HOST: db
|
||||||
USER: odoo
|
USER: odoo
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user