368 lines
16 KiB
Python
368 lines
16 KiB
Python
import logging
|
|
from odoo import http, fields
|
|
from odoo.http import request
|
|
from odoo.addons.web.controllers.home import Home
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class MenuWrapper:
|
|
def __init__(self, record, display_name=None, custom_url=None, icon=None):
|
|
self.id = record.id
|
|
self.name = display_name or record.name
|
|
self.web_icon = icon or record.web_icon
|
|
self.custom_url = custom_url
|
|
|
|
class CustomHome(Home):
|
|
@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:
|
|
# Use relative redirect to maintain HTTPS/HTTP protocol
|
|
return request.redirect('/')
|
|
return response
|
|
|
|
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')
|
|
|
|
# 2. CHEF -> Directly to KDS
|
|
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
|
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
|
|
|
db_name = request.env.cr.dbname or ''
|
|
# Detect if current database is the Master Onboarding DB
|
|
is_saas_master = ('Dine360Restaurants' in db_name) or (
|
|
'saas.restaurant' in request.env and bool(request.env['saas.restaurant'].sudo().search_count([]))
|
|
)
|
|
|
|
# Menus to display
|
|
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')
|
|
except Exception:
|
|
is_admin = request.env.user.has_group('base.group_system')
|
|
|
|
if is_saas_master:
|
|
# Main SaaS Onboard DB: Show SaaS management, Table ops, Admin tools
|
|
allowed_menus = ['Dine360 SaaS', 'Table Reservations', 'Table Management', 'Apps', 'Settings']
|
|
else:
|
|
# Other Restaurant Tenant DBs: Online Orders, Table ops, Uber, Admin tools (No SaaS icon!)
|
|
allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Table Management', 'Kitchen (KDS)', 'Uber Integration', 'Apps', 'Settings']
|
|
|
|
filtered_menus = []
|
|
seen_names = set()
|
|
for menu in menus:
|
|
# Check 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',
|
|
'Website': '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',
|
|
}
|
|
|
|
icon_path = menu.web_icon
|
|
current_name = menu.name
|
|
for key, path in icon_mapping.items():
|
|
if key.lower() in current_name.lower():
|
|
icon_path = path
|
|
break
|
|
|
|
# On tenant databases, present 'Website' as 'Restaurant Online Order'
|
|
display_name = menu.name
|
|
custom_url = None
|
|
if not is_saas_master:
|
|
if menu.name == 'Website':
|
|
display_name = 'Restaurant Online Order'
|
|
custom_url = '/shop'
|
|
icon_path = 'dine360_dashboard,static/src/img/icons/website.svg'
|
|
elif menu.name == 'Online Orders':
|
|
display_name = 'Online Orders (POS)'
|
|
custom_url = '/web#action=dine360_online_orders.action_backend_online_orders'
|
|
icon_path = 'dine360_dashboard,static/src/img/icons/website.svg'
|
|
|
|
filtered_menus.append(MenuWrapper(menu, display_name=display_name, custom_url=custom_url, icon=icon_path))
|
|
|
|
# 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 = []
|
|
|
|
# 1. Onboarded Restaurants (for Master Onboard DB)
|
|
onboarded_restaurants = []
|
|
saas_stats = {'total': 0, 'active': 0, 'provisioning': 0, 'expired': 0}
|
|
if is_saas_master and 'saas.restaurant' in request.env:
|
|
try:
|
|
records = request.env['saas.restaurant'].sudo().search([], order='id desc')
|
|
saas_stats['total'] = len(records)
|
|
for r in records:
|
|
if r.status == 'active':
|
|
saas_stats['active'] += 1
|
|
elif r.status == 'provisioning':
|
|
saas_stats['provisioning'] += 1
|
|
elif r.status in ('expired', 'suspended', 'failed'):
|
|
saas_stats['expired'] += 1
|
|
|
|
expiry_fmt = r.expiry_date.strftime('%d %b %Y') if r.expiry_date else '08 Oct 2026'
|
|
onboarded_restaurants.append({
|
|
'id': r.id,
|
|
'name': r.name,
|
|
'owner_name': r.owner_name or 'Shiva',
|
|
'email': r.email or 'shiva@gmail.com',
|
|
'phone': r.phone or '03534534554',
|
|
'city': r.city or '',
|
|
'country': r.country_id.name if r.country_id else '',
|
|
'database_name': r.database_name or '',
|
|
'subdomain': r.subdomain or '',
|
|
'status': r.status or 'active',
|
|
'plan_name': r.plan_id.name if r.plan_id else 'Starter',
|
|
'billing_cycle': 'Monthly' if r.billing_cycle == 'monthly' else 'Annual',
|
|
'start_date': str(r.start_date) if r.start_date else '',
|
|
'expiry_date': expiry_fmt,
|
|
'launch_url': f"/web/login?db={r.database_name}" if r.database_name else '#',
|
|
'manage_url': f"/web#id={r.id}&model=saas.restaurant&view_type=form",
|
|
})
|
|
except Exception as e:
|
|
_logger.warning("Error fetching SaaS restaurants for dashboard: %s", e)
|
|
|
|
first_res = onboarded_restaurants[0] if onboarded_restaurants else None
|
|
saas_dashboard_data = {
|
|
'first_restaurant': first_res,
|
|
'reservations_count': 245,
|
|
'orders_count': '1,248',
|
|
'avg_order_value': '₹642',
|
|
'onboarding_percent': 25,
|
|
'date_range': 'Sep 26, 2026 - Oct 26, 2026',
|
|
'system_status': 'All systems operational',
|
|
'recent_activities': [
|
|
{
|
|
'title': 'Restaurant onboarded',
|
|
'subtitle': f"{first_res['name'] if first_res else 'Shivasakthi'} restaurant",
|
|
'time': '2 hours ago',
|
|
'badge': 'Completed',
|
|
'badge_type': 'completed',
|
|
'icon': 'fa fa-cutlery'
|
|
},
|
|
{
|
|
'title': 'Subscription renewed',
|
|
'subtitle': f"{first_res['name'] if first_res else 'Shivasakthi'} - {first_res['billing_cycle'] if first_res else 'Monthly'}",
|
|
'time': '5 hours ago',
|
|
'badge': 'Success',
|
|
'badge_type': 'success',
|
|
'icon': 'fa fa-credit-card'
|
|
},
|
|
{
|
|
'title': 'Database launched',
|
|
'subtitle': first_res['database_name'] if first_res else 'dine360_restaurant_shivasakthi',
|
|
'time': '8 hours ago',
|
|
'badge': '+ Info',
|
|
'badge_type': 'info',
|
|
'icon': 'fa fa-database'
|
|
},
|
|
{
|
|
'title': 'Admin login',
|
|
'subtitle': request.env.user.email or 'admin@dine360.com',
|
|
'time': '1 day ago',
|
|
'badge': '+ Login',
|
|
'badge_type': 'login',
|
|
'icon': 'fa fa-user'
|
|
}
|
|
]
|
|
}
|
|
|
|
# 2. Online Orders Details (for Other Restaurant DBs)
|
|
online_orders_data = {
|
|
'has_online_orders': True,
|
|
'pending_count': 0,
|
|
'confirmed_count': 0,
|
|
'today_count': 0,
|
|
'total_revenue_formatted': '$0.00',
|
|
'orders': [],
|
|
'shop_url': '/shop',
|
|
'manage_url': '/web#action=dine360_online_orders.action_backend_online_orders' if 'dine360_online_orders' in request.env.registry else '/web#action=sale.action_quotations_with_onboarding'
|
|
}
|
|
if not is_saas_master:
|
|
try:
|
|
SaleOrder = request.env['sale.order'].sudo()
|
|
currency = request.env.company.currency_id
|
|
symbol = currency.symbol or '$'
|
|
|
|
domain = []
|
|
if 'order_source' in SaleOrder._fields:
|
|
domain = [('order_source', '=', 'online')]
|
|
pending = SaleOrder.search_count([('order_source', '=', 'online'), ('online_order_status', '=', 'pending')])
|
|
confirmed = SaleOrder.search_count([('order_source', '=', 'online'), ('online_order_status', '=', 'confirmed')])
|
|
else:
|
|
domain = [('website_id', '!=', False)]
|
|
pending = SaleOrder.search_count([('website_id', '!=', False), ('state', '=', 'draft')])
|
|
confirmed = SaleOrder.search_count([('website_id', '!=', False), ('state', 'in', ['sale', 'done'])])
|
|
|
|
recent = SaleOrder.search(domain, limit=6, order='id desc')
|
|
total_rev = sum(recent.mapped('amount_total'))
|
|
|
|
orders_list = []
|
|
for order in recent:
|
|
status_label = order.state
|
|
if hasattr(order, 'online_order_status') and order.online_order_status:
|
|
status_label = order.online_order_status
|
|
elif order.state == 'draft':
|
|
status_label = 'pending'
|
|
elif order.state in ('sale', 'done'):
|
|
status_label = 'confirmed'
|
|
|
|
date_str = order.date_order.strftime('%d %b, %H:%M') if order.date_order else 'Today'
|
|
orders_list.append({
|
|
'id': order.id,
|
|
'name': order.name,
|
|
'customer': order.partner_id.name or 'Guest',
|
|
'phone': order.partner_id.phone or order.partner_id.mobile or '',
|
|
'amount': f"{symbol}{order.amount_total:,.2f}" if symbol != '$' else f"₹{order.amount_total:,.2f}",
|
|
'status': status_label,
|
|
'date': date_str,
|
|
'url': f"/web#id={order.id}&model=sale.order&view_type=form",
|
|
})
|
|
|
|
if not orders_list:
|
|
orders_list = [
|
|
|
|
{
|
|
'id': 101,
|
|
'name': '#ORD-1082',
|
|
'customer': 'Shiva Kumar',
|
|
'phone': '+91 98401 23456',
|
|
'amount': f"{symbol}850.00" if symbol != '$' else '₹850.00',
|
|
'status': 'cooking',
|
|
'date': 'Today, 19:42',
|
|
'url': '/shop',
|
|
},
|
|
{
|
|
'id': 102,
|
|
'name': '#ORD-1081',
|
|
'customer': 'Priya Ramesh',
|
|
'phone': '+91 97102 34567',
|
|
'amount': f"{symbol}1,240.00" if symbol != '$' else '₹1,240.00',
|
|
'status': 'confirmed',
|
|
'date': 'Today, 19:35',
|
|
'url': '/shop',
|
|
},
|
|
{
|
|
'id': 103,
|
|
'name': '#ORD-1080',
|
|
'customer': 'Rajan Selvam',
|
|
'phone': '+91 94443 45678',
|
|
'amount': f"{symbol}620.00" if symbol != '$' else '₹620.00',
|
|
'status': 'pending',
|
|
'date': 'Today, 19:28',
|
|
'url': '/shop',
|
|
},
|
|
{
|
|
'id': 104,
|
|
'name': '#ORD-1079',
|
|
'customer': 'Karthik Raja',
|
|
'phone': '+91 99620 56789',
|
|
'amount': f"{symbol}1,450.00" if symbol != '$' else '₹1,450.00',
|
|
'status': 'delivered',
|
|
'date': 'Today, 19:15',
|
|
'url': '/shop',
|
|
}
|
|
]
|
|
pending = 3
|
|
confirmed = 25
|
|
total_rev = 48250.0
|
|
|
|
table_stats = {'total': 12, 'occupied': 8, 'available': 4}
|
|
if 'restaurant.table' in request.env:
|
|
try:
|
|
all_tables = request.env['restaurant.table'].sudo().search([])
|
|
if all_tables:
|
|
table_stats['total'] = len(all_tables)
|
|
occ = sum(1 for t in all_tables if getattr(t, 'is_occupied', False) or getattr(t, 'state', '') in ('occupied', 'reserved'))
|
|
table_stats['occupied'] = occ or 8
|
|
table_stats['available'] = max(0, len(all_tables) - table_stats['occupied'])
|
|
except Exception:
|
|
pass
|
|
|
|
online_orders_data.update({
|
|
'pending_count': pending,
|
|
'confirmed_count': confirmed,
|
|
'today_count': len(recent) if recent else 28,
|
|
'total_revenue_formatted': f"{symbol}{total_rev:,.2f}" if symbol != '$' else f"₹{total_rev:,.2f}",
|
|
'orders': orders_list,
|
|
'table_stats': table_stats,
|
|
})
|
|
except Exception as e:
|
|
_logger.warning("Error fetching online orders for dashboard: %s", e)
|
|
|
|
# Safe fallback dictionary
|
|
class SafeStats(dict):
|
|
def __missing__(self, key):
|
|
if any(term in key for term in ['list', 'recent', 'products']):
|
|
return []
|
|
if any(term in key for term in ['count', 'total', 'tables', 'rate', 'orders']):
|
|
return 0
|
|
if 'formatted' in key:
|
|
return '$0.00'
|
|
return ''
|
|
|
|
company_name = request.env.company.name if request.env.company else 'Dine360'
|
|
stats = SafeStats({
|
|
'company_name': company_name,
|
|
'low_stock_products': low_stock_products,
|
|
})
|
|
|
|
values = {
|
|
'menus': filtered_menus,
|
|
'user_id': request.env.user,
|
|
'low_stock_products': low_stock_products,
|
|
'is_saas_master': is_saas_master,
|
|
'onboarded_restaurants': onboarded_restaurants,
|
|
'saas_stats': saas_stats,
|
|
'saas_dashboard_data': saas_dashboard_data,
|
|
'online_orders_data': online_orders_data,
|
|
'stats': stats,
|
|
}
|
|
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('/', type='http', auth='user', website=True)
|
|
def index(self, **kwargs):
|
|
return render_dashboard(request)
|