Skip to content

[ADD] product_kits: new module for creating and selling product kits #746

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_kits/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
19 changes: 19 additions & 0 deletions product_kits/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
'name': 'Product Kits',
'version': '1.0',
'depends': ['base', 'sale_management'],
'author': 'Aryan Donga (ardo)',
'description': 'Simple module to create and sell product kits',
'application': False,
'installable': True,
'license': 'LGPL-3',
'data': [
'security/ir.model.access.csv',
'report/sale_order_report.xml',
'report/report_invoice.xml',
'report/sale_order_portal_view.xml',
'views/product_template_views.xml',
'wizard/product_kit_wizard_views.xml',
'views/sale_order_views.xml'
],
}
3 changes: 3 additions & 0 deletions product_kits/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import product_template
from . import sale_order_line
from . import sale_order
8 changes: 8 additions & 0 deletions product_kits/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import fields, models


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

is_kit = fields.Boolean(string='Is Kit', default=False)
sub_product_ids = fields.Many2many('product.product', string='Sub Products')
7 changes: 7 additions & 0 deletions product_kits/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from odoo import fields, models


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

print_in_report = fields.Boolean(string='Print in report')
30 changes: 30 additions & 0 deletions product_kits/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from odoo import api, fields, models


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

is_kit = fields.Boolean(compute='_compute_is_kit', store=True)
parent_kit_line_id = fields.Many2one(
comodel_name='sale.order.line',
string='Parent Kit Line',
ondelete='cascade',
index=True,
)

# Optional fields for sub-products
sub_product_line_ids = fields.One2many(
comodel_name='sale.order.line',
inverse_name='parent_kit_line_id',
string='Sub Product Lines',
copy=True,
)
custom_sub_product_price = fields.Float(
string='Custom Sub Product Price',
help='Overridden price of the sub product line.',
)

@api.depends('product_id')
def _compute_is_kit(self):
for line in self:
line.is_kit = line.product_id.is_kit if line.product_id else False
11 changes: 11 additions & 0 deletions product_kits/report/report_invoice.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_invoice_document_view_kits" inherit_id="account.report_invoice_document">
<xpath expr="//tbody[@class='invoice_tbody']/t/tr" position="attributes">
<attribute name="t-if">
(not any(line.sale_line_ids.mapped('parent_kit_line_id'))) or any(
line.sale_line_ids.mapped('order_id.print_in_report'))
</attribute>
</xpath>
</template>
</odoo>
10 changes: 10 additions & 0 deletions product_kits/report/sale_order_portal_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="sale_order_portal_content_view_kits" inherit_id="sale.sale_order_portal_content">
<xpath expr="//tbody[@class='sale_tbody']/t/tr" position="attributes">
<attribute name="t-if">
(not line.parent_kit_line_id) or sale_order.print_in_report
</attribute>
</xpath>
</template>
</odoo>
10 changes: 10 additions & 0 deletions product_kits/report/sale_order_report.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="report_saleorder_document_view_kits" inherit_id="sale.report_saleorder_document">
<xpath expr="//tbody/t/tr" position="attributes">
<attribute name="t-if">
(not line.parent_kit_line_id) or doc.print_in_report
</attribute>
</xpath>
</template>
</odoo>
3 changes: 3 additions & 0 deletions product_kits/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
product_kits.access_product_kit_wizard,access_product_kit_wizard,product_kits.model_product_kit_wizard,base.group_user,1,1,1,0
product_kits.access_product_kit_wizard_line,access_product_kit_wizard_line,product_kits.model_product_kit_wizard_line,base.group_user,1,1,1,0
15 changes: 15 additions & 0 deletions product_kits/views/product_template_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_view_kits" model="ir.ui.view">
<field name="name">product.template.form.view.kits</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<group name="group_general" position="inside">
<field name="is_kit"/>
<field name="sub_product_ids" invisible="not is_kit" widget="many2many_tags"
options="{'no_open':True,'no_create':True}"/>
</group>
</field>
</record>
</odoo>
33 changes: 33 additions & 0 deletions product_kits/views/sale_order_views.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_kits" model="ir.ui.view">
<field name="name">sale.order.form.view.kits</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="product_template_id" position="after">
<button icon="fa-gears" type="action" name="%(product_kits.action_product_kit_wizard)d"
title="Manage Kit Sub-products"
invisible="(not is_kit) or (state in ['sale', 'cancel'])"/>
</field>
<xpath expr="//field[@name='order_line']/list/field[@name='product_template_id']" position="attributes">
<attribute name="readonly">parent_kit_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='product_uom_qty']" position="attributes">
<attribute name="readonly">parent_kit_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='price_unit']" position="attributes">
<attribute name="readonly">parent_kit_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='tax_id']" position="attributes">
<attribute name="readonly">parent_kit_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list" position="attributes">
<attribute name="decoration-info">parent_kit_line_id</attribute>
</xpath>
<group name="order_details" position="inside">
<field name="print_in_report"/>
</group>
</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions product_kits/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import product_kit_wizard
from . import product_kit_wizard_line
115 changes: 115 additions & 0 deletions product_kits/wizard/product_kit_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from odoo import Command, api, fields, models


class ProductKitWizard(models.TransientModel):
_name = 'product.kit.wizard'
_description = 'Product Kit Wizard to configure sub-products'

product_name = fields.Char(readonly=True)
sale_order_line_id = fields.Many2one(
comodel_name='sale.order.line', string='Sale Order Line', required=True
)
wizard_line_ids = fields.One2many(
comodel_name='product.kit.wizard.line',
inverse_name='wizard_id',
string='Product Kit Wizard Lines',
required=True,
)

@api.depends('sale_order_line_id')
def _compute_product_name(self):
"""
Compute the product name based on the sale order line.
"""
for wizard in self:
if (
wizard.sale_order_line_id
and wizard.sale_order_line_id.product_template_id
):
wizard.product_name = wizard.sale_order_line_id.product_template_id.name
else:
wizard.product_name = 'UNKNOWN PRODUCT'

@api.model
def default_get(self, field_ids):
"""
Override default_get to set default values for the wizard.
Populates the wizard with existing values if available.
If no existing values are found, it initializes the wizard with 0 qty
"""
result = super().default_get(field_ids)
if self.env.context.get(
'active_model'
) == 'sale.order.line' and self.env.context.get('active_id'):
so_line = self.env['sale.order.line'].browse(
self.env.context.get('active_id')
)
existing_sub_lines = self.env['sale.order.line'].search([
('parent_kit_line_id', '=', so_line.id)
])
sub_products = so_line.product_id.sub_product_ids

# Use the existing sub-product lines if available
# Otherwise, create new lines with 0 qty and default price
wizard_lines = []
for component in sub_products:
component_line = existing_sub_lines.filtered(
lambda x: x.product_id.id == component.id
)
if component_line:
wizard_lines.append({
'product_id': component_line.product_id.id,
'quantity': component_line.product_uom_qty,
'price': component_line.custom_sub_product_price
or component.lst_price,
})
else:
wizard_lines.append({
'product_id': component.id,
'quantity': 0.0,
'price': component.lst_price,
})

wiz_lines = self.env['product.kit.wizard.line'].create(wizard_lines)

result.update({
'product_name': so_line.product_template_id.name,
'sale_order_line_id': so_line.id,
'wizard_line_ids': [Command.set(wiz_lines.ids)],
})
return result

def save_configuration(self):
self.ensure_one()
kit_so_line = self.sale_order_line_id
kit_price = 0
sub_so_lines = kit_so_line.sub_product_line_ids
current_sequence = kit_so_line.sequence

for line in self.wizard_line_ids:
sub_product_line = sub_so_lines.filtered(
lambda x: x.product_id.id == line.product_id.id
)
kit_price += line.price * line.quantity

# If the sub-product line already exists, update it
# Otherwise, create a new one
if not sub_product_line:
self.env['sale.order.line'].create({
'order_id': kit_so_line.order_id.id,
'parent_kit_line_id': kit_so_line.id,
'product_id': line.product_id.id,
'product_uom_qty': line.quantity,
'price_unit': 0,
'custom_sub_product_price': line.price,
'sequence': current_sequence,
})
else:
sub_product_line.write({
'product_uom_qty': line.quantity,
'custom_sub_product_price': line.price,
'price_unit': 0,
})

kit_so_line.write({'price_unit': kit_price})
return {'type': 'ir.actions.act_window_close'}
15 changes: 15 additions & 0 deletions product_kits/wizard/product_kit_wizard_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class ProductKitWizardLine(models.TransientModel):
_name = 'product.kit.wizard.line'
_description = 'Key-Value Line for Product Kit Wizard'

wizard_id = fields.Many2one(
'product.kit.wizard', string='Wizard', ondelete='cascade'
)
product_id = fields.Many2one(
'product.product', string='Product', required=True, ondelete='cascade'
)
quantity = fields.Float(string='Quantity', required=True)
price = fields.Float(string='Price', required=True)
36 changes: 36 additions & 0 deletions product_kits/wizard/product_kit_wizard_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="action_product_kit_wizard" model="ir.actions.act_window">
<field name="name">Configure Sub Products</field>
<field name="res_model">product.kit.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>

<record id="view_product_kit_wizard_form" model="ir.ui.view">
<field name="name">product.kit.wizard.form</field>
<field name="model">product.kit.wizard</field>
<field name="arch" type="xml">
<form string="Configure Sub Products">
<div>
<h2>
Product Kit: <field decoration-info="1" name="product_name"/>
</h2>
</div>
<sheet>
<field name="wizard_line_ids" force_save="1">
<list editable="bottom" create="False" delete="False" open_form_view="False">
<field name="product_id" readonly="True"/>
<field name="quantity"/>
<field name="price"/>
</list>
</field>
</sheet>
<footer>
<button name="save_configuration" string="Confirm" type="object" class="btn btn-primary"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>