Skip to content

Commit 74302d7

Browse files
committed
[ADD] kit_product_type: implement kit type product and sale order management
- Introduced a new product type Kit via is_kit Boolean field on product.template. - When is_kit is enabled, a Many2many field appears to select sub-products. - On Sales Orders, kit products now show a Add Sub Products button. - Sub-products can be customized (quantity and price) through a wizard. - Confirming the wizard adds sub-products to the order line with unit price and subtotal as 0. - The cost of sub-products is included in the main kit product’s subtotal. - Added `print_in_report` Boolean on sale.order to control printing sub-products in reports/customer preview. - Once the Sales Order is confirmed, the Add Sub Products button is hidden.
1 parent 7b52f58 commit 74302d7

13 files changed

+300
-0
lines changed

kit_product_type/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import wizard

kit_product_type/__manifest__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "kit_product_type",
3+
"depends": ["base", "product", "sale", "sale_management"],
4+
"data": [
5+
"views/product_template_view.xml",
6+
"views/sale_order_view.xml",
7+
"wizard/sale_order_line_wizard.xml",
8+
"security/ir.model.access.csv",
9+
],
10+
"license": "LGPL-3",
11+
}

kit_product_type/models/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import product_template
2+
from . import sale_order
3+
from . import sales_order_line
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from odoo import models, fields
2+
3+
4+
class ProductTemplate(models.Model):
5+
_inherit = "product.template"
6+
7+
is_kit = fields.Boolean(string="Is Kit?", default=False)
8+
sub_products = fields.Many2many("product.product", string="Sub Products")

kit_product_type/models/sale_order.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from odoo import fields, models
2+
3+
4+
class SaleOrder(models.Model):
5+
_inherit = "sale.order"
6+
7+
print_in_report = fields.Boolean(string="Print in Report?")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from odoo import api, models, fields
2+
3+
4+
class SaleOrderLine(models.Model):
5+
_inherit = "sale.order.line"
6+
7+
is_kit = fields.Boolean(related="product_template_id.is_kit")
8+
is_sub_product_ol = fields.Boolean(default=False)
9+
wizard_price = fields.Float()
10+
main_order_line_id = fields.Many2one(
11+
"sale.order.line",
12+
string="Parent Line",
13+
ondelete="cascade",
14+
)
15+
16+
child_line_ids = fields.One2many(
17+
"sale.order.line",
18+
"main_order_line_id",
19+
string="Child Lines",
20+
)
21+
display_price = fields.Float(
22+
compute="_compute_display_price", inverse="_compute_unit_price"
23+
)
24+
display_sub_total = fields.Float(compute="_compute_amount_price")
25+
26+
@api.depends("price_unit", "is_sub_product_ol")
27+
def _compute_display_price(self):
28+
for line in self:
29+
line.display_price = 0.0 if line.is_sub_product_ol else line.price_unit
30+
31+
@api.depends("display_price", "price_subtotal", "is_sub_product_ol")
32+
def _compute_amount_price(self):
33+
for line in self:
34+
line.display_sub_total = (
35+
0.0 if line.is_sub_product_ol else line.price_subtotal
36+
)
37+
38+
def _compute_unit_price(self):
39+
for line in self:
40+
line.price_unit = line.display_price
41+
42+
def unlink(self):
43+
for line in self:
44+
line.main_order_line_id.price_subtotal -= (
45+
line.product_uom_qty * line.wizard_price
46+
)
47+
48+
return super().unlink()
49+
50+
def open_sub_product_wizard(self):
51+
return {
52+
"name": "Sub Products Wizard",
53+
"type": "ir.actions.act_window",
54+
"res_model": "sale.order.line.wizard",
55+
"view_mode": "form",
56+
"target": "new",
57+
"context": {"active_id": self.id},
58+
}
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+
access_sale_order_line_wizard,sale.order.line.wizard,model_sale_order_line_wizard,base.group_user,1,1,1,0
3+
access_sale_order_line_wizard_line,sale.order.line.wizard.line,model_sale_order_line_wizard_line,base.group_user,1,1,1,0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="product_template_kit_inherit_view" model="ir.ui.view">
4+
<field name="name">product.template.inherit.view</field>
5+
<field name="model">product.template</field>
6+
<field name="inherit_id" ref="product.product_template_only_form_view"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//page[@name='general_information']//group[@name='group_general']" position="inside">
9+
<field name="is_kit"/>
10+
<field name="sub_products" widget="many2many_tags" invisible="not is_kit"/>
11+
</xpath>
12+
</field>
13+
</record>
14+
<template id="preview_saleorder_custom" inherit_id="sale.sale_order_portal_content">
15+
<xpath expr="//t[@t-foreach='lines_to_report']/tr" position="attributes">
16+
<attribute name="t-att-class">'d-none' if (not sale_order.print_in_report and line.is_sub_product_ol) else ''+
17+
('fw-bold o_line_section' if (line.display_type == 'line_section' or line.product_type == 'combo')
18+
else 'fst-italic o_line_note' if line.display_type == 'line_note' else '')</attribute>
19+
</xpath>
20+
</template>
21+
<template id="report_saleorder_custom" inherit_id="sale.report_saleorder_document">
22+
<xpath expr="//t[@t-foreach='lines_to_report']/tr" position="attributes">
23+
<attribute name="t-att-class">'d-none' if (not doc.print_in_report and line.is_sub_product_ol) else ''</attribute>
24+
</xpath>
25+
</template>
26+
</odoo>
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="sale_order_inherit_view_form" model="ir.ui.view">
4+
<field name="name">Sale.Order.Inherit.View.Form</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="//sheet//group[@name='sale_header']//group[@name='order_details']" position="inside">
9+
<field name="print_in_report"/>
10+
</xpath>
11+
<xpath expr="//field[@name='product_template_id']" position="after">
12+
<button name='open_sub_product_wizard' type="object" string="Add Sub-Product" invisible="not is_kit or state=='sale'"/>
13+
</xpath>
14+
<xpath expr="//field[@name='order_line']//list//field[@name='product_template_id']" position="attributes">
15+
<attribute name="readonly">is_sub_product_ol</attribute>
16+
</xpath>
17+
<xpath expr="//field[@name='order_line']//list//field[@name='product_uom_qty']" position="attributes">
18+
<attribute name="readonly">is_sub_product_ol</attribute>
19+
</xpath>
20+
<xpath expr="//field[@name='order_line']//list//field[@name='price_unit']" position="attributes">
21+
<attribute name="readonly">is_sub_product_ol</attribute>
22+
<attribute name="column_invisible">True</attribute>
23+
</xpath>
24+
<xpath expr="//field[@name='order_line']//list//field[@name='price_unit']" position="after">
25+
<field name="display_price" string="Unit Price"/>
26+
</xpath>
27+
<xpath expr="//field[@name='order_line']//list//field[@name='display_price']" position="attributes">
28+
<attribute name="readonly">is_sub_product_ol</attribute>
29+
</xpath>
30+
<xpath expr="//field[@name='order_line']//list//field[@name='price_subtotal']" position="after">
31+
<field name="display_sub_total" string="Amount"/>
32+
</xpath>
33+
<xpath expr="//field[@name='order_line']//list//field[@name='price_subtotal']" position="attributes">
34+
<attribute name="column_invisible">True</attribute>
35+
</xpath>
36+
</field>
37+
</record>
38+
</odoo>

kit_product_type/wizard/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import sale_order_line_wizard
2+
from . import sale_order_line_wizard_lines
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from odoo import api, fields, models
2+
3+
4+
class SubProducts(models.TransientModel):
5+
_name = "sale.order.line.wizard"
6+
_description = "change.quantity.of.subproducts.in.kit"
7+
8+
main_product = fields.Char(string="Main Product")
9+
line_ids = fields.One2many(
10+
"sale.order.line.wizard.line", "wizard_id", string="Sub Products"
11+
)
12+
13+
@api.model
14+
def default_get(self, fields_list):
15+
res = super().default_get(fields_list)
16+
active_id = self.env.context.get("active_id")
17+
main_order_line = self.env["sale.order.line"].browse(active_id)
18+
sale_order = main_order_line.order_id
19+
order_line = self.env["sale.order.line"].browse(active_id)
20+
res["main_product"] = order_line.product_template_id.name
21+
22+
if order_line:
23+
lines = []
24+
for product in order_line.product_template_id.sub_products:
25+
existing_line = self.env["sale.order.line"].search(
26+
[
27+
("order_id", "=", sale_order.id),
28+
("main_order_line_id", "=", main_order_line.id),
29+
("product_id", "=", product.id),
30+
],
31+
limit=1,
32+
)
33+
lines.append(
34+
(
35+
0,
36+
0,
37+
{
38+
"product_id": product.id,
39+
"quantity": existing_line.product_uom_qty
40+
if existing_line
41+
else 1,
42+
"price": existing_line.wizard_price
43+
if existing_line
44+
else product.list_price,
45+
},
46+
)
47+
)
48+
res["line_ids"] = lines
49+
return res
50+
51+
def confirm_sub_products(self):
52+
active_id = self.env.context.get("active_id")
53+
main_order_line = self.env["sale.order.line"].browse(active_id)
54+
sale_order = main_order_line.order_id
55+
main_product_subtotal = (
56+
main_order_line.price_unit * main_order_line.product_uom_qty
57+
)
58+
for wizard in self:
59+
for line in wizard.line_ids:
60+
if line.quantity > 0:
61+
main_product_subtotal += line.quantity * line.price
62+
63+
existing_line = self.env["sale.order.line"].search(
64+
[
65+
("order_id", "=", sale_order.id),
66+
("main_order_line_id", "=", main_order_line.id),
67+
("product_id", "=", line.product_id.id),
68+
],
69+
limit=1,
70+
)
71+
72+
if existing_line:
73+
existing_line.write(
74+
{
75+
"product_uom_qty": line.quantity,
76+
"wizard_price": line.price,
77+
"price_unit": line.price,
78+
}
79+
)
80+
else:
81+
self.env["sale.order.line"].create(
82+
{
83+
"order_id": sale_order.id,
84+
"product_id": line.product_id.id,
85+
"product_uom_qty": line.quantity,
86+
"name": line.product_id.name,
87+
"is_sub_product_ol": True,
88+
"wizard_price": line.price,
89+
"price_unit": line.price,
90+
"main_order_line_id": main_order_line.id,
91+
}
92+
)
93+
main_order_line.write({"price_subtotal": main_product_subtotal})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<record id="sub_products_wizard_view" model="ir.ui.view">
4+
<field name="name">Sub Products Wizard Form</field>
5+
<field name="model">sale.order.line.wizard</field>
6+
<field name="inherit_id" ref=""></field>
7+
<field name="arch" type="xml">
8+
<form string="Sub Products">
9+
<group>
10+
<div class="d-flex flex-column gap-2 ">
11+
<label for="main_product" string="Main Product"/>
12+
<field name="main_product" class="fw-bold fs-2" nolabel="0" readonly="1"/>
13+
<div class="fw-bold fs-4">Sub Products</div>
14+
<field name="line_ids" nolabel="1">
15+
<list editable="bottom" create="0">
16+
<field name="product_id"/>
17+
<field name="quantity"/>
18+
<field name="price"/>
19+
</list>
20+
</field>
21+
</div>
22+
</group>
23+
<footer>
24+
<button type="object" string="Confirm" name="confirm_sub_products"/>
25+
<button special="cancel" string="cancel"/>
26+
</footer>
27+
</form>
28+
</field>
29+
</record>
30+
31+
<record id="sub_products_wizard" model="ir.actions.act_window">
32+
<field name="name">Sub Products Wizard</field>
33+
<field name="res_model">sale.order.line.wizard</field>
34+
<field name="view_mode">form</field>
35+
<field name="target">new</field>
36+
</record>
37+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from odoo import fields, models
2+
3+
4+
class SaleOrderLines(models.TransientModel):
5+
_name = "sale.order.line.wizard.line"
6+
_description = "sale.order.lines.for.sub.products"
7+
8+
wizard_id = fields.Many2one("sale.order.line.wizard")
9+
product_id = fields.Many2one("product.product")
10+
quantity = fields.Float()
11+
wizard_price = fields.Float()
12+
price = fields.Float()

0 commit comments

Comments
 (0)