Skip to content
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 sales_products_kit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
21 changes: 21 additions & 0 deletions sales_products_kit/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "Sales - Product_Kit",
"version": "1.0",
"description": """
This custom module adds a function to Odoo to sell products as a Kit, but not using a BOM or the Manufacturing Module.
""",
"category": "Sales/Sales",
"depends": ["sale_management"],
"data": [
"security/ir.model.access.csv",
"wizard/sub_product_wizard.xml",
"views/product_view.xml",
"views/sale_order_view.xml",
"report/sale_order_report.xml",
"views/sale_portal_templates.xml",
"report/invoice_report.xml",
],
"installable": True,
"auto_install": False,
"license": "LGPL-3",
}
3 changes: 3 additions & 0 deletions sales_products_kit/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 sales_products_kit/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()
sub_product_ids = fields.Many2many("product.product", string="Sub Product")
7 changes: 7 additions & 0 deletions sales_products_kit/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from odoo import models, fields


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

print_in_report = fields.Boolean(string="Print in Report ?")
51 changes: 51 additions & 0 deletions sales_products_kit/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from odoo import models, fields, api
from odoo.exceptions import UserError


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

is_kit = fields.Boolean(related="product_template_id.is_kit")
parent_line_id = fields.Many2one("sale.order.line", ondelete="cascade")

@api.ondelete(at_uninstall=False)
def _ondelete_sale_order_line(self):
if not self.parent_line_id:
for line in self:
sub_product_lines = self.env["sale.order.line"].search(
[("parent_line_id", "=", line.id)]
)
sub_product_lines.with_context(allow_child_unlink=True).unlink()
else:
if not self.env.context.get("allow_child_unlink"):
raise UserError(
"You cannot delete a child line directly. Please delete the parent line instead."
)

def write(self, vals):
if "product_uom_qty" in vals and not self.parent_line_id:
for line in self:
sub_product_lines = self.env["sale.order.line"].search(
[("parent_line_id", "=", line.id)]
)
old_qty = line.product_uom_qty
for sub_line in sub_product_lines:
if sub_line:
if old_qty != 0:
qty = sub_line.product_uom_qty / old_qty
new_qty = vals["product_uom_qty"] * qty
sub_line.update({"product_uom_qty": new_qty})
else:
new_qty = vals["product_uom_qty"] * sub_line.product_uom_qty
sub_line.update({"product_uom_qty": new_qty})
return super().write(vals)

def action_subproduct(self):
return {
"type": "ir.actions.act_window",
"name": f"Product: {self.product_id.name}",
"res_model": "sub.product.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_sale_order_line_id": self.id},
}
10 changes: 10 additions & 0 deletions sales_products_kit/report/invoice_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="invoice_document_report_inherited" inherit_id="account.report_invoice_document">
<xpath expr="//tbody/t/tr" position="attributes">
<attribute name="t-if">line.sale_line_ids.order_id.print_in_report or (not line.sale_line_ids.parent_line_id)</attribute>
</xpath>
</template>

</odoo>
8 changes: 8 additions & 0 deletions sales_products_kit/report/sale_order_report.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_document_report_inherited" inherit_id="sale.report_saleorder_document">
<xpath expr="//tbody/t/tr" position="attributes">
<attribute name="t-if">doc.print_in_report or (not line.parent_line_id)</attribute>
</xpath>
</template>
</odoo>
3 changes: 3 additions & 0 deletions sales_products_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_sub_product_wizard,access_sub_product_wizard,model_sub_product_wizard,base.group_user,1,1,1,1
access_sub_product_line,access_sub_product_line,model_sub_product_line,base.group_user,1,1,1,1
1 change: 1 addition & 0 deletions sales_products_kit/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_subproduct
78 changes: 78 additions & 0 deletions sales_products_kit/tests/test_subproduct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from odoo.tests import TransactionCase


class TestSubProdcut(TransactionCase):
def setUp(self):
childproduct1 = self.env["product.template"].create({"name": "Child1"})
childproduct2 = self.env["product.template"].create({"name": "Child2"})
self.product1 = self.env["product.product"].search(
[("product_tmpl_id", "=", childproduct1.id)]
)
self.product2 = self.env["product.product"].search(
[("product_tmpl_id", "=", childproduct2.id)]
)

parentproduct = self.env["product.template"].create(
{
"name": "parent",
"is_kit": True,
"sub_product_ids": [self.product1.id, self.product2.id],
}
)
self.parent_product = self.env["product.product"].search(
[("product_tmpl_id", "=", parentproduct.id)]
)

sale_order = self.env["sale.order"].create(
{
"partner_id": self.env.ref("base.partner_demo").id,
}
)
self.parentline = self.env["sale.order.line"].create(
{
"product_id": self.parent_product.id,
"order_id": sale_order.id,
"product_uom_qty": 1,
"name": self.parent_product.name,
}
)
self.childline1 = self.env["sale.order.line"].create(
{
"product_template_id": self.product1.id,
"order_id": sale_order.id,
"product_uom_qty": 3,
"parent_line_id": self.parentline.id,
"name": self.product1.name,
}
)
self.childline2 = self.env["sale.order.line"].create(
{
"product_template_id": self.product2.id,
"order_id": sale_order.id,
"product_uom_qty": 2,
"parent_line_id": self.parentline.id,
"name": self.product2.name,
}
)

def test_parent_quantity_update_child(self):
old_parent_qty = self.parentline.product_uom_qty
old_child_qty1 = self.childline1.product_uom_qty
old_child_qty2 = self.childline2.product_uom_qty

new_parent_qty = 4
self.parentline.write({"product_uom_qty": new_parent_qty})

new_child_qty1 = (old_child_qty1 / old_parent_qty) * new_parent_qty
new_child_qty2 = (old_child_qty2 / old_parent_qty) * new_parent_qty

self.assertEqual(
self.childline1.product_uom_qty,
new_child_qty1,
f"expected {new_child_qty1}, but got {self.childline1.product_uom_qty}",
)
self.assertEqual(
self.childline2.product_uom_qty,
new_child_qty2,
f"expected {new_child_qty2}, but got {self.childline2.product_uom_qty}",
)
15 changes: 15 additions & 0 deletions sales_products_kit/views/product_view.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_inherit" model="ir.ui.view">
<field name="name">product.template.form.view.inherit.sales_products_kit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="sale.product_template_form_view" />
<field name="arch" type="xml">
<xpath expr="//group[@name='group_general']" position="inside">
<field name="is_kit"></field>
<field name="sub_product_ids" widget="many2many_tags" invisible="not is_kit"></field>
</xpath>
</field>
</record>
</odoo>
45 changes: 45 additions & 0 deletions sales_products_kit/views/sale_order_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>

<record id="view_order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</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='payment_term_id']" position="after">
<field name="print_in_report" />
</xpath>
<xpath expr="//field[@name='product_template_id']" position="after">
<button class="btn btn-primary" name="action_subproduct" type="object"
string="Add Subproduct" invisible="not is_kit or state == 'sale'" />
</xpath>


<xpath expr="//field[@name='order_line']/list/field[@name='product_template_id']"
position="attributes">
<attribute name="readonly">parent_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='product_id']"
position="attributes">
<attribute name="readonly">parent_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='product_uom_qty']"
position="attributes">
<attribute name="readonly">parent_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='customer_lead']"
position="attributes">
<attribute name="readonly">parent_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='price_unit']"
position="attributes">
<attribute name="readonly">parent_line_id</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/list/field[@name='tax_id']"
position="attributes">
<attribute name="readonly">parent_line_id</attribute>
</xpath>

</field>
</record>
</odoo>
8 changes: 8 additions & 0 deletions sales_products_kit/views/sale_portal_templates.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/t/tr" position="attributes">
<attribute name="t-if">sale_order.print_in_report or (not line.parent_line_id)</attribute>
</xpath>
</template>
</odoo>
2 changes: 2 additions & 0 deletions sales_products_kit/wizard/__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_line
15 changes: 15 additions & 0 deletions sales_products_kit/wizard/sub_product_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class SubProductLine(models.TransientModel):
_name = "sub.product.line"
_description = (
"A transient model representing a line item for sub-products in a wizard."
)

product_id = fields.Many2one("product.product", required=True, ondelete="cascade")
quantity = fields.Integer(string="Quantity", required=True, default=1)
price_unit = fields.Float(string="Unit Price", required=True)
sub_product_wizard_id = fields.Many2one(
"sub.product.wizard", required=True, ondelete="cascade"
)
96 changes: 96 additions & 0 deletions sales_products_kit/wizard/sub_product_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from odoo import fields, models, api, Command
from odoo.exceptions import UserError


class SubProductWizard(models.TransientModel):
_name = "sub.product.wizard"
_description = "A wizard the display sub-product included in the kit."

sale_order_line_id = fields.Many2one(
"sale.order.line", required=True, ondelete="cascade"
)
line_ids = fields.One2many(
comodel_name="sub.product.line",
inverse_name="sub_product_wizard_id",
string="Sub Products",
required=True,
)

@api.model
def default_get(self, fields):
defaults = super().default_get(fields)
sale_order_line_id = self.env.context.get("default_sale_order_line_id")
sale_order_line = self.env["sale.order.line"].browse(sale_order_line_id)
product_template = sale_order_line.product_template_id
existing_wizard = self.env["sub.product.wizard"].search(
[("sale_order_line_id", "=", sale_order_line_id)]
)

if existing_wizard:
defaults.update(
{
"sale_order_line_id": sale_order_line_id,
"line_ids": existing_wizard.line_ids,
}
)
else:
line_data = []
for sub_product in product_template.sub_product_ids:
line_data.append(
Command.create(
{
"product_id": sub_product.id,
"quantity": 1,
"price_unit": sub_product.lst_price,
}
)
)

defaults.update(
{
"sale_order_line_id": sale_order_line_id,
"line_ids": line_data,
}
)
return defaults

def action_confirm(self):
if sum(self.line_ids.mapped("quantity")) == 0:
raise UserError(
"You must select atleast 1 sub-product to purchase the kit product."
)

lines_to_create = []
total_price = 0

for line in self.line_ids:
if line.quantity > 0:
existing_line = self.env["sale.order.line"].search(
[
("order_id", "=", self.sale_order_line_id.order_id.id),
("product_id", "=", line.product_id.id),
("parent_line_id", "=", self.sale_order_line_id.id),
],
limit=1,
)

if existing_line:
existing_line.product_uom_qty = line.quantity
existing_line.price_unit = 0.0
else:
lines_to_create.append(
{
"order_id": self.sale_order_line_id.order_id.id,
"product_id": line.product_id.id,
"product_uom_qty": line.quantity,
"price_unit": 0.0,
"parent_line_id": self.sale_order_line_id.id,
}
)
total_price += line.quantity * line.price_unit

if lines_to_create:
self.env["sale.order.line"].create(lines_to_create)

self.sale_order_line_id.price_unit = total_price
return True
Loading