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_kds',
|
||||
'dine360_reservation',
|
||||
'dine360_table',
|
||||
'dine360_uber',
|
||||
'dine360_recipe',
|
||||
'dine360_self_order',
|
||||
|
||||
@ -4,13 +4,11 @@
|
||||
'license': 'LGPL-3',
|
||||
'category': 'Website',
|
||||
'summary': 'Redirect login to home and show icon grid',
|
||||
'depends': ['base', 'web', 'auth_signup', 'website', 'website_sale'],
|
||||
'depends': ['base', 'web', 'auth_signup'],
|
||||
'data': [
|
||||
'views/home_template.xml',
|
||||
'views/login_templates.xml',
|
||||
'views/web_title_template.xml',
|
||||
'views/website_logo.xml',
|
||||
'views/shop_template.xml',
|
||||
'data/branding_data.xml',
|
||||
],
|
||||
'assets': {
|
||||
@ -23,8 +21,6 @@
|
||||
'web.assets_frontend': [
|
||||
'dine360_dashboard/static/src/css/theme_variables.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': [
|
||||
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.home import 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):
|
||||
response = super(CustomHome, self).web_login(*args, **kw)
|
||||
if request.params.get('login_success') and request.session.uid:
|
||||
@ -11,137 +12,109 @@ class CustomHome(Home):
|
||||
return request.redirect('/')
|
||||
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')
|
||||
|
||||
class ImageHome(Website):
|
||||
@http.route('/', type='http', auth='public', website=True, sitemap=True)
|
||||
def index(self, **kwargs):
|
||||
# -----------------------------------------------------------
|
||||
# SUPER SAFE EDITOR & IFRAME DETECTION
|
||||
# -----------------------------------------------------------
|
||||
path = request.httprequest.path
|
||||
params = request.params
|
||||
headers = request.httprequest.headers
|
||||
referer = headers.get('Referer', '')
|
||||
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
||||
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')
|
||||
|
||||
# 1. If not logged in, always show standard homepage
|
||||
if not request.session.uid:
|
||||
return super(ImageHome, self).index(**kwargs)
|
||||
# 2. CHEF -> Directly to KDS
|
||||
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
||||
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
||||
|
||||
# 2. 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')
|
||||
# Remove sudo() to respect Odoo's standard menu group restrictions
|
||||
menus = request.env['ir.ui.menu'].search([
|
||||
('parent_id', '=', False)
|
||||
], order='sequence')
|
||||
|
||||
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')
|
||||
# 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')
|
||||
except Exception:
|
||||
is_admin = request.env.user.has_group('base.group_system')
|
||||
|
||||
# 2. CHEF -> Directly to KDS
|
||||
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
||||
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
||||
# 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']
|
||||
|
||||
# 3. SUPER SAFE EDITOR & IFRAME DETECTION
|
||||
path = request.httprequest.path
|
||||
params = request.params
|
||||
headers = request.httprequest.headers
|
||||
referer = headers.get('Referer', '')
|
||||
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
||||
# 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')
|
||||
|
||||
# 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'])
|
||||
# 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')
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
if path != '/':
|
||||
return super(ImageHome, self).index(**kwargs)
|
||||
# Hide "Apps" and "Settings" for non-admins
|
||||
if menu.name in ['Apps', 'Settings'] and not is_admin:
|
||||
continue
|
||||
|
||||
# Remove sudo() to respect Odoo's standard menu group restrictions
|
||||
menus = request.env['ir.ui.menu'].search([
|
||||
('parent_id', '=', False)
|
||||
], order='sequence')
|
||||
# De-duplicate by name
|
||||
if menu.name in seen_names:
|
||||
continue
|
||||
seen_names.add(menu.name)
|
||||
|
||||
# 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
|
||||
# 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',
|
||||
}
|
||||
|
||||
# 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']
|
||||
# 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
|
||||
|
||||
# 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.append(menu)
|
||||
|
||||
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 Alerts (Ingredients)
|
||||
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 = []
|
||||
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', {
|
||||
'menus': filtered_menus,
|
||||
'user_id': request.env.user,
|
||||
'low_stock_products': low_stock_products
|
||||
})
|
||||
values = {
|
||||
'menus': filtered_menus,
|
||||
'user_id': request.env.user,
|
||||
'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)
|
||||
|
||||
|
||||
@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')
|
||||
class ImageHomeBase(http.Controller):
|
||||
@http.route('/', type='http', auth='user', website=True)
|
||||
def index(self, **kwargs):
|
||||
# Render the dashboard for logged in users when website module is not installed
|
||||
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
|
||||
""",
|
||||
'author': 'Dine360',
|
||||
'depends': ['base', 'website', 'pos_restaurant'],
|
||||
'depends': ['base', 'website', 'pos_restaurant', 'dine360_table'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/reservation_sequence.xml',
|
||||
|
||||
@ -48,4 +48,22 @@
|
||||
</p>
|
||||
</field>
|
||||
</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>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
- Store Keeper
|
||||
""",
|
||||
'author': 'Dine360',
|
||||
'depends': ['point_of_sale', 'pos_restaurant', 'purchase', 'stock', 'website_sale'],
|
||||
'depends': ['point_of_sale', 'pos_restaurant', 'purchase', 'stock'],
|
||||
'data': [
|
||||
'security/restaurant_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
@ -12,13 +12,19 @@ class Dine360SelfOrderController(http.Controller):
|
||||
def self_order_menu(self, **kwargs):
|
||||
"""Displays the self-order menu for a specific table/kiosk"""
|
||||
table_id = kwargs.get('table_id')
|
||||
table = False
|
||||
pos_table = False
|
||||
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 = {
|
||||
'table': table,
|
||||
'floor': table.floor_id if table else False,
|
||||
'pos_table': pos_table,
|
||||
'pos_floor': pos_table.floor_id if pos_table else False,
|
||||
}
|
||||
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";
|
||||
|
||||
// We'll use a standard self-invoking function style since it's a public web module
|
||||
(function () {
|
||||
'use strict';
|
||||
publicWidget.registry.Dine360SelfOrder = publicWidget.Widget.extend({
|
||||
selector: '#self_order_app',
|
||||
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 () => {
|
||||
const app = document.querySelector('#self_order_app');
|
||||
if (!app) return;
|
||||
|
||||
const dataEl = document.querySelector('#self_order_data');
|
||||
const config = {
|
||||
tableId: dataEl.dataset.tableId,
|
||||
tableName: dataEl.dataset.tableName,
|
||||
};
|
||||
|
||||
const state = {
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.config = {};
|
||||
this.state = {
|
||||
products: [],
|
||||
cart: [],
|
||||
activeCategory: 'all',
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
|
||||
// --- UI Elements ---
|
||||
const productList = document.querySelector('#product_list');
|
||||
const categoryFilter = document.querySelector('#category_filter');
|
||||
const cartCount = document.querySelector('#cart_count');
|
||||
const cartTotal = document.querySelector('#cart_total');
|
||||
const footerCart = document.querySelector('#footer_cart');
|
||||
const loadingOverlay = document.querySelector('#loading_overlay');
|
||||
const contentArea = document.querySelector('#self_order_content');
|
||||
start: async function () {
|
||||
await this._super.apply(this, arguments);
|
||||
const dataEl = this.el.querySelector('#self_order_data');
|
||||
if (dataEl) {
|
||||
this.config.tableId = dataEl.dataset.tableId;
|
||||
this.config.tableName = dataEl.dataset.tableName;
|
||||
}
|
||||
|
||||
// --- Init ---
|
||||
try {
|
||||
const products = await jsonrpc('/dine360/self_order/products', {});
|
||||
state.products = products;
|
||||
renderCategories();
|
||||
renderProducts();
|
||||
loadingOverlay.classList.add('d-none');
|
||||
contentArea.classList.remove('d-none');
|
||||
this.state.products = products;
|
||||
this._renderCategories();
|
||||
this._renderProducts();
|
||||
this.$('#loading_overlay').addClass('d-none');
|
||||
this.$('#self_order_content').removeClass('d-none');
|
||||
} catch (e) {
|
||||
console.error("Failed to load products", e);
|
||||
alert("Error connecting to server. Please try again later.");
|
||||
}
|
||||
},
|
||||
|
||||
// --- Functions ---
|
||||
function renderCategories() {
|
||||
const categories = ['all', ...new Set(state.products.map(p => p.pos_categ_name))];
|
||||
categoryFilter.innerHTML = categories.map(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}
|
||||
</button>
|
||||
`).join('');
|
||||
_renderCategories: function () {
|
||||
const categories = ['all', ...new Set(this.state.products.map(p => p.pos_categ_name))];
|
||||
const html = 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}">
|
||||
${cat === 'all' ? 'All Items' : cat}
|
||||
</button>
|
||||
`).join('');
|
||||
this.$('#category_filter').html(html);
|
||||
},
|
||||
|
||||
categoryFilter.querySelectorAll('button').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
state.activeCategory = btn.dataset.category;
|
||||
renderCategories();
|
||||
renderProducts();
|
||||
};
|
||||
});
|
||||
_renderProducts: function () {
|
||||
let filtered = this.state.products;
|
||||
if (this.state.activeCategory !== 'all') {
|
||||
filtered = filtered.filter(p => p.pos_categ_name === this.state.activeCategory);
|
||||
}
|
||||
if (this.state.searchTerm) {
|
||||
const term = this.state.searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(p => p.display_name.toLowerCase().includes(term));
|
||||
}
|
||||
|
||||
function renderProducts() {
|
||||
let filtered = state.products;
|
||||
if (state.activeCategory !== 'all') {
|
||||
filtered = filtered.filter(p => p.pos_categ_name === state.activeCategory);
|
||||
}
|
||||
if (state.searchTerm) {
|
||||
const term = state.searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(p => p.display_name.toLowerCase().includes(term));
|
||||
}
|
||||
|
||||
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
|
||||
const html = 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>
|
||||
`;
|
||||
} else {
|
||||
alert(response.error || "Order submission failed");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Connection lost. Please check your internet.");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
</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>
|
||||
${p.is_kitchen_item ? '<i class="fa fa-fire text-danger small opacity-50"/>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
document.querySelectorAll('.service-select').forEach(label => {
|
||||
label.onclick = () => {
|
||||
document.querySelectorAll('.service-select').forEach(l => l.classList.remove('active', 'border-warning', 'bg-warning-light'));
|
||||
label.classList.add('active', 'border-warning', 'bg-warning-light');
|
||||
};
|
||||
});
|
||||
});
|
||||
})();
|
||||
try {
|
||||
const response = await jsonrpc('/dine360/self_order/submit_order', { order_data: orderData });
|
||||
if (response.success) {
|
||||
if (this.cartModal) this.cartModal.hide();
|
||||
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">
|
||||
<!-- Data injection for JS -->
|
||||
<div id="self_order_data" class="d-none"
|
||||
t-att-data-table-id="table.id if table else ''"
|
||||
t-att-data-floor-id="floor.id if floor else ''"
|
||||
t-att-data-table-name="table.display_name if table else ''"/>
|
||||
t-att-data-table-id="pos_table.id if pos_table else ''"
|
||||
t-att-data-floor-id="pos_floor.id if pos_floor else ''"
|
||||
t-att-data-table-name="pos_table.display_name if pos_table else ''"/>
|
||||
|
||||
<!-- Header -->
|
||||
<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">
|
||||
<i class="fa fa-cutlery me-2 text-danger"/> Dine360
|
||||
</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">
|
||||
Table <t t-esc="table.name"/>
|
||||
Table <t t-esc="pos_table.name"/>
|
||||
</span>
|
||||
</div>
|
||||
<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;">
|
||||
<!-- JS injects checkout lines here -->
|
||||
</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>
|
||||
<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">
|
||||
|
||||
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:
|
||||
- db
|
||||
ports:
|
||||
- "10002:8069"
|
||||
- "10006:8069"
|
||||
environment:
|
||||
HOST: db
|
||||
USER: odoo
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user