Skip to content

Commit c7554ff

Browse files
committed
[ADD] sale: added product as a kit feature
Product Kit Definition: Products can now be designated as kits on the product form. This allows for defining a default set of components and quantities, which serve as a template to streamline the sales process. Dynamic Order Configuration: A Products button is now available on the sale order line. This launches a wizard, enabling salespersons to dynamically adjust component quantities and pricing for that specific order. Automated Pricing: The main kit's price on the order line is automatically recalculated to accurately reflect the total price of its configured components. This ensures pricing integrity and eliminates manual calculation errors. Document Presentation Control: A new visibility toggle on the sales order provides explicit control over the presentation of customer-facing documents. Users can choose to either display the full, itemized component list or show only the main consolidated kit product on quotations and invoices. Invoice Integration: The invoice creation process has been extended to honor the visibility setting from the sales order. This ensures the final invoice presentation is consistent with the user's choice, meeting specific customer requirements for billing clarity. task-4957772
1 parent fbf9ee9 commit c7554ff

16 files changed

+338
-0
lines changed

product_kit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import wizards

product_kit/__manifest__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
'name': "product_kit",
3+
'version': '1.0',
4+
'license': 'LGPL-3',
5+
'depends': ['sale_management'],
6+
'author': "Kalpan Desai",
7+
'category': 'Sales/Sales',
8+
'description': """
9+
Module specifically designed to manage product as kits in Odoo.
10+
""",
11+
'installable': True,
12+
'application': True,
13+
'data': [
14+
'views/product_views.xml',
15+
'wizards/sub_product_wizard_view.xml',
16+
'views/sale_order_view.xml',
17+
'views/sale_portal_view.xml',
18+
'security/ir.model.access.csv',
19+
],
20+
}

product_kit/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from . import sale_order_line
2+
from . import product_template
3+
from . import sale_order
4+
from . import sale_invoice
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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', help='Check this box if the product is a kit that contains other products.', default=False)
8+
9+
kit_product_ids = fields.Many2many('product.product', string='Sub Products', help='Products included in this kit.')

product_kit/models/sale_invoice.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from odoo import fields, models
2+
3+
4+
class AccountMove(models.Model):
5+
_inherit = 'account.move'
6+
7+
is_printable_kit = fields.Boolean(string="Show Sub-Products", default=True)

product_kit/models/sale_order.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from odoo import api, fields, models
2+
3+
4+
class SaleOrder(models.Model):
5+
_inherit = "sale.order"
6+
7+
is_printable_kit = fields.Boolean(
8+
string="Print Kit in Report",
9+
default=False,
10+
help="If checked, kit products will be printed in the report."
11+
)
12+
13+
has_kit = fields.Boolean(
14+
string="Has Kit Product",
15+
compute="_compute_has_kit",
16+
store=True,
17+
default=False,
18+
)
19+
20+
@api.depends('order_line', 'order_line.is_kit')
21+
def _compute_has_kit(self):
22+
for order in self:
23+
order.has_kit = any(line.is_kit for line in order.order_line)
24+
25+
def _prepare_invoice(self):
26+
"""
27+
Prepare the dict of values to create the new invoice for a sales order.
28+
"""
29+
invoice_vals = super()._prepare_invoice()
30+
# Copy the value of your toggle to the invoice
31+
invoice_vals['is_printable_kit'] = self.is_printable_kit
32+
return invoice_vals
33+
34+
def _create_invoices(self, grouped=False, final=False, date=None):
35+
# First, create the invoice(s) using the standard Odoo method.
36+
# This will include lines for the main product and all sub-products.
37+
invoices = super()._create_invoices(grouped, final, date)
38+
39+
# Now, loop through the newly created invoices to apply your logic
40+
for invoice in invoices:
41+
# Check the toggle copied from the Sale Order
42+
if not invoice.is_printable_kit:
43+
44+
# Find all invoice lines that came from a sale order line
45+
# marked as a sub-product.
46+
sub_product_invoice_lines = invoice.invoice_line_ids.filtered(
47+
lambda line: line.sale_line_ids and line.sale_line_ids[0].is_subproduct
48+
)
49+
50+
# If any sub-product lines were found, delete them
51+
if sub_product_invoice_lines:
52+
sub_product_invoice_lines.unlink()
53+
54+
return invoices

product_kit/models/sale_order_line.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from odoo import fields, models
2+
from odoo.exceptions import UserError
3+
4+
5+
class SaleOrderLine(models.Model):
6+
_inherit = "sale.order.line"
7+
8+
is_kit = fields.Boolean(related="product_template_id.is_kit")
9+
parent_product_id = fields.Many2one('product.product')
10+
is_subproduct = fields.Boolean(default=False)
11+
12+
def unlink(self):
13+
"""
14+
Override unlink to also delete sub-product lines when a kit line is deleted.
15+
It also prevents the direct deletion of a sub-product line.
16+
"""
17+
# Prevent direct deletion of sub-product lines.
18+
sub_product_lines = self.filtered('is_subproduct')
19+
if sub_product_lines:
20+
# Find the parent kit lines for these sub-products
21+
parent_lines = self.env['sale.order.line'].search([
22+
('order_id', 'in', sub_product_lines.mapped('order_id').ids),
23+
('product_id', 'in', sub_product_lines.mapped('parent_product_id').ids),
24+
('is_kit', '=', True)
25+
])
26+
# If any of the required parent lines are not in the current deletion set, raise an error.
27+
if parent_lines - self:
28+
raise UserError("You cannot delete a component of a kit directly. Please remove the main kit product instead.")
29+
30+
# Cascade delete: find all sub-products of kits being deleted.
31+
sub_products_to_delete = self.env['sale.order.line']
32+
for line in self.filtered('is_kit'):
33+
sub_products_to_delete |= self.search([
34+
('order_id', '=', line.order_id.id),
35+
('parent_product_id', '=', line.product_id.id),
36+
('is_subproduct', '=', True)
37+
])
38+
39+
all_records_to_delete = self | sub_products_to_delete
40+
return super(SaleOrderLine, all_records_to_delete).unlink()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<template id="sale_order_report_inherit" inherit_id="sale.report_saleorder_document">
4+
<xpath expr="//tbody/t/tr" position="attributes">
5+
<attribute name="t-if">(sale_order.is_printable_kit and line.is_subproduct) or not line.is_subproduct</attribute>
6+
</xpath>
7+
</template>
8+
</odoo>
Lines changed: 3 additions & 0 deletions
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_subproduct,access_subproduct,model_product_kit_subproduct_wizard,base.group_user,1,1,1,1
3+
access_subproduct_line,access_subproduct_line,model_product_kit_subproduct_line,base.group_user,1,1,1,1

product_kit/views/product_views.xml

Lines changed: 14 additions & 0 deletions
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_form_view_kit" model="ir.ui.view">
4+
<field name="name">product.template.form.kit</field>
5+
<field name="model">product.template</field>
6+
<field name="inherit_id" ref="product.product_template_form_view"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//page[@name='general_information']//field[@name='company_id']" position="after">
9+
<field name="is_kit" />
10+
<field name="kit_product_ids" invisible="not is_kit" widget="many2many_tags" />
11+
</xpath>
12+
</field>
13+
</record>
14+
</odoo>

0 commit comments

Comments
 (0)