diff --git a/addons/dine360_reservation/__manifest__.py b/addons/dine360_reservation/__manifest__.py
index 3af7b7e..2e873d1 100644
--- a/addons/dine360_reservation/__manifest__.py
+++ b/addons/dine360_reservation/__manifest__.py
@@ -22,6 +22,7 @@
'views/restaurant_table_views.xml',
'views/reservation_schedule_views.xml',
'views/reservation_holiday_views.xml',
+ 'views/reservation_custom_field_views.xml',
'views/reservation_templates.xml',
'views/menu_items.xml',
],
diff --git a/addons/dine360_reservation/controllers/main.py b/addons/dine360_reservation/controllers/main.py
index 32c1f30..1336dfc 100644
--- a/addons/dine360_reservation/controllers/main.py
+++ b/addons/dine360_reservation/controllers/main.py
@@ -1,4 +1,4 @@
-from odoo import http, _
+from odoo import http, _
from odoo.http import request
import datetime
import pytz
@@ -8,8 +8,11 @@ class TableReservationController(http.Controller):
@http.route(['/reservation'], type='http', auth="public", website=True)
def reservation_form(self, **post):
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", {
- 'schedule': schedule
+ 'schedule': schedule,
+ 'custom_fields': custom_fields,
+ 'post': post,
})
def _get_slot_duration(self, schedule, time_float):
@@ -185,22 +188,68 @@ class TableReservationController(http.Controller):
@http.route(['/reservation/submit'], type='http', auth="public", website=True, methods=['POST'], csrf=True)
def reservation_submit(self, **post):
- # Extract data
- customer_name = post.get('customer_name')
- phone = post.get('phone')
- email = post.get('email')
- num_people = int(post.get('num_people', 1))
- start_time_str = post.get('start_time')
-
- if not start_time_str:
- return request.render("dine360_reservation.reservation_page_template", {'error': 'Please select a time slot.'})
+ schedule = request.env['reservation.schedule'].sudo().search([])
+ active_custom_fields = request.env['reservation.custom.field'].sudo().search([('is_active', '=', True)])
+ website = request.website
- if not email:
+ # Extract standard data
+ customer_name = post.get('customer_name')
+ phone = post.get('phone', '')
+ email = post.get('email', '')
+ num_people = int(post.get('num_people', 2) or 2)
+ start_time_str = post.get('start_time')
+
+ # Extract optional fields
+ 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 customer_name:
return request.render("dine360_reservation.reservation_page_template", {
- 'error': 'Email is required.',
+ 'error': _('Please provide your name.'),
'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)
restaurant_tz = pytz.timezone('America/Toronto')
try:
@@ -212,15 +261,17 @@ class TableReservationController(http.Controller):
return request.render("dine360_reservation.reservation_page_template", {
'error': _("Invalid date or time format."),
'post': post,
+ 'schedule': schedule,
+ 'custom_fields': active_custom_fields,
})
res_date = local_start.date() # Use local_start for date to get correct weekday
# Determine Duration
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
- 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)
# FIND TABLES (Nearest Merge)
@@ -252,22 +303,42 @@ class TableReservationController(http.Controller):
if len(combo) == 1: break
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
try:
- reservation = request.env['restaurant.reservation'].sudo().create({
+ reservation_vals = {
'customer_name': customer_name,
'phone': phone,
'email': email,
'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_id': assigned_tables[0].id,
+ 'table_id': assigned_tables[0].id if assigned_tables else False,
'start_time': start_time,
'end_time': end_time,
'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
try:
diff --git a/addons/dine360_reservation/models/__init__.py b/addons/dine360_reservation/models/__init__.py
index efba3da..d97bf8e 100644
--- a/addons/dine360_reservation/models/__init__.py
+++ b/addons/dine360_reservation/models/__init__.py
@@ -3,3 +3,4 @@ from . import restaurant_table
from . import reservation_schedule
from . import reservation_holiday
from . import reservation_peak_hour
+from . import reservation_custom_field
diff --git a/addons/dine360_reservation/models/reservation_custom_field.py b/addons/dine360_reservation/models/reservation_custom_field.py
new file mode 100644
index 0000000..fd20292
--- /dev/null
+++ b/addons/dine360_reservation/models/reservation_custom_field.py
@@ -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')
diff --git a/addons/dine360_reservation/models/restaurant_reservation.py b/addons/dine360_reservation/models/restaurant_reservation.py
index 17a1875..17c9e29 100644
--- a/addons/dine360_reservation/models/restaurant_reservation.py
+++ b/addons/dine360_reservation/models/restaurant_reservation.py
@@ -10,10 +10,23 @@ class RestaurantReservation(models.Model):
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)
- phone = fields.Char(string='Phone Number', required=True)
- email = fields.Char(string='Email', required=True)
+ phone = fields.Char(string='Phone Number', required=False)
+ email = fields.Char(string='Email', required=False)
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')
table_id = fields.Many2one('restaurant.table', string='Primary Table')
table_ids = fields.Many2many('restaurant.table', string='Tables')
diff --git a/addons/dine360_reservation/security/ir.model.access.csv b/addons/dine360_reservation/security/ir.model.access.csv
index aab549d..e261697 100644
--- a/addons/dine360_reservation/security/ir.model.access.csv
+++ b/addons/dine360_reservation/security/ir.model.access.csv
@@ -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_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_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
diff --git a/addons/dine360_reservation/views/reservation_custom_field_views.xml b/addons/dine360_reservation/views/reservation_custom_field_views.xml
new file mode 100644
index 0000000..568d039
--- /dev/null
+++ b/addons/dine360_reservation/views/reservation_custom_field_views.xml
@@ -0,0 +1,72 @@
+
+
+ Add an additional field or question to your Table Reservation form!
+
+ You can create custom questions such as Allergies, Company Name, Seating Preferences, or High Chair requirements.
+