implement restaurant reservation management system with scheduling, validation, and WhatsApp integration

This commit is contained in:
Alaguraj0361 2026-09-09 09:55:29 +05:30
parent 3a13d2b882
commit 43e7007a93
14 changed files with 554 additions and 53 deletions

View File

@ -22,6 +22,7 @@
'views/restaurant_table_views.xml', 'views/restaurant_table_views.xml',
'views/reservation_schedule_views.xml', 'views/reservation_schedule_views.xml',
'views/reservation_holiday_views.xml', 'views/reservation_holiday_views.xml',
'views/reservation_custom_field_views.xml',
'views/reservation_templates.xml', 'views/reservation_templates.xml',
'views/menu_items.xml', 'views/menu_items.xml',
], ],

View File

@ -1,4 +1,4 @@
from odoo import http, _ from odoo import http, _
from odoo.http import request from odoo.http import request
import datetime import datetime
import pytz import pytz
@ -8,8 +8,11 @@ class TableReservationController(http.Controller):
@http.route(['/reservation'], type='http', auth="public", website=True) @http.route(['/reservation'], type='http', auth="public", website=True)
def reservation_form(self, **post): def reservation_form(self, **post):
schedule = request.env['reservation.schedule'].sudo().search([]) schedule = request.env['reservation.schedule'].sudo().search([])
custom_fields = request.env['reservation.custom.field'].sudo().search([('is_active', '=', True)])
return request.render("dine360_reservation.reservation_page_template", { return request.render("dine360_reservation.reservation_page_template", {
'schedule': schedule 'schedule': schedule,
'custom_fields': custom_fields,
'post': post,
}) })
def _get_slot_duration(self, schedule, time_float): def _get_slot_duration(self, schedule, time_float):
@ -185,20 +188,66 @@ class TableReservationController(http.Controller):
@http.route(['/reservation/submit'], type='http', auth="public", website=True, methods=['POST'], csrf=True) @http.route(['/reservation/submit'], type='http', auth="public", website=True, methods=['POST'], csrf=True)
def reservation_submit(self, **post): def reservation_submit(self, **post):
# Extract data schedule = request.env['reservation.schedule'].sudo().search([])
active_custom_fields = request.env['reservation.custom.field'].sudo().search([('is_active', '=', True)])
website = request.website
# Extract standard data
customer_name = post.get('customer_name') customer_name = post.get('customer_name')
phone = post.get('phone') phone = post.get('phone', '')
email = post.get('email') email = post.get('email', '')
num_people = int(post.get('num_people', 1)) num_people = int(post.get('num_people', 2) or 2)
start_time_str = post.get('start_time') start_time_str = post.get('start_time')
if not start_time_str: # Extract optional fields
return request.render("dine360_reservation.reservation_page_template", {'error': 'Please select a time slot.'}) special_requests = post.get('special_requests')
occasion = post.get('occasion') or False
dietary_requirements = post.get('dietary_requirements')
preferred_zone = post.get('preferred_zone')
if not email: if not customer_name:
return request.render("dine360_reservation.reservation_page_template", { return request.render("dine360_reservation.reservation_page_template", {
'error': 'Email is required.', 'error': _('Please provide your name.'),
'post': post, 'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
})
if not start_time_str:
return request.render("dine360_reservation.reservation_page_template", {
'error': _('Please select an available time slot.'),
'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
})
# Check required fields based on website settings
if hasattr(website, 'reservation_show_email') and website.reservation_show_email and not email:
return request.render("dine360_reservation.reservation_page_template", {
'error': _('Email is required.'),
'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
})
if hasattr(website, 'reservation_show_phone') and website.reservation_show_phone and not phone:
return request.render("dine360_reservation.reservation_page_template", {
'error': _('Phone number is required.'),
'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
})
# Validate required custom fields
for cfield in active_custom_fields:
if cfield.is_required:
cval = post.get(f'custom_field_{cfield.id}')
if not cval:
return request.render("dine360_reservation.reservation_page_template", {
'error': _("%s is required.") % cfield.name,
'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
}) })
# Convert start_time to datetime object and localize to restaurant timezone (America/Toronto) # Convert start_time to datetime object and localize to restaurant timezone (America/Toronto)
@ -212,15 +261,17 @@ class TableReservationController(http.Controller):
return request.render("dine360_reservation.reservation_page_template", { return request.render("dine360_reservation.reservation_page_template", {
'error': _("Invalid date or time format."), 'error': _("Invalid date or time format."),
'post': post, 'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
}) })
res_date = local_start.date() # Use local_start for date to get correct weekday res_date = local_start.date() # Use local_start for date to get correct weekday
# Determine Duration # Determine Duration
day = str(res_date.weekday()) day = str(res_date.weekday())
schedule = request.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1) schedule_day = request.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1)
time_float = local_start.hour + local_start.minute / 60.0 time_float = local_start.hour + local_start.minute / 60.0
duration = self._get_slot_duration(schedule, time_float) duration = self._get_slot_duration(schedule_day, time_float)
end_time = start_time + datetime.timedelta(hours=duration) end_time = start_time + datetime.timedelta(hours=duration)
# FIND TABLES (Nearest Merge) # FIND TABLES (Nearest Merge)
@ -252,21 +303,41 @@ class TableReservationController(http.Controller):
if len(combo) == 1: break if len(combo) == 1: break
if not assigned_tables: if not assigned_tables:
return request.render("dine360_reservation.reservation_page_template", {'error': 'Sorry, no tables available for this time/group size.'}) return request.render("dine360_reservation.reservation_page_template", {
'error': _('Sorry, no tables available for this time/group size.'),
'post': post,
'schedule': schedule,
'custom_fields': active_custom_fields,
})
# Create Reservation # Create Reservation
try: try:
reservation = request.env['restaurant.reservation'].sudo().create({ reservation_vals = {
'customer_name': customer_name, 'customer_name': customer_name,
'phone': phone, 'phone': phone,
'email': email, 'email': email,
'num_people': num_people, 'num_people': num_people,
'floor_id': target_floor.id, 'special_requests': special_requests,
'occasion': occasion,
'dietary_requirements': dietary_requirements,
'preferred_zone': preferred_zone,
'floor_id': target_floor.id if target_floor else False,
'table_ids': [(6, 0, [t.id for t in assigned_tables])], 'table_ids': [(6, 0, [t.id for t in assigned_tables])],
'table_id': assigned_tables[0].id, 'table_id': assigned_tables[0].id if assigned_tables else False,
'start_time': start_time, 'start_time': start_time,
'end_time': end_time, 'end_time': end_time,
'state': 'confirmed' # Direct confirmation from website 'state': 'confirmed' # Direct confirmation from website
}
reservation = request.env['restaurant.reservation'].sudo().create(reservation_vals)
# Save Custom Field Answers
for cfield in active_custom_fields:
ans = post.get(f'custom_field_{cfield.id}')
if ans:
request.env['reservation.custom.field.value'].sudo().create({
'reservation_id': reservation.id,
'field_id': cfield.id,
'value': str(ans)
}) })
# Send Emails # Send Emails

View File

@ -3,3 +3,4 @@ from . import restaurant_table
from . import reservation_schedule from . import reservation_schedule
from . import reservation_holiday from . import reservation_holiday
from . import reservation_peak_hour from . import reservation_peak_hour
from . import reservation_custom_field

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
import re
class ReservationCustomField(models.Model):
_name = 'reservation.custom.field'
_description = 'Reservation Custom Field'
_order = 'sequence, id'
name = fields.Char(string='Field Label / Question', required=True, help="Label shown on the reservation form.")
field_key = fields.Char(string='Field Key', compute='_compute_field_key', store=True, readonly=False,
help="Technical identifier for form input.")
field_type = fields.Selection([
('char', 'Text (Single Line)'),
('text', 'Text (Multi-line)'),
('selection', 'Dropdown (Selection)'),
('boolean', 'Checkbox (Yes / No)'),
('integer', 'Number'),
], string='Field Type', default='char', required=True)
selection_options = fields.Text(string='Dropdown Options',
help="Comma-separated list of choices, e.g. Vegetarian, Vegan, Halal, Gluten-Free")
placeholder = fields.Char(string='Placeholder', help="Optional placeholder text for the input box.")
is_required = fields.Boolean(string='Required', default=False, help="Whether this field must be filled by customer.")
is_active = fields.Boolean(string='Active (Show on Form)', default=True, help="If checked, field will appear on reservation form.")
sequence = fields.Integer(string='Sequence', default=10)
@api.depends('name')
def _compute_field_key(self):
for rec in self:
if rec.name and not rec.field_key:
clean = re.sub(r'[^a-zA-Z0-9_]', '_', rec.name.lower().strip())
rec.field_key = f"custom_{clean[:30]}"
def get_options_list(self):
""" Return options as a list of strings for selection rendering """
self.ensure_one()
if not self.selection_options:
return []
return [opt.strip() for opt in self.selection_options.split(',') if opt.strip()]
class ReservationCustomFieldValue(models.Model):
_name = 'reservation.custom.field.value'
_description = 'Reservation Custom Field Answer'
_order = 'id'
reservation_id = fields.Many2one('restaurant.reservation', string='Reservation', ondelete='cascade', required=True)
field_id = fields.Many2one('reservation.custom.field', string='Custom Field', ondelete='cascade', required=True)
field_name = fields.Char(related='field_id.name', string='Field Label', readonly=True)
field_type = fields.Selection(related='field_id.field_type', string='Field Type', readonly=True)
value = fields.Text(string='Value / Answer')

View File

@ -10,10 +10,23 @@ class RestaurantReservation(models.Model):
name = fields.Char(string='Reservation Reference', required=True, copy=False, readonly=True, default=lambda self: _('New')) name = fields.Char(string='Reservation Reference', required=True, copy=False, readonly=True, default=lambda self: _('New'))
customer_name = fields.Char(string='Customer Name', required=True) customer_name = fields.Char(string='Customer Name', required=True)
phone = fields.Char(string='Phone Number', required=True) phone = fields.Char(string='Phone Number', required=False)
email = fields.Char(string='Email', required=True) email = fields.Char(string='Email', required=False)
num_people = fields.Integer(string='Number of People', default=1) num_people = fields.Integer(string='Number of People', default=1)
special_requests = fields.Text(string='Special Requests / Notes')
occasion = fields.Selection([
('birthday', 'Birthday'),
('anniversary', 'Anniversary'),
('business', 'Business Meal'),
('date', 'Date Night'),
('casual', 'Casual Dining'),
('other', 'Other Occasion')
], string='Dining Occasion')
dietary_requirements = fields.Char(string='Dietary Requirements / Allergies')
preferred_zone = fields.Char(string='Preferred Seating Area / Zone')
custom_field_value_ids = fields.One2many('reservation.custom.field.value', 'reservation_id', string='Additional Field Answers')
floor_id = fields.Many2one('restaurant.floor', string='Floor') floor_id = fields.Many2one('restaurant.floor', string='Floor')
table_id = fields.Many2one('restaurant.table', string='Primary Table') table_id = fields.Many2one('restaurant.table', string='Primary Table')
table_ids = fields.Many2many('restaurant.table', string='Tables') table_ids = fields.Many2many('restaurant.table', string='Tables')

View File

@ -3,3 +3,6 @@ access_restaurant_reservation_user,restaurant.reservation,model_restaurant_reser
access_reservation_schedule_user,reservation.schedule,model_reservation_schedule,base.group_user,1,1,1,1 access_reservation_schedule_user,reservation.schedule,model_reservation_schedule,base.group_user,1,1,1,1
access_reservation_holiday_user,reservation.holiday,model_reservation_holiday,base.group_user,1,1,1,1 access_reservation_holiday_user,reservation.holiday,model_reservation_holiday,base.group_user,1,1,1,1
access_reservation_peak_hour_user,reservation.peak_hour,model_reservation_peak_hour,base.group_user,1,1,1,1 access_reservation_peak_hour_user,reservation.peak_hour,model_reservation_peak_hour,base.group_user,1,1,1,1
access_reservation_custom_field_user,reservation.custom.field,model_reservation_custom_field,base.group_user,1,1,1,1
access_reservation_custom_field_value_user,reservation.custom.field.value,model_reservation_custom_field_value,base.group_user,1,1,1,1
access_reservation_custom_field_public,reservation.custom.field,model_reservation_custom_field,base.group_public,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_reservation_schedule_user reservation.schedule model_reservation_schedule base.group_user 1 1 1 1
4 access_reservation_holiday_user reservation.holiday model_reservation_holiday base.group_user 1 1 1 1
5 access_reservation_peak_hour_user reservation.peak_hour model_reservation_peak_hour base.group_user 1 1 1 1
6 access_reservation_custom_field_user reservation.custom.field model_reservation_custom_field base.group_user 1 1 1 1
7 access_reservation_custom_field_value_user reservation.custom.field.value model_reservation_custom_field_value base.group_user 1 1 1 1
8 access_reservation_custom_field_public reservation.custom.field model_reservation_custom_field base.group_public 1 0 0 0

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_reservation_custom_field_tree" model="ir.ui.view">
<field name="name">reservation.custom.field.tree</field>
<field name="model">reservation.custom.field</field>
<field name="arch" type="xml">
<tree string="Reservation Custom Fields" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="field_type"/>
<field name="selection_options" invisible="field_type != 'selection'" placeholder="e.g. Yes, No"/>
<field name="placeholder" invisible="field_type not in ['char', 'text']"/>
<field name="is_required"/>
<field name="is_active"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_reservation_custom_field_form" model="ir.ui.view">
<field name="name">reservation.custom.field.form</field>
<field name="model">reservation.custom.field</field>
<field name="arch" type="xml">
<form string="Custom Reservation Field">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Dietary Restrictions or Allergies"/>
</h1>
</div>
<group>
<group string="Field Configuration">
<field name="field_type"/>
<field name="field_key" groups="base.group_no_one"/>
<field name="selection_options" invisible="field_type != 'selection'" required="field_type == 'selection'" placeholder="Option 1, Option 2, Option 3..."/>
<field name="placeholder" invisible="field_type not in ['char', 'text']" placeholder="e.g. Please let us know if any guests have allergies..."/>
</group>
<group string="Display &amp; Validation">
<field name="is_required"/>
<field name="is_active"/>
<field name="sequence"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Window Action -->
<record id="action_reservation_custom_fields" model="ir.actions.act_window">
<field name="name">Additional Custom Fields</field>
<field name="res_model">reservation.custom.field</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add an additional field or question to your Table Reservation form!
</p>
<p>
You can create custom questions such as Allergies, Company Name, Seating Preferences, or High Chair requirements.
</p>
</field>
</record>
<!-- Submenu under Table Reservation -->
<menuitem id="menu_reservation_custom_fields"
name="Additional Fields"
parent="dine360_reservation.menu_restaurant_reservation_root"
action="action_reservation_custom_fields"
sequence="40"/>
</odoo>

View File

@ -128,22 +128,122 @@
<label class="form-label fw-bold">Full Name</label> <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 res-input" 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>
<!-- Phone Number -->
<t t-if="'reservation_show_phone' not in website._fields or website.reservation_show_phone">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-bold">Phone Number</label> <label class="form-label fw-bold">Phone Number</label>
<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 ''"/> <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>
</t>
<!-- Email Address -->
<t t-if="'reservation_show_email' not in website._fields or website.reservation_show_email">
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-bold">Email</label> <label class="form-label fw-bold">Email</label>
<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 ''"/> <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>
<div class="col-md-12"> </t>
<!-- Number of Guests -->
<t t-if="'reservation_show_guests' not in website._fields or website.reservation_show_guests">
<div class="col-md-6">
<label class="form-label fw-bold">Number of Guests</label> <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 res-input" 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>
</t>
<t t-else="">
<input type="hidden" name="num_people" value="2"/>
</t>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-bold">Reservation Date</label> <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 res-input" 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>
<!-- Dining Occasion -->
<t t-if="'reservation_show_occasion' in website._fields and website.reservation_show_occasion">
<div class="col-md-6">
<label class="form-label fw-bold">Dining Occasion</label>
<select name="occasion" class="form-select form-select-lg border-0 shadow-sm res-input">
<option value="">Select an occasion (optional)...</option>
<option value="birthday">🎂 Birthday</option>
<option value="anniversary">💍 Anniversary</option>
<option value="business">💼 Business Meal</option>
<option value="date">🌹 Date Night</option>
<option value="casual">🍽️ Casual Dining</option>
<option value="other">✨ Other Special Occasion</option>
</select>
</div>
</t>
<!-- Seating Area / Zone Preference -->
<t t-if="'reservation_show_zone' in website._fields and website.reservation_show_zone">
<div class="col-md-6">
<label class="form-label fw-bold">Seating Preference</label>
<input type="text" name="preferred_zone" class="form-control form-control-lg border-0 shadow-sm res-input" placeholder="e.g. Window Side, Garden, Quiet Corner..."/>
</div>
</t>
<!-- Dietary Requirements / Allergies -->
<t t-if="'reservation_show_dietary' in website._fields and website.reservation_show_dietary">
<div class="col-md-12">
<label class="form-label fw-bold">Dietary Requirements / Allergies</label>
<input type="text" name="dietary_requirements" class="form-control form-control-lg border-0 shadow-sm res-input" placeholder="e.g. Nut allergy, Gluten-free, Vegetarian..."/>
</div>
</t>
<!-- Special Requests / Notes -->
<t t-if="'reservation_show_special_requests' not in website._fields or website.reservation_show_special_requests">
<div class="col-md-12">
<label class="form-label fw-bold">Special Requests / Notes</label>
<textarea name="special_requests" rows="2" class="form-control border-0 shadow-sm res-input" placeholder="Any additional notes or requests for the team..."><t t-esc="post.get('special_requests') if post else ''"/></textarea>
</div>
</t>
<!-- Dynamic Additional Custom Fields -->
<t t-if="custom_fields">
<t t-foreach="custom_fields" t-as="cfield">
<div t-attf-class="col-md-#{'12' if cfield.field_type in ['text'] else '6'}">
<label class="form-label fw-bold">
<t t-esc="cfield.name"/>
<t t-if="cfield.is_required"><span class="text-danger ms-1">*</span></t>
</label>
<t t-if="cfield.field_type == 'char'">
<input type="text" t-att-name="'custom_field_' + str(cfield.id)" class="form-control form-control-lg border-0 shadow-sm res-input" t-att-placeholder="cfield.placeholder or ''" t-att-required="cfield.is_required and '1' or None"/>
</t>
<t t-elif="cfield.field_type == 'text'">
<textarea t-att-name="'custom_field_' + str(cfield.id)" rows="2" class="form-control border-0 shadow-sm res-input" t-att-placeholder="cfield.placeholder or ''" t-att-required="cfield.is_required and '1' or None"></textarea>
</t>
<t t-elif="cfield.field_type == 'integer'">
<input type="number" t-att-name="'custom_field_' + str(cfield.id)" class="form-control form-control-lg border-0 shadow-sm res-input" t-att-placeholder="cfield.placeholder or ''" t-att-required="cfield.is_required and '1' or None"/>
</t>
<t t-elif="cfield.field_type == 'selection'">
<select t-att-name="'custom_field_' + str(cfield.id)" class="form-select form-select-lg border-0 shadow-sm res-input" t-att-required="cfield.is_required and '1' or None">
<option value="">Select an option...</option>
<t t-foreach="cfield.get_options_list()" t-as="opt">
<option t-att-value="opt"><t t-esc="opt"/></option>
</t>
</select>
</t>
<t t-elif="cfield.field_type == 'boolean'">
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" t-att-name="'custom_field_' + str(cfield.id)" value="Yes" t-att-id="'custom_field_' + str(cfield.id)"/>
<label class="form-check-label ms-2" t-att-for="'custom_field_' + str(cfield.id)">
<t t-esc="cfield.placeholder or 'Yes'"/>
</label>
</div>
</t>
</div>
</t>
</t>
<!-- Available Time Slots -->
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-bold">Available Time Slots</label> <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="min-height: 100px;"> <div id="slot_container" class="d-flex flex-wrap gap-2 p-3 rounded" style="min-height: 100px;">
@ -172,7 +272,7 @@
function fetchSlots() { function fetchSlots() {
const date = dateInput.value; const date = dateInput.value;
const guests = guestsInput.value; const guests = guestsInput ? (guestsInput.value || 2) : 2;
if (!date || !guests) return; if (!date || !guests) return;
@ -224,7 +324,7 @@
} }
dateInput.addEventListener('change', fetchSlots); dateInput.addEventListener('change', fetchSlots);
guestsInput.addEventListener('change', fetchSlots); if (guestsInput) guestsInput.addEventListener('change', fetchSlots);
if(dateInput.value) fetchSlots(); if(dateInput.value) fetchSlots();
}); });

View File

@ -58,6 +58,28 @@
<field name="end_time"/> <field name="end_time"/>
</group> </group>
</group> </group>
<notebook>
<page string="Customer Preferences &amp; Notes" name="customer_requests">
<group>
<group string="Preferences">
<field name="occasion"/>
<field name="preferred_zone"/>
<field name="dietary_requirements"/>
</group>
<group string="Special Notes">
<field name="special_requests" placeholder="Customer special requests..."/>
</group>
</group>
</page>
<page string="Additional Field Answers" name="custom_fields">
<field name="custom_field_value_ids">
<tree string="Custom Field Answers" editable="bottom">
<field name="field_name"/>
<field name="value"/>
</tree>
</field>
</page>
</notebook>
</sheet> </sheet>
</form> </form>
</field> </field>

View File

@ -181,7 +181,10 @@ class SaasRestaurant(models.Model):
def action_create_database(self): def action_create_database(self):
self.ensure_one() self.ensure_one()
if self.database_name in db.list_dbs(): if self.database_name in db.list_dbs():
raise UserError(f"Database {self.database_name} already exists in PostgreSQL!") raise UserError(
f"Database '{self.database_name}' already exists in PostgreSQL!\n"
f"Please click 'Drop Database' to remove the existing leftover database, or choose a different Subdomain."
)
# 1. Provision PostgreSQL database # 1. Provision PostgreSQL database
admin_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=12)) admin_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=12))
@ -249,6 +252,38 @@ class SaasRestaurant(models.Model):
} }
} }
def action_drop_database(self):
""" Drop the tenant database from PostgreSQL """
self.ensure_one()
if self.database_name and self.database_name in db.list_dbs():
try:
db.exp_drop(self.database_name)
_logger.info(f"Database {self.database_name} dropped by user action.")
except Exception as e:
raise UserError(f"Failed to drop database from PostgreSQL: {str(e)}")
self.status = 'draft'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Database Removed',
'message': f'PostgreSQL database {self.database_name} has been dropped.',
'sticky': False,
'next': {'type': 'ir.actions.client', 'tag': 'reload'},
}
}
def unlink(self):
""" Drop the PostgreSQL database when a restaurant record is deleted """
for record in self:
if record.database_name and record.database_name in db.list_dbs():
try:
_logger.info(f"Dropping tenant database {record.database_name} on record deletion...")
db.exp_drop(record.database_name)
except Exception as e:
_logger.warning(f"Failed to drop database {record.database_name} during unlink: {str(e)}")
return super(SaasRestaurant, self).unlink()
def action_suspend(self): def action_suspend(self):
self.ensure_one() self.ensure_one()
# Suspend by disabling login in target database # Suspend by disabling login in target database

View File

@ -25,6 +25,7 @@
<form string="Restaurant Tenant"> <form string="Restaurant Tenant">
<header> <header>
<button name="action_create_database" string="Provision Database" type="object" class="oe_highlight" invisible="status not in ['draft', 'failed']"/> <button name="action_create_database" string="Provision Database" type="object" class="oe_highlight" invisible="status not in ['draft', 'failed']"/>
<button name="action_drop_database" string="Drop Database" type="object" class="btn-secondary" invisible="status not in ['draft', 'failed']" confirm="Are you sure you want to delete this tenant database from PostgreSQL?"/>
<button name="action_suspend" string="Suspend Subscription" type="object" class="btn-danger" invisible="status != 'active'"/> <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']"/> <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,provisioning,active,suspended,expired"/> <field name="status" widget="statusbar" statusbar_visible="draft,provisioning,active,suspended,expired"/>

View File

@ -19,3 +19,44 @@ class ResConfigSettings(models.TransientModel):
readonly=False, readonly=False,
string='Reservation Secondary Color' string='Reservation Secondary Color'
) )
# Form Field Visibility Settings
reservation_show_phone = fields.Boolean(
related='website_id.reservation_show_phone',
readonly=False,
string='Show Phone Number'
)
reservation_show_email = fields.Boolean(
related='website_id.reservation_show_email',
readonly=False,
string='Show Email Address'
)
reservation_show_guests = fields.Boolean(
related='website_id.reservation_show_guests',
readonly=False,
string='Show Number of Guests'
)
reservation_show_special_requests = fields.Boolean(
related='website_id.reservation_show_special_requests',
readonly=False,
string='Show Special Requests / Notes'
)
reservation_show_occasion = fields.Boolean(
related='website_id.reservation_show_occasion',
readonly=False,
string='Show Dining Occasion'
)
reservation_show_dietary = fields.Boolean(
related='website_id.reservation_show_dietary',
readonly=False,
string='Show Dietary Requirements / Allergies'
)
reservation_show_zone = fields.Boolean(
related='website_id.reservation_show_zone',
readonly=False,
string='Show Preferred Seating Area / Zone'
)
def action_open_custom_fields(self):
""" Open Custom Fields configuration list """
return self.env['ir.actions.act_window']._for_xml_id('dine360_reservation.action_reservation_custom_fields')

View File

@ -19,3 +19,40 @@ class Website(models.Model):
default='#171422', default='#171422',
help='Secondary color used for text within buttons and other components to ensure high contrast.' help='Secondary color used for text within buttons and other components to ensure high contrast.'
) )
# Form Fields Visibility Controls
reservation_show_phone = fields.Boolean(
string='Show Phone Number',
default=True,
help="If checked, Phone Number field will be displayed on the reservation form."
)
reservation_show_email = fields.Boolean(
string='Show Email Address',
default=True,
help="If checked, Email Address field will be displayed on the reservation form."
)
reservation_show_guests = fields.Boolean(
string='Show Number of Guests',
default=True,
help="If checked, Number of Guests field will be displayed on the reservation form."
)
reservation_show_special_requests = fields.Boolean(
string='Show Special Requests / Notes',
default=True,
help="If checked, Special Requests input box will be displayed on the reservation form."
)
reservation_show_occasion = fields.Boolean(
string='Show Dining Occasion',
default=False,
help="If checked, Dining Occasion dropdown (Birthday, Anniversary, etc.) will be displayed."
)
reservation_show_dietary = fields.Boolean(
string='Show Dietary Requirements / Allergies',
default=False,
help="If checked, Dietary Requirements input field will be displayed on the reservation form."
)
reservation_show_zone = fields.Boolean(
string='Show Preferred Seating Area / Zone',
default=False,
help="If checked, Seating Area preference dropdown will be displayed on the reservation form."
)

View File

@ -7,8 +7,9 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//form" position="inside"> <xpath expr="//form" position="inside">
<app string="Theme Settings" name="dine360_theme_reservation" data-string="Theme Settings" data-key="dine360_theme_reservation"> <app string="Theme Settings" name="dine360_theme_reservation" data-string="Theme Settings" data-key="dine360_theme_reservation">
<block title="Table Reservation Theme" id="theme_reservation_settings"> <!-- 1. Theme Styling Colors -->
<setting string="Table Reservation Theme" help="Configure custom styling colors for the Table Reservation system" id="theme_reservation_settings_detail"> <block title="Table Reservation Theme Colors" id="theme_reservation_settings">
<setting string="Branding &amp; Colors" help="Configure custom styling colors for the Table Reservation system" id="theme_reservation_settings_detail">
<field name="is_reservation_theme_active"/> <field name="is_reservation_theme_active"/>
<div class="content-group" invisible="not is_reservation_theme_active"> <div class="content-group" invisible="not is_reservation_theme_active">
<div class="row mt16"> <div class="row mt16">
@ -22,6 +23,58 @@
</div> </div>
</setting> </setting>
</block> </block>
<!-- 2. Form Field Visibility Checklist -->
<block title="Reservation Form Fields (Visibility Checklist)" id="reservation_fields_settings">
<setting string="Contact &amp; Guest Fields" help="Select which contact and guest inputs are shown on the reservation form" id="reservation_contact_fields">
<div class="content-group">
<div class="mt8">
<field name="reservation_show_phone" class="oe_inline"/>
<label for="reservation_show_phone" class="oe_inline ms-2"/>
</div>
<div class="mt8">
<field name="reservation_show_email" class="oe_inline"/>
<label for="reservation_show_email" class="oe_inline ms-2"/>
</div>
<div class="mt8">
<field name="reservation_show_guests" class="oe_inline"/>
<label for="reservation_show_guests" class="oe_inline ms-2"/>
</div>
</div>
</setting>
<setting string="Dining Preferences &amp; Requests" help="Enable additional dining preferences and customer note fields" id="reservation_preference_fields">
<div class="content-group">
<div class="mt8">
<field name="reservation_show_special_requests" class="oe_inline"/>
<label for="reservation_show_special_requests" class="oe_inline ms-2"/>
</div>
<div class="mt8">
<field name="reservation_show_occasion" class="oe_inline"/>
<label for="reservation_show_occasion" class="oe_inline ms-2"/>
</div>
<div class="mt8">
<field name="reservation_show_dietary" class="oe_inline"/>
<label for="reservation_show_dietary" class="oe_inline ms-2"/>
</div>
<div class="mt8">
<field name="reservation_show_zone" class="oe_inline"/>
<label for="reservation_show_zone" class="oe_inline ms-2"/>
</div>
</div>
</setting>
</block>
<!-- 3. Additional Custom Fields -->
<block title="Additional Custom Fields" id="reservation_custom_fields_block">
<setting string="Custom Questions &amp; Inputs" help="Add your own custom questions or fields (e.g. High Chair, Allergies, Special Occasion) to the reservation form">
<div class="mt8">
<button name="action_open_custom_fields" type="object" string="Manage Additional Fields" class="btn-primary" icon="fa-plus-circle"/>
</div>
<div class="text-muted mt8">
Click to create, activate, or reorder dynamic custom fields.
</div>
</setting>
</block>
</app> </app>
</xpath> </xpath>
</field> </field>