# -*- coding: utf-8 -*- import json import logging from datetime import datetime, timedelta 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 SafeStats(dict): def __missing__(self, key): if any(term in key for term in ['list', 'recent', 'products', 'orders', 'tables', 'reservations']): return [] if any(term in key for term in ['count', 'total', 'capacity', 'rate', 'active', 'provisioning', 'expired']): return 0 if 'formatted' in key: return '$0.00' return '' 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: return request.redirect('/') return response def get_dashboard_context(active_page='dashboard'): """Helper to collect unified metrics and data for all Dine360 dashboard pages.""" db_name = request.env.cr.dbname or '' is_saas_master = ('Dine360Restaurants' in db_name) or ( 'saas.restaurant' in request.env and bool(request.env['saas.restaurant'].sudo().search_count([])) ) user = request.env.user.sudo() company_name = request.env.company.name if request.env.company else 'Dine360' # 1. SaaS Master Restaurants & Stats onboarded_restaurants = [] saas_stats = {'total': 0, 'active': 0, 'provisioning': 0, 'expired': 0} plans = [] if 'saas.plan' in request.env: try: for p in request.env['saas.plan'].sudo().search([]): plans.append({ 'id': p.id, 'name': p.name, 'price_monthly': p.price_monthly, 'price_yearly': p.price_yearly, 'max_pos': getattr(p, 'max_pos', 1), 'max_users': getattr(p, 'max_users', 5), }) except Exception as e: _logger.warning("Error fetching saas.plan: %s", e) 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 'N/A' onboarded_restaurants.append({ 'id': r.id, 'name': r.name, 'owner_name': r.owner_name or 'Admin', 'email': r.email or '', 'phone': r.phone or '', 'city': r.city or '', '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 'Standard', '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: %s", e) first_res = onboarded_restaurants[0] if onboarded_restaurants else None # SaaS master real activities and counts currency = request.env.company.currency_id if request.env.company else None symbol = currency.symbol if currency and currency.symbol else '₹' res_count = 0 if 'restaurant.reservation' in request.env: try: res_count = request.env['restaurant.reservation'].sudo().search_count([]) except Exception: pass orders_count = 0 total_sales = 0.0 if 'sale.order' in request.env: try: orders = request.env['sale.order'].sudo().search([]) orders_count += len(orders) total_sales += sum(orders.mapped('amount_total')) except Exception: pass if 'pos.order' in request.env: try: pos_orders = request.env['pos.order'].sudo().search([]) orders_count += len(pos_orders) total_sales += sum(pos_orders.mapped('amount_total')) except Exception: pass avg_val = (total_sales / orders_count) if orders_count else 0.0 recent_activities = [] if is_saas_master and 'saas.restaurant' in request.env: try: for r in request.env['saas.restaurant'].sudo().search([], order='create_date desc', limit=5): recent_activities.append({ 'title': f"Restaurant {r.status.capitalize() if r.status else 'Registered'}", 'subtitle': f"{r.name} ({r.database_name or 'N/A'})", 'time': r.create_date.strftime('%d %b, %H:%M') if getattr(r, 'create_date', False) else '', 'badge': (r.status or 'active').capitalize(), 'badge_type': 'completed' if r.status == 'active' else 'info', 'icon': 'fa fa-cutlery' }) except Exception: pass tot_restaurants = saas_stats.get('total', 0) act_restaurants = saas_stats.get('active', 0) onboard_pct = int((act_restaurants / tot_restaurants) * 100) if tot_restaurants > 0 else 0 saas_dashboard_data = { 'first_restaurant': first_res, 'reservations_count': res_count, 'orders_count': f"{orders_count:,}", 'avg_order_value': f"{symbol}{avg_val:,.2f}", 'onboarding_percent': onboard_pct, 'date_range': f"{(datetime.now() - timedelta(days=30)).strftime('%b %d, %Y')} - {datetime.now().strftime('%b %d, %Y')}", 'system_status': 'All systems operational', 'recent_activities': recent_activities, } # 2. Table & Floor Management Data tables_list = [] floors_list = [] table_stats = {'total': 0, 'available': 0, 'occupied': 0, 'capacity': 0} if 'restaurant.floor' in request.env: try: for fl in request.env['restaurant.floor'].sudo().search([]): t_count = len(fl.table_ids) floors_list.append({ 'id': fl.id, 'name': fl.name, 'table_count': t_count, }) except Exception: pass if 'restaurant.table' in request.env: try: all_tables = request.env['restaurant.table'].sudo().search([]) table_stats['total'] = len(all_tables) total_seats = 0 occupied_count = 0 for t in all_tables: seats = getattr(t, 'seats', 4) or 4 total_seats += seats is_occ = getattr(t, 'is_occupied', False) or getattr(t, 'state', '') in ('occupied', 'reserved') if is_occ: occupied_count += 1 tables_list.append({ 'id': t.id, 'name': t.name or f"T-{t.id}", 'seats': seats, 'floor_name': t.floor_id.name if t.floor_id else 'Main Hall', 'floor_id': t.floor_id.id if t.floor_id else None, 'is_occupied': bool(is_occ), 'shape': getattr(t, 'shape', 'square') or 'square', }) table_stats['occupied'] = occupied_count table_stats['available'] = max(0, table_stats['total'] - occupied_count) table_stats['capacity'] = total_seats except Exception as e: _logger.warning("Error fetching restaurant.table: %s", e) # 3. Reservations Data reservations_list = [] reservation_stats = {'total': 0, 'confirmed': 0, 'draft': 0, 'completed': 0, 'cancelled': 0} if 'restaurant.reservation' in request.env: try: records = request.env['restaurant.reservation'].sudo().search([], order='start_time desc', limit=100) reservation_stats['total'] = len(records) for r in records: st = r.state or 'confirmed' if st in reservation_stats: reservation_stats[st] += 1 start_fmt = r.start_time.strftime('%I:%M %p') if r.start_time else '' date_fmt = r.start_time.strftime('%d %b %Y') if r.start_time else '' whatsapp_url = getattr(r, 'whatsapp_url', False) if not whatsapp_url and getattr(r, 'phone', False): cleaned_phone = r.phone.replace('+', '').replace(' ', '').replace('-', '') whatsapp_url = f"https://wa.me/{cleaned_phone}?text=Hello%20{r.customer_name or 'Guest'},%20your%20table%20reservation%20is%20confirmed!" reservations_list.append({ 'id': r.id, 'name': r.name or f"RES-{r.id:04d}", 'customer_name': r.customer_name or 'Guest', 'phone': r.phone or '', 'email': r.email or '', 'num_people': r.num_people or 2, 'start_time_fmt': start_fmt, 'date_fmt': date_fmt, 'floor_name': r.floor_id.name if getattr(r, 'floor_id', False) else '', 'table_name': r.table_id.name if getattr(r, 'table_id', False) else (r.table_ids[0].name if getattr(r, 'table_ids', False) else ''), 'occasion': r.occasion or 'casual', 'special_requests': r.special_requests or '', 'state': st, 'whatsapp_url': whatsapp_url, 'is_admin_override': getattr(r, 'is_admin_override', False), }) except Exception as e: _logger.warning("Error fetching reservations: %s", e) # 4. Online Orders Data online_orders_data = { 'has_online_orders': False, 'pending_count': 0, 'confirmed_count': 0, 'today_count': 0, 'total_revenue_formatted': f"{symbol}0.00", 'avg_order_value': f"{symbol}0.00", 'orders': [], 'shop_url': '/shop', } try: if 'sale.order' in request.env: SaleOrder = request.env['sale.order'].sudo() domain = [('order_source', '=', 'online')] if 'order_source' in SaleOrder._fields else [('website_id', '!=', False)] all_online = SaleOrder.search(domain) online_orders_data['has_online_orders'] = bool(all_online) pending_domain = domain + [('state', 'in', ['draft', 'sent'])] confirmed_domain = domain + [('state', 'in', ['sale', 'done'])] online_orders_data['pending_count'] = SaleOrder.search_count(pending_domain) online_orders_data['confirmed_count'] = SaleOrder.search_count(confirmed_domain) today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) today_orders = SaleOrder.search(domain + [('date_order', '>=', today_start)]) online_orders_data['today_count'] = len(today_orders) if today_orders else len(all_online) revenue_orders = today_orders if today_orders else all_online tot_rev = sum(revenue_orders.mapped('amount_total')) if revenue_orders else 0.0 online_orders_data['total_revenue_formatted'] = f"{symbol}{tot_rev:,.2f}" if revenue_orders: avg_ticket = tot_rev / len(revenue_orders) online_orders_data['avg_order_value'] = f"{symbol}{avg_ticket:,.2f}" recent = SaleOrder.search(domain, limit=20, order='id desc') if recent: orders_list = [] for o in recent: orders_list.append({ 'id': o.id, 'name': o.name, 'customer': o.partner_id.name or 'Guest', 'phone': o.partner_id.phone or o.partner_id.mobile or '', 'amount': f"{symbol}{o.amount_total:,.2f}", 'status': getattr(o, 'online_order_status', o.state) or 'confirmed', 'date': o.date_order.strftime('%d %b, %H:%M') if o.date_order else '', 'url': f"/web#id={o.id}&model=sale.order&view_type=form", }) online_orders_data['orders'] = orders_list except Exception as e: _logger.warning("Error fetching sale.order: %s", e) # 5. Backups Data backups_list = [] if is_saas_master and 'saas.backup' in request.env: try: records = request.env['saas.backup'].sudo().search([], order='timestamp desc', limit=20) for b in records: db_name = b.database_name or (b.restaurant_id.database_name if getattr(b, 'restaurant_id', False) else '') fname = b.backup_file or f"{db_name}_backup.zip" backups_list.append({ 'id': b.id, 'database_name': db_name, 'filename': fname, 'backup_type': b.storage_type.upper() if getattr(b, 'storage_type', False) else 'Local', 'size': 'Archive', 'date': b.timestamp.strftime('%d %b %Y, %I:%M %p') if getattr(b, 'timestamp', False) else '', 'download_url': b.s3_url or '#', }) except Exception as e: _logger.warning("Error fetching saas.backup: %s", e) # 6. Low stock products low_stock = [] if 'product.product' in request.env: try: low_stock = request.env['product.product'].sudo().search([ ('type', '=', 'product'), ('qty_available', '<=', 5) ], limit=5) except Exception: pass stats = SafeStats({ 'company_name': company_name, 'low_stock_products': low_stock, }) return { 'active_page': active_page, 'user_id': user, 'is_saas_master': is_saas_master, 'saas_stats': saas_stats, 'saas_dashboard_data': saas_dashboard_data, 'onboarded_restaurants': onboarded_restaurants, 'plans': plans, 'table_stats': table_stats, 'tables': tables_list, 'available_tables': [t for t in tables_list if not t.get('is_occupied')], 'floors': floors_list, 'reservation_stats': reservation_stats, 'reservations': reservations_list, 'online_orders_data': online_orders_data, 'current_date_str': datetime.now().strftime('%d %b %Y'), 'stats': stats, 'low_stock_products': low_stock, 'backups': backups_list, } def render_dashboard(request): 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: 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') if user.has_group('dine360_restaurant.group_restaurant_kitchen'): return request.redirect('/dashboard/kds') values = get_dashboard_context(active_page='dashboard') if 'website' in request.env: values['website'] = request.env['website'].get_current_website() return request.render('dine360_dashboard.image_home_template', values) class Dine360DashboardController(http.Controller): # ------------------------------------------------------------- # 1. Main Home & Overview # ------------------------------------------------------------- @http.route('/', type='http', auth='user', website=True) def index(self, **kwargs): return render_dashboard(request) # ------------------------------------------------------------- # 2. Restaurants Module Page # ------------------------------------------------------------- @http.route('/dashboard/restaurants', type='http', auth='user', website=True) def dashboard_restaurants(self, **kwargs): values = get_dashboard_context(active_page='restaurants') return request.render('dine360_dashboard.restaurants_page_template', values) # ------------------------------------------------------------- # 3. Reservations Module Page # ------------------------------------------------------------- @http.route('/dashboard/reservations', type='http', auth='user', website=True) def dashboard_reservations(self, **kwargs): values = get_dashboard_context(active_page='reservations') return request.render('dine360_dashboard.reservations_page_template', values) # ------------------------------------------------------------- # 4. Table Management Module Page # ------------------------------------------------------------- @http.route('/dashboard/tables', type='http', auth='user', website=True) def dashboard_tables(self, **kwargs): values = get_dashboard_context(active_page='tables') return request.render('dine360_dashboard.tables_page_template', values) # ------------------------------------------------------------- # 5. Apps Marketplace Page # ------------------------------------------------------------- @http.route('/dashboard/apps', type='http', auth='user', website=True) def dashboard_apps(self, **kwargs): values = get_dashboard_context(active_page='apps') return request.render('dine360_dashboard.apps_page_template', values) # ------------------------------------------------------------- # 6. Settings Page # ------------------------------------------------------------- @http.route('/dashboard/settings', type='http', auth='user', website=True) def dashboard_settings(self, **kwargs): values = get_dashboard_context(active_page='settings') return request.render('dine360_dashboard.settings_page_template', values) # ------------------------------------------------------------- # 7. Subscription Plans & Billing Page # ------------------------------------------------------------- @http.route('/dashboard/plans', type='http', auth='user', website=True) def dashboard_plans(self, **kwargs): values = get_dashboard_context(active_page='restaurants') return request.render('dine360_dashboard.plans_page_template', values) # ------------------------------------------------------------- # 8. Online Orders Page # ------------------------------------------------------------- @http.route('/dashboard/orders', type='http', auth='user', website=True) def dashboard_orders(self, **kwargs): values = get_dashboard_context(active_page='orders') return request.render('dine360_dashboard.orders_page_template', values) # ------------------------------------------------------------- # 9. Database Backups Page # ------------------------------------------------------------- @http.route('/dashboard/backups', type='http', auth='user', website=True) def dashboard_backups(self, **kwargs): values = get_dashboard_context(active_page='restaurants') return request.render('dine360_dashboard.backups_page_template', values) # ------------------------------------------------------------- # 10. Kitchen Display (KDS) Safe Handler # ------------------------------------------------------------- @http.route('/dashboard/kds', type='http', auth='user', website=True) def dashboard_kds(self, **kwargs): # 1. Try dedicated dine360_kds action if installed kds_action = request.env['ir.model.data'].sudo().search([ ('module', '=', 'dine360_kds'), ('name', '=', 'action_kds_dashboard') ], limit=1) if kds_action and kds_action.res_id: return request.redirect(f'/web#action={kds_action.res_id}') # 2. Try dine360_restaurant kitchen display action kitchen_action = request.env['ir.model.data'].sudo().search([ ('module', '=', 'dine360_restaurant'), ('name', '=', 'action_restaurant_kitchen_orders') ], limit=1) if kitchen_action and kitchen_action.res_id: return request.redirect(f'/web#action={kitchen_action.res_id}') # 3. Fallback to POS register return request.redirect('/pos/web') # ------------------------------------------------------------- # APIs: Interactive Operations & Form Handlers # ------------------------------------------------------------- @http.route('/dashboard/api/toggle_table', type='http', auth='user', methods=['POST'], csrf=False) def api_toggle_table(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params table_id = int(data.get('table_id', 0)) is_occupied = data.get('is_occupied') if isinstance(is_occupied, str): is_occupied = (is_occupied.lower() == 'true') if table_id and 'restaurant.table' in request.env: table = request.env['restaurant.table'].sudo().browse(table_id) if table.exists(): if hasattr(table, 'is_occupied'): table.write({'is_occupied': is_occupied}) elif hasattr(table, 'state'): table.write({'state': 'occupied' if is_occupied else 'available'}) return request.make_response( json.dumps({'success': True, 'table_id': table_id, 'is_occupied': is_occupied}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error in api_toggle_table: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] ) @http.route('/dashboard/api/update_reservation_status', type='http', auth='user', methods=['POST'], csrf=False) def api_update_reservation_status(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params res_id = int(data.get('reservation_id', 0)) status = data.get('status') if res_id and status and 'restaurant.reservation' in request.env: reservation = request.env['restaurant.reservation'].sudo().browse(res_id) if reservation.exists(): reservation.write({'state': status}) return request.make_response( json.dumps({'success': True, 'reservation_id': res_id, 'status': status}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error in api_update_reservation_status: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] ) @http.route('/dashboard/api/create_reservation', type='http', auth='user', methods=['POST'], csrf=False) def api_create_reservation(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params customer_name = data.get('customer_name') phone = data.get('phone') email = data.get('email') num_people = int(data.get('num_people', 2)) start_time_str = data.get('start_time') occasion = data.get('occasion', 'casual') table_id = int(data.get('table_id')) if data.get('table_id') else False special_requests = data.get('special_requests', '') # Parse start time and compute 90 min end time if start_time_str: try: start_dt = datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M') except Exception: start_dt = datetime.now() + timedelta(hours=2) else: start_dt = datetime.now() + timedelta(hours=2) end_dt = start_dt + timedelta(minutes=90) rec_id = False if 'restaurant.reservation' in request.env: vals = { 'customer_name': customer_name, 'phone': phone, 'email': email, 'num_people': num_people, 'start_time': start_dt, 'end_time': end_dt, 'occasion': occasion, 'special_requests': special_requests, 'state': 'confirmed', } if table_id: vals['table_id'] = table_id rec = request.env['restaurant.reservation'].sudo().create(vals) rec_id = rec.id return request.make_response( json.dumps({'success': True, 'id': rec_id, 'message': 'Reservation confirmed successfully!'}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error creating reservation: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] ) @http.route('/dashboard/api/create_restaurant', type='http', auth='user', methods=['POST'], csrf=False) def api_create_restaurant(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params name = data.get('name') owner_name = data.get('owner_name') email = data.get('email') phone = data.get('phone') plan_id = int(data.get('plan_id', 1)) billing_cycle = data.get('billing_cycle', 'monthly') subdomain = data.get('subdomain', '').lower().strip() days = 30 if billing_cycle == 'monthly' else 365 start_d = fields.Date.today() expiry_d = start_d + timedelta(days=days) rec_id = False if 'saas.restaurant' in request.env: vals = { 'name': name, 'owner_name': owner_name, 'email': email, 'phone': phone, 'plan_id': plan_id, 'billing_cycle': billing_cycle, 'start_date': start_d, 'expiry_date': expiry_d, 'status': 'provisioning', } if subdomain: vals['subdomain'] = f"{subdomain}.dine360.com" vals['database_name'] = f"dine360_restaurant_{subdomain}" rec = request.env['saas.restaurant'].sudo().create(vals) rec_id = rec.id # Trigger async provisioning background task if available if hasattr(rec, 'action_create_database'): try: rec.action_create_database() except Exception as err: _logger.warning("Database provisioning background trigger: %s", err) return request.make_response( json.dumps({'success': True, 'id': rec_id, 'message': 'Restaurant registered and database provisioning initialized!'}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error onboarding restaurant: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] ) @http.route('/dashboard/api/create_table', type='http', auth='user', methods=['POST'], csrf=False) def api_create_table(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params name = data.get('name') floor_id = int(data.get('floor_id')) if data.get('floor_id') else False seats = int(data.get('seats', 4)) shape = data.get('shape', 'square') rec_id = False if 'restaurant.table' in request.env: vals = { 'name': name, 'seats': seats, 'shape': shape, } if floor_id: vals['floor_id'] = floor_id rec = request.env['restaurant.table'].sudo().create(vals) rec_id = rec.id return request.make_response( json.dumps({'success': True, 'id': rec_id, 'message': 'Table added successfully!'}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error creating table: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] ) @http.route('/dashboard/api/save_settings', type='http', auth='user', methods=['POST'], csrf=False) def api_save_settings(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params ICP = request.env['ir.config_parameter'].sudo() for key, val in data.items(): ICP.set_param(f"dine360.{key}", str(val)) return request.make_response( json.dumps({'success': True, 'message': 'All Dine360 settings saved successfully!'}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error saving settings: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] ) @http.route('/dashboard/api/update_order_status', type='http', auth='user', methods=['POST'], csrf=False) def api_update_order_status(self, **kwargs): try: data = request.httprequest.get_json(silent=True) or request.params order_id = int(data.get('order_id', 0)) status = data.get('status') if order_id and 'sale.order' in request.env: order = request.env['sale.order'].sudo().browse(order_id) if order.exists(): if hasattr(order, 'online_order_status'): order.write({'online_order_status': status}) elif status == 'confirmed' and order.state == 'draft': order.action_confirm() return request.make_response( json.dumps({'success': True, 'order_id': order_id, 'status': status}), headers=[('Content-Type', 'application/json')] ) except Exception as e: _logger.exception("Error updating order status: %s", e) return request.make_response( json.dumps({'success': False, 'message': str(e)}), headers=[('Content-Type', 'application/json')] )