From db4f4f7ef126519e9e34cdb708bd5537f756448b Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Fri, 12 Jun 2026 15:12:50 +0530 Subject: [PATCH] first commit --- addons/event_rental/__init__.py | 4 + addons/event_rental/__manifest__.py | 27 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 200 bytes addons/event_rental/controllers/__init__.py | 3 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 176 bytes .../__pycache__/main.cpython-310.pyc | Bin 0 -> 7110 bytes addons/event_rental/controllers/main.py | 251 +++++++ addons/event_rental/data/mail_templates.xml | 53 ++ addons/event_rental/data/sequence.xml | 12 + addons/event_rental/models/__init__.py | 5 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 259 bytes .../event_rental_request.cpython-310.pyc | Bin 0 -> 12013 bytes .../product_template.cpython-310.pyc | Bin 0 -> 817 bytes .../__pycache__/sale_order.cpython-310.pyc | Bin 0 -> 1200 bytes .../models/event_rental_request.py | 368 ++++++++++ .../event_rental/models/product_template.py | 20 + addons/event_rental/models/sale_order.py | 36 + .../event_rental/security/ir.model.access.csv | 10 + addons/event_rental/security/security.xml | 58 ++ addons/event_rental/tests/__init__.py | 3 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 177 bytes .../__pycache__/test_rental.cpython-310.pyc | Bin 0 -> 3426 bytes addons/event_rental/tests/test_rental.py | 121 +++ .../views/event_rental_request_views.xml | 201 +++++ .../event_rental/views/portal_templates.xml | 110 +++ .../views/product_template_views.xml | 41 ++ .../event_rental/views/website_templates.xml | 213 ++++++ addons/theme_aakriti_events/__init__.py | 2 + addons/theme_aakriti_events/__manifest__.py | 22 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 143 bytes .../views/portal_templates.xml | 188 +++++ .../views/website_templates.xml | 691 ++++++++++++++++++ docker-compose.yml | 39 + 33 files changed, 2478 insertions(+) create mode 100644 addons/event_rental/__init__.py create mode 100644 addons/event_rental/__manifest__.py create mode 100644 addons/event_rental/__pycache__/__init__.cpython-310.pyc create mode 100644 addons/event_rental/controllers/__init__.py create mode 100644 addons/event_rental/controllers/__pycache__/__init__.cpython-310.pyc create mode 100644 addons/event_rental/controllers/__pycache__/main.cpython-310.pyc create mode 100644 addons/event_rental/controllers/main.py create mode 100644 addons/event_rental/data/mail_templates.xml create mode 100644 addons/event_rental/data/sequence.xml create mode 100644 addons/event_rental/models/__init__.py create mode 100644 addons/event_rental/models/__pycache__/__init__.cpython-310.pyc create mode 100644 addons/event_rental/models/__pycache__/event_rental_request.cpython-310.pyc create mode 100644 addons/event_rental/models/__pycache__/product_template.cpython-310.pyc create mode 100644 addons/event_rental/models/__pycache__/sale_order.cpython-310.pyc create mode 100644 addons/event_rental/models/event_rental_request.py create mode 100644 addons/event_rental/models/product_template.py create mode 100644 addons/event_rental/models/sale_order.py create mode 100644 addons/event_rental/security/ir.model.access.csv create mode 100644 addons/event_rental/security/security.xml create mode 100644 addons/event_rental/tests/__init__.py create mode 100644 addons/event_rental/tests/__pycache__/__init__.cpython-310.pyc create mode 100644 addons/event_rental/tests/__pycache__/test_rental.cpython-310.pyc create mode 100644 addons/event_rental/tests/test_rental.py create mode 100644 addons/event_rental/views/event_rental_request_views.xml create mode 100644 addons/event_rental/views/portal_templates.xml create mode 100644 addons/event_rental/views/product_template_views.xml create mode 100644 addons/event_rental/views/website_templates.xml create mode 100644 addons/theme_aakriti_events/__init__.py create mode 100644 addons/theme_aakriti_events/__manifest__.py create mode 100644 addons/theme_aakriti_events/__pycache__/__init__.cpython-310.pyc create mode 100644 addons/theme_aakriti_events/views/portal_templates.xml create mode 100644 addons/theme_aakriti_events/views/website_templates.xml create mode 100644 docker-compose.yml diff --git a/addons/event_rental/__init__.py b/addons/event_rental/__init__.py new file mode 100644 index 0000000..c3d410e --- /dev/null +++ b/addons/event_rental/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/addons/event_rental/__manifest__.py b/addons/event_rental/__manifest__.py new file mode 100644 index 0000000..259fea2 --- /dev/null +++ b/addons/event_rental/__manifest__.py @@ -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', +} diff --git a/addons/event_rental/__pycache__/__init__.cpython-310.pyc b/addons/event_rental/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b55446aae6b2e64994f386f6a7fc0704538b9aa1 GIT binary patch literal 200 zcmd1j<>g`k0*+%z278UzxGT&k-0vT7t3?x=E6tRNXAmW#n zer{fgeriQYQKD{QN=klSv3_b{j0BGJUjQ{`u literal 0 HcmV?d00001 diff --git a/addons/event_rental/controllers/__init__.py b/addons/event_rental/controllers/__init__.py new file mode 100644 index 0000000..65a8c12 --- /dev/null +++ b/addons/event_rental/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import main diff --git a/addons/event_rental/controllers/__pycache__/__init__.cpython-310.pyc b/addons/event_rental/controllers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df6702b85ebd839e9854eb387c0a77c055e97037 GIT binary patch literal 176 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', 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/'], 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/'], 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) diff --git a/addons/event_rental/data/mail_templates.xml b/addons/event_rental/data/mail_templates.xml new file mode 100644 index 0000000..b4bcbb2 --- /dev/null +++ b/addons/event_rental/data/mail_templates.xml @@ -0,0 +1,53 @@ + + + + + Event Rental: Approved & Quotation Ready + + Your Event Rental Request {{ object.name }} has been Approved + {{ object.create_uid.email_formatted or object.env.company.email_formatted }} + {{ object.customer_email }} + +
+

Rental Request Approved!

+

Dear ,

+

We are happy to inform you that your event rental request has been approved for your event scheduled on .

+

We have generated a Sales Quotation for you. Please review the rental details:

+ + + + + + + + + + + + + + + + + +
Request Number:
Rental Period: + From to +
Event Location:
Event Type:
+ +

To view your detailed quotation, check pricing, and make online payment to confirm your booking, please click the button below:

+ + +

Please note that your items are tentatively held. Payment is required to guarantee your rental reservation.

+

Thank you for choosing us for your event!

+

Best regards,
+ Event Rental Team

+
+
+
+
+
diff --git a/addons/event_rental/data/sequence.xml b/addons/event_rental/data/sequence.xml new file mode 100644 index 0000000..180455c --- /dev/null +++ b/addons/event_rental/data/sequence.xml @@ -0,0 +1,12 @@ + + + + + Event Rental Request Sequence + event.rental.request + RENT/%(year)s/ + 4 + + + + diff --git a/addons/event_rental/models/__init__.py b/addons/event_rental/models/__init__.py new file mode 100644 index 0000000..0a1c454 --- /dev/null +++ b/addons/event_rental/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import product_template +from . import event_rental_request +from . import sale_order diff --git a/addons/event_rental/models/__pycache__/__init__.cpython-310.pyc b/addons/event_rental/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d6d277cca34c93b534f69b584b064c81ed36e2a GIT binary patch literal 259 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 FzX6AuIfeiL literal 0 HcmV?d00001 diff --git a/addons/event_rental/models/event_rental_request.py b/addons/event_rental/models/event_rental_request.py new file mode 100644 index 0000000..361b688 --- /dev/null +++ b/addons/event_rental/models/event_rental_request.py @@ -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 = _( + "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: + 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) diff --git a/addons/event_rental/models/product_template.py b/addons/event_rental/models/product_template.py new file mode 100644 index 0000000..406a8a4 --- /dev/null +++ b/addons/event_rental/models/product_template.py @@ -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." + ) diff --git a/addons/event_rental/models/sale_order.py b/addons/event_rental/models/sale_order.py new file mode 100644 index 0000000..e349b08 --- /dev/null +++ b/addons/event_rental/models/sale_order.py @@ -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 diff --git a/addons/event_rental/security/ir.model.access.csv b/addons/event_rental/security/ir.model.access.csv new file mode 100644 index 0000000..7dc7128 --- /dev/null +++ b/addons/event_rental/security/ir.model.access.csv @@ -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 diff --git a/addons/event_rental/security/security.xml b/addons/event_rental/security/security.xml new file mode 100644 index 0000000..7c12e13 --- /dev/null +++ b/addons/event_rental/security/security.xml @@ -0,0 +1,58 @@ + + + + + + Event Rental Management + Category for event rental management permissions. + 20 + + + + + Rental Officer + + + + + + + Rental Manager + + + + + + + + Portal Rental Requests + + [('partner_id', '=', user.partner_id.id)] + + + + + + + + + + Portal Rental Documents + + [('partner_id', '=', user.partner_id.id)] + + + + + + + + + + Officer Rental Requests + + [(1, '=', 1)] + + + + diff --git a/addons/event_rental/tests/__init__.py b/addons/event_rental/tests/__init__.py new file mode 100644 index 0000000..45c1165 --- /dev/null +++ b/addons/event_rental/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_rental diff --git a/addons/event_rental/tests/__pycache__/__init__.cpython-310.pyc b/addons/event_rental/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2da8a72d0170407f049532db12b7470191dd8a52 GIT binary patch literal 177 zcmd1j<>g`k0(44TX@8G*u@ zjJLQ;Qj1ICi&FDS5_9}CnQk!@fi$jUC}IIpVB(j7er{fgeriQYQKD{QN=klSv3_b9 qPyMs(ccP9W zWdOSKaqgbZGkNl~4}J85^yn|q>F2;}9y)TwoW#0=xL_F49KB zHQ=}Zub+?3HVxxzWLAGRFn##M=Rl}INoM7loXe#hwEB*mQ8j_qCXXebD8^kyg9aPt8+uLio)SGOC=Kdentc2UgXdl9>&X(x`f3 zj;gdy8_%pcnYqh7ntG2GZPN|f!7EcuJ?C>=kX+_>xf?CHvmaFo?LS z6!QE30R!OolYZA$b`p%4vgPrFscIyAu8mh)dFxMj6f%FpxKD%QtR-X`9`ud^&W7n! zuovhfxRz2JjU$=W^5I9*Adw(#MhUtz<;VUwrL5TXkNvKt+~O#HL{%+L!vJA`Oy_)! zRujqNtj3w>O#&_x#6e7YES(yJE;O+Y%9>({d z8!tcJ9w%}eH1pu2fYLM(+YJ3#T+ucfEi^48y~#1Z2xe8`6CVO;nyzJ%{O^9V<=CW5 zI`G*bE59n+i_zw*?9yxK@11*QEfCOc=++(FV+hwTjTxC6(!>~hO5QdQ+`w4{mjm2J z*lJ*xPsoXh;Xx}H49_Zaa%wGt0OnYuGH{hqnbv3>0e%YWX3mKX;o#859CA*X}=o5P-101p9T0(MBFlAhkwKx;Zzs&K7WOx>ayE)*fwu&bQv6 z6V3>$%k%<$J0BgjXPq^z?`S>NY;#R|aYg$U%{goK*4ms)YjfU3j$VcU|3UV^)7Y0o z<^k3xF_X;8aS9FO#X}uMS3Hbai25rYppd7BK@6hccnr`1Q9?aPrqLne$KGC?9(lY8 zR|(j&A;858OmHu^G|qatZfG7&0czRCXOOX}_m?0JLgsxQ#IdRz7?LZZmgXtjvyBx z&YwdrDGmsG@eXQrmYL(`pJ1LhO(LnC!aMbN+8THmo z7zZ*Oo?}PfZ$6{A#M}MsXTR6zfu#@A^Z={K!g-Le4#1`&x2q~7z`ck=kyh2>4x#IC zxILbra1wn(k@k>bFMe-gJ56NX9ss?G0ad`(^9b>t`GK5}M z&Acy)TT%63oPv?QHc)lKAqx-u!h2B+^+Pq!lQM(@gJcfwoC@XUTioYUrWyjA>T8!F zW}Ai5!h1^)V1L`cLpVby7!E0hS)iRx%mGZd?@SIvR^NfXdpcR17Cof z2a$lN3cTeD6LSH=esK+XrsP;y`4uD|0m-M@QNFxno#glpkQHII+vlGE zx7)4hIN-N%`gg}cJqPJseLP;q1-F5~UVp~^`ChxozJ`<^=C^j;U$GQYo=&Ck;9{^u zctASlDrlT<-0}i0^@>a0`(M_3GaUY(;;y3{-^Z7w?$KBPq+$n1U^U{!vrIo%kAH3`EIwWHd30V zJ#>M<$Kg0lxCt{=P0RNp7E{5o;N_Qr_n=yOj6Dw71T(J4J&wDq9N|0^o!{oB-NJ{k NK1ZF@NgKW`^IIo0(0u>^ literal 0 HcmV?d00001 diff --git a/addons/event_rental/tests/test_rental.py b/addons/event_rental/tests/test_rental.py new file mode 100644 index 0000000..f146ae4 --- /dev/null +++ b/addons/event_rental/tests/test_rental.py @@ -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() diff --git a/addons/event_rental/views/event_rental_request_views.xml b/addons/event_rental/views/event_rental_request_views.xml new file mode 100644 index 0000000..056e31c --- /dev/null +++ b/addons/event_rental/views/event_rental_request_views.xml @@ -0,0 +1,201 @@ + + + + + event.rental.line.form + event.rental.line + +
+ + + + + + + + +
+
+
+ + + + event.rental.request.tree + event.rental.request + + + + + + + + + + + + + + + + + event.rental.request.calendar + event.rental.request + + + + + + + + + + + + event.rental.request.form + event.rental.request + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + + event.rental.request.search + event.rental.request + + + + + + + + + + + + + + + + + + + + + + + + + + + Rental Requests + ir.actions.act_window + event.rental.request + tree,calendar,form + + +

+ Create your first Event Rental Request! +

+
+
+ + + + Rental Products + product.template + kanban,tree,form + [('is_rental', '=', True)] + {'default_is_rental': True} + + + + + + +
diff --git a/addons/event_rental/views/portal_templates.xml b/addons/event_rental/views/portal_templates.xml new file mode 100644 index 0000000..4261a80 --- /dev/null +++ b/addons/event_rental/views/portal_templates.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + diff --git a/addons/event_rental/views/product_template_views.xml b/addons/event_rental/views/product_template_views.xml new file mode 100644 index 0000000..25fed82 --- /dev/null +++ b/addons/event_rental/views/product_template_views.xml @@ -0,0 +1,41 @@ + + + + + product.template.form.inherit.rental + product.template + + + + + + + + + + + + + + + + + + + + + + + + + product.template.search.inherit.rental + product.template + + + + + + + + diff --git a/addons/event_rental/views/website_templates.xml b/addons/event_rental/views/website_templates.xml new file mode 100644 index 0000000..9dc56a3 --- /dev/null +++ b/addons/event_rental/views/website_templates.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + diff --git a/addons/theme_aakriti_events/__init__.py b/addons/theme_aakriti_events/__init__.py new file mode 100644 index 0000000..d764367 --- /dev/null +++ b/addons/theme_aakriti_events/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Theme views only, no Python imports needed. diff --git a/addons/theme_aakriti_events/__manifest__.py b/addons/theme_aakriti_events/__manifest__.py new file mode 100644 index 0000000..b6bb94b --- /dev/null +++ b/addons/theme_aakriti_events/__manifest__.py @@ -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', +} diff --git a/addons/theme_aakriti_events/__pycache__/__init__.cpython-310.pyc b/addons/theme_aakriti_events/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66818dc7f68718bcf6ac4b98dd81bff94be487c6 GIT binary patch literal 143 zcmd1j<>g`kf`!Mmv)q96V-N=!FakLaKwQiMBvKfH88jLFRx%WUgb~CqBmLaG68+SQ zlA=W2#FUi$ykh;5jMUuJ_{7BQqRf)a_|&r0ypm%5`1s7c%#!$cy@JYH95%W6DWy57 Nb|ABgnScZf0|0Q;Ad&z8 literal 0 HcmV?d00001 diff --git a/addons/theme_aakriti_events/views/portal_templates.xml b/addons/theme_aakriti_events/views/portal_templates.xml new file mode 100644 index 0000000..a0dc896 --- /dev/null +++ b/addons/theme_aakriti_events/views/portal_templates.xml @@ -0,0 +1,188 @@ + + + + + + + + diff --git a/addons/theme_aakriti_events/views/website_templates.xml b/addons/theme_aakriti_events/views/website_templates.xml new file mode 100644 index 0000000..021bf05 --- /dev/null +++ b/addons/theme_aakriti_events/views/website_templates.xml @@ -0,0 +1,691 @@ + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..24d695e --- /dev/null +++ b/docker-compose.yml @@ -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