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:
Alaguraj0361 2026-07-03 22:52:18 +05:30
parent a8d7ece3a8
commit dd4e83d2da
15 changed files with 455 additions and 103 deletions

View File

@ -12,6 +12,7 @@
'dine360_theme_shivasakthi',
'dine360_kds',
'dine360_reservation',
'dine360_theme_reservation',
'dine360_table',
'dine360_uber',
'dine360_recipe',

View File

@ -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

View 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}")

View File

@ -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 &amp; 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() {

View File

@ -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()

View File

@ -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"/>

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

View 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',
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import website
from . import res_config_settings

View File

@ -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'
)

View 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.'
)

View 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>

View File

@ -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>

View File

@ -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
View 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)