Skip to content

[ADD] sale: added product as a kit feature #896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions product_kit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizards
20 changes: 20 additions & 0 deletions product_kit/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': "product_kit",
'version': '1.0',
'license': 'LGPL-3',
'depends': ['sale_management'],
'author': "Kalpan Desai",
'category': 'Sales/Sales',
'description': """
Module specifically designed to manage product as kits in Odoo.
""",
'installable': True,
'application': True,
'data': [
'views/product_views.xml',
'wizards/sub_product_wizard_view.xml',
'views/sale_order_view.xml',
'views/sale_portal_view.xml',
'security/ir.model.access.csv',
],
}
4 changes: 4 additions & 0 deletions product_kit/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import sale_order_line
from . import product_template
from . import sale_order
from . import sale_invoice
9 changes: 9 additions & 0 deletions product_kit/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from odoo import fields, models


class ProductTemplate(models.Model):
_inherit = 'product.template'

is_kit = fields.Boolean(string='Is Kit', help='Check this box if the product is a kit that contains other products.', default=False)

kit_product_ids = fields.Many2many('product.product', string='Sub Products', help='Products included in this kit.')
7 changes: 7 additions & 0 deletions product_kit/models/sale_invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from odoo import fields, models


class AccountMove(models.Model):
_inherit = 'account.move'

is_printable_kit = fields.Boolean(string="Show Sub-Products", default=True)
54 changes: 54 additions & 0 deletions product_kit/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from odoo import api, fields, models


class SaleOrder(models.Model):
_inherit = "sale.order"

is_printable_kit = fields.Boolean(
string="Print Kit in Report",
default=False,
help="If checked, kit products will be printed in the report."
)

has_kit = fields.Boolean(
string="Has Kit Product",
compute="_compute_has_kit",
store=True,
default=False,
)

@api.depends('order_line', 'order_line.is_kit')
def _compute_has_kit(self):
for order in self:
order.has_kit = any(line.is_kit for line in order.order_line)

def _prepare_invoice(self):
"""
Prepare the dict of values to create the new invoice for a sales order.
"""
invoice_vals = super()._prepare_invoice()
# Copy the value of your toggle to the invoice
invoice_vals['is_printable_kit'] = self.is_printable_kit
return invoice_vals

def _create_invoices(self, grouped=False, final=False, date=None):
# First, create the invoice(s) using the standard Odoo method.
# This will include lines for the main product and all sub-products.
invoices = super()._create_invoices(grouped, final, date)

# Now, loop through the newly created invoices to apply your logic
for invoice in invoices:
# Check the toggle copied from the Sale Order
if not invoice.is_printable_kit:

# Find all invoice lines that came from a sale order line
# marked as a sub-product.
sub_product_invoice_lines = invoice.invoice_line_ids.filtered(
lambda line: line.sale_line_ids and line.sale_line_ids[0].is_subproduct
)

# If any sub-product lines were found, delete them
if sub_product_invoice_lines:
sub_product_invoice_lines.unlink()

return invoices
40 changes: 40 additions & 0 deletions product_kit/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from odoo import fields, models
from odoo.exceptions import UserError


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

is_kit = fields.Boolean(related="product_template_id.is_kit")
parent_product_id = fields.Many2one('product.product')
is_subproduct = fields.Boolean(default=False)

def unlink(self):
"""
Override unlink to also delete sub-product lines when a kit line is deleted.
It also prevents the direct deletion of a sub-product line.
"""
# Prevent direct deletion of sub-product lines.
sub_product_lines = self.filtered('is_subproduct')
if sub_product_lines:
# Find the parent kit lines for these sub-products
parent_lines = self.env['sale.order.line'].search([
('order_id', 'in', sub_product_lines.mapped('order_id').ids),
('product_id', 'in', sub_product_lines.mapped('parent_product_id').ids),
('is_kit', '=', True)
])
# If any of the required parent lines are not in the current deletion set, raise an error.
if parent_lines - self:
raise UserError("You cannot delete a component of a kit directly. Please remove the main kit product instead.")

# Cascade delete: find all sub-products of kits being deleted.
sub_products_to_delete = self.env['sale.order.line']
for line in self.filtered('is_kit'):
sub_products_to_delete |= self.search([
('order_id', '=', line.order_id.id),
('parent_product_id', '=', line.product_id.id),
('is_subproduct', '=', True)
])

all_records_to_delete = self | sub_products_to_delete
return super(SaleOrderLine, all_records_to_delete).unlink()
8 changes: 8 additions & 0 deletions product_kit/report/sale_order_report_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_report_inherit" inherit_id="sale.report_saleorder_document">
<xpath expr="//tbody/t/tr" position="attributes">
<attribute name="t-if">(sale_order.is_printable_kit and line.is_subproduct) or not line.is_subproduct</attribute>
</xpath>
</template>
</odoo>
3 changes: 3 additions & 0 deletions product_kit/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_subproduct,access_subproduct,model_product_kit_subproduct_wizard,base.group_user,1,1,1,1
access_subproduct_line,access_subproduct_line,model_product_kit_subproduct_line,base.group_user,1,1,1,1
14 changes: 14 additions & 0 deletions product_kit/views/product_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_view_kit" model="ir.ui.view">
<field name="name">product.template.form.kit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='general_information']//field[@name='company_id']" position="after">
<field name="is_kit" />
<field name="kit_product_ids" invisible="not is_kit" widget="many2many_tags" />
</xpath>
</field>
</record>
</odoo>
33 changes: 33 additions & 0 deletions product_kit/views/sale_order_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_view" model="ir.ui.view">
<field name="name">sale.order.form.view.kit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="product_template_id"]' position="after">
<button type="action" name="product_kit.action_subproduct_wizard" string="Products" class="btn btn-primary" invisible="not is_kit or state == 'sale'"
context="{'default_product_id': product_id, 'default_order_line_id': id}" save="false"/>
</xpath>

<xpath expr="//field[@name='order_line']/list" position="attributes">
<attribute name="decoration-warning">is_subproduct</attribute>
</xpath>

<xpath expr="//field[@name='order_line']/list/field[@name='product_uom_qty']" position="attributes">
<attribute name="readonly">is_subproduct</attribute>
</xpath>

<xpath expr="//field[@name='order_line']/list/field[@name='price_unit']" position="attributes">
<attribute name="readonly">is_subproduct</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='tax_id']" position="attributes">
<attribute name="readonly">is_subproduct</attribute>
</xpath>
<xpath expr="//field[@name='payment_term_id']" position="after">
<field name="has_kit" invisible="1"/>
<field name="is_printable_kit" readonly="has_kit == False"/>
</xpath>
</field>
</record>
</odoo>
8 changes: 8 additions & 0 deletions product_kit/views/sale_portal_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_portal_content_inherited" inherit_id="sale.sale_order_portal_content">
<xpath expr="//tbody[hasclass('sale_tbody')]/t[@t-as='line']" position="attributes">
<attribute name="t-if">(sale_order.is_printable_kit and line.is_subproduct) or not line.is_subproduct</attribute>
</xpath>
</template>
</odoo>
2 changes: 2 additions & 0 deletions product_kit/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import sub_product_wizard
from . import sub_product_wizard_line
89 changes: 89 additions & 0 deletions product_kit/wizards/sub_product_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from odoo import api, fields, models


class SubproductWizard(models.TransientModel):
_name = 'product_kit.subproduct.wizard'
_description = 'Sub-product Wizard'

product_id = fields.Many2one('product.product', string="Product", required=True)
sub_product_ids = fields.One2many('product_kit.subproduct.line', 'subproduct_line_id')
order_line_id = fields.Many2one('sale.order.line')

@api.model
def default_get(self, fields):
res = super().default_get(fields)

product_id = self.env.context.get('default_product_id')
order_line_id = self.env.context.get('default_order_line_id')
product = self.env['product.product'].browse(product_id)
sub_products = self.env['product_kit.subproduct.wizard'].search(
[('order_line_id', '=', order_line_id),
('product_id', '=', product_id)],
order='id desc', limit=1)

sub_products_list = []

if sub_products.exists():
# Update wizard data
for subproduct, selected_product in zip(sub_products.sub_product_ids, product.kit_product_ids):
sub_products_list.append((0, 0, {
'product_id': selected_product.id,
'sub_product_name': selected_product.product_tmpl_id.name,
'quantity': subproduct.quantity,
'unit_price': subproduct.unit_price
}))
else:
# New Data
for selected_product in product.kit_product_ids:
sub_products_list.append((0, 0, {
'product_id': selected_product.id,
'sub_product_name': selected_product.product_tmpl_id.name,
'quantity': 1.0,
'unit_price': selected_product.list_price,
}))

res['sub_product_ids'] = sub_products_list
return res

def action_confirm_subproduct(self):
order_line = self.env['sale.order.line'].search([('id', '=', self.order_line_id.id)])

# Explicitly ensure the main parent product line is NOT a sub-product.
# This is the key to fixing your invoice issue.
order_line.write({
'is_subproduct': False,
'is_kit': True, # Recommended: also set a flag that it IS a kit
})

product = self.env['product.product'].browse(self.product_id.id)
order_line_subproduct = self.env['sale.order.line'].search(
[('order_id', '=', order_line.order_id.id),
('parent_product_id', '=', self.product_id.id)])

total = 0
if order_line_subproduct.exists():
# Update Order-line
for sub_product_data, order_line_rec in zip(self.sub_product_ids, order_line_subproduct):
total += (sub_product_data.unit_price * sub_product_data.quantity)

order_line_rec.product_uom_qty = sub_product_data.quantity
order_line_rec.price_unit = 0
else:
# Create Order-line
for sub_product_data, selected_products in zip(self.sub_product_ids, product.kit_product_ids):
total += (sub_product_data.unit_price * sub_product_data.quantity)

self.env['sale.order.line'].create({
'order_id': order_line.order_id.id,
'product_id': selected_products.id,
'name': selected_products.product_tmpl_id.name,
'price_unit': 0,
'product_uom_qty': sub_product_data.quantity,
'parent_product_id': self.product_id.id,
'is_subproduct': True, # This part is correct
'is_kit': False,
'sequence': order_line.sequence
})

# Update the total price on the main line
order_line.price_unit = total + product.product_tmpl_id.list_price
12 changes: 12 additions & 0 deletions product_kit/wizards/sub_product_wizard_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo import fields, models


class SubproductWizardLine(models.TransientModel):
_name = 'product_kit.subproduct.line'
_description = 'Sub-product Wizard Line'

subproduct_line_id = fields.Many2one('product_kit.subproduct.wizard')
product_id = fields.Many2one('product.product')
sub_product_name = fields.Char(string="Product Name")
quantity = fields.Integer(string="Quantity")
unit_price = fields.Float(string="Unit Price")
33 changes: 33 additions & 0 deletions product_kit/wizards/sub_product_wizard_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="action_subproduct_wizard" model="ir.actions.act_window">
<field name="name">Sub-Products-Wizard</field>
<field name="res_model">product_kit.subproduct.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>

<record id="action_subproduct_wizard_view" model="ir.ui.view">
<field name="name">Sub-Products-Wizard</field>
<field name="model">product_kit.subproduct.wizard</field>
<field name="arch" type="xml">
<form string="Subproduct">
<group>
<field name="product_id" readonly="1"/>
<field name="sub_product_ids">
<list editable="bottom" create="false" delete="false">
<field name="sub_product_name" readonly="1"/>
<field name="quantity"/>
<field name="unit_price"/>
</list>
</field>
</group>
<footer>
<button type="object" name="action_confirm_subproduct" string="Confirm" class="btn-primary" />
<button string="Close" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>