336 lines
14 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.
Bypassed per request - returning infinite availability.
"""
return 999999.0
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 bypassed per request
# 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:
line.is_available = True
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)