Skip to content

Commit 9659ebb

Browse files
committed
[ADD] product_kit: kit product wizard and subproduct handling
In this commit - Added a button for kit-type products in the sale order line - Button opens a popup wizard displaying all related sub-products - Button is invisible if the product is unmarked as a kit or SO is confirmed - Wizard fields are editable for quantity and price adjustments - Confirming the wizard adds sub-products as separate sale order lines - Automatically calculates the main product's price based on sub-products - Sub-product sale order lines have their price set to 0 - Added a "Print in Report" boolean field on the sale order - If enabled, sub-product info is displayed in invoice report and preview page - When the main kit product is deleted, related sub-product lines are deleted
1 parent fd5bdf1 commit 9659ebb

12 files changed

+254
-4
lines changed

product_kit/__manifest__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
'name': "Product As Kit",
33
'version': '1.0',
4-
'depends': ['product', 'sale_management'],
4+
'depends': ['stock', 'sale_management'],
55
'author': "Rishav Shah",
66
'category': 'product',
77
'description': """
@@ -11,6 +11,11 @@
1111
'application': True,
1212
'license': 'LGPL-3',
1313
'data': [
14+
'security/ir.model.access.csv',
1415
'views/product_views.xml',
16+
'views/sale_order_views.xml',
17+
'views/kit_products_wizard_views.xml',
18+
'views/sale_order_report.xml',
19+
'views/invoice_report.xml',
1520
],
1621
}

product_kit/models/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
from . import product_template
2+
from . import sale_order_line
3+
from . import sale_order
4+
from . import kit_products_wizard
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from odoo import models, fields, api
2+
3+
4+
class KitProductsWizardLine(models.TransientModel):
5+
_name = "kit.products.wizard.line"
6+
_description = "Kit Products Wizard Line"
7+
8+
wizard_line_id = fields.Many2one("kit.products.wizard", required=True)
9+
product_id = fields.Many2one("product.product", string="Product")
10+
product_quantity = fields.Float(string="Quantity")
11+
price = fields.Float(string="Unit Price")
12+
13+
14+
class KitProductsWizard(models.TransientModel):
15+
_name = "kit.products.wizard"
16+
_description = "Kit Products Wizard"
17+
18+
sale_order_id = fields.Many2one("sale.order", string="Sale Order", readonly=True)
19+
product_template_id = fields.Many2one("product.template", string="Product", readonly=True)
20+
subProduct_ids = fields.One2many("kit.products.wizard.line", "wizard_line_id", string="Sub Products")
21+
22+
@api.model
23+
def default_get(self, fields_list):
24+
res = super().default_get(fields_list)
25+
sale_order_id = self.env.context.get("default_sale_order_id")
26+
product_template_id = self.env.context.get("default_product_template_id")
27+
28+
res.update({
29+
"sale_order_id": sale_order_id,
30+
"product_template_id": product_template_id,
31+
})
32+
33+
if sale_order_id and product_template_id:
34+
product_template = self.env["product.template"].browse(product_template_id)
35+
sub_products = product_template.subProduct_ids
36+
37+
existing_lines = self.env["sale.order.line"].search([
38+
("order_id", "=", sale_order_id),
39+
("product_id", "in", sub_products.ids)
40+
])
41+
42+
line_data = []
43+
for sub_product in sub_products:
44+
existing_line = existing_lines.filtered(lambda l: l.product_id.id == sub_product.id)
45+
line_data.append((0, 0, {
46+
"product_id": sub_product.id,
47+
"product_quantity": existing_line.product_uom_qty if existing_line else 1,
48+
"price": existing_line.last_updated_price if existing_line else sub_product.list_price,
49+
}))
50+
51+
res["subProduct_ids"] = line_data
52+
53+
return res
54+
55+
def action_open_wizard_popup(self):
56+
self.ensure_one()
57+
58+
so_line_model = self.env["sale.order.line"]
59+
total_price = 0.0
60+
61+
for sub in self.subProduct_ids:
62+
sub_total = sub.product_quantity * sub.price
63+
total_price += sub_total
64+
65+
line_vals = {
66+
"product_uom_qty": sub.product_quantity,
67+
"price_unit": 0.0,
68+
"last_updated_price": sub.price,
69+
}
70+
71+
existing_line = so_line_model.search([
72+
("order_id", "=", self.sale_order_id.id),
73+
("product_id", "=", sub.product_id.id),
74+
("is_subProduct", "=", True),
75+
], limit=1)
76+
77+
if existing_line:
78+
existing_line.write(line_vals)
79+
else:
80+
line_vals.update({
81+
"name": sub.product_id.name,
82+
"order_id": self.sale_order_id.id,
83+
"product_id": sub.product_id.id,
84+
"is_subProduct": True,
85+
})
86+
so_line_model.create(line_vals)
87+
88+
main_product = self.product_template_id.product_variant_id
89+
main_line = so_line_model.search([
90+
("order_id", "=", self.sale_order_id.id),
91+
("product_id", "=", main_product.id),
92+
], limit=1)
93+
94+
if main_line:
95+
main_line.price_unit = total_price * main_line.product_uom_qty
96+
else:
97+
so_line_model.create({
98+
"order_id": self.sale_order_id.id,
99+
"product_id": main_product.id,
100+
"product_uom_qty": 1,
101+
"price_unit": total_price,
102+
"name": self.product_template_id.name,
103+
})
104+
105+
return {"type": "ir.actions.act_window_close"}

product_kit/models/product_template.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class ProductTemplate(models.Model):
55
_inherit = 'product.template'
66

77
isKit = fields.Boolean(string="is Kit", default=False)
8-
subProduct = fields.Many2many('product.product', string="Sub Products")
8+
subProduct_ids = fields.Many2many('product.product', string="Sub Products")
99
subProductVisibility = fields.Boolean(string="is Kit", default=False, compute='_compute_sub_product_visiblity')
1010

1111
@api.depends_context('isKit')

product_kit/models/sale_order.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from odoo import models, fields
2+
3+
4+
class SaleOrder(models.Model):
5+
_inherit = "sale.order"
6+
7+
is_kit_printable = fields.Boolean(string="Print in report ?", default=True)

product_kit/models/sale_order_line.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from odoo import models, fields
2+
3+
4+
class SaleOrderLine(models.Model):
5+
_inherit = "sale.order.line"
6+
7+
isKit = fields.Boolean(related="product_template_id.isKit", store=True)
8+
is_subProduct = fields.Boolean(string="Created from Wizard", default=False)
9+
last_updated_price = fields.Float("Kit Price", help="Custom price stored for use in kit wizard")
10+
11+
def action_open_kit_wizard(self):
12+
return {
13+
"name": "Kit Products",
14+
"type": "ir.actions.act_window",
15+
"res_model": "kit.products.wizard",
16+
"view_mode": "form",
17+
"target": "new",
18+
"context": {
19+
"default_sale_order_id": self.order_id.id,
20+
"default_product_template_id": self.product_template_id.id,
21+
}
22+
}
23+
24+
def unlink(self):
25+
"""When main product line is deleted, all the related wizard-generated sale order lines are deleted."""
26+
main_product_line = self.filtered(lambda line: not line.is_subProduct)
27+
28+
for sub_product_lines in main_product_line:
29+
kit_lines = self.search([
30+
("order_id", "=", sub_product_lines.order_id.id),
31+
("is_subProduct", "=", True),
32+
("product_id", "in", sub_product_lines.product_id.subProduct_ids.ids),
33+
])
34+
kit_lines.unlink()
35+
36+
return super().unlink()
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
2+
product_kit.access_kit_products_wizard_line,access_kit_products_wizard_line,product_kit.model_kit_products_wizard_line,base.group_user,1,1,1,1
3+
product_kit.access_kit_products_wizard,access_kit_products_wizard,product_kit.model_kit_products_wizard,base.group_user,1,1,1,1

product_kit/views/invoice_report.xml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<template id="report_invoice_document_subproduct" inherit_id="account.report_invoice_document">
4+
<xpath expr="//tbody[hasclass('invoice_tbody')]//tr" position="attributes">
5+
<attribute name="t-if">
6+
(line.sale_line_ids.order_id.is_kit_printable) or (not line.sale_line_ids.filtered('is_subProduct'))
7+
</attribute>
8+
</xpath>
9+
</template>
10+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="action_kit_products_wizard" model="ir.actions.act_window">
4+
<field name="name">Kit Product</field>
5+
<field name="res_model">kit.products.wizard</field>
6+
<field name="view_mode">form</field>
7+
<field name="target">new</field>
8+
</record>
9+
<record id="view_kit_products_wizard" model="ir.ui.view">
10+
<field name="name">view_kit_products_wizard</field>
11+
<field name="model">kit.products.wizard</field>
12+
<field name="arch" type="xml">
13+
<form>
14+
<sheet>
15+
<group>
16+
<h1>PRODUCT </h1>
17+
<h1><field name="product_template_id"/></h1>
18+
</group>
19+
<group>
20+
<h2>SubProducts</h2>
21+
</group>
22+
<group>
23+
<field name="subProduct_ids" nolabel="1">
24+
<list editable="bottom">
25+
<field name="product_id"/>
26+
<field name="product_quantity"/>
27+
<field name="price"/>
28+
</list>
29+
</field>
30+
</group>
31+
<footer>
32+
<button name="action_open_wizard_popup" type="object" string="Confirm" class="btn-primary"/>
33+
<button string="Close" class="oe_secondary" special="cancel"/>
34+
</footer>
35+
</sheet>
36+
</form>
37+
</field>
38+
</record>
39+
</odoo>

product_kit/views/product_views.xml

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
<field name="arch" type="xml">
88
<xpath expr="//page[@name='general_information']/group/group[@name='group_general']" position="inside">
99
<field name="isKit" string="Is Kit"/>
10-
<field name="subProduct" widget="many2many_tags" string="Sub Products" invisible="not subProductVisibility"/>
10+
<field name="subProduct_ids" widget="many2many_tags" string="Sub Products" invisible="not subProductVisibility"/>
1111
</xpath>
12-
1312
</field>
1413
</record>
1514
</odoo>
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<template id="report_saleorder_document_inherit" inherit_id="sale.sale_order_portal_content">
4+
<xpath expr="//table[@id='sales_order_table']//tbody[@class='sale_tbody']//t[@t-foreach='lines_to_report']" position="attributes">
5+
<attribute name="t-if">
6+
(not line.is_subProduct) or (sale_order.is_kit_printable and line.is_subProduct)
7+
</attribute>
8+
</xpath>
9+
</template>
10+
<template id="report_saleorder_document" inherit_id="sale.report_saleorder_document">
11+
<xpath expr="//tbody/t[@t-foreach='lines_to_report']" position="attributes">
12+
<attribute name="t-if">
13+
not line.is_subProduct or doc.is_kit_printable
14+
</attribute>
15+
</xpath>
16+
</template>
17+
</odoo>
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="view_order_form_inherit" model="ir.ui.view">
4+
<field name="name">sale.order.form.view.inherit</field>
5+
<field name="model">sale.order</field>
6+
<field name="inherit_id" ref="sale.view_order_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='payment_term_id']" position="after">
9+
<field name="is_kit_printable"/>
10+
</xpath>
11+
<xpath expr="//field[@name='order_line']/list//field[@name='product_template_id']" position="after">
12+
<button name="action_open_kit_wizard" type="object" string="Show Sub Products" class="oe_highlight"
13+
invisible = "not isKit"/>
14+
</xpath>
15+
<xpath expr="//field[@name='order_line']/list//field[@name='product_template_id']" position="attributes">
16+
<attribute name="readonly">is_subProduct</attribute>
17+
</xpath>
18+
<xpath expr="//field[@name='order_line']/list//field[@name='product_uom_qty']" position="attributes">
19+
<attribute name="readonly">is_subProduct</attribute>
20+
</xpath>
21+
<xpath expr="//field[@name='order_line']/list//field[@name='price_unit']" position="attributes">
22+
<attribute name="readonly">is_subProduct</attribute>
23+
</xpath>
24+
</field>
25+
</record>
26+
</odoo>

0 commit comments

Comments
 (0)