from odoo import models, fields, api, _ from odoo.exceptions import ValidationError from datetime import timedelta import pytz class RestaurantReservation(models.Model): _name = 'restaurant.reservation' _description = 'Restaurant Table Reservation' _order = 'start_time desc' 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) num_people = fields.Integer(string='Number of People', default=1) 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) whatsapp_url = fields.Char(compute='_compute_whatsapp_url') def _compute_whatsapp_url(self): for rec in self: if rec.phone and rec.customer_name and rec.start_time: msg = f"Hello {rec.customer_name}, your reservation {rec.name or ''} for {rec.start_time.strftime('%I:%M %p')} is confirmed!" rec.whatsapp_url = f"https://wa.me/{rec.phone}?text={msg.replace(' ', '%20')}" else: rec.whatsapp_url = False def action_whatsapp(self): self.ensure_one() if self.whatsapp_url: return { 'type': 'ir.actions.act_url', 'url': self.whatsapp_url, 'target': 'new', } return False state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('completed', 'Completed'), ('cancelled', 'Cancelled') ], string='Status', default='draft', tracking=True) @api.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) 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_ids', 'in', tables.ids), ('state', '=', 'confirmed'), ('start_time', '<', rec.end_time), ('end_time', '>', rec.start_time), ]) if overlap: raise ValidationError(_('One or more tables are already reserved for the selected time slot.')) @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) 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 # 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 # 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 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): self.ensure_one() self.write({'state': 'confirmed'}) self._send_confirmation_notification() # Auto-open WhatsApp on confirmation if self.whatsapp_url: return { 'type': 'ir.actions.act_url', 'url': self.whatsapp_url, 'target': 'new', } def action_complete(self): self.write({'state': 'completed'}) def action_cancel(self): self.write({'state': 'cancelled'}) def _send_confirmation_notification(self): """ Placeholder for WhatsApp/SMS logic """ for rec in self: # Logic for WhatsApp/SMS can be added here # e.g., self.env['sms.api']._send_sms(rec.phone, "Your table is confirmed!") pass @api.model def _auto_complete_reservations(self): """ Scheduled action to mark past reservations as completed """ now = fields.Datetime.now() past_reservations = self.search([ ('state', '=', 'confirmed'), ('end_time', '<', now) ]) past_reservations.write({'state': 'completed'})