369 lines
16 KiB
Python
369 lines
16 KiB
Python
# -*- 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)
|