diff --git a/addons/dine360_reservation/__manifest__.py b/addons/dine360_reservation/__manifest__.py
index 81ef4f0..72ea698 100644
--- a/addons/dine360_reservation/__manifest__.py
+++ b/addons/dine360_reservation/__manifest__.py
@@ -16,8 +16,12 @@
'security/ir.model.access.csv',
'data/reservation_sequence.xml',
'data/reservation_cron.xml',
+ 'data/reservation_schedule_data.xml',
'data/website_menu.xml',
'views/reservation_views.xml',
+ 'views/restaurant_table_views.xml',
+ 'views/reservation_schedule_views.xml',
+ 'views/reservation_holiday_views.xml',
'views/reservation_templates.xml',
'views/menu_items.xml',
],
diff --git a/addons/dine360_reservation/controllers/__pycache__/main.cpython-310.pyc b/addons/dine360_reservation/controllers/__pycache__/main.cpython-310.pyc
index 6522f02..b4c5937 100644
Binary files a/addons/dine360_reservation/controllers/__pycache__/main.cpython-310.pyc and b/addons/dine360_reservation/controllers/__pycache__/main.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/controllers/main.py b/addons/dine360_reservation/controllers/main.py
index 8d5888d..70ce2de 100644
--- a/addons/dine360_reservation/controllers/main.py
+++ b/addons/dine360_reservation/controllers/main.py
@@ -7,47 +7,260 @@ class TableReservationController(http.Controller):
@http.route(['/reservation'], type='http', auth="public", website=True)
def reservation_form(self, **post):
- floors = request.env['restaurant.floor'].sudo().search([])
- tables = request.env['restaurant.table'].sudo().search([])
+ schedule = request.env['reservation.schedule'].sudo().search([])
return request.render("dine360_reservation.reservation_page_template", {
- 'floors': floors,
- 'tables': tables,
+ 'schedule': schedule
})
+ def _get_slot_duration(self, schedule, time_float):
+ """ Returns the slot duration for a given time based on peak hour settings """
+ if not schedule:
+ return 1.0
+ for peak in schedule.peak_hour_ids:
+ if peak.start_time <= time_float < peak.end_time:
+ return float(peak.slot_duration)
+ return float(schedule.default_slot_duration or 1.0)
+
+ def _find_best_tables(self, floor, available_floor_tables, num_people):
+ """ Find the best combination of nearby tables on a floor, preferring same zone """
+ # Case 1: Single table fits
+ singles = [t for t in available_floor_tables if t.min_party_size <= num_people <= t.max_party_size]
+ if singles:
+ best_single = min(singles, key=lambda t: t.max_party_size)
+ return [best_single]
+
+ # Case 2: Merge needed
+ # Group tables by zone
+ zones = {}
+ for t in available_floor_tables:
+ z = t.zone or 'Default'
+ if z not in zones: zones[z] = []
+ zones[z].append(t)
+
+ best_overall_combo = []
+ min_overall_dist = float('inf')
+
+ # Try merging within each zone first
+ for zone_name, zone_tables in zones.items():
+ if sum(t.max_party_size for t in zone_tables) < num_people:
+ continue
+
+ for start_table in zone_tables:
+ current_combo = [start_table]
+ current_cap = start_table.max_party_size
+ others = [t for t in zone_tables if t.id != start_table.id]
+
+ while current_cap < num_people and others:
+ nearest = min(others, key=lambda o: min(
+ (((o.position_h or 0) - (c.position_h or 0))**2 + ((o.position_v or 0) - (c.position_v or 0))**2)**0.5
+ for c in current_combo
+ ))
+ current_combo.append(nearest)
+ current_cap += nearest.max_party_size
+ others.remove(nearest)
+
+ if current_cap >= num_people:
+ dist_sum = 0
+ for i in range(len(current_combo)):
+ for j in range(i + 1, len(current_combo)):
+ t1, t2 = current_combo[i], current_combo[j]
+ dist_sum += (((t1.position_h or 0) - (t2.position_h or 0))**2 + ((t1.position_v or 0) - (t2.position_v or 0))**2)**0.5
+
+ if dist_sum < min_overall_dist:
+ min_overall_dist = dist_sum
+ best_overall_combo = current_combo
+
+ # If no same-zone combo found, use the best combo across the whole floor
+ if not best_overall_combo:
+ for start_table in available_floor_tables:
+ current_combo = [start_table]
+ current_cap = start_table.max_party_size
+ others = [t for t in available_floor_tables if t.id != start_table.id]
+ while current_cap < num_people and others:
+ nearest = min(others, key=lambda o: min(
+ (((o.position_h or 0) - (c.position_h or 0))**2 + ((o.position_v or 0) - (c.position_v or 0))**2)**0.5
+ for c in current_combo
+ ))
+ current_combo.append(nearest)
+ current_cap += nearest.max_party_size
+ others.remove(nearest)
+ if current_cap >= num_people:
+ dist_sum = 0
+ for i in range(len(current_combo)):
+ for j in range(i + 1, len(current_combo)):
+ t1, t2 = current_combo[i], current_combo[j]
+ dist_sum += (((t1.position_h or 0) - (t2.position_h or 0))**2 + ((t1.position_v or 0) - (t2.position_v or 0))**2)**0.5
+ if dist_sum < min_overall_dist:
+ min_overall_dist = dist_sum
+ best_overall_combo = current_combo
+
+ return best_overall_combo
+
+ @http.route(['/reservation/get_slots'], type='json', auth="public", website=True)
+ def get_available_slots(self, date_str, num_people):
+ try:
+ res_date = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
+ num_people = int(num_people)
+ except:
+ return {'error': 'Invalid date or guest count'}
+
+ # 1. Check for Holiday
+ holiday = request.env['reservation.holiday'].sudo().search([('date', '=', res_date)], limit=1)
+ if holiday and holiday.is_closed:
+ return {'error': 'The restaurant is closed on this day for %s.' % holiday.name}
+
+ # 2. Get Schedule
+ day = str(res_date.weekday())
+ schedule = request.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1)
+ if not schedule or schedule.is_closed:
+ return {'error': 'Reservations are not available on this day.'}
+
+ # Determine operating hours
+ opening = holiday.opening_time if (holiday and holiday.opening_time) else schedule.opening_time
+ closing = holiday.closing_time if (holiday and holiday.closing_time) else schedule.closing_time
+
+ # 3. Generate Slots (intervals based on table duration)
+ slots = []
+ current_time = opening
+ restaurant_tz = pytz.timezone('America/Toronto')
+ now_local = datetime.datetime.now(restaurant_tz).replace(tzinfo=None)
+
+ all_tables = request.env['restaurant.table'].sudo().search([('is_reservation_enabled', '=', True)])
+ floors = request.env['restaurant.floor'].sudo().search([])
+
+ while current_time < closing:
+ duration = self._get_slot_duration(schedule, current_time)
+ slot_start_dt = datetime.datetime.combine(res_date, datetime.time(int(current_time), int((current_time % 1) * 60)))
+ slot_end_dt = slot_start_dt + datetime.timedelta(hours=duration)
+
+ if slot_start_dt > now_local:
+ # Check for Break
+ is_in_break = False
+ if schedule.has_break:
+ if current_time < schedule.break_end_time and (current_time + duration) > schedule.break_start_time:
+ is_in_break = True
+
+ if not is_in_break:
+ best_available_combo = []
+
+ for floor in floors:
+ floor_tables = all_tables.filtered(lambda t: t.floor_id == floor)
+ available_on_floor = []
+ for table in floor_tables:
+ overlap = request.env['restaurant.reservation'].sudo().search([
+ ('table_ids', 'in', [table.id]),
+ ('state', 'in', ['confirmed', 'draft']),
+ ('start_time', '<', slot_end_dt),
+ ('end_time', '>', slot_start_dt),
+ ])
+ if not overlap:
+ available_on_floor.append(table)
+
+ combo = self._find_best_tables(floor, available_on_floor, num_people)
+ if combo and (not best_available_combo or len(combo) < len(best_available_combo)):
+ best_available_combo = combo
+ if len(combo) == 1: break # Perfect match found
+
+ if best_available_combo:
+ hours_int = int(current_time)
+ mins_int = int((current_time % 1) * 60)
+ ampm = "AM" if hours_int < 12 else "PM"
+ disp_h = hours_int if hours_int <= 12 else hours_int - 12
+ if disp_h == 0: disp_h = 12
+
+ table_names = ", ".join([t.name for t in best_available_combo])
+ floor_name = best_available_combo[0].floor_id.name
+
+ slots.append({
+ 'time': f"{hours_int:02d}:{mins_int:02d}",
+ 'display': f"{disp_h}:{mins_int:02d} {ampm}",
+ 'full_dt': slot_start_dt.strftime('%Y-%m-%dT%H:%M'),
+ 'tables': f"{floor_name} - {table_names}"
+ })
+
+ # Use the calculated duration as the interval to respect user configuration
+ current_time += duration
+
+ return {'slots': slots}
+
@http.route(['/reservation/submit'], type='http', auth="public", website=True, methods=['POST'], csrf=True)
def reservation_submit(self, **post):
- # Extract data from post
+ # Extract data
customer_name = post.get('customer_name')
phone = post.get('phone')
email = post.get('email')
- floor_id = int(post.get('floor_id'))
- table_id = int(post.get('table_id'))
- start_time_str = post.get('start_time') # Format: 2024-05-20T18:00
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.'})
- # Convert start_time to datetime object and localize to restaurant timezone (Brampton)
+ # Convert start_time to datetime object and localize to restaurant timezone (America/Toronto)
restaurant_tz = pytz.timezone('America/Toronto')
- local_start = datetime.datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M')
- local_start = restaurant_tz.localize(local_start)
+ try:
+ local_start = datetime.datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M')
+ local_start = restaurant_tz.localize(local_start)
+ # Convert to UTC for Odoo
+ start_time = local_start.astimezone(pytz.utc).replace(tzinfo=None)
+ except Exception:
+ return request.render("dine360_reservation.reservation_page_template", {
+ 'error': _("Invalid date or time format."),
+ 'post': post,
+ })
- # Convert to UTC for Odoo
- start_time = local_start.astimezone(pytz.utc).replace(tzinfo=None)
+ res_date = local_start.date() # Use local_start for date to get correct weekday
- # Standard duration of 1 hour for now
- end_time = start_time + datetime.timedelta(hours=1)
+ # Determine Duration
+ day = str(res_date.weekday())
+ schedule = 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)
+ end_time = start_time + datetime.timedelta(hours=duration)
- # Create reservation
+ # FIND TABLES (Nearest Merge)
+ assigned_tables = []
+ target_floor = False
+
+ all_floors = request.env['restaurant.floor'].sudo().search([])
+ for floor in all_floors:
+ floor_tables = request.env['restaurant.table'].sudo().search([
+ ('floor_id', '=', floor.id),
+ ('is_reservation_enabled', '=', True)
+ ])
+
+ available_tables = []
+ for table in floor_tables:
+ overlap = request.env['restaurant.reservation'].sudo().search([
+ ('table_ids', 'in', [table.id]),
+ ('state', 'in', ['confirmed', 'draft']),
+ ('start_time', '<', end_time),
+ ('end_time', '>', start_time),
+ ])
+ if not overlap:
+ available_tables.append(table)
+
+ combo = self._find_best_tables(floor, available_tables, num_people)
+ if combo and (not assigned_tables or len(combo) < len(assigned_tables)):
+ assigned_tables = combo
+ target_floor = floor
+ 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.'})
+
+ # Create Reservation
try:
reservation = request.env['restaurant.reservation'].sudo().create({
'customer_name': customer_name,
'phone': phone,
'email': email,
- 'floor_id': floor_id,
- 'table_id': table_id,
+ 'num_people': num_people,
+ 'floor_id': target_floor.id,
+ 'table_ids': [(6, 0, [t.id for t in assigned_tables])],
+ 'table_id': assigned_tables[0].id,
'start_time': start_time,
'end_time': end_time,
- 'num_people': num_people,
- 'state': 'draft'
+ 'state': 'confirmed' # Direct confirmation from website
})
return request.render("dine360_reservation.reservation_success_template", {
'reservation': reservation,
@@ -55,7 +268,5 @@ class TableReservationController(http.Controller):
except Exception as e:
return request.render("dine360_reservation.reservation_page_template", {
'error': str(e),
- 'floors': request.env['restaurant.floor'].sudo().search([]),
- 'tables': request.env['restaurant.table'].sudo().search([]),
'post': post,
})
diff --git a/addons/dine360_reservation/data/reservation_schedule_data.xml b/addons/dine360_reservation/data/reservation_schedule_data.xml
new file mode 100644
index 0000000..4522cde
--- /dev/null
+++ b/addons/dine360_reservation/data/reservation_schedule_data.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ 0
+ 12.0
+ 21.0
+
+
+ 1
+ 12.0
+ 21.0
+
+
+ 2
+ 12.0
+ 21.0
+
+
+ 3
+ 12.0
+ 21.0
+
+
+ 4
+ 12.0
+ 23.0
+
+
+ 5
+ 12.0
+ 23.0
+
+
+ 6
+ 12.0
+ 21.0
+
+
+
diff --git a/addons/dine360_reservation/models/__init__.py b/addons/dine360_reservation/models/__init__.py
index 7c07528..efba3da 100644
--- a/addons/dine360_reservation/models/__init__.py
+++ b/addons/dine360_reservation/models/__init__.py
@@ -1 +1,5 @@
from . import restaurant_reservation
+from . import restaurant_table
+from . import reservation_schedule
+from . import reservation_holiday
+from . import reservation_peak_hour
diff --git a/addons/dine360_reservation/models/__pycache__/__init__.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/__init__.cpython-310.pyc
index a7b05de..12bdb0d 100644
Binary files a/addons/dine360_reservation/models/__pycache__/__init__.cpython-310.pyc and b/addons/dine360_reservation/models/__pycache__/__init__.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/models/__pycache__/reservation_holiday.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/reservation_holiday.cpython-310.pyc
new file mode 100644
index 0000000..6d99c1a
Binary files /dev/null and b/addons/dine360_reservation/models/__pycache__/reservation_holiday.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/models/__pycache__/reservation_peak_hour.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/reservation_peak_hour.cpython-310.pyc
new file mode 100644
index 0000000..b9e7637
Binary files /dev/null and b/addons/dine360_reservation/models/__pycache__/reservation_peak_hour.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/models/__pycache__/reservation_schedule.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/reservation_schedule.cpython-310.pyc
new file mode 100644
index 0000000..6e5535e
Binary files /dev/null and b/addons/dine360_reservation/models/__pycache__/reservation_schedule.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/models/__pycache__/restaurant_reservation.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/restaurant_reservation.cpython-310.pyc
index 628bbd5..b2bf0a2 100644
Binary files a/addons/dine360_reservation/models/__pycache__/restaurant_reservation.cpython-310.pyc and b/addons/dine360_reservation/models/__pycache__/restaurant_reservation.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/models/__pycache__/restaurant_table.cpython-310.pyc b/addons/dine360_reservation/models/__pycache__/restaurant_table.cpython-310.pyc
new file mode 100644
index 0000000..f513e3c
Binary files /dev/null and b/addons/dine360_reservation/models/__pycache__/restaurant_table.cpython-310.pyc differ
diff --git a/addons/dine360_reservation/models/reservation_holiday.py b/addons/dine360_reservation/models/reservation_holiday.py
new file mode 100644
index 0000000..238c358
--- /dev/null
+++ b/addons/dine360_reservation/models/reservation_holiday.py
@@ -0,0 +1,26 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class ReservationHoliday(models.Model):
+ _name = 'reservation.holiday'
+ _description = 'Reservation Holiday/Override'
+ _order = 'date desc'
+
+ name = fields.Char(string='Description', required=True, placeholder="e.g. Christmas Day")
+ date = fields.Date(string='Date', required=True)
+
+ is_closed = fields.Boolean(string='Closed for Reservations', default=True)
+
+ opening_time = fields.Float(string='Override Opening Time', help="Leave empty to use regular schedule if not closed")
+ closing_time = fields.Float(string='Override Closing Time')
+
+ @api.constrains('opening_time', 'closing_time', 'is_closed')
+ def _check_times(self):
+ for rec in self:
+ if not rec.is_closed and (rec.opening_time or rec.closing_time):
+ if rec.opening_time >= rec.closing_time:
+ raise ValidationError(_("Override Opening time must be before closing time."))
+
+ _sql_constraints = [
+ ('date_unique', 'unique(date)', 'An override for this date already exists!')
+ ]
diff --git a/addons/dine360_reservation/models/reservation_peak_hour.py b/addons/dine360_reservation/models/reservation_peak_hour.py
new file mode 100644
index 0000000..6e397c2
--- /dev/null
+++ b/addons/dine360_reservation/models/reservation_peak_hour.py
@@ -0,0 +1,23 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class ReservationPeakHour(models.Model):
+ _name = 'reservation.peak.hour'
+ _description = 'Restaurant Peak Hour Duration'
+
+ schedule_id = fields.Many2one('reservation.schedule', string='Schedule', ondelete='cascade')
+ start_time = fields.Float(string='Peak Start Time', required=True)
+ end_time = fields.Float(string='Peak End Time', required=True)
+
+ slot_duration = fields.Selection([
+ ('0.5', '30 Minutes'),
+ ('1.0', '1 Hour'),
+ ('1.5', '1.5 Hours'),
+ ('2.0', '2 Hours'),
+ ], string='Peak Slot Duration', default='1.0', required=True)
+
+ @api.constrains('start_time', 'end_time')
+ def _check_times(self):
+ for rec in self:
+ if rec.start_time >= rec.end_time:
+ raise ValidationError(_("Peak start time must be before end time."))
diff --git a/addons/dine360_reservation/models/reservation_schedule.py b/addons/dine360_reservation/models/reservation_schedule.py
new file mode 100644
index 0000000..662eb67
--- /dev/null
+++ b/addons/dine360_reservation/models/reservation_schedule.py
@@ -0,0 +1,78 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class ReservationSchedule(models.Model):
+ _name = 'reservation.schedule'
+ _description = 'Restaurant Reservation Schedule'
+ _order = 'day asc'
+
+ day = fields.Selection([
+ ('0', 'Monday'),
+ ('1', 'Tuesday'),
+ ('2', 'Wednesday'),
+ ('3', 'Thursday'),
+ ('4', 'Friday'),
+ ('5', 'Saturday'),
+ ('6', 'Sunday')
+ ], string='Day of Week', required=True)
+
+ opening_time = fields.Float(string='Opening Time', default=12.0, help="Format: 14.5 for 2:30 PM")
+ closing_time = fields.Float(string='Closing Time', default=22.0)
+
+ has_break = fields.Boolean(string='Has Break Time', default=False)
+ break_start_time = fields.Float(string='Break Start Time')
+ break_end_time = fields.Float(string='Break End Time')
+
+ is_closed = fields.Boolean(string='Closed for Reservations', default=False)
+
+ default_slot_duration = fields.Selection([
+ ('0.5', '30 Minutes'),
+ ('1.0', '1 Hour'),
+ ('1.5', '1.5 Hours'),
+ ('2.0', '2 Hours'),
+ ('3.0', '3 Hours'),
+ ], string='Default Slot Duration', default='1.0', required=True)
+
+ peak_hour_ids = fields.One2many('reservation.peak.hour', 'schedule_id', string='Peak Hour Slot Overrides')
+
+ display_opening = fields.Char(compute='_compute_display_times')
+ display_closing = fields.Char(compute='_compute_display_times')
+ display_break = fields.Char(compute='_compute_display_times')
+
+ def _compute_display_times(self):
+ for rec in self:
+ rec.display_opening = self._float_to_time_str(rec.opening_time)
+ rec.display_closing = self._float_to_time_str(rec.closing_time)
+ if rec.has_break:
+ rec.display_break = f"{self._float_to_time_str(rec.break_start_time)} - {self._float_to_time_str(rec.break_end_time)}"
+ else:
+ rec.display_break = ""
+
+ def _float_to_time_str(self, time_float):
+ hours = int(time_float)
+ minutes = int((time_float - hours) * 60)
+ ampm = "am" if hours < 12 else "pm"
+ display_hours = hours if hours <= 12 else hours - 12
+ if display_hours == 0: display_hours = 12
+ return f"{display_hours}:{minutes:02d}{ampm}"
+
+ _sql_constraints = [
+ ('day_unique', 'unique(day)', 'Schedule for this day already exists!')
+ ]
+
+ @api.constrains('opening_time', 'closing_time', 'break_start_time', 'break_end_time', 'has_break')
+ def _check_times(self):
+ for rec in self:
+ if not rec.is_closed:
+ if rec.opening_time >= rec.closing_time:
+ raise ValidationError(_("Opening time must be before closing time."))
+ if rec.has_break:
+ if not (rec.opening_time < rec.break_start_time < rec.break_end_time < rec.closing_time):
+ raise ValidationError(_("Break time must be within opening and closing hours, and start before it ends."))
+
+ def name_get(self):
+ result = []
+ for rec in self:
+ day_name = dict(self._fields['day'].selection).get(rec.day)
+ result.append((rec.id, day_name))
+ return result
diff --git a/addons/dine360_reservation/models/restaurant_reservation.py b/addons/dine360_reservation/models/restaurant_reservation.py
index e09df98..1731b4a 100644
--- a/addons/dine360_reservation/models/restaurant_reservation.py
+++ b/addons/dine360_reservation/models/restaurant_reservation.py
@@ -14,8 +14,13 @@ class RestaurantReservation(models.Model):
email = fields.Char(string='Email')
num_people = fields.Integer(string='Number of People', default=1)
- floor_id = fields.Many2one('restaurant.floor', string='Floor', required=True)
- table_id = fields.Many2one('restaurant.table', string='Table', required=True)
+ 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')
+
+ is_admin_override = fields.Boolean(string='Admin Override', default=False, help="Skip all validation and availability checks")
+ override_reason = fields.Text(string='Override Reason')
+ overridden_by_id = fields.Many2one('res.users', string='Overridden By', readonly=True)
start_time = fields.Datetime(string='Start Time', required=True)
end_time = fields.Datetime(string='End Time', required=True)
@@ -51,43 +56,104 @@ class RestaurantReservation(models.Model):
def create(self, vals):
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('restaurant.reservation') or _('New')
+ if vals.get('is_admin_override'):
+ vals['overridden_by_id'] = self.env.user.id
return super(RestaurantReservation, self).create(vals)
- @api.constrains('table_id', 'start_time', 'end_time', 'state')
+ def write(self, vals):
+ if vals.get('is_admin_override'):
+ vals['overridden_by_id'] = self.env.user.id
+ return super(RestaurantReservation, self).write(vals)
+
+ @api.constrains('is_admin_override', 'override_reason')
+ def _check_override_reason(self):
+ for rec in self:
+ if rec.is_admin_override and not rec.override_reason:
+ raise ValidationError(_("Please provide a reason for the admin override."))
+
+ @api.constrains('table_ids', 'start_time', 'end_time', 'state', 'is_admin_override')
def _check_overlap(self):
for rec in self:
+ if rec.is_admin_override:
+ continue
if rec.state in ['confirmed', 'completed']:
+ tables = rec.table_ids or rec.table_id
+ if not tables:
+ continue
overlap = self.search([
('id', '!=', rec.id),
- ('table_id', '=', rec.table_id.id),
+ ('table_ids', 'in', tables.ids),
('state', '=', 'confirmed'),
('start_time', '<', rec.end_time),
('end_time', '>', rec.start_time),
])
if overlap:
- raise ValidationError(_('This table is already reserved for the selected time slot.'))
+ raise ValidationError(_('One or more tables are already reserved for the selected time slot.'))
- @api.constrains('start_time', 'end_time')
+ @api.constrains('start_time', 'end_time', 'is_admin_override')
def _check_opening_hours(self):
restaurant_tz = pytz.timezone('America/Toronto')
for rec in self:
+ if rec.is_admin_override:
+ continue
local_start = pytz.utc.localize(rec.start_time).astimezone(restaurant_tz)
local_end = pytz.utc.localize(rec.end_time).astimezone(restaurant_tz)
- day = local_start.weekday() # 0=Mon, 6=Sun
+ res_date = local_start.date()
+ day = str(local_start.weekday()) # 0=Mon, 6=Sun
time_start = local_start.hour + local_start.minute / 60.0
time_end = local_end.hour + local_end.minute / 60.0
- if day in [6, 0, 1, 2, 3]: # Sun-Thu
- if time_start < 12.0 or time_end > 21.0:
- raise ValidationError(_('Reservations for Sunday - Thursday must be between 12:00 PM and 9:00 PM (Local Time).'))
- else: # Fri-Sat
- if time_start < 12.0 or time_end > 23.0:
- raise ValidationError(_('Reservations for Friday & Saturday must be between 12:00 PM and 11:00 PM (Local Time).'))
+ # 1. Check for Holiday/Override
+ holiday = self.env['reservation.holiday'].sudo().search([('date', '=', res_date)], limit=1)
+
+ if holiday:
+ if holiday.is_closed:
+ raise ValidationError(_("Reservations are closed on %s for %s.") % (res_date.strftime('%B %d, %Y'), holiday.name))
+
+ # Check holiday override hours if specified
+ if holiday.opening_time or holiday.closing_time:
+ opening = holiday.opening_time or 0.0
+ closing = holiday.closing_time or 24.0
+ if time_start < opening or time_end > closing:
+ raise ValidationError(_("Special hours for %s (%s): %s to %s.") % (
+ holiday.name,
+ res_date.strftime('%B %d'),
+ self._float_to_time_str(opening),
+ self._float_to_time_str(closing)
+ ))
+ return # If holiday found and validated, skip regular schedule
- @api.onchange('start_time')
+ # 2. Check Regular Schedule
+ schedule = self.env['reservation.schedule'].sudo().search([('day', '=', day)], limit=1)
+
+ if not schedule:
+ continue
+
+ if schedule.is_closed:
+ raise ValidationError(_("Reservations are closed for %s.") % dict(schedule._fields['day'].selection).get(day))
+
+ if time_start < schedule.opening_time or time_end > schedule.closing_time:
+ raise ValidationError(_("Reservations for %s must be between %s and %s.") % (
+ dict(schedule._fields['day'].selection).get(day),
+ self._float_to_time_str(schedule.opening_time),
+ self._float_to_time_str(schedule.closing_time)
+ ))
+
+ if schedule.has_break:
+ if time_start < schedule.break_end_time and time_end > schedule.break_start_time:
+ raise ValidationError(_("The restaurant has a break between %s and %s on %s.") % (
+ self._float_to_time_str(schedule.break_start_time),
+ self._float_to_time_str(schedule.break_end_time),
+ dict(schedule._fields['day'].selection).get(day)
+ ))
+
+ @api.onchange('start_time', 'table_id')
def _onchange_start_time(self):
- if self.start_time:
+ if self.start_time and self.table_id:
+ duration = float(self.table_id.reservation_slot_duration or 1.0)
+ self.end_time = self.start_time + timedelta(hours=duration)
+ elif self.start_time:
self.end_time = self.start_time + timedelta(hours=1)
def action_confirm(self):
diff --git a/addons/dine360_reservation/models/restaurant_table.py b/addons/dine360_reservation/models/restaurant_table.py
new file mode 100644
index 0000000..4a6539c
--- /dev/null
+++ b/addons/dine360_reservation/models/restaurant_table.py
@@ -0,0 +1,29 @@
+from odoo import models, fields, api
+
+class RestaurantTable(models.Model):
+ _inherit = 'restaurant.table'
+
+ is_reservation_enabled = fields.Boolean(string='Enabled for Reservation', default=True)
+ min_party_size = fields.Integer(string='Min Party Size', default=1)
+ max_party_size = fields.Integer(string='Max Party Size', default=1)
+ zone = fields.Char(string='Zone/Section', help="e.g. Window Side, Garden, VIP Section")
+ reservation_slot_duration = fields.Selection([
+ ('0.5', '30 Minutes'),
+ ('1.0', '1 Hour'),
+ ('1.5', '1.5 Hours'),
+ ('2.0', '2 Hours'),
+ ('3.0', '3 Hours'),
+ ], string='Slot Duration', default='1.0', required=True)
+
+ @api.onchange('seats')
+ def _onchange_seats_for_reservation(self):
+ for record in self:
+ if record.seats == 6:
+ record.min_party_size = 4
+ record.max_party_size = 6
+ elif record.seats == 4:
+ record.min_party_size = 2
+ record.max_party_size = 4
+ else:
+ record.min_party_size = 1
+ record.max_party_size = record.seats or 1
diff --git a/addons/dine360_reservation/security/ir.model.access.csv b/addons/dine360_reservation/security/ir.model.access.csv
index 6191133..aab549d 100644
--- a/addons/dine360_reservation/security/ir.model.access.csv
+++ b/addons/dine360_reservation/security/ir.model.access.csv
@@ -1,2 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_restaurant_reservation_user,restaurant.reservation,model_restaurant_reservation,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_peak_hour_user,reservation.peak_hour,model_reservation_peak_hour,base.group_user,1,1,1,1
diff --git a/addons/dine360_reservation/views/menu_items.xml b/addons/dine360_reservation/views/menu_items.xml
index 0106528..3c2a6fb 100644
--- a/addons/dine360_reservation/views/menu_items.xml
+++ b/addons/dine360_reservation/views/menu_items.xml
@@ -12,4 +12,22 @@
parent="menu_restaurant_reservation_root"
action="action_restaurant_reservation"
sequence="10"/>
+
+
+
+
+
+
diff --git a/addons/dine360_reservation/views/reservation_holiday_views.xml b/addons/dine360_reservation/views/reservation_holiday_views.xml
new file mode 100644
index 0000000..abc6612
--- /dev/null
+++ b/addons/dine360_reservation/views/reservation_holiday_views.xml
@@ -0,0 +1,30 @@
+
+
+
+ reservation.holiday.tree
+ reservation.holiday
+
+
+
+
+
+
+
+
+
+
+
+
+ Holiday / Special Overrides
+ reservation.holiday
+ tree
+
+
+ Add holiday or festival overrides.
+
+
+ You can mark a specific date as closed or set special opening/closing hours that override the regular schedule.
+
+
+
+
diff --git a/addons/dine360_reservation/views/reservation_schedule_views.xml b/addons/dine360_reservation/views/reservation_schedule_views.xml
new file mode 100644
index 0000000..2ced474
--- /dev/null
+++ b/addons/dine360_reservation/views/reservation_schedule_views.xml
@@ -0,0 +1,67 @@
+
+
+
+ reservation.schedule.tree
+ reservation.schedule
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ reservation.schedule.form
+ reservation.schedule
+
+
+
+
+
+
+ Reservation Schedule
+ reservation.schedule
+ tree,form
+
+
+ Manage your daily reservation hours and break times.
+
+
+
+
diff --git a/addons/dine360_reservation/views/reservation_templates.xml b/addons/dine360_reservation/views/reservation_templates.xml
index d0ce5f6..dfb94e0 100644
--- a/addons/dine360_reservation/views/reservation_templates.xml
+++ b/addons/dine360_reservation/views/reservation_templates.xml
@@ -12,9 +12,35 @@
Table Reservation
Book your spot for an authentic South Indian dining experience.
-
- Sun - Thu: 12pm - 9pm | Fri & Sat: 12pm - 11pm
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Closed
+
+
+ -
+
+
Break:
+
+
+
+
+
+
+
+
+
@@ -39,32 +65,28 @@
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Select a date and guest count to see available slots
+
+
+
+
+
+
+
+
@@ -72,26 +94,83 @@
CONFIRM RESERVATION
-
+
+
+ function fetchSlots() {
+ const date = dateInput.value;
+ const guests = guestsInput.value;
+
+ if (!date || !guests) return;
+
+ container.innerHTML = 'Loading slots...
';
+ hiddenInput.value = "";
+
+ fetch('/reservation/get_slots', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ params: { date_str: date, num_people: guests }
+ })
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.error) {
+ container.innerHTML = `${data.error.message}
`;
+ return;
+ }
+ const result = data.result;
+ container.innerHTML = "";
+
+ if (result.error) {
+ container.innerHTML = `${result.error}
`;
+ return;
+ }
+
+ if (result.slots.length === 0) {
+ container.innerHTML = 'No available slots for this selection. Try another date or party size.
';
+ return;
+ }
+
+ 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.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;
+
+ const infoDiv = document.getElementById('selection_info');
+ const infoText = document.getElementById('assigned_table_text');
+ infoDiv.style.display = 'block';
+ infoText.innerHTML = "Assignment: " + slot.tables + "";
+ };
+ container.appendChild(btn);
+ });
+ });
+ }
+
+ dateInput.addEventListener('change', fetchSlots);
+ guestsInput.addEventListener('change', fetchSlots);
+
+ if(dateInput.value) fetchSlots();
+ });
+
@@ -117,7 +196,14 @@
Floor:
-
Table:
+
Table(s):
+
+
+
+
+
+
+
Guests:
Time:
diff --git a/addons/dine360_reservation/views/reservation_views.xml b/addons/dine360_reservation/views/reservation_views.xml
index 175cad5..ed315dd 100644
--- a/addons/dine360_reservation/views/reservation_views.xml
+++ b/addons/dine360_reservation/views/reservation_views.xml
@@ -10,7 +10,7 @@
-
+
@@ -47,10 +47,13 @@
+
+
+
-
+
@@ -69,12 +72,11 @@
-
+
-
diff --git a/addons/dine360_reservation/views/restaurant_table_views.xml b/addons/dine360_reservation/views/restaurant_table_views.xml
new file mode 100644
index 0000000..9505ee7
--- /dev/null
+++ b/addons/dine360_reservation/views/restaurant_table_views.xml
@@ -0,0 +1,51 @@
+
+
+
+
+ restaurant.table.form.inherit
+ restaurant.table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ restaurant.table.tree.reservation.settings
+ restaurant.table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reservation Settings
+ restaurant.table
+ tree,form
+
+
+
+ Configure your tables for reservation.
+
+
+
+