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 @@
+
+
+
+
+
+
+ Deposit Required:
+
+
+
+
+
+