Implement a new Odoo module for restaurant table reservations with a public web form.

This commit is contained in:
Alaguraj0361 2026-02-05 18:39:38 +05:30
parent 4621645621
commit d0ecc73453
18 changed files with 459 additions and 0 deletions

View File

@ -0,0 +1,2 @@
from . import models
from . import controllers

View File

@ -0,0 +1,27 @@
{
'name': 'Dine360 Table Reservation',
'version': '1.0',
'category': 'Sales/Restaurant',
'summary': 'Advanced Table Reservation System with Time Slots and Overlap Prevention',
'description': """
Advanced Table Reservation System:
- Time Slot based reservations
- Overlap prevention
- Auto-completion of past reservations
- WhatsApp/SMS notification hooks
""",
'author': 'Dine360',
'depends': ['base', 'website', 'pos_restaurant'],
'data': [
'security/ir.model.access.csv',
'data/reservation_sequence.xml',
'data/reservation_cron.xml',
'data/website_menu.xml',
'views/reservation_views.xml',
'views/reservation_templates.xml',
'views/menu_items.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -0,0 +1,54 @@
from odoo import http, _
from odoo.http import request
import datetime
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([])
return request.render("dine360_reservation.reservation_page_template", {
'floors': floors,
'tables': tables,
})
@http.route(['/reservation/submit'], type='http', auth="public", website=True, methods=['POST'], csrf=True)
def reservation_submit(self, **post):
# Extract data from post
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))
# Convert start_time to datetime object
start_time = datetime.datetime.strptime(start_time_str, '%Y-%m-%dT%H:%M')
# Standard duration of 1 hour for now
end_time = start_time + datetime.timedelta(hours=1)
# 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,
'start_time': start_time,
'end_time': end_time,
'num_people': num_people,
'state': 'draft'
})
return request.render("dine360_reservation.reservation_success_template", {
'reservation': reservation,
})
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,
})

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_auto_complete_reservations" model="ir.cron">
<field name="name">Restaurant: Auto-complete Past Reservations</field>
<field name="model_id" ref="model_restaurant_reservation"/>
<field name="state">code</field>
<field name="code">model._auto_complete_reservations()</field>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_restaurant_reservation" model="ir.sequence">
<field name="name">Restaurant Reservation</field>
<field name="code">restaurant.reservation</field>
<field name="prefix">RES/%(year)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="menu_reservation" model="website.menu">
<field name="name">Table Reservation</field>
<field name="url">/reservation</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">40</field>
<field name="website_id" eval="False"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1 @@
from . import restaurant_reservation

View File

@ -0,0 +1,89 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import timedelta
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')
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)
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:
msg = f"Hello {rec.customer_name}, your reservation {rec.name} 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
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')
return super(RestaurantReservation, self).create(vals)
@api.constrains('table_id', 'start_time', 'end_time', 'state')
def _check_overlap(self):
for rec in self:
if rec.state in ['confirmed', 'completed']:
overlap = self.search([
('id', '!=', rec.id),
('table_id', '=', rec.table_id.id),
('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.'))
@api.onchange('start_time')
def _onchange_start_time(self):
if self.start_time:
self.end_time = self.start_time + timedelta(hours=1)
def action_confirm(self):
self.write({'state': 'confirmed'})
self._send_confirmation_notification()
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'})

View File

@ -0,0 +1,2 @@
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_restaurant_reservation_user restaurant.reservation model_restaurant_reservation base.group_user 1 1 1 1

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Main Menu -->
<menuitem id="menu_restaurant_reservation_root"
name="Reservations"
sequence="20"
web_icon="dine360_reservation,static/description/icon.png"/>
<!-- Sub Menu -->
<menuitem id="menu_restaurant_reservation_list"
name="Table Reservations"
parent="menu_restaurant_reservation_root"
action="action_restaurant_reservation"
sequence="10"/>
</odoo>

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="reservation_page_template" name="Table Reservation Form">
<t t-call="website.layout">
<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_chennora/static/src/img/bg_reservation.jpg') center/cover no-repeat;">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-7">
<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>
<p class="text-muted">Book your spot for an authentic South Indian dining experience.</p>
</div>
<t t-if="error">
<div class="alert alert-danger" role="alert">
<t t-esc="error"/>
</div>
</t>
<form action="/reservation/submit" method="post" class="s_website_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<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 ''"/>
</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 ''"/>
</div>
<div class="col-md-12">
<label class="form-label fw-bold">Email (Optional)</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" t-att-value="post.get('email') if post else ''"/>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">Select Floor</label>
<select name="floor_id" id="floor_id" class="form-select form-select-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" required="1">
<option value="">Choose a Floor...</option>
<t t-foreach="floors" t-as="floor">
<option t-att-value="floor.id"><t t-esc="floor.name"/></option>
</t>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">Select Table</label>
<select name="table_id" id="table_id" class="form-select form-select-lg border-0 shadow-sm" style="background: #f8f9fa; border-radius: 12px; color: #171422 !important; border: 1px solid #fecd4f !important;" required="1">
<option value="">Choose a Table...</option>
<t t-foreach="tables" t-as="table">
<option t-att-value="table.id" t-att-data-floor="table.floor_id.id"><t t-esc="table.name"/> (Cap: <t t-esc="table.seats"/>)</option>
</t>
</select>
</div>
<div class="col-md-6">
<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"/>
</div>
<div class="col-md-12">
<label class="form-label fw-bold">Reservation Date &amp; Time</label>
<input type="datetime-local" name="start_time" 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('start_time') if post else ''"/>
</div>
</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;">
CONFIRM RESERVATION
</button>
</div>
</form>
<script type="text/javascript">
document.getElementById('floor_id').addEventListener('change', function() {
var floorId = this.value;
var tableSelect = document.getElementById('table_id');
var tableOptions = tableSelect.querySelectorAll('option');
tableSelect.value = "";
tableOptions.forEach(function(option) {
if (option.value === "") {
option.style.display = "block";
} else if (option.getAttribute('data-floor') === floorId) {
option.style.display = "block";
} else {
option.style.display = "none";
}
});
});
</script>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</t>
</template>
<template id="reservation_success_template" name="Reservation Success">
<t t-call="website.layout">
<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>
<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-sm p-4 mb-5" style="border-radius: 15px; background: #f8f9fa;">
<div class="row text-start g-3">
<div class="col-6"><strong>Floor:</strong> <t t-esc="reservation.floor_id.name"/></div>
<div class="col-6"><strong>Table:</strong> <t t-esc="reservation.table_id.name"/></div>
<div class="col-6"><strong>Guests:</strong> <t t-esc="reservation.num_people"/></div>
<div class="col-12"><strong>Time:</strong> <t t-esc="reservation.start_time.strftime('%B %d, %Y at %I:%M %p')"/></div>
</div>
</div>
<a href="/" class="btn btn-lg text-dark" style="background: #fecd4f; border-radius: 12px; padding-left: 40px; padding-right: 40px; font-weight: 600;">BACK TO HOME</a>
</div>
</div>
</div>
</section>
</div>
</t>
</template>
</odoo>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_restaurant_reservation_tree" model="ir.ui.view">
<field name="name">restaurant.reservation.tree</field>
<field name="model">restaurant.reservation</field>
<field name="arch" type="xml">
<tree string="Reservations" decoration-info="state == 'draft'" decoration-success="state == 'confirmed'" decoration-muted="state == 'completed'">
<field name="name"/>
<field name="customer_name"/>
<field name="phone"/>
<field name="floor_id"/>
<field name="table_id"/>
<field name="start_time"/>
<field name="end_time"/>
<field name="num_people"/>
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'confirmed'"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_restaurant_reservation_form" model="ir.ui.view">
<field name="name">restaurant.reservation.form</field>
<field name="model">restaurant.reservation</field>
<field name="arch" type="xml">
<form string="Reservation">
<header>
<button name="action_confirm" string="Confirm" type="object" class="oe_highlight" invisible="state != 'draft'"/>
<button name="action_complete" string="Mark Completed" type="object" invisible="state != 'confirmed'"/>
<button name="action_cancel" string="Cancel" type="object" invisible="state not in ['draft', 'confirmed']"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,completed"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<field name="whatsapp_url" invisible="1"/>
<button name="whatsapp_url" type="url" class="oe_stat_button" icon="fa-whatsapp" string="WhatsApp" invisible="not whatsapp_url"/>
</div>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group string="Customer Information">
<field name="customer_name"/>
<field name="phone"/>
<field name="email"/>
<field name="num_people"/>
</group>
<group string="Reservation Details">
<field name="floor_id"/>
<field name="table_id" domain="[('floor_id', '=', floor_id)]"/>
<field name="start_time"/>
<field name="end_time"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_restaurant_reservation_search" model="ir.ui.view">
<field name="name">restaurant.reservation.search</field>
<field name="model">restaurant.reservation</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="customer_name"/>
<field name="phone"/>
<field name="table_id"/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
<filter string="Completed" name="completed" domain="[('state', '=', 'completed')]"/>
<group expand="0" string="Group By">
<filter string="Table" name="group_by_table" context="{'group_by': 'table_id'}"/>
<filter string="Status" name="group_by_status" context="{'group_by': 'state'}"/>
<filter string="Date" name="group_by_date" context="{'group_by': 'start_time'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_restaurant_reservation" model="ir.actions.act_window">
<field name="name">Table Reservations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">restaurant.reservation</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_restaurant_reservation_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first table reservation!
</p>
</field>
</record>
</odoo>