Skip to content

Commit 662b14a

Browse files
committed
[ADD] new_product_type_kit: completed kit product type with wizard & sub-product
- Introduced 'Is Kit' field on product templates to define kit-type products - Added Many2many field to select sub-products for a kit - Added smart button 'Configure Kit' on sale order line, visible only for kit products - Created a wizard to select sub-product quantity and price per main product - Sub-products are auto-added as separate sale order lines under the main kit line - Sub-product lines are read-only & priced at 0 (cost included in main product) - Sub-products support storable/consumable product types for stock tracking - Main product's unit price remains unchanged; subtotal includes sub-product costs - Sub-product lines are auto-deleted if the main kit line is removed - Added 'Print in Report' checkbox on Sale Order to control sub-product visibility - Applied conditional display of sub-products in: - Sale Order PDF - Portal Order Preview - Invoice PDF (QWeb-safe logic using t-set variables)
1 parent 137fcbf commit 662b14a

16 files changed

+289
-0
lines changed

Diff for: new_product_type_kit/__init__.py

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

Diff for: new_product_type_kit/__manifest__.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
'name': 'Dev Zero Stock Blockage',
3+
'version': '1.0',
4+
'summary': 'Add a new prodcut type kit',
5+
'description': """
6+
Add kit-type products with configurable sub-products and conditional report visibility
7+
""",
8+
'author': 'Raghav Agiwal',
9+
'depends': ['sale_management', 'stock', 'product'],
10+
'data': [
11+
'security/ir.model.access.csv',
12+
'views/product_template_view.xml',
13+
'views/sale_order_line_view.xml',
14+
'views/kit_wizard_views.xml',
15+
'views/portal_saleorder_templates.xml',
16+
'report/report_saleorder_templates.xml',
17+
'report/report_invoice_templates.xml',
18+
],
19+
'installable': True,
20+
'application': True,
21+
'license': 'LGPL-3'
22+
}

Diff for: new_product_type_kit/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 sale_order_line

Diff for: new_product_type_kit/models/product_template.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from odoo import fields, models
2+
3+
4+
class ProductTemplate(models.Model):
5+
_inherit = 'product.template'
6+
7+
is_kit = fields.Boolean(string="Is Kit")
8+
sub_product_ids = fields.Many2many(
9+
'product.product',
10+
string="Sub Products",
11+
required=True,
12+
)

Diff for: new_product_type_kit/models/sale_order.py

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

Diff for: new_product_type_kit/models/sale_order_line.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from odoo import _, api, fields, models
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_kit_component = fields.Boolean(string="Is Subproduct")
9+
kit_parent_line_id = fields.Many2one('sale.order.line', string="Kit Parent Line", ondelete="cascade")
10+
kit_unit_cost = fields.Float(string="Unit Price (Wizard)", default=0.0)
11+
12+
def action_open_kit_wizard(self):
13+
return {
14+
'name': 'Kit Components',
15+
'type': 'ir.actions.act_window',
16+
'res_model': 'kit.wizard',
17+
'view_mode': 'form',
18+
'target': 'new',
19+
'context': {
20+
"active_id": self.order_id.id,
21+
'default_product_id': self.product_id.id,
22+
'default_sale_order_line_id': self.id,
23+
}
24+
}
25+
26+
def unlink(self):
27+
# Identify sub-product lines
28+
sub_products = self.filtered(lambda line: line.is_kit_component)
29+
30+
if sub_products and not self.env.context.get('allow_sub_product_deletion'):
31+
if self == sub_products:
32+
raise models.UserError(_("You cannot delete kit sub-products directly. Delete the main kit line instead."))
33+
return (self - sub_products).unlink()
34+
35+
child_lines = self.env['sale.order.line'].search([
36+
('kit_parent_line_id', 'in', self.ids)
37+
])
38+
39+
if child_lines:
40+
child_lines.with_context(allow_sub_product_deletion=True).unlink()
41+
42+
return super().unlink()
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<template id="report_invoice_document_inherit_kit" inherit_id="account.report_invoice_document">
4+
<xpath expr="//table[@name='invoice_line_table']/tbody/t/tr" position="attributes">
5+
<attribute name="t-if">
6+
not any(line.sale_line_ids.mapped('kit_parent_line_id')) or
7+
any(line.sale_line_ids.mapped('kit_parent_line_id') and
8+
line.sale_line_ids.mapped('order_id.print_in_report'))
9+
</attribute>
10+
</xpath>
11+
</template>
12+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<template id="report_saleorder_document_inherit_kit" inherit_id="sale.report_saleorder_document">
4+
<xpath expr="//tbody/t/tr" position="attributes">
5+
<attribute name="t-if">
6+
(not line.kit_parent_line_id) or (doc.print_in_report and line.kit_parent_line_id)
7+
</attribute>
8+
</xpath>
9+
</template>
10+
</odoo>

Diff for: new_product_type_kit/security/ir.model.access.csv

+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+
access_kit_wizard,access.kit.wizard,model_kit_wizard,,1,1,1,1
3+
access_kit_wizard_line,access.kit.wizard.line,model_kit_wizard_line,,1,1,1,1

Diff for: new_product_type_kit/views/kit_wizard_views.xml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="view_kit_wizard_form" model="ir.ui.view">
4+
<field name="name">kit.wizard.form</field>
5+
<field name="model">kit.wizard</field>
6+
<field name="arch" type="xml">
7+
<form string="Configure Kit" create="0">
8+
<group>
9+
<field name="product_id" readonly="1"/>
10+
</group>
11+
12+
<group string="Sub Products">
13+
<field name="kit_line_ids" nolabel="1">
14+
<list editable="bottom" create="0">
15+
<field name="product_id" readonly="1" force_save="1"/>
16+
<field name="quantity"/>
17+
<field name="price"/>
18+
</list>
19+
</field>
20+
</group>
21+
22+
<footer>
23+
<button string="Confirm" type="object" name="action_confirm" class="btn-primary"/>
24+
<button string="Cancel" class="btn-secondary" special="cancel"/>
25+
</footer>
26+
</form>
27+
</field>
28+
</record>
29+
</odoo>
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<template id="sale_order_portal_content_inherit_kit" inherit_id="sale.sale_order_portal_content">
4+
<xpath expr="//tbody/t/tr" position="attributes">
5+
<attribute name="t-if">
6+
(not line.kit_parent_line_id) or (sale_order.print_in_report and line.kit_parent_line_id)
7+
</attribute>
8+
</xpath>
9+
</template>
10+
</odoo>

Diff for: new_product_type_kit/views/product_template_view.xml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="product_template_kit_form" model="ir.ui.view">
4+
<field name="name">product.template.kit.form</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" invisible="type != 'consu'"/>
10+
<field name="sub_product_ids" widget="many2many_tags" invisible="not is_kit or type != 'consu'" />
11+
</xpath>
12+
</field>
13+
</record>
14+
</odoo>

Diff for: new_product_type_kit/views/sale_order_line_view.xml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<odoo>
2+
<record id="sale_order_line_kit_button_view" model="ir.ui.view">
3+
<field name="name">sale.order.line.kit.button</field>
4+
<field name="model">sale.order</field>
5+
<field name="inherit_id" ref="sale.view_order_form"/>
6+
<field name="arch" type="xml">
7+
<xpath expr="//field[@name='product_template_id']" position="after">
8+
<button invisible="state != 'draft' or not is_kit" name="action_open_kit_wizard" string="Configure Kit" type="object" class="btn-primary" icon="fa-wrench"></button>
9+
</xpath>
10+
11+
<xpath expr="//field[@name='payment_term_id']" position="after">
12+
<field name="print_in_report"/>
13+
</xpath>
14+
15+
<xpath expr="//field[@name='order_line']/list//field[@name='product_template_id']" position="attributes">
16+
<attribute name="readonly">is_kit_component</attribute>
17+
</xpath>
18+
19+
<xpath expr="//field[@name='order_line']/list//field[@name='product_uom_qty']" position="attributes">
20+
<attribute name="readonly">is_kit_component</attribute>
21+
</xpath>
22+
23+
<xpath expr="//field[@name='order_line']/list//field[@name='price_unit']" position="attributes">
24+
<attribute name="readonly">is_kit_component</attribute>
25+
</xpath>
26+
</field>
27+
</record>
28+
</odoo>

Diff for: new_product_type_kit/wizard/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import kit_wizard
2+
from . import kit_wizard_line

Diff for: new_product_type_kit/wizard/kit_wizard.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from odoo import api, Command, fields, models
2+
3+
4+
class KitWizard(models.TransientModel):
5+
_name = 'kit.wizard'
6+
_description = 'Kit Wizard'
7+
8+
product_id = fields.Many2one('product.product', string='Product', required=True)
9+
kit_line_ids = fields.One2many('kit.wizard.line', 'wizard_id', string="Sub Products")
10+
sale_order_line_id = fields.Many2one('sale.order.line', string="Parent Line")
11+
12+
@api.model
13+
def default_get(self, fields):
14+
res = super().default_get(fields)
15+
product_id = self.env.context.get("default_product_id")
16+
order_id = self.env.context.get("active_id")
17+
sale_order_line_id = self.env.context.get("default_sale_order_line_id")
18+
19+
if not product_id or not order_id:
20+
return res
21+
22+
product = self.env["product.product"].browse(product_id)
23+
order = self.env["sale.order"].browse(order_id)
24+
sub_products = product.sub_product_ids
25+
lines = []
26+
27+
for sub in sub_products:
28+
existing_line = order.order_line.filtered(
29+
lambda l: l.product_id.id == sub.id
30+
and l.kit_parent_line_id
31+
and l.kit_parent_line_id.id == sale_order_line_id
32+
)
33+
lines.append(Command.create({
34+
'product_id': sub.id,
35+
'quantity': existing_line.product_uom_qty if existing_line else 1.0,
36+
'price': existing_line.kit_unit_cost if existing_line else sub.lst_price,
37+
}))
38+
39+
res.update({
40+
'product_id': product_id,
41+
'kit_line_ids': lines,
42+
'sale_order_line_id': sale_order_line_id,
43+
})
44+
return res
45+
46+
def action_confirm(self):
47+
order_id = self.env.context.get("active_id")
48+
sale_order_line_id = self.env.context.get("default_sale_order_line_id")
49+
order = self.env["sale.order"].browse(order_id)
50+
parent_line = order.order_line.filtered(lambda l: l.id == sale_order_line_id)
51+
total_price = self.product_id.lst_price
52+
53+
for line in self.kit_line_ids:
54+
existing_line = order.order_line.filtered(
55+
lambda l: l.product_id.id == line.product_id.id
56+
and l.kit_parent_line_id
57+
and l.kit_parent_line_id.id == sale_order_line_id
58+
)
59+
if existing_line:
60+
existing_line.write({
61+
'product_uom_qty': line.quantity,
62+
'kit_unit_cost': line.price,
63+
'price_unit': 0,
64+
})
65+
else:
66+
self.env['sale.order.line'].create({
67+
'order_id': order.id,
68+
'product_id': line.product_id.id,
69+
'product_uom_qty': line.quantity,
70+
'kit_unit_cost': line.price,
71+
'price_unit': 0.0,
72+
'is_kit_component': True,
73+
"sequence": parent_line.sequence,
74+
'kit_parent_line_id': sale_order_line_id,
75+
# 'name': product.name,
76+
})
77+
78+
total_price += line.quantity * line.price
79+
parent_line.write({"price_unit": total_price})

Diff for: new_product_type_kit/wizard/kit_wizard_line.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from odoo import fields, models
2+
3+
4+
class KitWizardLine(models.TransientModel):
5+
_name = 'kit.wizard.line'
6+
_description = 'Kit Wizard Line'
7+
8+
wizard_id = fields.Many2one('kit.wizard', string="Wizard", required=True, ondelete="cascade")
9+
product_id = fields.Many2one('product.product', string="Sub Product")
10+
quantity = fields.Float(string="Quantity", default=1.0, required=True)
11+
price = fields.Float(string="Price")

0 commit comments

Comments
 (0)