# -*- coding: utf-8 -*- import string import random import logging from odoo import models, fields, api, registry, SUPERUSER_ID from odoo.exceptions import UserError from odoo.service import db from odoo.tools import config _logger = logging.getLogger(__name__) 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'), ('active', 'Active'), ('suspended', 'Suspended'), ('expired', 'Expired') ], 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') _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 for c in vals.get('name', '').lower() if c.isalnum()) if not name_slug: name_slug = 'restaurant' # Ensure uniqueness base_slug = name_slug counter = 1 while self.env['saas.restaurant'].search([('subdomain', '=', f"{name_slug}.dine360.com")]): name_slug = f"{base_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 master_pwd = config.get('admin_passwd', 'admin') 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(master_pwd, self.database_name, False, 'en_US', admin_pass) _logger.info(f"Database {self.database_name} created successfully.") except Exception as e: raise UserError(f"PostgreSQL database creation failed: {str(e)}") # 2. Boot environment on new database and install modules tenant_registry = registry(self.database_name) modules_to_install = [ 'base', 'contacts', 'sale', 'stock', 'purchase', 'account', 'point_of_sale', 'hr', 'restaurant' ] try: _logger.info(f"Installing base modules on {self.database_name}...") with tenant_registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) module_objs = env['ir.module.module'].search([('name', 'in', modules_to_install)]) module_objs.write({'state': 'to_install'}) cr.commit() # Run immediate install for Odoo base module_objs.button_immediate_install() cr.commit() _logger.info("Base modules installed successfully. Configuring company...") # 3. Configure Company & Admin User inside the new database with tenant_registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) # Company setup company = env['res.company'].search([], limit=1) company.write({ '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 user setup (ID 2 is usually admin) admin_user = env['res.users'].browse(2) admin_user.write({ 'name': self.owner_name, 'login': self.email, 'email': self.email, }) admin_user._set_password(admin_pass) # Set timezone admin_user.partner_id.tz = self.timezone cr.commit() except Exception as e: # If provisioning fails midway, drop the database to allow clean retry _logger.error(f"Failed configuring tenant registry: {str(e)}") try: db.exp_drop(master_pwd, self.database_name) except Exception: pass raise UserError(f"Database provisioning / module installation failed: {str(e)}") self.status = 'active' # Send Welcome Email template = self.env.ref('dine360_saas_master.saas_welcome_email_template', raise_if_not_found=False) if template: # Context for temporary password self.with_context(temp_password=admin_pass).message_post_with_source( source_ref=template, subtype_xmlid='mail.mt_comment' ) return True 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)}")