Alaguraj0361 dd4e83d2da Add Dine360 Table Reservation Theme Customizer
- Introduced a new module for customizing the Table Reservation theme with primary and secondary branding colors.
- Updated the __manifest__.py to include dependencies and module details.
- Created models for website settings to manage theme colors.
- Added views for configuration settings and menu items for easy access.
- Enhanced reservation templates to apply custom styles dynamically.
- Implemented a verification script to check if the theme is applied correctly.
- Updated docker-compose.yml to change the exposed port for the Odoo service.
2026-07-03 22:52:18 +05:30

293 lines
13 KiB
Python

# -*- coding: utf-8 -*-
import string
import random
import logging
import threading
from odoo import models, fields, api, registry, SUPERUSER_ID
from odoo.exceptions import UserError
from odoo.service import db
from odoo.tools import config
from odoo.modules.registry import Registry
_logger = logging.getLogger(__name__)
def _async_provisioning_worker(parent_db, saas_res_id, db_name, modules, company_vals, admin_vals, timezone, admin_pass):
_logger.info(f"Starting async provisioning background thread for database: {db_name}")
try:
# 1. Run module installation via Odoo CLI subprocess to avoid registry/cache pollution
import subprocess
# Remove 'base' from modules list if present since exp_create_database already installs it
cli_modules = [m for m in modules if m != 'base']
cmd = ["odoo", "-i", ",".join(cli_modules), "-d", db_name, "--db_host=db", "-r", "odoo", "-w", "odoo", "--stop-after-init"]
_logger.info(f"Executing background CLI installation: {' '.join(cmd)}")
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode != 0:
raise Exception(f"Odoo CLI installation failed (exit code {result.returncode}): {result.stderr or result.stdout}")
_logger.info(f"Background thread: Base modules installed on {db_name}. Configuring company & admin...")
# 2. Configure Company and Admin User inside the new database
tenant_registry = registry(db_name)
with tenant_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
# Company setup
company = env['res.company'].search([], limit=1)
company.write(company_vals)
# Admin user setup (ID 2 is usually admin)
admin_user = env['res.users'].browse(2)
admin_user.write(admin_vals)
# Set timezone
admin_user.partner_id.tz = timezone or 'America/New_York'
# Force Odoo registry signaling update
env.registry.registry_invalidated = True
env.registry.signal_changes()
cr.commit()
# Force a registry reload locally in memory to sync the active process immediately
_logger.info(f"Background thread: Forcing local registry reload in memory for {db_name}...")
Registry.new(db_name)
_logger.info(f"Background thread: Configuration completed for {db_name}. Activating SaaS Master record...")
# 3. Update SaaS Master record status and send Welcome Email
parent_registry = registry(parent_db)
with parent_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
saas_record = env['saas.restaurant'].browse(saas_res_id)
saas_record.write({'status': 'active'})
# Send Welcome Email
template = env.ref('dine360_saas_master.saas_welcome_email_template', raise_if_not_found=False)
if template:
saas_record.with_context(temp_password=admin_pass).message_post_with_source(
source_ref=template,
subtype_xmlid='mail.mt_comment'
)
cr.commit()
_logger.info(f"Background thread: Async provisioning finished successfully for {db_name}!")
except Exception as e:
_logger.error(f"Background thread: Provisioning failed for database {db_name}: {str(e)}")
# Drop the corrupted database shell
try:
db.exp_drop(db_name)
except Exception as drop_err:
_logger.error(f"Background thread: Failed to drop database {db_name} on cleanup: {str(drop_err)}")
# Update SaaS record to failed
try:
parent_registry = registry(parent_db)
with parent_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
saas_record = env['saas.restaurant'].browse(saas_res_id)
saas_record.write({'status': 'failed'})
saas_record.message_post(body=f"Failed background database provisioning: {str(e)}")
cr.commit()
except Exception as update_err:
_logger.error(f"Background thread: Failed to mark status as failed on SaaS record: {str(update_err)}")
class SaasRestaurant(models.Model):
_name = 'saas.restaurant'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Dine360 SaaS Restaurant Tenant'
name = fields.Char(string='Restaurant Name', required=True, tracking=True)
owner_name = fields.Char(string='Owner Name', required=True, tracking=True)
email = fields.Char(string='Owner Email', required=True, tracking=True)
phone = fields.Char(string='Phone')
street = fields.Char(string='Street')
city = fields.Char(string='City')
country_id = fields.Many2one('res.country', string='Country')
plan_id = fields.Many2one('saas.plan', string='Subscription Plan', required=True, tracking=True)
billing_cycle = fields.Selection([
('monthly', 'Monthly Billing'),
('yearly', 'Annual Billing')
], string='Billing Cycle', default='monthly', required=True)
database_name = fields.Char(string='PostgreSQL DB Name', readonly=True, copy=False)
subdomain = fields.Char(string='Subdomain', readonly=True, copy=False)
status = fields.Selection([
('draft', 'Draft'),
('provisioning', 'Provisioning'),
('active', 'Active'),
('suspended', 'Suspended'),
('expired', 'Expired'),
('failed', 'Failed')
], string='Status', default='draft', required=True, tracking=True)
start_date = fields.Date(string='Start Date', default=fields.Date.context_today)
expiry_date = fields.Date(string='Expiry Date', required=True)
currency_id = fields.Many2one('res.currency', string='Base Currency')
timezone = fields.Char(string='Timezone', default='America/New_York')
logo = fields.Binary(string='Restaurant Logo')
activate_table_reservation = fields.Boolean(
string='Activate Table Reservation Theme',
default=False,
help='If checked, automatically installs and configures the themed Table Reservation module on this restaurant database.'
)
_sql_constraints = [
('unique_subdomain', 'unique(subdomain)', 'This subdomain is already taken!'),
('unique_database_name', 'unique(database_name)', 'This database name is already registered!')
]
@api.model
def create(self, vals):
# Generate subdomain and database name from restaurant name
name_slug = "".join([c.lower() for c in vals.get('name', '') if c.isalnum()])
if not name_slug:
name_slug = "restaurant"
counter = 1
original_slug = name_slug
while self.search([('subdomain', '=', f"{name_slug}.dine360.com")]):
name_slug = f"{original_slug}{counter}"
counter += 1
vals['subdomain'] = f"{name_slug}.dine360.com"
vals['database_name'] = f"dine360_restaurant_{name_slug}"
return super(SaasRestaurant, self).create(vals)
def action_create_database(self):
self.ensure_one()
if self.database_name in db.list_dbs():
raise UserError(f"Database {self.database_name} already exists in PostgreSQL!")
# 1. Provision PostgreSQL database
admin_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=12))
try:
_logger.info(f"Creating database {self.database_name}...")
db.exp_create_database(
self.database_name,
False,
'en_US',
user_password=admin_pass,
login=self.email,
country_code=self.country_id.code if self.country_id else None,
phone=self.phone
)
_logger.info(f"Database {self.database_name} created successfully.")
except Exception as e:
raise UserError(f"PostgreSQL database creation failed: {str(e)}")
# 2. Extract configuration values
company_vals = {
'name': self.name,
'phone': self.phone,
'email': self.email,
'street': self.street,
'city': self.city,
'country_id': self.country_id.id if self.country_id else False,
'currency_id': self.currency_id.id if self.currency_id else False,
'logo': self.logo,
}
admin_vals = {
'name': self.owner_name,
'login': self.email,
'email': self.email,
'password': admin_pass,
}
modules_to_install = [
'base', 'contacts', 'sale', 'stock', 'purchase',
'account', 'point_of_sale', 'hr', 'pos_restaurant'
]
if self.activate_table_reservation:
modules_to_install.extend(['website', 'dine360_table', 'dine360_reservation', 'dine360_theme_reservation'])
# Set status to provisioning
self.status = 'provisioning'
# 3. Spawn background worker thread
parent_db = self.env.cr.dbname
t = threading.Thread(
target=_async_provisioning_worker,
args=(parent_db, self.id, self.database_name, modules_to_install, company_vals, admin_vals, self.timezone, admin_pass),
daemon=True
)
t.start()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Database Provisioning Started',
'message': 'Your database creation has started in the background. Please wait 1-2 minutes for completion before logging in.',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
}
def action_suspend(self):
self.ensure_one()
# Suspend by disabling login in target database
tenant_registry = registry(self.database_name)
try:
with tenant_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
# Disable all users except superuser
users = env['res.users'].search([('id', '!=', SUPERUSER_ID)])
users.write({'active': False})
cr.commit()
self.status = 'suspended'
except Exception as e:
raise UserError(f"Failed to suspend database users: {str(e)}")
def action_activate(self):
self.ensure_one()
# Activate by re-enabling login in target database
tenant_registry = registry(self.database_name)
try:
with tenant_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
# Enable all users
users = env['res.users'].search([('active', '=', False), ('id', '!=', SUPERUSER_ID)])
users.write({'active': True})
cr.commit()
self.status = 'active'
except Exception as e:
raise UserError(f"Failed to activate database users: {str(e)}")
@api.model
def cron_check_subscriptions(self):
""" Runs nightly to find expired accounts and enforce subscription plan limits """
today = fields.Date.context_today(self)
expired_tenants = self.search([('expiry_date', '<', today), ('status', '=', 'active')])
for tenant in expired_tenants:
tenant.status = 'expired'
tenant.action_suspend()
# Enforcement check for limits
active_tenants = self.search([('status', '=', 'active')])
for tenant in active_tenants:
plan = tenant.plan_id
tenant_registry = registry(tenant.database_name)
try:
with tenant_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
# 1. Count active users
user_count = env['res.users'].search_count([('active', '=', True), ('id', '!=', SUPERUSER_ID)])
# 2. Count active POS terminals
pos_count = env['pos.config'].search_count([('active', '=', True)])
# Log warnings/limits
if plan.max_users > 0 and user_count > plan.max_users:
_logger.warning(f"Tenant {tenant.name} exceeds user limit: {user_count}/{plan.max_users}")
# Auto suspend or flag
if plan.max_pos > 0 and pos_count > plan.max_pos:
_logger.warning(f"Tenant {tenant.name} exceeds POS limit: {pos_count}/{plan.max_pos}")
except Exception as e:
_logger.error(f"Error checking limits for {tenant.database_name}: {str(e)}")