diff --git a/product_warranty/__init__.py b/product_warranty/__init__.py
new file mode 100644
index 0000000000..b046ff82fc
--- /dev/null
+++ b/product_warranty/__init__.py
@@ -0,0 +1 @@
+from . import models, wizard
diff --git a/product_warranty/__manifest__.py b/product_warranty/__manifest__.py
new file mode 100644
index 0000000000..f8c48aa46b
--- /dev/null
+++ b/product_warranty/__manifest__.py
@@ -0,0 +1,21 @@
+{
+ "name": "Product Warranty",
+ "version": "1.0",
+ "category": "Sales/Warranty",
+ "summary": "Add warranty information to products and display it on sale order lines.",
+ "description": """
+This module provides functionality to manage product warranties.
+Users can define warranties for products and see warranty details in sale order lines.
+""",
+ "license": "AGPL-3",
+ "depends": ["base", "sale_management"],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/product_template_views.xml",
+ "views/sale_order_views.xml",
+ "views/product_warranty_config_views.xml",
+ "views/product_warranty_config_menu.xml",
+ "wizard/product_warranty_wizard_view.xml",
+ ],
+ "auto_install": True,
+}
diff --git a/product_warranty/models/__init__.py b/product_warranty/models/__init__.py
new file mode 100644
index 0000000000..179fea5840
--- /dev/null
+++ b/product_warranty/models/__init__.py
@@ -0,0 +1 @@
+from . import warranty_config, product_template, sale_order_line, sale_order
diff --git a/product_warranty/models/product_template.py b/product_warranty/models/product_template.py
new file mode 100644
index 0000000000..caacda89a1
--- /dev/null
+++ b/product_warranty/models/product_template.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ is_warranty_product = fields.Boolean("Is Warranty Product")
diff --git a/product_warranty/models/sale_order.py b/product_warranty/models/sale_order.py
new file mode 100644
index 0000000000..6f3366bd4e
--- /dev/null
+++ b/product_warranty/models/sale_order.py
@@ -0,0 +1,35 @@
+from odoo import api, Command, models
+
+
+class InheritedSaleOrder(models.Model):
+ _inherit = "sale.order"
+
+ def action_open_warranty_wizard(self):
+ return {
+ "name": "Add Warranty",
+ "type": "ir.actions.act_window",
+ "res_model": "product.warranty.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {"default_sale_order_id": self.id},
+ }
+
+ @api.onchange("order_line")
+ def _onchange_order_line(self):
+ super()._onchange_order_line()
+ deleted_product_template_ids = []
+ for line in self.order_line:
+ # Find each products that is not in Sale Order currently
+ if (
+ line.linked_line_id.id
+ and line.linked_line_id.id not in self.order_line.ids
+ ):
+ deleted_product_template_ids.append(line.linked_line_id.id)
+
+ linked_line_ids_to_delete = self.order_line.search(
+ [("linked_line_id", "in", deleted_product_template_ids)]
+ )
+ self.order_line = [
+ Command.unlink(linked_line_id)
+ for linked_line_id in linked_line_ids_to_delete.ids
+ ]
diff --git a/product_warranty/models/sale_order_line.py b/product_warranty/models/sale_order_line.py
new file mode 100644
index 0000000000..0655415ea7
--- /dev/null
+++ b/product_warranty/models/sale_order_line.py
@@ -0,0 +1,9 @@
+from odoo import models, fields
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ related_line_id = fields.Many2one(
+ "sale.order.line", string="Related Sale Order Line", ondelete="cascade"
+ )
diff --git a/product_warranty/models/warranty_config.py b/product_warranty/models/warranty_config.py
new file mode 100644
index 0000000000..c1107bcfd9
--- /dev/null
+++ b/product_warranty/models/warranty_config.py
@@ -0,0 +1,17 @@
+from odoo import fields, models
+
+
+class ProductWarrantyConfig(models.Model):
+ _name = "product.warranty.config"
+ _description = "Product Warranty Configuration"
+
+ name = fields.Char(string="Name", required=True)
+ product_template_id = fields.Many2one(
+ "product.template", string="Product", required=True
+ )
+ percentage = fields.Float(string="Percentage", required=True)
+ years = fields.Integer(string="Years", required=True)
+
+ _sql_constraints = [
+ ("name_uniq", "unique (name)", "Two Waranties can not be of same name"),
+ ]
diff --git a/product_warranty/security/ir.model.access.csv b/product_warranty/security/ir.model.access.csv
new file mode 100644
index 0000000000..b9eda372cc
--- /dev/null
+++ b/product_warranty/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+product_warranty.access_product_warranty_config,access_product_warranty_config,product_warranty.model_product_warranty_config,base.group_user,1,1,1,1
+product_warranty.access_prodcut_warranty_wizard,access_prodcut_warranty_wizard,product_warranty.model_product_warranty_wizard,base.group_user,1,1,1,1
+product_warranty.access_product_warranty_wizard_line,access_product_warranty_wizard_line,product_warranty.model_product_warranty_wizard_line,base.group_user,1,1,1,0
diff --git a/product_warranty/views/product_template_views.xml b/product_warranty/views/product_template_views.xml
new file mode 100644
index 0000000000..ad45790d4e
--- /dev/null
+++ b/product_warranty/views/product_template_views.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ product.template.view.form.inherit
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/product_warranty/views/product_warranty_config_menu.xml b/product_warranty/views/product_warranty_config_menu.xml
new file mode 100644
index 0000000000..886929bba0
--- /dev/null
+++ b/product_warranty/views/product_warranty_config_menu.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/product_warranty/views/product_warranty_config_views.xml b/product_warranty/views/product_warranty_config_views.xml
new file mode 100644
index 0000000000..07de3ae778
--- /dev/null
+++ b/product_warranty/views/product_warranty_config_views.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ Warranty Config
+ product.warranty.config
+ list
+
+
+ Define new warranty
+
+
+
+
+
+ product.warranty.config.view.tree
+ product.warranty.config
+
+
+
+
+
+
+
+
+
+
+
diff --git a/product_warranty/views/sale_order_views.xml b/product_warranty/views/sale_order_views.xml
new file mode 100644
index 0000000000..ba672e2fc6
--- /dev/null
+++ b/product_warranty/views/sale_order_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ sale.order.view.form.inherit
+ sale.order
+
+
+
+
+
+
+
+
+
diff --git a/product_warranty/wizard/__init__.py b/product_warranty/wizard/__init__.py
new file mode 100644
index 0000000000..af31546c4f
--- /dev/null
+++ b/product_warranty/wizard/__init__.py
@@ -0,0 +1 @@
+from . import product_warranty_wizard,product_warranty_wizard_line
diff --git a/product_warranty/wizard/product_warranty_wizard.py b/product_warranty/wizard/product_warranty_wizard.py
new file mode 100644
index 0000000000..b6813f76a8
--- /dev/null
+++ b/product_warranty/wizard/product_warranty_wizard.py
@@ -0,0 +1,53 @@
+from odoo import api, Command, fields, models
+
+
+class WarrantyWizard(models.TransientModel):
+ _name = "product.warranty.wizard"
+ _description = "Warranty Selection Wizard"
+
+ sale_order_id = fields.Many2one("sale.order", string="Sale Order")
+ line_ids = fields.One2many(
+ "product.warranty.wizard.line", "wizard_id", string="Warranty Lines"
+ )
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ sale_order = self.env["sale.order"].browse(self.env.context.get("active_id"))
+ warranty_lines = []
+
+ for line in sale_order.order_line:
+ if (
+ line.product_template_id
+ and line.product_template_id.is_warranty_product
+ ):
+ warranty = self.env["product.warranty.wizard.line"].create(
+ {
+ "sale_order_line_id": line.id,
+ "warranty_config_id": False,
+ }
+ )
+ warranty_lines.append(Command.link(warranty.id))
+
+ res["sale_order_id"] = sale_order.id
+ res["line_ids"] = warranty_lines
+ return res
+
+ def apply_warranty(self):
+ for line in self.line_ids:
+ product = self.env["product.template"].browse(line.product_template_id.id)
+ if line.warranty_config_id:
+ self.env["sale.order.line"].create(
+ {
+ "order_id": self.sale_order_id.id,
+ "product_template_id": line.warranty_config_id.product_template_id.id,
+ "name": "Extended Warranty of %d Years - %s"
+ % (line.warranty_config_id.years, product.name),
+ "product_uom_qty": 1,
+ "linked_line_id": line.sale_order_line_id.id,
+ "price_unit": (line.warranty_config_id.percentage / 100)
+ * line.sale_order_line_id.price_subtotal,
+ }
+ )
+
+ return {"type": "ir.actions.act_window_close"}
diff --git a/product_warranty/wizard/product_warranty_wizard_line.py b/product_warranty/wizard/product_warranty_wizard_line.py
new file mode 100644
index 0000000000..d616c69525
--- /dev/null
+++ b/product_warranty/wizard/product_warranty_wizard_line.py
@@ -0,0 +1,34 @@
+from datetime import datetime
+from dateutil.relativedelta import relativedelta
+
+from odoo import models, fields, api
+
+
+class WarrantyWizardLine(models.TransientModel):
+ _name = "product.warranty.wizard.line"
+ _description = "Warranty Wizard Line"
+
+ wizard_id = fields.Many2one("product.warranty.wizard", string="Wizard")
+ sale_order_line_id = fields.Many2one("sale.order.line", string="Sale Order Line")
+ product_template_id = fields.Many2one(
+ "product.template", string="Product", compute="_compute_product_template_id"
+ )
+ warranty_config_id = fields.Many2one(
+ "product.warranty.config", string="Warranty Type"
+ )
+ date_end = fields.Date(string="Date End", compute="_compute_date_end")
+
+ @api.depends("warranty_config_id.years")
+ def _compute_date_end(self):
+ for line in self:
+ line.date_end = datetime.today() + relativedelta(
+ years=line.warranty_config_id.years
+ )
+
+ @api.depends("sale_order_line_id")
+ def _compute_product_template_id(self):
+ for line in self:
+ if line.sale_order_line_id:
+ line.product_template_id = line.sale_order_line_id.product_template_id
+ else:
+ line.product_template_id = False
diff --git a/product_warranty/wizard/product_warranty_wizard_view.xml b/product_warranty/wizard/product_warranty_wizard_view.xml
new file mode 100644
index 0000000000..bb842a3bb0
--- /dev/null
+++ b/product_warranty/wizard/product_warranty_wizard_view.xml
@@ -0,0 +1,27 @@
+
+
+
+ product.warranty.wizard.form
+ product.warranty.wizard
+
+
+
+
+