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.
This commit is contained in:
parent
a8d7ece3a8
commit
dd4e83d2da
@ -12,6 +12,7 @@
|
||||
'dine360_theme_shivasakthi',
|
||||
'dine360_kds',
|
||||
'dine360_reservation',
|
||||
'dine360_theme_reservation',
|
||||
'dine360_table',
|
||||
'dine360_uber',
|
||||
'dine360_recipe',
|
||||
|
||||
@ -10,7 +10,9 @@ def uninstall_hook(env):
|
||||
'dine360_restaurant',
|
||||
'dine360_theme_shivasakthi',
|
||||
'dine360_kds',
|
||||
'dine360_reservation'
|
||||
'dine360_reservation',
|
||||
'dine360_theme_reservation',
|
||||
'dine360_table'
|
||||
]
|
||||
|
||||
# Search for these modules if they are installed
|
||||
|
||||
18
addons/check_stuck_modules.py
Normal file
18
addons/check_stuck_modules.py
Normal file
@ -0,0 +1,18 @@
|
||||
import odoo
|
||||
from odoo import api, registry
|
||||
|
||||
odoo.tools.config['db_host'] = 'db'
|
||||
odoo.tools.config['db_user'] = 'odoo'
|
||||
odoo.tools.config['db_password'] = 'odoo'
|
||||
|
||||
db_name = 'dine360_restaurant_testing5'
|
||||
print(f"Checking modules on: {db_name}")
|
||||
try:
|
||||
reg = registry(db_name)
|
||||
with reg.cursor() as cr:
|
||||
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
modules = env['ir.module.module'].search([('state', '=', 'installed')])
|
||||
for m in modules:
|
||||
print(f" Installed module: {m.name}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
@ -2,6 +2,67 @@
|
||||
<odoo>
|
||||
<template id="reservation_page_template" name="Table Reservation Form">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="head">
|
||||
<style>
|
||||
:root {
|
||||
--res-primary-color: #fecd4f;
|
||||
--res-secondary-color: #171422;
|
||||
--res-bg-light: #f8f9fa;
|
||||
--res-border-radius: 12px;
|
||||
}
|
||||
.res-title {
|
||||
color: var(--res-primary-color) !important;
|
||||
}
|
||||
.opening-hours-box {
|
||||
background: color-mix(in srgb, var(--res-primary-color) 10%, transparent) !important;
|
||||
border: 1px solid var(--res-primary-color) !important;
|
||||
}
|
||||
.opening-hours-day {
|
||||
color: var(--res-secondary-color) !important;
|
||||
}
|
||||
.res-input {
|
||||
background: var(--res-bg-light) !important;
|
||||
border-radius: var(--res-border-radius) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
border: 1px solid var(--res-primary-color) !important;
|
||||
}
|
||||
#slot_container {
|
||||
background: var(--res-bg-light) !important;
|
||||
border: 1px dashed var(--res-primary-color) !important;
|
||||
}
|
||||
.res-confirm-btn {
|
||||
background: var(--res-primary-color) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
border-radius: var(--res-border-radius) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.res-confirm-btn:hover {
|
||||
background: color-mix(in srgb, var(--res-primary-color) 80%, black) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.slot-btn {
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 10px 15px !important;
|
||||
border: 1px solid var(--res-primary-color) !important;
|
||||
min-width: 100px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
background-color: transparent;
|
||||
color: var(--res-secondary-color);
|
||||
}
|
||||
.slot-btn:hover {
|
||||
background-color: color-mix(in srgb, var(--res-primary-color) 20%, transparent) !important;
|
||||
border-color: var(--res-primary-color) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
}
|
||||
.slot-btn.active {
|
||||
background-color: var(--res-primary-color) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
border-color: var(--res-primary-color) !important;
|
||||
}
|
||||
</style>
|
||||
</t>
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<section class="s_reservation_form pt64 pb64" style="background: linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.6)), url('/dine360_theme_shivasakthi/static/src/img/bg_reservation.jpg') center/cover no-repeat;">
|
||||
<div class="container">
|
||||
@ -10,18 +71,18 @@
|
||||
<div class="card border-0 shadow-lg" style="background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 20px;">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="display-4 fw-bold mb-2" style="color: #fecd4f;">Table Reservation</h2>
|
||||
<h2 class="display-4 fw-bold mb-2 res-title">Table Reservation</h2>
|
||||
<p class="text-muted mb-1">Book your spot for an authentic South Indian dining experience.</p>
|
||||
<div class="mt-2 text-center">
|
||||
<button class="btn btn-sm btn-outline-dark border-0 fw-bold" type="button" data-bs-toggle="collapse" data-bs-target="#openingHours" aria-expanded="false">
|
||||
<i class="fa fa-clock-o me-1"/> View Opening Hours
|
||||
</button>
|
||||
<div class="collapse mt-2" id="openingHours">
|
||||
<div class="p-3 rounded shadow-sm" style="background: rgba(254, 205, 79, 0.1); border: 1px solid #fecd4f;">
|
||||
<div class="p-3 rounded shadow-sm opening-hours-box">
|
||||
<div class="row g-2">
|
||||
<t t-foreach="schedule" t-as="day_schedule">
|
||||
<div class="col-6 col-md-4 text-start">
|
||||
<div class="small fw-bold text-uppercase" style="font-size: 0.7rem; color: #171422;">
|
||||
<div class="small fw-bold text-uppercase opening-hours-day" style="font-size: 0.7rem;">
|
||||
<t t-esc="dict(day_schedule._fields['day'].selection).get(day_schedule.day)"/>
|
||||
</div>
|
||||
<div class="small">
|
||||
@ -55,27 +116,27 @@
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Full Name</label>
|
||||
<input type="text" name="customer_name" class="form-control form-control-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" placeholder="John Doe" required="1" t-att-value="post.get('customer_name') if post else ''"/>
|
||||
<input type="text" name="customer_name" class="form-control form-control-lg border-0 shadow-sm res-input" placeholder="John Doe" required="1" t-att-value="post.get('customer_name') if post else ''"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Phone Number</label>
|
||||
<input type="tel" name="phone" class="form-control form-control-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" placeholder="+1 (647) 000-0000" required="1" t-att-value="post.get('phone') if post else ''"/>
|
||||
<input type="tel" name="phone" class="form-control form-control-lg border-0 shadow-sm res-input" placeholder="+1 (647) 000-0000" required="1" t-att-value="post.get('phone') if post else ''"/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Email</label>
|
||||
<input type="email" name="email" class="form-control form-control-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" placeholder="john@example.com" required="1" t-att-value="post.get('email') if post else ''"/>
|
||||
<input type="email" name="email" class="form-control form-control-lg border-0 shadow-sm res-input" placeholder="john@example.com" required="1" t-att-value="post.get('email') if post else ''"/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Number of Guests</label>
|
||||
<input type="number" name="num_people" class="form-control form-control-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" value="2" min="1" required="1" t-att-value="post.get('num_people') if post else 2"/>
|
||||
<input type="number" name="num_people" class="form-control form-control-lg border-0 shadow-sm res-input" value="2" min="1" required="1" t-att-value="post.get('num_people') if post else 2"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Reservation Date</label>
|
||||
<input type="date" id="res_date" name="res_date" class="form-control form-control-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" required="1" t-att-value="post.get('res_date') if post else datetime.date.today().strftime('%Y-%m-%d')"/>
|
||||
<input type="date" id="res_date" name="res_date" class="form-control form-control-lg border-0 shadow-sm res-input" required="1" t-att-value="post.get('res_date') if post else datetime.date.today().strftime('%Y-%m-%d')"/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Available Time Slots</label>
|
||||
<div id="slot_container" class="d-flex flex-wrap gap-2 p-3 rounded" style="background: #f8f9fa; border: 1px dashed #fecd4f; min-height: 100px;">
|
||||
<div id="slot_container" class="d-flex flex-wrap gap-2 p-3 rounded" style="min-height: 100px;">
|
||||
<div class="text-muted w-100 text-center py-4" id="slot_placeholder">
|
||||
<i class="fa fa-calendar-check-o mb-2 d-block" style="font-size: 1.5rem;"></i>
|
||||
Select a date and guest count to see available slots
|
||||
@ -87,7 +148,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<button type="submit" class="btn btn-lg w-100 shadow-sm transition-all text-dark" style="background: #fecd4f; border-radius: 12px; height: 60px; font-weight: 700; font-size: 1.1rem;">
|
||||
<button type="submit" class="btn btn-lg w-100 shadow-sm transition-all res-confirm-btn" style="height: 60px; font-weight: 700; font-size: 1.1rem; border: none;">
|
||||
CONFIRM RESERVATION
|
||||
</button>
|
||||
</div>
|
||||
@ -138,21 +199,14 @@
|
||||
result.slots.forEach(slot => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = "button";
|
||||
btn.className = "btn btn-outline-dark slot-btn transition-all m-1";
|
||||
btn.style.cssText = "border-radius: 8px; font-weight: 600; padding: 10px 15px; border: 1px solid #fecd4f; min-width: 100px;";
|
||||
btn.className = "btn slot-btn m-1";
|
||||
btn.innerHTML = slot.display;
|
||||
btn.onclick = function() {
|
||||
document.querySelectorAll('.slot-btn').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.style.backgroundColor = "";
|
||||
b.style.color = "";
|
||||
});
|
||||
this.classList.add('active');
|
||||
this.style.backgroundColor = "#fecd4f";
|
||||
this.style.color = "#171422";
|
||||
hiddenInput.value = slot.full_dt;
|
||||
|
||||
|
||||
};
|
||||
container.appendChild(btn);
|
||||
});
|
||||
@ -177,29 +231,71 @@
|
||||
|
||||
<template id="reservation_success_template" name="Reservation Success">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="head">
|
||||
<style>
|
||||
:root {
|
||||
--res-primary-color: #fecd4f;
|
||||
--res-secondary-color: #171422;
|
||||
}
|
||||
.res-success-icon-box {
|
||||
background: color-mix(in srgb, var(--res-primary-color) 10%, transparent) !important;
|
||||
}
|
||||
.res-success-icon {
|
||||
color: var(--res-primary-color) !important;
|
||||
}
|
||||
.res-success-card {
|
||||
border-radius: 20px !important;
|
||||
background: white !important;
|
||||
max-width: 500px !important;
|
||||
border: 1px solid color-mix(in srgb, var(--res-primary-color) 30%, transparent) !important;
|
||||
}
|
||||
.res-success-card-icon {
|
||||
font-size: 2rem !important;
|
||||
color: var(--res-primary-color) !important;
|
||||
}
|
||||
.res-success-separator {
|
||||
border-right: 2px dashed color-mix(in srgb, var(--res-primary-color) 40%, transparent) !important;
|
||||
height: 80px !important;
|
||||
}
|
||||
.res-back-btn {
|
||||
background: var(--res-primary-color) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
border-radius: 12px !important;
|
||||
padding-left: 40px !important;
|
||||
padding-right: 40px !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
.res-back-btn:hover {
|
||||
background: color-mix(in srgb, var(--res-primary-color) 80%, black) !important;
|
||||
color: var(--res-secondary-color) !important;
|
||||
}
|
||||
</style>
|
||||
</t>
|
||||
<div id="wrap" class="oe_structure">
|
||||
<section class="pt64 pb64 text-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="bg-success-light p-5 rounded-circle d-inline-block mb-4" style="background: rgba(43, 177, 165, 0.1);">
|
||||
<i class="fa fa-check-circle text-success display-1"></i>
|
||||
<div class="p-5 rounded-circle d-inline-block mb-4 res-success-icon-box">
|
||||
<i class="fa fa-check-circle display-1 res-success-icon"></i>
|
||||
</div>
|
||||
<h1 class="display-3 fw-bold mb-3">Thank You!</h1>
|
||||
<p class="lead text-muted mb-5">Your reservation request <strong><t t-esc="reservation.name"/></strong> has been received. We will contact you shortly to confirm.</p>
|
||||
<div class="card border-0 shadow-lg p-4 mb-5 mx-auto" style="border-radius: 20px; background: white; max-width: 500px; border: 1px solid rgba(254, 205, 79, 0.3) !important;">
|
||||
<div class="p-4 mb-5 mx-auto res-success-card">
|
||||
<div class="d-flex justify-content-around align-items-center text-center">
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<i class="fa fa-users" style="font-size: 2rem; color: #fecd4f;"></i>
|
||||
<i class="fa fa-users res-success-card-icon"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-0 text-dark">Guests</h5>
|
||||
<p class="text-muted mb-0 fs-5"><t t-esc="reservation.num_people"/></p>
|
||||
</div>
|
||||
<div style="border-right: 2px dashed rgba(254, 205, 79, 0.4); height: 80px;"></div>
|
||||
<div class="res-success-separator"></div>
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<i class="fa fa-clock-o" style="font-size: 2rem; color: #fecd4f;"></i>
|
||||
<i class="fa fa-clock-o res-success-card-icon"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-0 text-dark">Date & Time</h5>
|
||||
<p class="text-muted mb-0"><t t-esc="reservation.start_time.strftime('%b %d, %Y')"/><br/><strong class="text-dark"><t t-esc="reservation.start_time.strftime('%I:%M %p')"/></strong></p>
|
||||
@ -208,7 +304,7 @@
|
||||
</div>
|
||||
<a t-att-href="reservation.whatsapp_url" id="whatsapp_auto_link" class="d-none" target="_blank"></a>
|
||||
|
||||
<a href="/" class="btn btn-lg text-dark mt-3" style="background: #fecd4f; border-radius: 12px; padding-left: 40px; padding-right: 40px; font-weight: 600;">BACK TO HOME</a>
|
||||
<a href="/" class="btn btn-lg res-back-btn mt-3">BACK TO HOME</a>
|
||||
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
|
||||
@ -2,13 +2,97 @@
|
||||
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']
|
||||
@ -33,9 +117,11 @@ class SaasRestaurant(models.Model):
|
||||
|
||||
status = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('provisioning', 'Provisioning'),
|
||||
('active', 'Active'),
|
||||
('suspended', 'Suspended'),
|
||||
('expired', 'Expired')
|
||||
('expired', 'Expired'),
|
||||
('failed', 'Failed')
|
||||
], string='Status', default='draft', required=True, tracking=True)
|
||||
|
||||
start_date = fields.Date(string='Start Date', default=fields.Date.context_today)
|
||||
@ -44,6 +130,11 @@ class SaasRestaurant(models.Model):
|
||||
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!'),
|
||||
@ -53,15 +144,14 @@ class SaasRestaurant(models.Model):
|
||||
@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())
|
||||
name_slug = "".join([c.lower() for c in vals.get('name', '') if c.isalnum()])
|
||||
if not name_slug:
|
||||
name_slug = 'restaurant'
|
||||
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}"
|
||||
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"
|
||||
@ -91,77 +181,53 @@ class SaasRestaurant(models.Model):
|
||||
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)
|
||||
# 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', 'restaurant'
|
||||
'account', 'point_of_sale', 'hr', 'pos_restaurant'
|
||||
]
|
||||
if self.activate_table_reservation:
|
||||
modules_to_install.extend(['website', 'dine360_table', 'dine360_reservation', 'dine360_theme_reservation'])
|
||||
|
||||
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()
|
||||
# Set status to provisioning
|
||||
self.status = 'provisioning'
|
||||
|
||||
_logger.info("Base modules installed successfully. Configuring company...")
|
||||
# 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()
|
||||
|
||||
# 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,
|
||||
'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(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
|
||||
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()
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<field name="email"/>
|
||||
<field name="subdomain"/>
|
||||
<field name="plan_id"/>
|
||||
<field name="status" widget="badge" decoration-success="status == 'active'" decoration-info="status == 'draft'" decoration-danger="status == 'suspended' or status == 'expired'"/>
|
||||
<field name="status" widget="badge" decoration-success="status == 'active'" decoration-info="status == 'draft'" decoration-warning="status == 'provisioning'" decoration-danger="status in ['suspended', 'expired', 'failed']"/>
|
||||
<field name="expiry_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
@ -24,10 +24,10 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Restaurant Tenant">
|
||||
<header>
|
||||
<button name="action_create_database" string="Provision Database" type="object" class="oe_highlight" invisible="status != 'draft'"/>
|
||||
<button name="action_create_database" string="Provision Database" type="object" class="oe_highlight" invisible="status not in ['draft', 'failed']"/>
|
||||
<button name="action_suspend" string="Suspend Subscription" type="object" class="btn-danger" invisible="status != 'active'"/>
|
||||
<button name="action_activate" string="Activate Subscription" type="object" class="oe_highlight" invisible="status not in ['suspended', 'expired']"/>
|
||||
<field name="status" widget="statusbar" statusbar_visible="draft,active,suspended,expired"/>
|
||||
<field name="status" widget="statusbar" statusbar_visible="draft,provisioning,active,suspended,expired"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="logo" widget="image" class="oe_avatar" options="{'preview_image': 'logo'}"/>
|
||||
@ -53,6 +53,7 @@
|
||||
<group string="Tenant Deployment">
|
||||
<field name="database_name"/>
|
||||
<field name="subdomain"/>
|
||||
<field name="activate_table_reservation"/>
|
||||
</group>
|
||||
<group string="Subscription Details">
|
||||
<field name="plan_id"/>
|
||||
|
||||
2
addons/dine360_theme_reservation/__init__.py
Normal file
2
addons/dine360_theme_reservation/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
23
addons/dine360_theme_reservation/__manifest__.py
Normal file
23
addons/dine360_theme_reservation/__manifest__.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Dine360 Table Reservation Theme Customizer',
|
||||
'version': '1.0.0',
|
||||
'category': 'Website/Theme',
|
||||
'summary': 'Configure Primary and Secondary branding colors for the Table Reservation page.',
|
||||
'description': """
|
||||
Table Reservation Theme Customizer:
|
||||
- Adds website-specific settings for Table Reservation layout colors
|
||||
- Dynamically colors header, buttons, inputs, time slots, hover states, cards, and borders
|
||||
- Fully compatible and responsive across all devices
|
||||
""",
|
||||
'author': 'Dine360',
|
||||
'depends': ['website', 'dine360_reservation'],
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/reservation_templates_inherit.xml',
|
||||
'views/menu_items.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
3
addons/dine360_theme_reservation/models/__init__.py
Normal file
3
addons/dine360_theme_reservation/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import website
|
||||
from . import res_config_settings
|
||||
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
is_reservation_theme_active = fields.Boolean(
|
||||
related='website_id.is_reservation_theme_active',
|
||||
readonly=False,
|
||||
string='Activate Custom Reservation Theme'
|
||||
)
|
||||
reservation_primary_color = fields.Char(
|
||||
related='website_id.reservation_primary_color',
|
||||
readonly=False,
|
||||
string='Reservation Primary Color'
|
||||
)
|
||||
reservation_secondary_color = fields.Char(
|
||||
related='website_id.reservation_secondary_color',
|
||||
readonly=False,
|
||||
string='Reservation Secondary Color'
|
||||
)
|
||||
21
addons/dine360_theme_reservation/models/website.py
Normal file
21
addons/dine360_theme_reservation/models/website.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class Website(models.Model):
|
||||
_inherit = 'website'
|
||||
|
||||
is_reservation_theme_active = fields.Boolean(
|
||||
string='Activate Custom Reservation Theme',
|
||||
default=True,
|
||||
help='If checked, applies custom primary and secondary colors to the Table Reservation page.'
|
||||
)
|
||||
reservation_primary_color = fields.Char(
|
||||
string='Reservation Primary Color',
|
||||
default='#fecd4f',
|
||||
help='Primary accent color used for headers, buttons, borders, highlights, and active/selected states.'
|
||||
)
|
||||
reservation_secondary_color = fields.Char(
|
||||
string='Reservation Secondary Color',
|
||||
default='#171422',
|
||||
help='Secondary color used for text within buttons and other components to ensure high contrast.'
|
||||
)
|
||||
16
addons/dine360_theme_reservation/views/menu_items.xml
Normal file
16
addons/dine360_theme_reservation/views/menu_items.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_reservation_theme_settings" model="ir.actions.act_window">
|
||||
<field name="name">Theme Settings</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'module': 'dine360_theme_reservation'}</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_reservation_theme_settings"
|
||||
name="Theme Settings"
|
||||
parent="dine360_reservation.menu_restaurant_reservation_root"
|
||||
action="action_reservation_theme_settings"
|
||||
sequence="50"/>
|
||||
</odoo>
|
||||
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form_inherit_theme_reservation" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.theme.reservation</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@id='website_info_settings']" position="inside">
|
||||
<setting string="Table Reservation Theme" help="Configure custom styling colors for the Table Reservation system" id="theme_reservation_settings">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="is_reservation_theme_active" class="col-lg-3 o_light_label"/>
|
||||
<field name="is_reservation_theme_active"/>
|
||||
</div>
|
||||
<div class="row mt8" invisible="not is_reservation_theme_active">
|
||||
<label for="reservation_primary_color" class="col-lg-3 o_light_label"/>
|
||||
<field name="reservation_primary_color" widget="color"/>
|
||||
</div>
|
||||
<div class="row mt8" invisible="not is_reservation_theme_active">
|
||||
<label for="reservation_secondary_color" class="col-lg-3 o_light_label"/>
|
||||
<field name="reservation_secondary_color" widget="color"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="reservation_page_template_inherit_theme" inherit_id="dine360_reservation.reservation_page_template" name="Table Reservation Theme Override">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<t t-if="website.is_reservation_theme_active">
|
||||
<style>
|
||||
:root {
|
||||
--res-primary-color: <t t-esc="website.reservation_primary_color or '#fecd4f'"/> !important;
|
||||
--res-secondary-color: <t t-esc="website.reservation_secondary_color or '#171422'"/> !important;
|
||||
}
|
||||
</style>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="reservation_success_template_inherit_theme" inherit_id="dine360_reservation.reservation_success_template" name="Reservation Success Theme Override">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<t t-if="website.is_reservation_theme_active">
|
||||
<style>
|
||||
:root {
|
||||
--res-primary-color: <t t-esc="website.reservation_primary_color or '#fecd4f'"/> !important;
|
||||
--res-secondary-color: <t t-esc="website.reservation_secondary_color or '#171422'"/> !important;
|
||||
}
|
||||
</style>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
26
addons/verify_theme.py
Normal file
26
addons/verify_theme.py
Normal file
@ -0,0 +1,26 @@
|
||||
import urllib.request
|
||||
import re
|
||||
from http.cookiejar import CookieJar
|
||||
|
||||
cj = CookieJar()
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
opener.open('http://localhost:8069/web/login?db=dine360_restaurant_antalya')
|
||||
|
||||
try:
|
||||
resp = opener.open('http://localhost:8069/reservation')
|
||||
html = resp.read().decode('utf-8')
|
||||
print('STATUS:', resp.status)
|
||||
print('HTML length:', len(html))
|
||||
|
||||
# Check if CSS variables exist in the HTML (which means QWeb loaded them from website)
|
||||
contains_variables = '--res-primary-color:' in html and '--res-secondary-color:' in html
|
||||
print('HTML contains custom theme CSS variables:', contains_variables)
|
||||
|
||||
# Print the style blocks found in HTML
|
||||
style_blocks = re.findall(r'<style>.*?</style>', html, re.DOTALL)
|
||||
for block in style_blocks:
|
||||
if '--res-primary-color' in block:
|
||||
print('Found custom :root variables style block:')
|
||||
print(block)
|
||||
except Exception as e:
|
||||
print('ERROR:', e)
|
||||
Loading…
x
Reference in New Issue
Block a user