# -*- 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 = _( "WhatsApp Notification Queued:
" "Recipient Phone: %s
" "Message: \"%s\"
" "Send via WhatsApp Web" ) % (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)