first commit
This commit is contained in:
commit
db4f4f7ef1
4
addons/event_rental/__init__.py
Normal file
4
addons/event_rental/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
27
addons/event_rental/__manifest__.py
Normal file
27
addons/event_rental/__manifest__.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Event Rental Management',
|
||||
'version': '17.0.1.0.0',
|
||||
'category': 'Sales/Rental',
|
||||
'summary': 'Manage event decoration and equipment rentals',
|
||||
'description': """
|
||||
Event Rental Management System for Wedding Decorations, Birthday Decorations,
|
||||
Corporate Event Setup, Stage Decorations, Sound Systems, Lighting, Furniture, Tents, etc.
|
||||
""",
|
||||
'author': 'Antigravity',
|
||||
'depends': ['website', 'sale_management', 'stock', 'portal', 'calendar'],
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/sequence.xml',
|
||||
'data/mail_templates.xml',
|
||||
'views/product_template_views.xml',
|
||||
'views/event_rental_request_views.xml',
|
||||
'views/website_templates.xml',
|
||||
'views/portal_templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
BIN
addons/event_rental/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/event_rental/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
3
addons/event_rental/controllers/__init__.py
Normal file
3
addons/event_rental/controllers/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import main
|
||||
Binary file not shown.
BIN
addons/event_rental/controllers/__pycache__/main.cpython-310.pyc
Normal file
BIN
addons/event_rental/controllers/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
251
addons/event_rental/controllers/main.py
Normal file
251
addons/event_rental/controllers/main.py
Normal file
@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from odoo import http, fields, _, exceptions
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def parse_html_datetime(dt_str):
|
||||
if not dt_str:
|
||||
return None
|
||||
for fmt in (
|
||||
"%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S",
|
||||
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d",
|
||||
"%m/%d/%Y %I:%M %p", "%d/%m/%Y %I:%M %p",
|
||||
"%m/%d/%Y %H:%M", "%d/%m/%Y %H:%M",
|
||||
"%m/%d/%Y", "%d/%m/%Y"
|
||||
):
|
||||
try:
|
||||
return datetime.strptime(dt_str.strip(), fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
try:
|
||||
return fields.Datetime.to_datetime(dt_str)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
class EventRentalController(http.Controller):
|
||||
|
||||
@http.route('/rentals', type='http', auth='public', website=True)
|
||||
def rental_catalog(self, search='', category=None, **post):
|
||||
domain = [('is_rental', '=', True)]
|
||||
if search:
|
||||
domain.append(('name', 'ilike', search))
|
||||
|
||||
# Fetch products matching domain
|
||||
products = request.env['product.template'].sudo().search(domain)
|
||||
|
||||
# Extract categories represented by these products to display in the filter sidebar
|
||||
categories = products.mapped('categ_id')
|
||||
|
||||
if category:
|
||||
domain.append(('categ_id', '=', int(category)))
|
||||
products = request.env['product.template'].sudo().search(domain)
|
||||
|
||||
values = {
|
||||
'products': products,
|
||||
'categories': categories,
|
||||
'search': search,
|
||||
'current_category': int(category) if category else None,
|
||||
}
|
||||
return request.render('event_rental.rental_catalog_template', values)
|
||||
|
||||
@http.route('/rentals/<model("product.template"):product>', type='http', auth='public', website=True)
|
||||
def rental_product_detail(self, product, **post):
|
||||
return request.render('event_rental.rental_product_detail_template', {'product': product})
|
||||
|
||||
@http.route('/rental/request', type='http', auth='public', website=True, methods=['GET', 'POST'])
|
||||
def rental_request(self, product_id=None, **post):
|
||||
if request.httprequest.method == 'GET':
|
||||
selected_product = None
|
||||
if product_id:
|
||||
# Browse product product variant
|
||||
product_tmpl = request.env['product.template'].sudo().browse(int(product_id))
|
||||
selected_product = product_tmpl.product_variant_id
|
||||
all_products = request.env['product.product'].sudo().search([('is_rental', '=', True)])
|
||||
return request.render('event_rental.rental_request_form_template', {
|
||||
'all_products': all_products,
|
||||
'selected_product': selected_product,
|
||||
'error_message': None,
|
||||
'post': post
|
||||
})
|
||||
|
||||
# Process POST request
|
||||
try:
|
||||
_logger.info("RENTAL REQUEST POST PARAMS (product_id=%s): %s", product_id, post)
|
||||
customer_name = post.get('customer_name')
|
||||
customer_email = post.get('customer_email')
|
||||
customer_phone = post.get('customer_phone')
|
||||
company_name = post.get('company_name')
|
||||
customer_address = post.get('customer_address')
|
||||
|
||||
start_date_str = post.get('start_date')
|
||||
end_date_str = post.get('end_date')
|
||||
location = post.get('location')
|
||||
event_type = post.get('event_type')
|
||||
|
||||
req_product_id = int(product_id or post.get('product_id') or 0)
|
||||
quantity = float(post.get('quantity') or 1.0)
|
||||
doc_type = post.get('doc_type', 'aadhaar')
|
||||
id_proof_file = request.httprequest.files.get('id_proof')
|
||||
|
||||
# Simple UI checks
|
||||
if not customer_name or not customer_email or not customer_phone or not start_date_str or not end_date_str or not location or not req_product_id:
|
||||
raise ValueError(_("All required fields must be filled."))
|
||||
|
||||
if not id_proof_file or id_proof_file.filename == '':
|
||||
raise ValueError(_("Please upload a valid Government ID Proof document."))
|
||||
|
||||
start_date = parse_html_datetime(start_date_str)
|
||||
end_date = parse_html_datetime(end_date_str)
|
||||
|
||||
if not start_date or not end_date:
|
||||
raise ValueError(_("Invalid start or end date format."))
|
||||
|
||||
if start_date >= end_date:
|
||||
raise ValueError(_("Event Start Date must be earlier than End Date."))
|
||||
|
||||
product = request.env['product.product'].sudo().browse(req_product_id)
|
||||
if not product:
|
||||
raise ValueError(_("Invalid rental product selected."))
|
||||
|
||||
# Availability checking
|
||||
dummy_request = request.env['event.rental.request'].sudo()
|
||||
available_qty = dummy_request.check_availability(start_date, end_date, product)
|
||||
|
||||
if available_qty < quantity:
|
||||
raise ValueError(_("Product '%s' is not available in the quantity requested (%s) for the selected dates. Only %s units are currently available.") % (
|
||||
product.name, quantity, available_qty
|
||||
))
|
||||
|
||||
# Find or create partner
|
||||
partner = request.env['res.partner'].sudo().search([('email', '=', customer_email)], limit=1)
|
||||
if not partner:
|
||||
partner = request.env['res.partner'].sudo().create({
|
||||
'name': customer_name,
|
||||
'email': customer_email,
|
||||
'phone': customer_phone,
|
||||
'company_name': company_name,
|
||||
'street': customer_address,
|
||||
})
|
||||
|
||||
# Create rental request record
|
||||
rental_request = request.env['event.rental.request'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'customer_name': customer_name,
|
||||
'customer_email': customer_email,
|
||||
'customer_phone': customer_phone,
|
||||
'company_name': company_name,
|
||||
'customer_address': customer_address,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'location': location,
|
||||
'event_type': event_type,
|
||||
'status': 'under_review',
|
||||
})
|
||||
|
||||
# Create request line
|
||||
request.env['event.rental.line'].sudo().create({
|
||||
'request_id': rental_request.id,
|
||||
'product_id': product.id,
|
||||
'quantity': quantity,
|
||||
})
|
||||
|
||||
# Store ID proof attachment
|
||||
file_content = id_proof_file.read()
|
||||
file_base64 = base64.b64encode(file_content)
|
||||
attachment = request.env['ir.attachment'].sudo().create({
|
||||
'name': id_proof_file.filename,
|
||||
'type': 'binary',
|
||||
'datas': file_base64,
|
||||
'res_model': 'event.rental.request',
|
||||
'res_id': rental_request.id,
|
||||
})
|
||||
|
||||
# Create document record
|
||||
request.env['event.document'].sudo().create({
|
||||
'request_id': rental_request.id,
|
||||
'partner_id': partner.id,
|
||||
'doc_type': doc_type,
|
||||
'attachment_id': attachment.id,
|
||||
'verification_status': 'pending',
|
||||
})
|
||||
|
||||
# Post submission trace
|
||||
rental_request.message_post(body=_("Rental request submitted successfully from public website."))
|
||||
|
||||
return request.redirect(f'/rental/request/success?name={rental_request.name}')
|
||||
|
||||
except Exception as e:
|
||||
selected_product = None
|
||||
if post.get('product_id'):
|
||||
selected_product = request.env['product.product'].sudo().browse(int(post.get('product_id')))
|
||||
all_products = request.env['product.product'].sudo().search([('is_rental', '=', True)])
|
||||
return request.render('event_rental.rental_request_form_template', {
|
||||
'all_products': all_products,
|
||||
'selected_product': selected_product,
|
||||
'error_message': str(e),
|
||||
'post': post
|
||||
})
|
||||
|
||||
@http.route('/rental/request/success', type='http', auth='public', website=True)
|
||||
def rental_request_success(self, name, **post):
|
||||
return request.render('event_rental.rental_request_success_template', {'name': name})
|
||||
|
||||
|
||||
class CustomerPortalRental(CustomerPortal):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super()._prepare_home_portal_values(counters)
|
||||
if 'rental_count' in counters:
|
||||
partner = request.env.user.partner_id
|
||||
rental_count = request.env['event.rental.request'].search_count([
|
||||
('partner_id', '=', partner.id)
|
||||
])
|
||||
values['rental_count'] = rental_count
|
||||
return values
|
||||
|
||||
@http.route(['/my/rentals', '/my/rentals/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def portal_my_rentals(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
partner = request.env.user.partner_id
|
||||
RentalRequest = request.env['event.rental.request']
|
||||
domain = [('partner_id', '=', partner.id)]
|
||||
|
||||
# Count for pagination
|
||||
rental_count = RentalRequest.search_count(domain)
|
||||
pager = portal_pager(
|
||||
url="/my/rentals",
|
||||
total=rental_count,
|
||||
page=page,
|
||||
step=10
|
||||
)
|
||||
|
||||
requests = RentalRequest.search(domain, limit=10, offset=pager['offset'])
|
||||
|
||||
values.update({
|
||||
'requests': requests,
|
||||
'page_name': 'rental_requests',
|
||||
'pager': pager,
|
||||
'default_url': '/my/rentals',
|
||||
})
|
||||
return request.render("event_rental.portal_my_rental_requests", values)
|
||||
|
||||
@http.route(['/my/rentals/<int:request_id>'], type='http', auth="user", website=True)
|
||||
def portal_rental_request_detail(self, request_id, **kw):
|
||||
rental_request = request.env['event.rental.request'].browse(request_id)
|
||||
try:
|
||||
# Enforce native record rules / access rights
|
||||
rental_request.check_access_rights('read')
|
||||
rental_request.check_access_rule('read')
|
||||
except exceptions.AccessError:
|
||||
return request.redirect('/my')
|
||||
|
||||
values = {
|
||||
'rental_request': rental_request,
|
||||
'page_name': 'rental_request_detail',
|
||||
}
|
||||
return request.render("event_rental.portal_rental_request_detail_template", values)
|
||||
53
addons/event_rental/data/mail_templates.xml
Normal file
53
addons/event_rental/data/mail_templates.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="email_template_rental_approved" model="mail.template">
|
||||
<field name="name">Event Rental: Approved & Quotation Ready</field>
|
||||
<field name="model_id" ref="event_rental.model_event_rental_request"/>
|
||||
<field name="subject">Your Event Rental Request {{ object.name }} has been Approved</field>
|
||||
<field name="email_from">{{ object.create_uid.email_formatted or object.env.company.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.customer_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #333333;">
|
||||
<h2 style="color: #007bff; font-weight: 500; margin-top: 0px;">Rental Request Approved!</h2>
|
||||
<p>Dear <strong t-out="object.customer_name or 'Customer'"/>,</p>
|
||||
<p>We are happy to inform you that your event rental request <strong t-out="object.name"/> has been approved for your event scheduled on <strong t-out="object.event_date"/>.</p>
|
||||
<p>We have generated a Sales Quotation for you. Please review the rental details:</p>
|
||||
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd; background-color: #f9f9f9; width: 30%;"><strong>Request Number:</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd;"><span t-out="object.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd; background-color: #f9f9f9;"><strong>Rental Period:</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd;">
|
||||
From <span t-out="object.start_date"/> to <span t-out="object.end_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd; background-color: #f9f9f9;"><strong>Event Location:</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd;"><span t-out="object.location"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd; background-color: #f9f9f9;"><strong>Event Type:</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #dddddd;"><span t-out="object.event_type.capitalize()"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>To view your detailed quotation, check pricing, and make online payment to confirm your booking, please click the button below:</p>
|
||||
<div style="margin: 25px 0px; text-align: center;">
|
||||
<a t-att-href="object.sale_order_id.get_portal_url() if object.sale_order_id else '#'"
|
||||
style="background-color: #28a745; color: #ffffff; padding: 12px 25px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block; box-shadow: 0 2px 5px rgba(0,0,0,0.15);">
|
||||
Review & Pay Rental Quotation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>Please note that your items are tentatively held. Payment is required to guarantee your rental reservation.</p>
|
||||
<p>Thank you for choosing us for your event!</p>
|
||||
<p style="margin-top: 30px;">Best regards,<br/>
|
||||
<strong>Event Rental Team</strong></p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
12
addons/event_rental/data/sequence.xml
Normal file
12
addons/event_rental/data/sequence.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="seq_event_rental_request" model="ir.sequence">
|
||||
<field name="name">Event Rental Request Sequence</field>
|
||||
<field name="code">event.rental.request</field>
|
||||
<field name="prefix">RENT/%(year)s/</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
5
addons/event_rental/models/__init__.py
Normal file
5
addons/event_rental/models/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import product_template
|
||||
from . import event_rental_request
|
||||
from . import sale_order
|
||||
BIN
addons/event_rental/models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/event_rental/models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
368
addons/event_rental/models/event_rental_request.py
Normal file
368
addons/event_rental/models/event_rental_request.py
Normal file
@ -0,0 +1,368 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from odoo import models, fields, api, exceptions, _
|
||||
from odoo.exceptions import UserError
|
||||
import urllib.parse
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class EventRentalRequest(models.Model):
|
||||
_name = 'event.rental.request'
|
||||
_description = 'Event Rental Request'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'id desc'
|
||||
|
||||
name = fields.Char(string='Request Number', required=True, copy=False, readonly=True, index=True, default=lambda self: _('New'))
|
||||
partner_id = fields.Many2one('res.partner', string='Customer', tracking=True)
|
||||
|
||||
# Customer Details (Useful for Guest/Website checkouts)
|
||||
customer_name = fields.Char(string='Customer Name', tracking=True)
|
||||
customer_email = fields.Char(string='Email', tracking=True)
|
||||
customer_phone = fields.Char(string='Mobile Number', tracking=True)
|
||||
company_name = fields.Char(string='Company Name')
|
||||
customer_address = fields.Text(string='Address')
|
||||
|
||||
# Event Details
|
||||
event_date = fields.Date(string='Event Date', compute='_compute_event_date', store=True)
|
||||
start_date = fields.Datetime(string='Start Date & Time', required=True, tracking=True)
|
||||
end_date = fields.Datetime(string='End Date & Time', required=True, tracking=True)
|
||||
location = fields.Text(string='Event Location', required=True)
|
||||
event_type = fields.Selection([
|
||||
('wedding', 'Wedding'),
|
||||
('birthday', 'Birthday'),
|
||||
('corporate', 'Corporate Event'),
|
||||
('stage', 'Stage Setup'),
|
||||
('festival', 'Festival'),
|
||||
('exhibition', 'Exhibition'),
|
||||
('other', 'Other')
|
||||
], string='Event Type', default='other', required=True)
|
||||
|
||||
# Financial details
|
||||
delivery_charge = fields.Float(string='Delivery Charge', default=0.0, tracking=True)
|
||||
setup_charge = fields.Float(string='Setup Charge', default=0.0, tracking=True)
|
||||
amount_total = fields.Float(string='Total Amount', compute='_compute_amount_total', store=True)
|
||||
|
||||
status = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('under_review', 'Under Review'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected'),
|
||||
('quotation_sent', 'Quotation Sent'),
|
||||
('confirmed', 'Confirmed'),
|
||||
('delivered', 'Delivered'),
|
||||
('returned', 'Returned'),
|
||||
('completed', 'Completed')
|
||||
], string='Status', default='draft', tracking=True, required=True)
|
||||
|
||||
line_ids = fields.One2many('event.rental.line', 'request_id', string='Rental Lines')
|
||||
document_ids = fields.One2many('event.document', 'request_id', string='Uploaded Documents')
|
||||
|
||||
# Sales Integration
|
||||
sale_order_id = fields.Many2one('sale.order', string='Sales Quotation', readonly=True, copy=False)
|
||||
|
||||
# Delivery tracking
|
||||
delivery_date = fields.Datetime(string='Delivery Scheduled Date')
|
||||
delivery_staff_id = fields.Many2one('res.users', string='Delivery Staff')
|
||||
delivery_status = fields.Selection([
|
||||
('pending', 'Pending'),
|
||||
('delivered', 'Delivered'),
|
||||
('picked_up', 'Picked Up'),
|
||||
('returned', 'Returned')
|
||||
], string='Delivery Status', default='pending', tracking=True)
|
||||
|
||||
is_all_available = fields.Boolean(string='All Items Available', compute='_compute_is_all_available')
|
||||
|
||||
@api.depends('start_date')
|
||||
def _compute_event_date(self):
|
||||
for rec in self:
|
||||
if rec.start_date:
|
||||
rec.event_date = rec.start_date.date()
|
||||
else:
|
||||
rec.event_date = False
|
||||
|
||||
@api.depends('line_ids.price_subtotal', 'delivery_charge', 'setup_charge')
|
||||
def _compute_amount_total(self):
|
||||
for rec in self:
|
||||
lines_sum = sum(rec.line_ids.mapped('price_subtotal'))
|
||||
rec.amount_total = lines_sum + rec.delivery_charge + rec.setup_charge
|
||||
|
||||
@api.depends('line_ids.is_available')
|
||||
def _compute_is_all_available(self):
|
||||
for rec in self:
|
||||
rec.is_all_available = all(line.is_available for line in rec.line_ids) if rec.line_ids else False
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('event.rental.request') or _('New')
|
||||
# If partner is set, automatically pre-fill customer details if empty
|
||||
if vals.get('partner_id'):
|
||||
partner = self.env['res.partner'].browse(vals['partner_id'])
|
||||
if not vals.get('customer_name'):
|
||||
vals['customer_name'] = partner.name
|
||||
if not vals.get('customer_email'):
|
||||
vals['customer_email'] = partner.email
|
||||
if not vals.get('customer_phone'):
|
||||
vals['customer_phone'] = partner.phone or partner.mobile
|
||||
if not vals.get('customer_address'):
|
||||
vals['customer_address'] = partner.contact_address
|
||||
return super(EventRentalRequest, self).create(vals_list)
|
||||
|
||||
def check_availability(self, start_date, end_date, product_id, exclude_request_id=None):
|
||||
"""
|
||||
Check the available inventory of a product for the selected date range.
|
||||
Standard qty_available acts as total pool.
|
||||
"""
|
||||
if not product_id or not start_date or not end_date:
|
||||
return 0.0
|
||||
|
||||
# Non-storable products are assumed to have infinite quantity
|
||||
if product_id.type != 'product':
|
||||
return 999999.0
|
||||
|
||||
total_capacity = product_id.qty_available
|
||||
|
||||
# Overlapping criteria:
|
||||
# (Start1 < End2) AND (End1 > Start2)
|
||||
# Statuses blocking inventory: approved, quotation_sent, confirmed, delivered, returned
|
||||
domain = [
|
||||
('product_id', '=', product_id.id),
|
||||
('request_id.status', 'in', ['approved', 'quotation_sent', 'confirmed', 'delivered', 'returned']),
|
||||
('request_id.start_date', '<', end_date),
|
||||
('request_id.end_date', '>', start_date),
|
||||
]
|
||||
if exclude_request_id:
|
||||
domain.append(('request_id', '!=', exclude_request_id))
|
||||
|
||||
overlapping_lines = self.env['event.rental.line'].search(domain)
|
||||
reserved_qty = sum(overlapping_lines.mapped('quantity'))
|
||||
|
||||
return max(0.0, total_capacity - reserved_qty)
|
||||
|
||||
def _get_or_create_service_product(self, name, default_code):
|
||||
product = self.env['product.product'].search([('default_code', '=', default_code)], limit=1)
|
||||
if not product:
|
||||
product = self.env['product.product'].create({
|
||||
'name': name,
|
||||
'type': 'service',
|
||||
'default_code': default_code,
|
||||
'sale_ok': True,
|
||||
'purchase_ok': False,
|
||||
})
|
||||
return product
|
||||
|
||||
def action_approve(self):
|
||||
"""
|
||||
Approve the request. Check availability, find/create customer partner,
|
||||
create sales quotation, set status to quotation_sent, and send notification.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.line_ids:
|
||||
raise UserError(_("Please add at least one rental product line."))
|
||||
|
||||
# Re-check availability before approving
|
||||
for line in self.line_ids:
|
||||
available_qty = self.check_availability(self.start_date, self.end_date, line.product_id, exclude_request_id=self.id)
|
||||
if available_qty < line.quantity:
|
||||
raise UserError(_("Product '%s' is not available in the required quantity (%s) for the selected dates. Only %s units are available.") % (
|
||||
line.product_id.display_name, line.quantity, available_qty
|
||||
))
|
||||
|
||||
# Check/create partner
|
||||
partner = self.partner_id
|
||||
if not partner:
|
||||
partner = self.env['res.partner'].search([('email', '=', self.customer_email)], limit=1)
|
||||
if not partner:
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': self.customer_name or self.customer_email or _('Guest Customer'),
|
||||
'email': self.customer_email,
|
||||
'phone': self.customer_phone,
|
||||
'company_name': self.company_name,
|
||||
'street': self.customer_address,
|
||||
})
|
||||
self.partner_id = partner
|
||||
|
||||
# Create Odoo Sales Order
|
||||
so_vals = {
|
||||
'partner_id': partner.id,
|
||||
'origin': self.name,
|
||||
'event_rental_request_id': self.id,
|
||||
}
|
||||
sale_order = self.env['sale.order'].create(so_vals)
|
||||
self.sale_order_id = sale_order
|
||||
|
||||
# Rental product lines
|
||||
for line in self.line_ids:
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom_qty': line.quantity,
|
||||
'price_unit': line.price_unit,
|
||||
'name': _("Rental: %s (Period: %s to %s)") % (line.product_id.display_name, self.start_date, self.end_date),
|
||||
})
|
||||
|
||||
# Add Delivery charges
|
||||
if self.delivery_charge > 0:
|
||||
delivery_product = self._get_or_create_service_product("Delivery Charges", "RENTAL_DELIVERY")
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': delivery_product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': self.delivery_charge,
|
||||
'name': _("Delivery Charges for Rental %s") % self.name,
|
||||
})
|
||||
|
||||
# Add Setup charges
|
||||
if self.setup_charge > 0:
|
||||
setup_product = self._get_or_create_service_product("Setup Charges", "RENTAL_SETUP")
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': setup_product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': self.setup_charge,
|
||||
'name': _("Setup Charges for Rental %s") % self.name,
|
||||
})
|
||||
|
||||
# Set status to approved and then quotation_sent
|
||||
self.write({
|
||||
'status': 'quotation_sent',
|
||||
'delivery_status': 'pending'
|
||||
})
|
||||
|
||||
# Send Email Notification
|
||||
template = self.env.ref('event_rental.email_template_rental_approved', raise_if_not_found=False)
|
||||
if template:
|
||||
template.send_mail(self.id, force_send=True)
|
||||
self.message_post(body=_("Quotation created and email notification sent to customer."))
|
||||
else:
|
||||
self.message_post(body=_("Rental Request Approved. Quotation %s generated.") % sale_order.name)
|
||||
|
||||
# Log WhatsApp Notification & Generate Link
|
||||
self._log_whatsapp_notification()
|
||||
|
||||
def action_reject(self):
|
||||
self.write({'status': 'rejected'})
|
||||
self.message_post(body=_("Rental Request has been Rejected."))
|
||||
|
||||
def action_deliver(self):
|
||||
self.write({
|
||||
'status': 'delivered',
|
||||
'delivery_status': 'delivered',
|
||||
'delivery_date': fields.Datetime.now()
|
||||
})
|
||||
self.message_post(body=_("Products have been delivered to the event location."))
|
||||
|
||||
def action_pickup(self):
|
||||
self.write({
|
||||
'status': 'returned',
|
||||
'delivery_status': 'picked_up'
|
||||
})
|
||||
self.message_post(body=_("Products have been picked up from the location."))
|
||||
|
||||
def action_return(self):
|
||||
self.write({
|
||||
'status': 'returned',
|
||||
'delivery_status': 'returned'
|
||||
})
|
||||
self.message_post(body=_("Products returned and checked back in inventory."))
|
||||
|
||||
def action_complete(self):
|
||||
self.write({'status': 'completed'})
|
||||
self.message_post(body=_("Rental Request completed."))
|
||||
|
||||
def action_reset_draft(self):
|
||||
self.write({'status': 'draft'})
|
||||
self.message_post(body=_("Request reset to Draft."))
|
||||
|
||||
def _log_whatsapp_notification(self):
|
||||
"""
|
||||
Log WhatsApp message details and prepare a quick-action link for the administrator
|
||||
"""
|
||||
if not self.customer_phone:
|
||||
return
|
||||
|
||||
message = _("Hello %s, your rental request %s has been approved. Please review the quotation: %s") % (
|
||||
self.customer_name,
|
||||
self.name,
|
||||
self.sale_order_id.get_portal_url() if self.sale_order_id else ''
|
||||
)
|
||||
|
||||
encoded_message = urllib.parse.quote(message)
|
||||
wa_url = f"https://web.whatsapp.com/send?phone={self.customer_phone}&text={encoded_message}"
|
||||
|
||||
chatter_body = _(
|
||||
"<strong>WhatsApp Notification Queued:</strong><br/>"
|
||||
"Recipient Phone: %s<br/>"
|
||||
"Message: <em>\"%s\"</em><br/>"
|
||||
"<a href=\"%s\" target=\"_blank\" class=\"btn btn-sm btn-primary mt-2\">Send via WhatsApp Web</a>"
|
||||
) % (self.customer_phone, message, wa_url)
|
||||
|
||||
self.message_post(body=chatter_body)
|
||||
|
||||
|
||||
class EventRentalLine(models.Model):
|
||||
_name = 'event.rental.line'
|
||||
_description = 'Event Rental Line'
|
||||
|
||||
request_id = fields.Many2one('event.rental.request', string='Rental Request', ondelete='cascade', required=True)
|
||||
product_id = fields.Many2one('product.product', string='Product', domain=[('is_rental', '=', True)], required=True)
|
||||
quantity = fields.Float(string='Quantity Required', default=1.0, required=True)
|
||||
price_unit = fields.Float(string='Rental Price Unit', compute='_compute_price_unit', readonly=False, store=True)
|
||||
price_subtotal = fields.Float(string='Subtotal', compute='_compute_price_subtotal', store=True)
|
||||
is_available = fields.Boolean(string='Available', compute='_compute_is_available')
|
||||
|
||||
@api.depends('product_id', 'request_id.start_date', 'request_id.end_date')
|
||||
def _compute_price_unit(self):
|
||||
for line in self:
|
||||
if line.product_id:
|
||||
duration = 1.0
|
||||
if line.request_id.start_date and line.request_id.end_date:
|
||||
delta = line.request_id.end_date - line.request_id.start_date
|
||||
days = delta.days
|
||||
if delta.seconds > 0 or delta.days == 0:
|
||||
days += 1
|
||||
duration = max(1.0, float(days))
|
||||
line.price_unit = line.product_id.rental_price_per_day * duration
|
||||
else:
|
||||
line.price_unit = 0.0
|
||||
|
||||
@api.depends('quantity', 'price_unit')
|
||||
def _compute_price_subtotal(self):
|
||||
for line in self:
|
||||
line.price_subtotal = line.quantity * line.price_unit
|
||||
|
||||
@api.depends('product_id', 'quantity', 'request_id.start_date', 'request_id.end_date')
|
||||
def _compute_is_available(self):
|
||||
for line in self:
|
||||
if not line.product_id or not line.request_id.start_date or not line.request_id.end_date:
|
||||
line.is_available = True
|
||||
continue
|
||||
available_qty = line.request_id.check_availability(
|
||||
line.request_id.start_date,
|
||||
line.request_id.end_date,
|
||||
line.product_id,
|
||||
exclude_request_id=line.request_id.id
|
||||
)
|
||||
line.is_available = available_qty >= line.quantity
|
||||
|
||||
|
||||
class EventDocument(models.Model):
|
||||
_name = 'event.document'
|
||||
_description = 'Event Rental Document'
|
||||
|
||||
request_id = fields.Many2one('event.rental.request', string='Rental Request', ondelete='cascade', required=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Customer')
|
||||
doc_type = fields.Selection([
|
||||
('aadhaar', 'Aadhaar Card'),
|
||||
('driving_license', 'Driving License'),
|
||||
('passport', 'Passport'),
|
||||
('voter_id', 'Voter ID'),
|
||||
('other', 'Other ID Proof')
|
||||
], string='ID Proof Type', required=True)
|
||||
attachment_id = fields.Many2one('ir.attachment', string='Attachment File', required=True)
|
||||
verification_status = fields.Selection([
|
||||
('pending', 'Pending Verification'),
|
||||
('verified', 'Verified'),
|
||||
('rejected', 'Rejected')
|
||||
], string='Verification Status', default='pending', required=True)
|
||||
20
addons/event_rental/models/product_template.py
Normal file
20
addons/event_rental/models/product_template.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
is_rental = fields.Boolean(
|
||||
string='Can be Rented',
|
||||
default=False,
|
||||
help="Check this if the product is available for event rental."
|
||||
)
|
||||
rental_price_per_day = fields.Float(
|
||||
string='Rental Price Per Day',
|
||||
default=0.0,
|
||||
help="Price charged per day for renting this product."
|
||||
)
|
||||
rental_terms = fields.Html(
|
||||
string='Rental Terms',
|
||||
help="Rental terms and conditions specific to this product."
|
||||
)
|
||||
36
addons/event_rental/models/sale_order.py
Normal file
36
addons/event_rental/models/sale_order.py
Normal file
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
event_rental_request_id = fields.Many2one(
|
||||
'event.rental.request',
|
||||
string='Event Rental Request',
|
||||
readonly=True,
|
||||
copy=False
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
for order in self:
|
||||
if order.event_rental_request_id:
|
||||
order.event_rental_request_id.write({
|
||||
'status': 'confirmed'
|
||||
})
|
||||
order.event_rental_request_id.message_post(
|
||||
body=f"Sales Order {order.name} has been confirmed. Rental booking status set to Confirmed."
|
||||
)
|
||||
return res
|
||||
|
||||
def action_cancel(self):
|
||||
res = super(SaleOrder, self).action_cancel()
|
||||
for order in self:
|
||||
if order.event_rental_request_id:
|
||||
order.event_rental_request_id.write({
|
||||
'status': 'rejected'
|
||||
})
|
||||
order.event_rental_request_id.message_post(
|
||||
body=f"Sales Order {order.name} was cancelled. Rental booking status set to Rejected."
|
||||
)
|
||||
return res
|
||||
10
addons/event_rental/security/ir.model.access.csv
Normal file
10
addons/event_rental/security/ir.model.access.csv
Normal file
@ -0,0 +1,10 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_event_rental_request_officer,event.rental.request.officer,model_event_rental_request,group_event_rental_officer,1,1,1,1
|
||||
access_event_rental_request_manager,event.rental.request.manager,model_event_rental_request,group_event_rental_manager,1,1,1,1
|
||||
access_event_rental_request_portal,event.rental.request.portal,model_event_rental_request,base.group_portal,1,1,0,0
|
||||
access_event_rental_line_officer,event.rental.line.officer,model_event_rental_line,group_event_rental_officer,1,1,1,1
|
||||
access_event_rental_line_manager,event.rental.line.manager,model_event_rental_line,group_event_rental_manager,1,1,1,1
|
||||
access_event_rental_line_portal,event.rental.line.portal,model_event_rental_line,base.group_portal,1,1,0,0
|
||||
access_event_document_officer,event.document.officer,model_event_document,group_event_rental_officer,1,1,1,1
|
||||
access_event_document_manager,event.document.manager,model_event_document,group_event_rental_manager,1,1,1,1
|
||||
access_event_document_portal,event.document.portal,model_event_document,base.group_portal,1,1,1,0
|
||||
|
58
addons/event_rental/security/security.xml
Normal file
58
addons/event_rental/security/security.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Security Category -->
|
||||
<record id="module_category_event_rental" model="ir.module.category">
|
||||
<field name="name">Event Rental Management</field>
|
||||
<field name="description">Category for event rental management permissions.</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<!-- Rental User Group -->
|
||||
<record id="group_event_rental_officer" model="res.groups">
|
||||
<field name="name">Rental Officer</field>
|
||||
<field name="category_id" ref="module_category_event_rental"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Rental Manager Group -->
|
||||
<record id="group_event_rental_manager" model="res.groups">
|
||||
<field name="name">Rental Manager</field>
|
||||
<field name="category_id" ref="module_category_event_rental"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_event_rental_officer'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Record Rule: Portal Users can only see their own requests -->
|
||||
<record id="event_rental_request_portal_rule" model="ir.rule">
|
||||
<field name="name">Portal Rental Requests</field>
|
||||
<field name="model_id" ref="model_event_rental_request"/>
|
||||
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Record Rule: Portal Documents can only see their own documents -->
|
||||
<record id="event_document_portal_rule" model="ir.rule">
|
||||
<field name="name">Portal Rental Documents</field>
|
||||
<field name="model_id" ref="model_event_document"/>
|
||||
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Record Rule: Staff can see all requests -->
|
||||
<record id="event_rental_request_officer_rule" model="ir.rule">
|
||||
<field name="name">Officer Rental Requests</field>
|
||||
<field name="model_id" ref="model_event_rental_request"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_event_rental_officer'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
3
addons/event_rental/tests/__init__.py
Normal file
3
addons/event_rental/tests/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_rental
|
||||
BIN
addons/event_rental/tests/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/event_rental/tests/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
121
addons/event_rental/tests/test_rental.py
Normal file
121
addons/event_rental/tests/test_rental.py
Normal file
@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.fields import Datetime
|
||||
from datetime import datetime, timedelta
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class TestEventRental(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEventRental, cls).setUpClass()
|
||||
|
||||
# Create a rental product template first, then get variant
|
||||
cls.rental_product = cls.env['product.product'].create({
|
||||
'name': 'Wedding Chair',
|
||||
'type': 'product',
|
||||
'is_rental': True,
|
||||
'rental_price_per_day': 10.0,
|
||||
})
|
||||
|
||||
# Set quantity on hand using Odoo stock.quant
|
||||
warehouse = cls.env['stock.warehouse'].search([], limit=1)
|
||||
if warehouse:
|
||||
location = warehouse.lot_stock_id
|
||||
cls.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': cls.rental_product.id,
|
||||
'location_id': location.id,
|
||||
'inventory_quantity': 50.0,
|
||||
}).action_apply_inventory()
|
||||
|
||||
# Create a customer partner
|
||||
cls.partner = cls.env['res.partner'].create({
|
||||
'name': 'Test Client',
|
||||
'email': 'client@test.com',
|
||||
'phone': '1234567890',
|
||||
})
|
||||
|
||||
def test_01_rental_flow(self):
|
||||
""" Test the complete rental reservation, pricing, approval, payment, and delivery flow """
|
||||
# Define dates: 3 days rental duration
|
||||
start_date = datetime.now() + timedelta(days=1)
|
||||
end_date = start_date + timedelta(days=3)
|
||||
|
||||
# Create request
|
||||
request = self.env['event.rental.request'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'location': 'Grand Palace Hall',
|
||||
'event_type': 'wedding',
|
||||
'delivery_charge': 50.0,
|
||||
'setup_charge': 30.0,
|
||||
})
|
||||
|
||||
# Create line: booking 10 chairs
|
||||
line = self.env['event.rental.line'].create({
|
||||
'request_id': request.id,
|
||||
'product_id': self.rental_product.id,
|
||||
'quantity': 10.0,
|
||||
})
|
||||
|
||||
# Verify duration-based pricing: 3 days * $10 per day = $30 per chair unit
|
||||
self.assertEqual(line.price_unit, 30.0, "Line unit price should be rental_price_per_day * duration")
|
||||
self.assertEqual(line.price_subtotal, 300.0, "Subtotal should be quantity * price_unit")
|
||||
|
||||
# Verify grand total: $300 (subtotal) + $50 (delivery) + $30 (setup) = $380
|
||||
self.assertEqual(request.amount_total, 380.0, "Grand total is incorrect")
|
||||
|
||||
# Check availability
|
||||
avail = request.check_availability(start_date, end_date, self.rental_product)
|
||||
self.assertEqual(avail, 50.0, "Before approval, all 50 chairs should be available")
|
||||
|
||||
# Approve request
|
||||
request.action_approve()
|
||||
|
||||
# Status should become quotation_sent
|
||||
self.assertEqual(request.status, 'quotation_sent')
|
||||
self.assertTrue(request.sale_order_id, "Sales Order should be generated")
|
||||
self.assertEqual(request.sale_order_id.amount_untaxed, 380.0, "Sales order untaxed total should match rental request total")
|
||||
|
||||
# Check availability again: since approved request blocks inventory, available should drop by 10
|
||||
avail_after_approval = request.check_availability(start_date, end_date, self.rental_product)
|
||||
self.assertEqual(avail_after_approval, 40.0, "Approved booking should block inventory")
|
||||
|
||||
# Confirm Sale Order (simulating payment)
|
||||
request.sale_order_id.action_confirm()
|
||||
|
||||
# Request status should automatically become confirmed
|
||||
self.assertEqual(request.status, 'confirmed')
|
||||
|
||||
# Deliver products
|
||||
request.action_deliver()
|
||||
self.assertEqual(request.status, 'delivered')
|
||||
self.assertEqual(request.delivery_status, 'delivered')
|
||||
|
||||
# Return products
|
||||
request.action_pickup()
|
||||
self.assertEqual(request.status, 'returned')
|
||||
self.assertEqual(request.delivery_status, 'picked_up')
|
||||
|
||||
def test_02_overbooking_prevention(self):
|
||||
""" Test that overbooking raises a UserError during approval """
|
||||
start_date = datetime.now() + timedelta(days=1)
|
||||
end_date = start_date + timedelta(days=3)
|
||||
|
||||
# Request 60 chairs (only 50 are on hand)
|
||||
request = self.env['event.rental.request'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'location': 'Venue Hall',
|
||||
})
|
||||
self.env['event.rental.line'].create({
|
||||
'request_id': request.id,
|
||||
'product_id': self.rental_product.id,
|
||||
'quantity': 60.0,
|
||||
})
|
||||
|
||||
# Approving should raise UserError
|
||||
with self.assertRaises(UserError):
|
||||
request.action_approve()
|
||||
201
addons/event_rental/views/event_rental_request_views.xml
Normal file
201
addons/event_rental/views/event_rental_request_views.xml
Normal file
@ -0,0 +1,201 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Rental Line Form View -->
|
||||
<record id="view_event_rental_line_form" model="ir.ui.view">
|
||||
<field name="name">event.rental.line.form</field>
|
||||
<field name="model">event.rental.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="product_id"/>
|
||||
<field name="quantity"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="price_subtotal"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Rental Request Tree View -->
|
||||
<record id="view_event_rental_request_tree" model="ir.ui.view">
|
||||
<field name="name">event.rental.request.tree</field>
|
||||
<field name="model">event.rental.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Rental Requests" decoration-info="status == 'under_review'" decoration-success="status in ('confirmed', 'completed')" decoration-muted="status == 'rejected'">
|
||||
<field name="name"/>
|
||||
<field name="customer_name"/>
|
||||
<field name="event_date"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="amount_total" sum="Total Amount"/>
|
||||
<field name="status" widget="badge" decoration-info="status == 'under_review'" decoration-success="status == 'confirmed'" decoration-warning="status == 'quotation_sent'" decoration-danger="status == 'rejected'"/>
|
||||
<field name="delivery_status" widget="badge" decoration-info="delivery_status == 'pending'" decoration-success="delivery_status == 'delivered'" decoration-warning="delivery_status == 'picked_up'"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Rental Request Calendar View -->
|
||||
<record id="view_event_rental_request_calendar" model="ir.ui.view">
|
||||
<field name="name">event.rental.request.calendar</field>
|
||||
<field name="model">event.rental.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="Rental Calendar" date_start="start_date" date_stop="end_date" color="status" quick_create="false">
|
||||
<field name="customer_name"/>
|
||||
<field name="name"/>
|
||||
<field name="event_type"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Rental Request Form View -->
|
||||
<record id="view_event_rental_request_form" model="ir.ui.view">
|
||||
<field name="name">event.rental.request.form</field>
|
||||
<field name="model">event.rental.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rental Request">
|
||||
<header>
|
||||
<button name="action_approve" string="Approve Request" type="object" class="oe_highlight" invisible="status not in ('draft', 'under_review')"/>
|
||||
<button name="action_reject" string="Reject Request" type="object" class="btn-danger" invisible="status not in ('draft', 'under_review')"/>
|
||||
<button name="action_deliver" string="Mark as Delivered" type="object" class="oe_highlight" invisible="status != 'confirmed'"/>
|
||||
<button name="action_pickup" string="Mark as Picked Up" type="object" invisible="status != 'delivered'"/>
|
||||
<button name="action_return" string="Mark as Returned" type="object" class="oe_highlight" invisible="status != 'returned' or delivery_status == 'returned'"/>
|
||||
<button name="action_complete" string="Complete Booking" type="object" class="oe_highlight" invisible="status not in ('returned', 'confirmed', 'delivered') or delivery_status != 'returned'"/>
|
||||
<button name="action_reset_draft" string="Reset to Draft" type="object" invisible="status != 'rejected'"/>
|
||||
<field name="status" widget="statusbar" statusbar_visible="draft,under_review,quotation_sent,confirmed,delivered,returned,completed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Customer Details">
|
||||
<field name="partner_id"/>
|
||||
<field name="customer_name" required="1"/>
|
||||
<field name="customer_email" required="1"/>
|
||||
<field name="customer_phone" required="1"/>
|
||||
<field name="company_name"/>
|
||||
<field name="customer_address"/>
|
||||
</group>
|
||||
<group string="Event Details">
|
||||
<field name="event_date" readonly="1"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="location"/>
|
||||
<field name="event_type"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Requested Products" name="products">
|
||||
<field name="line_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="quantity"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="price_subtotal" sum="Subtotal"/>
|
||||
<field name="is_available" widget="boolean" readonly="1" decoration-danger="not is_available" decoration-success="is_available"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Uploaded Documents" name="documents">
|
||||
<field name="document_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="doc_type"/>
|
||||
<field name="attachment_id"/>
|
||||
<field name="verification_status" widget="badge" decoration-warning="verification_status == 'pending'" decoration-success="verification_status == 'verified'" decoration-danger="verification_status == 'rejected'"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Delivery Management" name="delivery">
|
||||
<group>
|
||||
<group>
|
||||
<field name="delivery_date"/>
|
||||
<field name="delivery_staff_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="delivery_status" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Pricing & Sales" name="pricing">
|
||||
<group>
|
||||
<group>
|
||||
<field name="sale_order_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="delivery_charge"/>
|
||||
<field name="setup_charge"/>
|
||||
<field name="amount_total" widget="monetary" readonly="1" class="oe_subtotal_footer_separator"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" groups="base.group_user"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Rental Request Search View -->
|
||||
<record id="view_event_rental_request_search" model="ir.ui.view">
|
||||
<field name="name">event.rental.request.search</field>
|
||||
<field name="model">event.rental.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Rental Requests">
|
||||
<field name="name"/>
|
||||
<field name="customer_name"/>
|
||||
<field name="customer_email"/>
|
||||
<field name="location"/>
|
||||
<filter string="Draft" name="draft" domain="[('status', '=', 'draft')]"/>
|
||||
<filter string="Under Review" name="under_review" domain="[('status', '=', 'under_review')]"/>
|
||||
<filter string="Approved" name="approved" domain="[('status', '=', 'approved')]"/>
|
||||
<filter string="Confirmed" name="confirmed" domain="[('status', '=', 'confirmed')]"/>
|
||||
<filter string="Delivered" name="delivered" domain="[('status', '=', 'delivered')]"/>
|
||||
<filter string="Returned" name="returned" domain="[('status', '=', 'returned')]"/>
|
||||
<filter string="Completed" name="completed" domain="[('status', '=', 'completed')]"/>
|
||||
<filter string="Rejected" name="rejected" domain="[('status', '=', 'rejected')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Status" name="status" context="{'group_by': 'status'}"/>
|
||||
<filter string="Event Date" name="event_date" context="{'group_by': 'event_date'}"/>
|
||||
<filter string="Event Type" name="event_type" context="{'group_by': 'event_type'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rental Requests -->
|
||||
<record id="action_event_rental_request" model="ir.actions.act_window">
|
||||
<field name="name">Rental Requests</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">event.rental.request</field>
|
||||
<field name="view_mode">tree,calendar,form</field>
|
||||
<field name="search_view_id" ref="view_event_rental_request_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first Event Rental Request!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rental Products -->
|
||||
<record id="action_event_rental_products" model="ir.actions.act_window">
|
||||
<field name="name">Rental Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="domain">[('is_rental', '=', True)]</field>
|
||||
<field name="context">{'default_is_rental': True}</field>
|
||||
</record>
|
||||
|
||||
<!-- Menus -->
|
||||
<menuitem id="menu_event_rental_root" name="Event Rentals" sequence="15"/>
|
||||
<menuitem id="menu_event_rental_request" name="Rental Requests" parent="menu_event_rental_root" action="action_event_rental_request" sequence="10"/>
|
||||
<menuitem id="menu_event_rental_products" name="Rental Products" parent="menu_event_rental_root" action="action_event_rental_products" sequence="20"/>
|
||||
</odoo>
|
||||
110
addons/event_rental/views/portal_templates.xml
Normal file
110
addons/event_rental/views/portal_templates.xml
Normal file
@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Portal Breadcrumbs -->
|
||||
<template id="portal_my_home_menu_rental" name="Portal layout : Rental menu entries" inherit_id="portal.portal_breadcrumbs" priority="30">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'rental_requests' or page_name == 'rental_request_detail'" t-att-class="'breadcrumb-item active' if page_name == 'rental_requests' else 'breadcrumb-item'">
|
||||
<a t-if="page_name == 'rental_request_detail'" href="/my/rentals">My Rentals</a>
|
||||
<t t-else="">My Rentals</t>
|
||||
</li>
|
||||
<li t-if="page_name == 'rental_request_detail'" class="breadcrumb-item active">
|
||||
<span t-field="rental_request.name"/>
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Portal Home Counter -->
|
||||
<template id="portal_my_home_rental" name="Portal My Home : Rental entries" inherit_id="portal.portal_my_home" priority="30">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">My Rentals</t>
|
||||
<t t-set="url" t-value="'/my/rentals'"/>
|
||||
<t t-set="placeholder_count" t-value="'rental_count'"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Portal List View -->
|
||||
<template id="portal_my_rental_requests" name="My Rental Requests">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
<t t-call="portal.portal_searchbar" t-if="requests">
|
||||
<t t-set="title">Rental Requests</t>
|
||||
</t>
|
||||
|
||||
<div t-if="not requests" class="alert alert-warning mt-3" role="alert">
|
||||
There are currently no rental requests in your account.
|
||||
</div>
|
||||
|
||||
<table t-if="requests" class="table table-hover bg-white mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reference</th>
|
||||
<th>Event Date</th>
|
||||
<th>Event Type</th>
|
||||
<th class="text-right">Total Amount</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="requests" t-as="req">
|
||||
<td>
|
||||
<a t-att-href="'/my/rentals/%s' % req.id" t-out="req.name"/>
|
||||
</td>
|
||||
<td><span t-field="req.event_date"/></td>
|
||||
<td><span t-field="req.event_type"/></td>
|
||||
<td class="text-right">
|
||||
<span t-field="req.amount_total" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</td>
|
||||
<td class="text-center"><span t-field="req.status"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div t-if="pager" class="o_portal_pager d-flex justify-content-center mt-3">
|
||||
<t t-call="portal.pager"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Portal Details View -->
|
||||
<template id="portal_rental_request_detail_template" name="Rental Request Detail">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container py-3">
|
||||
<div class="card p-4">
|
||||
<h3>Request <span t-field="rental_request.name"/></h3>
|
||||
<p>Status: <span t-field="rental_request.status"/></p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Start:</strong> <span t-field="rental_request.start_date"/></p>
|
||||
<p><strong>End:</strong> <span t-field="rental_request.end_date"/></p>
|
||||
<p><strong>Location:</strong> <span t-field="rental_request.location"/></p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<p><strong>Total:</strong> <span t-field="rental_request.amount_total" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></p>
|
||||
<a t-if="rental_request.sale_order_id" t-att-href="rental_request.sale_order_id.get_portal_url()" class="btn btn-primary">Pay Quotation</a>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="mt-4">Requested Products</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="rental_request.line_ids" t-as="line">
|
||||
<td><span t-field="line.product_id.name"/></td>
|
||||
<td><span t-field="line.quantity"/></td>
|
||||
<td><span t-field="line.price_unit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
|
||||
<td><span t-field="line.price_subtotal" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
41
addons/event_rental/views/product_template_views.xml
Normal file
41
addons/event_rental/views/product_template_views.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Product Template Form Inheritance -->
|
||||
<record id="product_template_form_view_rental" model="ir.ui.view">
|
||||
<field name="name">product.template.form.inherit.rental</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_only_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='options']" position="inside">
|
||||
<span class="d-inline-block">
|
||||
<field name="is_rental"/>
|
||||
<label for="is_rental"/>
|
||||
</span>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Event Rental" name="event_rental" invisible="not is_rental">
|
||||
<group>
|
||||
<group>
|
||||
<field name="rental_price_per_day" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Rental Terms & Conditions">
|
||||
<field name="rental_terms" nolabel="1" placeholder="Define terms specific to renting this item..." class="oe-bordered-editor"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Product Template Search Inheritance -->
|
||||
<record id="product_template_search_view_rental" model="ir.ui.view">
|
||||
<field name="name">product.template.search.inherit.rental</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_search_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='filter_to_sell']" position="after">
|
||||
<filter string="Can be Rented" name="filter_is_rental" domain="[('is_rental', '=', True)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
213
addons/event_rental/views/website_templates.xml
Normal file
213
addons/event_rental/views/website_templates.xml
Normal file
@ -0,0 +1,213 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Basic Catalog Page -->
|
||||
<template id="rental_catalog_template" name="Rental Catalog">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="container py-4">
|
||||
<h1 class="mb-4">Event Rentals</h1>
|
||||
<div class="row">
|
||||
<!-- Categories sidebar -->
|
||||
<div class="col-md-3 mb-4">
|
||||
<div class="card p-3">
|
||||
<h5>Categories</h5>
|
||||
<div class="list-group">
|
||||
<a t-att-href="'/rentals' + (search and ('?search=' + search) or '')"
|
||||
t-att-class="'list-group-item list-group-item-action ' + (not current_category and 'active' or '')">
|
||||
All
|
||||
</a>
|
||||
<t t-foreach="categories" t-as="cat">
|
||||
<a t-att-href="'/rentals?category=' + str(cat.id) + (search and ('&search=' + search) or '')"
|
||||
t-att-class="'list-group-item list-group-item-action ' + (current_category == cat.id and 'active' or '')">
|
||||
<span t-field="cat.name"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Product list -->
|
||||
<div class="col-md-9">
|
||||
<div class="mb-4">
|
||||
<form action="/rentals" method="get" class="input-group">
|
||||
<input type="hidden" name="category" t-att-value="current_category" t-if="current_category"/>
|
||||
<input type="text" name="search" class="form-control" placeholder="Search rental items..." t-att-value="search"/>
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row" t-if="products">
|
||||
<t t-foreach="products" t-as="product">
|
||||
<div class="col-md-4 col-sm-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div style="height: 150px; background: #eee; text-align: center; line-height: 150px;">
|
||||
<img t-if="product.image_1920" t-att-src="image_data_uri(product.image_1920)" class="img-fluid h-100" style="object-fit: cover;"/>
|
||||
<span t-else="" class="text-muted">No Image</span>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title" t-field="product.name"/>
|
||||
<p class="card-text text-muted flex-grow-1" t-field="product.description_sale"/>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<span class="font-weight-bold" t-field="product.rental_price_per_day" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
<a t-att-href="'/rentals/%s' % slug(product)" class="btn btn-sm btn-outline-primary">Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Basic Product Detail Page -->
|
||||
<template id="rental_product_detail_template" name="Rental Product Detail">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div style="height: 350px; background: #eee; text-align: center; line-height: 350px;">
|
||||
<img t-if="product.image_1920" t-att-src="image_data_uri(product.image_1920)" class="img-fluid h-100" style="object-fit: cover;"/>
|
||||
<span t-else="" class="text-muted">No Image</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h1 t-field="product.name"/>
|
||||
<h4 class="text-primary my-3" t-field="product.rental_price_per_day" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
<p t-field="product.description_sale"/>
|
||||
<a t-att-href="'/rental/request?product_id=%s' % product.product_variant_id.id" class="btn btn-primary btn-lg btn-block mt-4">
|
||||
Request Booking
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4" t-if="product.rental_terms">
|
||||
<h3>Terms & Conditions</h3>
|
||||
<div t-field="product.rental_terms"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Basic Request Form Template -->
|
||||
<template id="rental_request_form_template" name="Request Rental Form">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="container py-4">
|
||||
<h2 class="mb-4">Request Rental Form</h2>
|
||||
<div class="alert alert-danger" role="alert" t-if="error_message">
|
||||
<t t-out="error_message"/>
|
||||
</div>
|
||||
<form action="/rental/request" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<h4>Customer Information</h4>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="customer_name">Name</label>
|
||||
<input type="text" name="customer_name" id="customer_name" class="form-control" required="1" t-att-value="post.get('customer_name', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="customer_email">Email</label>
|
||||
<input type="email" name="customer_email" id="customer_email" class="form-control" required="1" t-att-value="post.get('customer_email', '')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="customer_phone">Phone</label>
|
||||
<input type="tel" name="customer_phone" id="customer_phone" class="form-control" required="1" t-att-value="post.get('customer_phone', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="company_name">Company Name</label>
|
||||
<input type="text" name="company_name" id="company_name" class="form-control" t-att-value="post.get('company_name', '')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer_address">Address</label>
|
||||
<textarea name="customer_address" id="customer_address" class="form-control"><t t-out="post.get('customer_address', '')"/></textarea>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Event Details</h4>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="start_date">Start Date & Time</label>
|
||||
<input type="datetime-local" name="start_date" id="start_date" class="form-control" required="1" t-att-value="post.get('start_date', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="end_date">End Date & Time</label>
|
||||
<input type="datetime-local" name="end_date" id="end_date" class="form-control" required="1" t-att-value="post.get('end_date', '')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="location">Location</label>
|
||||
<input type="text" name="location" id="location" class="form-control" required="1" t-att-value="post.get('location', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="event_type">Event Type</label>
|
||||
<select name="event_type" id="event_type" class="form-control">
|
||||
<option value="wedding">Wedding</option>
|
||||
<option value="birthday">Birthday</option>
|
||||
<option value="corporate">Corporate Event</option>
|
||||
<option value="stage">Stage Setup</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Rental Product</h4>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-8">
|
||||
<label for="product_id">Product</label>
|
||||
<select name="product_id" id="product_id" class="form-control">
|
||||
<t t-foreach="all_products" t-as="prod">
|
||||
<option t-att-value="prod.id" t-att-selected="selected_product and selected_product.id == prod.id">
|
||||
<t t-out="prod.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label for="quantity">Quantity</label>
|
||||
<input type="number" name="quantity" id="quantity" class="form-control" min="1" value="1" required="1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Documents</h4>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="doc_type">ID Proof Type</label>
|
||||
<select name="doc_type" id="doc_type" class="form-control">
|
||||
<option value="aadhaar">Aadhaar Card</option>
|
||||
<option value="driving_license">Driving License</option>
|
||||
<option value="passport">Passport</option>
|
||||
<option value="voter_id">Voter ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="id_proof">Upload ID Proof</label>
|
||||
<input type="file" name="id_proof" id="id_proof" class="form-control-file" accept=".pdf,.jpg,.jpeg,.png" required="1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg mt-4">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Basic Success Page -->
|
||||
<template id="rental_request_success_template" name="Rental Request Success">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="container py-5 text-center">
|
||||
<h2 class="text-success">Request Submitted</h2>
|
||||
<p class="lead">Your request has been successfully submitted for review.</p>
|
||||
<div class="alert alert-secondary d-inline-block">
|
||||
Reference: <strong t-out="name"/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="/rentals" class="btn btn-primary">Back to Catalog</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
2
addons/theme_aakriti_events/__init__.py
Normal file
2
addons/theme_aakriti_events/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Theme views only, no Python imports needed.
|
||||
22
addons/theme_aakriti_events/__manifest__.py
Normal file
22
addons/theme_aakriti_events/__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Aakriti Events Website Theme',
|
||||
'version': '17.0.1.0.0',
|
||||
'category': 'Theme/Creative',
|
||||
'summary': 'Premium visual theme for Aakriti Design Events website',
|
||||
'description': """
|
||||
Aakriti Events Website Theme.
|
||||
Redesigns the public rental catalog, product details, booking wizard,
|
||||
and customer portal to match events.aakritidesign.com design.
|
||||
""",
|
||||
'author': 'Antigravity',
|
||||
'depends': ['website', 'event_rental'],
|
||||
'data': [
|
||||
'views/website_templates.xml',
|
||||
'views/portal_templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
BIN
addons/theme_aakriti_events/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/theme_aakriti_events/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
188
addons/theme_aakriti_events/views/portal_templates.xml
Normal file
188
addons/theme_aakriti_events/views/portal_templates.xml
Normal file
@ -0,0 +1,188 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Inherit and Override Portal List View -->
|
||||
<template id="portal_my_rental_requests_inherit" inherit_id="event_rental.portal_my_rental_requests">
|
||||
<xpath expr="//table" position="replace">
|
||||
<div class="card border-0 shadow-sm mt-3" style="border-radius: 12px; overflow: hidden;">
|
||||
<table class="table table-hover bg-white mb-0" style="font-size: 0.9rem;">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="border-0">Reference</th>
|
||||
<th class="border-0">Event Date</th>
|
||||
<th class="border-0">Event Type</th>
|
||||
<th class="border-0 text-right">Total Amount</th>
|
||||
<th class="border-0 text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="requests" t-as="req">
|
||||
<td>
|
||||
<a t-att-href="'/my/rentals/%s' % req.id" t-out="req.name" class="font-weight-bold" style="color: #714B67;"/>
|
||||
</td>
|
||||
<td><span t-field="req.event_date"/></td>
|
||||
<td><span t-field="req.event_type"/></td>
|
||||
<td class="text-right">
|
||||
<span t-field="req.amount_total" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="req.status == 'draft'" class="badge badge-secondary p-2">Draft</span>
|
||||
<span t-elif="req.status == 'under_review'" class="badge badge-info p-2 text-white" style="background-color: #17a2b8;">Under Review</span>
|
||||
<span t-elif="req.status == 'approved'" class="badge badge-success p-2" style="background-color: #28a745;">Approved</span>
|
||||
<span t-elif="req.status == 'quotation_sent'" class="badge badge-warning p-2" style="background-color: #ffc107;">Quotation Sent</span>
|
||||
<span t-elif="req.status == 'confirmed'" class="badge badge-success p-2" style="background-color: #28a745;">Confirmed</span>
|
||||
<span t-elif="req.status == 'delivered'" class="badge badge-primary p-2" style="background-color: #007bff;">Delivered</span>
|
||||
<span t-elif="req.status == 'returned'" class="badge badge-info p-2 text-white" style="background-color: #17a2b8;">Returned</span>
|
||||
<span t-elif="req.status == 'completed'" class="badge badge-success p-2" style="background-color: #28a745;">Completed</span>
|
||||
<span t-elif="req.status == 'rejected'" class="badge badge-danger p-2" style="background-color: #dc3545;">Rejected</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Inherit and Override Portal Request Details View -->
|
||||
<template id="portal_rental_request_detail_template_inherit" inherit_id="event_rental.portal_rental_request_detail_template">
|
||||
<xpath expr="//div[@class='card p-4']" position="replace">
|
||||
<div class="row">
|
||||
<!-- Main details -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<div class="card border-0 shadow-sm p-4" style="border-radius: 12px; background: white;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
|
||||
<h3 class="font-weight-bold mb-0" style="color: #714B67;">
|
||||
Request <t t-out="rental_request.name"/>
|
||||
</h3>
|
||||
<span class="h5 mb-0">
|
||||
<span t-if="rental_request.status == 'draft'" class="badge badge-pill badge-secondary p-2">Draft</span>
|
||||
<span t-elif="rental_request.status == 'under_review'" class="badge badge-pill badge-info p-2 text-white" style="background-color: #17a2b8;">Under Review</span>
|
||||
<span t-elif="rental_request.status == 'approved'" class="badge badge-pill badge-success p-2" style="background-color: #28a745;">Approved</span>
|
||||
<span t-elif="rental_request.status == 'quotation_sent'" class="badge badge-pill badge-warning p-2" style="background-color: #ffc107;">Quotation Sent</span>
|
||||
<span t-elif="rental_request.status == 'confirmed'" class="badge badge-pill badge-success p-2" style="background-color: #28a745;">Confirmed</span>
|
||||
<span t-elif="rental_request.status == 'delivered'" class="badge badge-pill badge-primary p-2" style="background-color: #007bff;">Delivered</span>
|
||||
<span t-elif="rental_request.status == 'returned'" class="badge badge-pill badge-info p-2 text-white" style="background-color: #17a2b8;">Returned</span>
|
||||
<span t-elif="rental_request.status == 'completed'" class="badge badge-pill badge-success p-2" style="background-color: #28a745;">Completed</span>
|
||||
<span t-elif="rental_request.status == 'rejected'" class="badge badge-pill badge-danger p-2" style="background-color: #dc3545;">Rejected</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Call to Action for Quotation & Online Payment -->
|
||||
<div t-if="rental_request.status == 'quotation_sent' and rental_request.sale_order_id" class="alert alert-success p-4 mb-4 text-center" style="border-radius: 10px; background-color: #d4edda; border-color: #c3e6cb; color: #155724;">
|
||||
<h5 class="font-weight-bold mb-2">🎉 Quotation Ready for Approval</h5>
|
||||
<p class="mb-3">Your rental request has been reviewed and approved. Please click below to review pricing, make payments, and confirm booking.</p>
|
||||
<a t-att-href="rental_request.sale_order_id.get_portal_url()" class="btn btn-success font-weight-bold px-4 py-2 shadow-sm" style="border-radius: 30px; background-color: #28a745; border-color: #28a745;">
|
||||
View and Pay Quotation Online
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Booking details -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<h6 class="font-weight-bold text-muted mb-2">Rental Period</h6>
|
||||
<p class="mb-1"><strong>Start:</strong> <span t-field="rental_request.start_date"/></p>
|
||||
<p class="mb-0"><strong>End:</strong> <span t-field="rental_request.end_date"/></p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<h6 class="font-weight-bold text-muted mb-2">Location & Type</h6>
|
||||
<p class="mb-1"><strong>Venue:</strong> <span t-field="rental_request.location"/></p>
|
||||
<p class="mb-0"><strong>Event Type:</strong> <span t-field="rental_request.event_type"/></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products lines -->
|
||||
<h5 class="font-weight-bold mb-3 mt-4" style="color: #714B67;">Requested Products</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0" style="font-size: 0.9rem;">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th class="text-center">Quantity</th>
|
||||
<th class="text-right">Price</th>
|
||||
<th class="text-right">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="rental_request.line_ids" t-as="line">
|
||||
<td><span t-field="line.product_id.name"/></td>
|
||||
<td class="text-center"><span t-field="line.quantity"/></td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.price_unit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.price_subtotal" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar details -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Pricing Card -->
|
||||
<div class="card border-0 shadow-sm p-4 mb-4" style="border-radius: 12px; background: white;">
|
||||
<h5 class="font-weight-bold mb-3" style="color: #714B67;">Billing Details</h5>
|
||||
<div class="d-flex justify-content-between mb-2" style="font-size: 0.9rem;">
|
||||
<span class="text-muted">Rental Total:</span>
|
||||
<span>
|
||||
<t t-out="sum(rental_request.line_ids.mapped('price_subtotal'))" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2" style="font-size: 0.9rem;">
|
||||
<span class="text-muted">Delivery Charges:</span>
|
||||
<span><span t-field="rental_request.delivery_charge" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2" style="font-size: 0.9rem;">
|
||||
<span class="text-muted">Setup Charges:</span>
|
||||
<span><span t-field="rental_request.setup_charge" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></span>
|
||||
</div>
|
||||
<hr class="my-2"/>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<strong>Grand Total:</strong>
|
||||
<strong class="h5 font-weight-bold mb-0" style="color: #714B67;">
|
||||
<span t-field="rental_request.amount_total" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</strong>
|
||||
</div>
|
||||
<div t-if="rental_request.sale_order_id" class="pt-2">
|
||||
<a t-att-href="rental_request.sale_order_id.get_portal_url()" class="btn btn-outline-primary btn-block py-2 font-weight-bold" style="border-radius: 30px; border-color: #714B67; color: #714B67; font-size: 0.85rem;">
|
||||
View Original Sales Quotation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents status -->
|
||||
<div class="card border-0 shadow-sm p-4 mb-4" style="border-radius: 12px; background: white;">
|
||||
<h5 class="font-weight-bold mb-3" style="color: #714B67;">Document Status</h5>
|
||||
<t t-foreach="rental_request.document_ids" t-as="doc">
|
||||
<div class="d-flex align-items-center justify-content-between p-2 mb-2 bg-light" style="border-radius: 8px; font-size: 0.85rem;">
|
||||
<div>
|
||||
<strong><span t-field="doc.doc_type"/></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span t-if="doc.verification_status == 'pending'" class="badge badge-warning p-1">Pending</span>
|
||||
<span t-elif="doc.verification_status == 'verified'" class="badge badge-success p-1">Verified</span>
|
||||
<span t-elif="doc.verification_status == 'rejected'" class="badge badge-danger p-1">Rejected</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Delivery logistics -->
|
||||
<div class="card border-0 shadow-sm p-4" style="border-radius: 12px; background: white;" t-if="rental_request.status in ('confirmed', 'delivered', 'returned', 'completed')">
|
||||
<h5 class="font-weight-bold mb-3" style="color: #714B67;">Logistics Tracking</h5>
|
||||
<div style="font-size: 0.9rem;">
|
||||
<p class="mb-2"><strong>Delivery:</strong>
|
||||
<span t-if="rental_request.delivery_status == 'pending'" class="badge badge-warning">Pending Delivery</span>
|
||||
<span t-elif="rental_request.delivery_status == 'delivered'" class="badge badge-primary">Delivered to Venue</span>
|
||||
<span t-elif="rental_request.delivery_status == 'picked_up'" class="badge badge-info text-white">Picked Up</span>
|
||||
<span t-elif="rental_request.delivery_status == 'returned'" class="badge badge-success">Returned & Closed</span>
|
||||
</p>
|
||||
<p t-if="rental_request.delivery_date" class="mb-0"><strong>Scheduled On:</strong> <span t-field="rental_request.delivery_date"/></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
691
addons/theme_aakriti_events/views/website_templates.xml
Normal file
691
addons/theme_aakriti_events/views/website_templates.xml
Normal file
@ -0,0 +1,691 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Inherit and Override Catalog Page -->
|
||||
<template id="rental_catalog_template_inherit" inherit_id="event_rental.rental_catalog_template">
|
||||
<xpath expr="//div[@id='wrap']" position="replace">
|
||||
<!-- Inject Custom Styles and Google Fonts -->
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&family=Playball&display=swap');
|
||||
|
||||
#wrap {
|
||||
background-color: #FAF8F5 !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .display-4, .display-3 {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 600;
|
||||
color: #1A1519;
|
||||
}
|
||||
|
||||
/* Signature Accent Font */
|
||||
.aakriti-cursive {
|
||||
font-family: 'Playball', cursive !important;
|
||||
color: #C5A880 !important;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Brand colors */
|
||||
.text-brand-plum {
|
||||
color: #714B67 !important;
|
||||
}
|
||||
.bg-brand-plum {
|
||||
background-color: #714B67 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.text-brand-gold {
|
||||
color: #C5A880 !important;
|
||||
}
|
||||
.bg-brand-gold {
|
||||
background-color: #C5A880 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Hero Slider Style Banner */
|
||||
.aakriti-hero {
|
||||
position: relative;
|
||||
background: linear-gradient(rgba(113, 75, 103, 0.8), rgba(26, 21, 25, 0.9)),
|
||||
url('https://images.unsplash.com/photo-1519167758481-83f550bb49b3?q=80&w=1920') no-repeat center center;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
padding: 90px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.aakriti-hero h1 {
|
||||
color: #ffffff !important;
|
||||
font-weight: 800;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.aakriti-hero p {
|
||||
font-weight: 300;
|
||||
color: #F3EFF2;
|
||||
max-width: 700px;
|
||||
margin: 15px auto 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Sidebar Filter Styling */
|
||||
.filter-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #EAE5E8;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(113, 75, 103, 0.03);
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid #F3EFF2;
|
||||
padding-bottom: 15px;
|
||||
color: #714B67;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: none !important;
|
||||
padding: 12px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
background-color: #FAF4F8 !important;
|
||||
color: #714B67 !important;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.list-group-item.active-category {
|
||||
background-color: #714B67 !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Search Bar Styling */
|
||||
.search-input {
|
||||
border-radius: 30px !important;
|
||||
border: 1.5px solid #EAE5E8 !important;
|
||||
padding: 24px 20px !important;
|
||||
font-size: 1rem !important;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.02) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #714B67 !important;
|
||||
box-shadow: 0 4px 15px rgba(113, 75, 103, 0.1) !important;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
border-radius: 30px !important;
|
||||
background-color: #714B67 !important;
|
||||
border: none !important;
|
||||
padding: 12px 28px !important;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background-color: #59294C !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(113, 75, 103, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Product Card Styling */
|
||||
.premium-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #EAE5E8;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(113, 75, 103, 0.03);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.premium-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 30px rgba(113, 75, 103, 0.08);
|
||||
border-color: #C5A880;
|
||||
}
|
||||
|
||||
.img-container {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
overflow: hidden;
|
||||
background-color: #F8F5F7;
|
||||
}
|
||||
|
||||
.img-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.premium-card:hover .img-container img {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.card-price {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #C5A880; /* Champagne Gold color */
|
||||
}
|
||||
|
||||
.card-action-btn {
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1.5px solid #714B67;
|
||||
color: #714B67;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card-action-btn:hover {
|
||||
background-color: #714B67 !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<!-- Premium Hero Section -->
|
||||
<div class="aakriti-hero">
|
||||
<div class="container">
|
||||
<span class="aakriti-cursive mb-2 d-block">Aakriti Design | Events</span>
|
||||
<h1 class="display-4 text-white">Transforming Venues, Creating Memories</h1>
|
||||
<p>Bespoke event decoration and luxury equipment rentals curated with elegance, cultural understanding, and impeccable detail.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Section -->
|
||||
<div class="container py-5">
|
||||
<div class="row">
|
||||
<!-- Filters (Left Sidebar) -->
|
||||
<div class="col-lg-3 mb-4">
|
||||
<div class="filter-card p-4">
|
||||
<h5 class="filter-title mb-4">RENTAL CATEGORIES</h5>
|
||||
<div class="list-group">
|
||||
<a t-att-href="'/rentals' + (search and ('?search=' + search) or '')"
|
||||
t-att-class="'list-group-item list-group-item-action ' + (not current_category and 'active-category' or '')">
|
||||
All Collections
|
||||
</a>
|
||||
<t t-foreach="categories" t-as="cat">
|
||||
<a t-att-href="'/rentals?category=' + str(cat.id) + (search and ('&search=' + search) or '')"
|
||||
t-att-class="'list-group-item list-group-item-action ' + (current_category == cat.id and 'active-category' or '')">
|
||||
<span t-field="cat.name"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Catalog Grid (Right Panel) -->
|
||||
<div class="col-lg-9">
|
||||
<!-- Premium Search -->
|
||||
<div class="mb-5">
|
||||
<form action="/rentals" method="get" class="d-flex">
|
||||
<input type="hidden" name="category" t-att-value="current_category" t-if="current_category"/>
|
||||
<input type="text" name="search" class="form-control search-input" placeholder="Search our premium inventory..." t-att-value="search"/>
|
||||
<button type="submit" class="btn btn-primary search-btn ml-2">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Products Grid -->
|
||||
<div class="row" t-if="products">
|
||||
<t t-foreach="products" t-as="product">
|
||||
<div class="col-md-4 col-sm-6 mb-4">
|
||||
<div class="premium-card h-100 d-flex flex-column">
|
||||
<a t-att-href="'/rentals/%s' % slug(product)">
|
||||
<div class="img-container">
|
||||
<img t-if="product.image_1920" t-att-src="image_data_uri(product.image_1920)"/>
|
||||
<div t-else="" class="d-flex align-items-center justify-content-center h-100 text-muted bg-light">
|
||||
<i class="fa fa-picture-o fa-3x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="card-body d-flex flex-column p-4">
|
||||
<h5 class="font-weight-bold mb-2" style="font-size: 1.05rem; line-height: 1.4; min-height: 44px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
<a t-att-href="'/rentals/%s' % slug(product)" class="text-dark text-decoration-none" t-field="product.name"/>
|
||||
</h5>
|
||||
<p class="text-muted flex-grow-1 mb-3" style="font-size: 0.85rem; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;" t-out="product.description_sale or 'Luxury styling and bespoke design options.'"/>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-auto">
|
||||
<div>
|
||||
<span class="card-price">
|
||||
<span t-field="product.rental_price_per_day" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</span>
|
||||
<span class="text-muted" style="font-size: 0.75rem;">/day</span>
|
||||
</div>
|
||||
<a t-att-href="'/rentals/%s' % slug(product)" class="btn btn-sm card-action-btn px-3">Rent</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-else="" class="text-center py-5 filter-card p-5">
|
||||
<i class="fa fa-search fa-3x text-muted mb-3"></i>
|
||||
<h4 class="font-weight-bold">No rental items found</h4>
|
||||
<p class="text-muted">Modify your filter categories or search query.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Inherit and Override Product Detail Page -->
|
||||
<template id="rental_product_detail_template_inherit" inherit_id="event_rental.rental_product_detail_template">
|
||||
<xpath expr="//div[@id='wrap']" position="replace">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&family=Playball&display=swap');
|
||||
|
||||
#wrap {
|
||||
background-color: #FAF8F5 !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700;
|
||||
color: #1A1519;
|
||||
}
|
||||
|
||||
.aakriti-cursive {
|
||||
font-family: 'Playball', cursive !important;
|
||||
color: #C5A880 !important;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
color: #714B67;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #EAE5E8;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 25px rgba(113, 75, 103, 0.02);
|
||||
}
|
||||
|
||||
.detail-price {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: #714B67;
|
||||
}
|
||||
|
||||
.detail-action-btn {
|
||||
border-radius: 30px !important;
|
||||
background-color: #714B67 !important;
|
||||
border: none !important;
|
||||
padding: 16px 24px !important;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-action-btn:hover {
|
||||
background-color: #59294C !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(113, 75, 103, 0.3) !important;
|
||||
}
|
||||
|
||||
.terms-title {
|
||||
font-size: 1.4rem;
|
||||
border-left: 4px solid #C5A880;
|
||||
padding-left: 12px;
|
||||
color: #714B67;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="wrap" class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- Product Gallery -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="detail-card p-3">
|
||||
<div style="height: 450px; overflow: hidden; border-radius: 10px; background-color: #F8F5F7;">
|
||||
<img t-if="product.image_1920" t-att-src="image_data_uri(product.image_1920)" class="w-100 h-100" style="object-fit: cover;"/>
|
||||
<div t-else="" class="d-flex align-items-center justify-content-center h-100 text-muted bg-light">
|
||||
<i class="fa fa-picture-o fa-5x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product Purchase Details -->
|
||||
<div class="col-md-6">
|
||||
<div class="detail-card p-4 p-md-5 h-100 d-flex flex-column">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb bg-transparent p-0 mb-4" style="font-size: 0.85rem;">
|
||||
<li class="breadcrumb-item"><a href="/rentals" class="breadcrumb-link">Collections</a></li>
|
||||
<li class="breadcrumb-item active text-muted" aria-current="page">
|
||||
<span t-field="product.name"/>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<span class="aakriti-cursive mb-1 d-block" style="font-size: 1.5rem;">Bespoke Design Collection</span>
|
||||
<h1 class="display-5 mb-3" style="font-size: 2.2rem;" t-field="product.name"/>
|
||||
|
||||
<div class="mb-4">
|
||||
<span t-if="product.qty_available > 0" class="badge badge-pill badge-success p-2 px-3" style="background-color: #28a745; font-size: 0.8rem; font-weight: 600;">In Stock & Available</span>
|
||||
<span t-else="" class="badge badge-pill badge-warning p-2 px-3" style="font-size: 0.8rem; font-weight: 600;">Custom Order - Checking Dates</span>
|
||||
</div>
|
||||
|
||||
<div class="my-4 py-3 border-top border-bottom d-flex align-items-baseline">
|
||||
<span class="detail-price">
|
||||
<span t-field="product.rental_price_per_day" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/>
|
||||
</span>
|
||||
<span class="text-muted ml-2 font-weight-normal lead" style="font-size: 1.15rem;">/ Day</span>
|
||||
</div>
|
||||
|
||||
<h5 class="font-weight-bold mb-2" style="font-size: 1rem; text-transform: uppercase; letter-spacing: 0.5px; color: #714B67;">Product Description</h5>
|
||||
<p class="text-muted mb-5" style="line-height: 1.6; font-size: 0.95rem;" t-field="product.description_sale"/>
|
||||
|
||||
<div class="mt-auto">
|
||||
<a t-att-href="'/rental/request?product_id=%s' % product.product_variant_id.id" class="btn btn-primary btn-lg btn-block detail-action-btn shadow-sm">
|
||||
Request Booking Quotation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5" t-if="product.rental_terms">
|
||||
<div class="col-12">
|
||||
<div class="detail-card p-5 bg-white">
|
||||
<h4 class="font-weight-bold terms-title mb-4">RESERVATION TERMS & GENERAL POLICIES</h4>
|
||||
<div class="text-muted" style="line-height: 1.7; font-size: 0.95rem;" t-field="product.rental_terms"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Inherit and Override Request Form Template -->
|
||||
<template id="rental_request_form_template_inherit" inherit_id="event_rental.rental_request_form_template">
|
||||
<xpath expr="//div[@id='wrap']" position="replace">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&family=Playball&display=swap');
|
||||
|
||||
#wrap {
|
||||
background-color: #FAF8F5 !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
h2, h4, h5 {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700;
|
||||
color: #1A1519;
|
||||
}
|
||||
|
||||
.aakriti-cursive {
|
||||
font-family: 'Playball', cursive !important;
|
||||
color: #C5A880 !important;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: #ffffff;
|
||||
border: 1px solid #EAE5E8;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 30px rgba(113, 75, 103, 0.03);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
color: #714B67;
|
||||
border-left: 4px solid #C5A880;
|
||||
padding-left: 12px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px !important;
|
||||
border: 1.5px solid #EAE5E8 !important;
|
||||
padding: 12px 16px !important;
|
||||
font-size: 0.95rem !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #714B67 !important;
|
||||
box-shadow: 0 0 0 3px rgba(113, 75, 103, 0.1) !important;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #4A3E48;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
border-radius: 30px !important;
|
||||
background-color: #714B67 !important;
|
||||
border: none !important;
|
||||
padding: 16px 24px !important;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 1.05rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #59294C !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(113, 75, 103, 0.3) !important;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
border: 2px dashed #C5A880;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #FAF8F5;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="wrap" class="py-5">
|
||||
<div class="container col-lg-8 col-md-10">
|
||||
<div class="form-container p-4 p-md-5 bg-white">
|
||||
<div class="text-center mb-5">
|
||||
<span class="aakriti-cursive d-block" style="font-size: 1.8rem;">Let's Design Together</span>
|
||||
<h2 class="display-5" style="font-size: 2rem;">Inquire About Your Event</h2>
|
||||
<p class="text-muted" style="max-width: 600px; margin: 8px auto 0; font-size: 0.95rem;">Enter details below. Our reservations engine will instantly check stock availability and generate an itemized quote.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<div class="alert alert-danger p-3 mb-4" role="alert" style="border-radius: 10px;" t-if="error_message">
|
||||
<i class="fa fa-exclamation-circle mr-2"></i> <t t-out="error_message"/>
|
||||
</div>
|
||||
|
||||
<form action="/rental/request" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<!-- Customer Details -->
|
||||
<h5 class="section-header mb-4">1. Customer Information</h5>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="customer_name">Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="customer_name" id="customer_name" class="form-control" required="1" t-att-value="post.get('customer_name', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="customer_email">Email Address <span class="text-danger">*</span></label>
|
||||
<input type="email" name="customer_email" id="customer_email" class="form-control" required="1" t-att-value="post.get('customer_email', '')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="customer_phone">Mobile Number <span class="text-danger">*</span></label>
|
||||
<input type="tel" name="customer_phone" id="customer_phone" class="form-control" required="1" t-att-value="post.get('customer_phone', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="company_name">Company Name <span class="text-muted">(Optional)</span></label>
|
||||
<input type="text" name="company_name" id="company_name" class="form-control" t-att-value="post.get('company_name', '')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label for="customer_address">Full Delivery Address</label>
|
||||
<textarea name="customer_address" id="customer_address" rows="2" class="form-control" placeholder="Provide complete delivery address..."><t t-out="post.get('customer_address', '')"/></textarea>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" style="border-top: 1px solid #FAF4F8;"/>
|
||||
|
||||
<!-- Event Details -->
|
||||
<h5 class="section-header mb-4">2. Event & Schedule Details</h5>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="start_date">Rental Start Date & Time <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" name="start_date" id="start_date" class="form-control" required="1" t-att-value="post.get('start_date', '')"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="end_date">Rental End Date & Time <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" name="end_date" id="end_date" class="form-control" required="1" t-att-value="post.get('end_date', '')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="location">Event Venue Location <span class="text-danger">*</span></label>
|
||||
<input type="text" name="location" id="location" class="form-control" required="1" t-att-value="post.get('location', '')" placeholder="City or Venue Hall name"/>
|
||||
</div>
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="event_type">Event Type <span class="text-danger">*</span></label>
|
||||
<select name="event_type" id="event_type" class="form-control" required="1">
|
||||
<option value="wedding" t-att-selected="post.get('event_type') == 'wedding'">Wedding</option>
|
||||
<option value="birthday" t-att-selected="post.get('event_type') == 'birthday'">Birthday</option>
|
||||
<option value="corporate" t-att-selected="post.get('event_type') == 'corporate'">Corporate Event</option>
|
||||
<option value="stage" t-att-selected="post.get('event_type') == 'stage'">Stage Setup</option>
|
||||
<option value="festival" t-att-selected="post.get('event_type') == 'festival'">Festival</option>
|
||||
<option value="exhibition" t-att-selected="post.get('event_type') == 'exhibition'">Exhibition</option>
|
||||
<option value="other" t-att-selected="post.get('event_type') == 'other' or not post.get('event_type')">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" style="border-top: 1px solid #FAF4F8;"/>
|
||||
|
||||
<!-- Product Details -->
|
||||
<h5 class="section-header mb-4">3. Rental Item Selected</h5>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-8 mb-3">
|
||||
<label for="product_id">Selected Product / Service <span class="text-danger">*</span></label>
|
||||
<select name="product_id" id="product_id" class="form-control" required="1">
|
||||
<t t-foreach="all_products" t-as="prod">
|
||||
<option t-att-value="prod.id" t-att-selected="(selected_product and selected_product.id == prod.id) or (post.get('product_id') == str(prod.id))">
|
||||
<t t-out="prod.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4 mb-3">
|
||||
<label for="quantity">Quantity Required <span class="text-danger">*</span></label>
|
||||
<input type="number" name="quantity" id="quantity" class="form-control" min="1" step="1" required="1" t-att-value="post.get('quantity', '1')"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" style="border-top: 1px solid #FAF4F8;"/>
|
||||
|
||||
<!-- Document Upload -->
|
||||
<h5 class="section-header mb-4">4. Document Verification</h5>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label for="doc_type">Government ID Proof <span class="text-danger">*</span></label>
|
||||
<select name="doc_type" id="doc_type" class="form-control" required="1">
|
||||
<option value="aadhaar" t-att-selected="post.get('doc_type') == 'aadhaar'">Aadhaar Card</option>
|
||||
<option value="driving_license" t-att-selected="post.get('doc_type') == 'driving_license'">Driving License</option>
|
||||
<option value="passport" t-att-selected="post.get('doc_type') == 'passport'">Passport</option>
|
||||
<option value="voter_id" t-att-selected="post.get('doc_type') == 'voter_id'">Voter ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-6 mb-3">
|
||||
<label class="d-block mb-2">Upload File (PDF, JPG, PNG) <span class="text-danger">*</span></label>
|
||||
<div class="upload-box">
|
||||
<i class="fa fa-cloud-upload fa-2x text-brand-plum mb-2"></i>
|
||||
<input type="file" name="id_proof" id="id_proof" class="form-control-file d-block mx-auto" accept=".pdf,.jpg,.jpeg,.png" required="1" style="font-size: 0.85rem; max-width: 200px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block submit-btn mt-4 shadow-sm">
|
||||
Submit Rental Request
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Inherit and Override Success Confirmation Page -->
|
||||
<template id="rental_request_success_template_inherit" inherit_id="event_rental.rental_request_success_template">
|
||||
<xpath expr="//div[@id='wrap']" position="replace">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&family=Playball&display=swap');
|
||||
|
||||
#wrap {
|
||||
background-color: #FAF8F5 !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
h2, h4 {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.success-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #EAE5E8;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 30px rgba(113, 75, 103, 0.03);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.reference-badge {
|
||||
padding: 12px 20px;
|
||||
background-color: #FAF4F8;
|
||||
border-radius: 30px;
|
||||
display: inline-block;
|
||||
border: 1px dashed #C5A880;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
color: #C5A880;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="wrap" class="py-5">
|
||||
<div class="container text-center">
|
||||
<div class="success-card p-5 col-lg-6 col-md-8 mx-auto bg-white">
|
||||
<div class="mb-4">
|
||||
<i class="fa fa-envelope-o fa-5x success-icon"></i>
|
||||
</div>
|
||||
<h2 class="display-5 text-brand-plum mb-3" style="font-size: 2rem;">Inquiry Registered!</h2>
|
||||
<p class="lead text-muted mb-4" style="font-size: 1rem;">We have successfully registered your event rental inquiry in our booking database.</p>
|
||||
|
||||
<div class="reference-badge mb-4">
|
||||
<span class="text-muted" style="font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px;">Reference Number</span><br/>
|
||||
<h4 class="font-weight-bold text-brand-plum mb-0 mt-1" t-out="name"/>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-4" style="font-size: 0.9rem; line-height: 1.6;">Our operations team is evaluating real-time stock allocations. You will receive an email and WhatsApp message shortly with a portal checkout link to review and pay the quotation.</p>
|
||||
<a href="/rentals" class="btn btn-primary bg-brand-plum border-0 py-2 px-5 font-weight-bold shadow-sm" style="border-radius: 30px; font-size: 0.95rem;">Back to Catalog</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
39
docker-compose.yml
Normal file
39
docker-compose.yml
Normal file
@ -0,0 +1,39 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: odoo_client4_db
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: odoo
|
||||
POSTGRES_PASSWORD: odoo
|
||||
volumes:
|
||||
- client4_pgdata:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
odoo:
|
||||
image: odoo:17.0
|
||||
container_name: odoo_client4
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "10004:8069"
|
||||
environment:
|
||||
HOST: db
|
||||
USER: odoo
|
||||
PASSWORD: odoo
|
||||
LIST_DB: "True"
|
||||
volumes:
|
||||
- client4_odoo_data:/var/lib/odoo
|
||||
- ./addons:/mnt/extra-addons
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
client4_pgdata:
|
||||
client4_odoo_data:
|
||||
|
||||
|
||||
# backups:
|
||||
# .\backup_db.ps1
|
||||
|
||||
# Team Members – Restore
|
||||
# cat d:\Odoo\backups\YOUR_BACKUP_FILE.sql | docker exec -i odoo_client4_db psql -U odoo -d postgres
|
||||
Loading…
x
Reference in New Issue
Block a user