From 7495d2dbd81a81c40e61887094ac4ed27299a445 Mon Sep 17 00:00:00 2001 From: utpat-odoo Date: Tue, 16 Dec 2025 11:04:55 +0530 Subject: [PATCH] [ADD] deposit_rental: adds a deposit feature to the Rental application. Business Context: The deposit feature is required to secure rental transactions by collecting a refundable amount from customers. This helps protect the company against damage, loss, or delayed returns of rented items and ensures customer accountability. Technical changes: 1. add deposit product in rental settings 2. add Deposit Configuration Fields in product page - Added requires_deposit (Boolean) and deposit_amount (Float) fields on the product.template model to define whether a rental product requires a deposit and the deposit amount per unit. 3. Deposit Line Identification - Added is_deposit_line Boolean field on sale.order.line 4. Automatic Deposit Line Creation 5. Deposit Line Protection - Prevented manual editing of deposit lines by overriding the write method. - Prevented manual deletion of deposit lines by overriding the unlink method. 6. Add deposit amount on product website page - when user change quantity of product then deposit amount also update task-5403952 --- deposit_rental/__init__.py | 1 + deposit_rental/__manifest__.py | 22 ++++ deposit_rental/models/__init__.py | 4 + deposit_rental/models/product_template.py | 8 ++ deposit_rental/models/res_company.py | 7 ++ deposit_rental/models/res_config_settings.py | 12 +++ deposit_rental/models/sale_order_line.py | 102 ++++++++++++++++++ deposit_rental/static/src/deposit_amount.js | 22 ++++ .../views/product_template_views.xml | 14 +++ .../views/res_config_settings_views.xml | 13 +++ deposit_rental/views/template_views.xml | 13 +++ 11 files changed, 218 insertions(+) create mode 100644 deposit_rental/__init__.py create mode 100644 deposit_rental/__manifest__.py create mode 100644 deposit_rental/models/__init__.py create mode 100644 deposit_rental/models/product_template.py create mode 100644 deposit_rental/models/res_company.py create mode 100644 deposit_rental/models/res_config_settings.py create mode 100644 deposit_rental/models/sale_order_line.py create mode 100644 deposit_rental/static/src/deposit_amount.js create mode 100644 deposit_rental/views/product_template_views.xml create mode 100644 deposit_rental/views/res_config_settings_views.xml create mode 100644 deposit_rental/views/template_views.xml diff --git a/deposit_rental/__init__.py b/deposit_rental/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/deposit_rental/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/deposit_rental/__manifest__.py b/deposit_rental/__manifest__.py new file mode 100644 index 00000000000..d6f0b9ea7b3 --- /dev/null +++ b/deposit_rental/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'author': 'Odoo S.A.', + 'name': 'Deposit Rental App', + 'description': """ + Implements a security deposit feature in the Rental app + to manage refundable deposits for rental products. + """, + 'depends': ['sale_renting', 'website_sale'], + 'license': 'LGPL-3', + 'data': [ + 'views/res_config_settings_views.xml', + 'views/product_template_views.xml', + 'views/template_views.xml' + ], + 'assets': { + 'web.assets_frontend': [ + 'deposit_rental/static/src/deposit_amount.js', + ], + }, + 'application': True, + 'installable': True +} diff --git a/deposit_rental/models/__init__.py b/deposit_rental/models/__init__.py new file mode 100644 index 00000000000..90b06a9bf48 --- /dev/null +++ b/deposit_rental/models/__init__.py @@ -0,0 +1,4 @@ +from . import res_config_settings +from . import product_template +from . import sale_order_line +from . import res_company diff --git a/deposit_rental/models/product_template.py b/deposit_rental/models/product_template.py new file mode 100644 index 00000000000..76d32810754 --- /dev/null +++ b/deposit_rental/models/product_template.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + requires_deposit = fields.Boolean(string="Requires Deposit") + deposit_amount = fields.Float() diff --git a/deposit_rental/models/res_company.py b/deposit_rental/models/res_company.py new file mode 100644 index 00000000000..20d11bd60a1 --- /dev/null +++ b/deposit_rental/models/res_company.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + deposit_product = fields.Many2one("product.product") diff --git a/deposit_rental/models/res_config_settings.py b/deposit_rental/models/res_config_settings.py new file mode 100644 index 00000000000..35ef96e4b71 --- /dev/null +++ b/deposit_rental/models/res_config_settings.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + deposit_product = fields.Many2one( + "product.product", + related="company_id.deposit_product", + string="Deposit", + readonly=False + ) diff --git a/deposit_rental/models/sale_order_line.py b/deposit_rental/models/sale_order_line.py new file mode 100644 index 00000000000..a32581818f8 --- /dev/null +++ b/deposit_rental/models/sale_order_line.py @@ -0,0 +1,102 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError, MissingError +from odoo.tools import _ + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + is_deposit_line = fields.Boolean(default=False) + + @api.model + def create(self, vals_list): + lines = super().create(vals_list) + rental_lines = lines.filtered( + lambda l: l.product_id.rent_ok + and l.product_id.requires_deposit + and l.product_id.deposit_amount > 0 + ) + if not rental_lines: + return lines + company_map = {} + for line in rental_lines: + company = line.company_id + if company not in company_map: + if not company.deposit_product: + raise UserError(_("Please set deposit product in settings.")) + company_map[company] = company.deposit_product + deposit_vals = [] + for line in rental_lines: + deposit_product = company_map[line.company_id] + deposit_vals.append({ + 'order_id': line.order_id.id, + 'product_id': deposit_product.id, + 'product_uom_qty': line.product_uom_qty, + 'price_unit': line.product_id.deposit_amount, + 'name': f"This amount is deposit for {line.product_id.name} product", + 'is_deposit_line': True, + }) + self.create(deposit_vals) + return lines + + @api.ondelete(at_uninstall=False) + def _unlink_deposit_fee(self): + if not self.env.context.get("bypass_deposit_protection_for_delete"): + for record in self: + if record.is_deposit_line: + raise UserError(_("You can't delete a Deposit Product line directly.")) + rental_lines = self.filtered( + lambda l: l.product_id.rent_ok + and l.product_id.requires_deposit + and l.product_id.deposit_amount > 0 + ) + if not rental_lines: + return + deposit_lines = self.search([ + ('order_id', 'in', rental_lines.mapped('order_id').ids), + ('is_deposit_line', '=', True), + ]) + deposit_map = {} + for line in deposit_lines: + deposit_map[line.name] = line + for line in rental_lines: + deposit_line = deposit_map.get( + f"This amount is deposit for {line.product_id.name} product" + ) + if not deposit_line: + raise MissingError(_("Deposit fee is not present.")) + deposit_line.with_context(bypass_deposit_protection_for_delete=True).unlink() + + def write(self, vals): + if not self.env.context.get("bypass_deposit_protection_for_write"): + for record in self: + if record.is_deposit_line: + raise UserError(_("You can't edit Deposit Product line directly.")) + res = super().write(vals) + if 'product_uom_qty' not in vals: + return res + rental_lines = self.filtered( + lambda l: l.product_id.rent_ok + and l.product_id.requires_deposit + and l.product_id.deposit_amount > 0 + ) + if not rental_lines: + return res + deposit_lines = self.search([ + ('order_id', 'in', rental_lines.mapped('order_id').ids), + ('is_deposit_line', '=', True), + ]) + deposit_map = {} + for line in deposit_lines: + deposit_map[line.name] = line + for line in rental_lines: + deposit_line = deposit_map.get( + f"This amount is deposit for {line.product_id.name} product" + ) + if not deposit_line: + raise MissingError(_("Deposit product line not found.")) + deposit_line.with_context(bypass_deposit_protection_for_write=True).write({ + 'product_uom_qty': line.product_uom_qty, + 'price_unit': line.product_id.deposit_amount, + }) + return res diff --git a/deposit_rental/static/src/deposit_amount.js b/deposit_rental/static/src/deposit_amount.js new file mode 100644 index 00000000000..15a51b0a716 --- /dev/null +++ b/deposit_rental/static/src/deposit_amount.js @@ -0,0 +1,22 @@ +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.DepositRental = publicWidget.Widget.extend({ + selector: "#product_detail", + events: { + 'change input[name="add_qty"]': '_updateDepositAmount', + }, + start: function () { + this._super.apply(this, arguments); + if ($("#deposit_amount").length) { + this._updateDepositAmount(); + } + else { + this.$el.off('change input[name="add_qty"]'); + } + }, + _updateDepositAmount: function () { + var qty = parseFloat($("#o_wsale_cta_wrapper").find("input[name='add_qty']").val()); + var depositAmount = parseFloat($("#deposit_amount").attr("data-base-amount")) || 0; + $("#deposit_amount").text(depositAmount * qty); + } +}) diff --git a/deposit_rental/views/product_template_views.xml b/deposit_rental/views/product_template_views.xml new file mode 100644 index 00000000000..818dd99fde5 --- /dev/null +++ b/deposit_rental/views/product_template_views.xml @@ -0,0 +1,14 @@ + + + + product.template.view.form.inherit.rental.deposit + product.template + + + + + + + + + diff --git a/deposit_rental/views/res_config_settings_views.xml b/deposit_rental/views/res_config_settings_views.xml new file mode 100644 index 00000000000..0688dc17ab3 --- /dev/null +++ b/deposit_rental/views/res_config_settings_views.xml @@ -0,0 +1,13 @@ + + + res.config.settings.view.form.inherit.deposit + res.config.settings + + + + + + + diff --git a/deposit_rental/views/template_views.xml b/deposit_rental/views/template_views.xml new file mode 100644 index 00000000000..b3c06041bb4 --- /dev/null +++ b/deposit_rental/views/template_views.xml @@ -0,0 +1,13 @@ + + + +