Skip to content

Commit c7a20f9

Browse files
[ADD] repair_kit_to_sale
1 parent 98cf1bb commit c7a20f9

File tree

10 files changed

+724
-0
lines changed

10 files changed

+724
-0
lines changed

repair_kit_to_sale/README.rst

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
==================
2+
Repair Kit to Sale
3+
==================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:6be052bcfbbc776b62718e72db60669a83dd23fdec5e9d88a20cdacf399d1213
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frepair-lightgray.png?logo=github
20+
:target: https://github.com/OCA/repair/tree/17.0/repair_kit_to_sale
21+
:alt: OCA/repair
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/repair-17-0/repair-17-0-repair_kit_to_sale
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/repair&target_branch=17.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module ensures that when a kit product (with a BoM) is added to a
32+
repair order, it is correctly reflected in the corresponding sale order
33+
as a kit.
34+
35+
Key Features:
36+
37+
- When a kit product is added to a repair order, its Bill of Materials
38+
(BoM) components are automatically exploded into separate stock
39+
moves.
40+
- Instead of creating individual sale order lines for each component,
41+
this module groups them into a single sale order line for the
42+
original kit product when the sale order is generated.
43+
- Ensures that delivered quantities in the sale order are correctly
44+
updated upon repair completion, counting only fully completed kits
45+
(where all required components are done in the repair).
46+
47+
**Table of contents**
48+
49+
.. contents::
50+
:local:
51+
52+
Bug Tracker
53+
===========
54+
55+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/repair/issues>`_.
56+
In case of trouble, please check there if your issue has already been reported.
57+
If you spotted it first, help us to smash it by providing a detailed and welcomed
58+
`feedback <https://github.com/OCA/repair/issues/new?body=module:%20repair_kit_to_sale%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
59+
60+
Do not contact contributors directly about support or help with technical issues.
61+
62+
Credits
63+
=======
64+
65+
Authors
66+
-------
67+
68+
* ForgeFlow
69+
70+
Contributors
71+
------------
72+
73+
- `ForgeFlow <https://forgeflow.com>`__:
74+
75+
- Andreu Orensanz <[email protected]>
76+
77+
Maintainers
78+
-----------
79+
80+
This module is maintained by the OCA.
81+
82+
.. image:: https://odoo-community.org/logo.png
83+
:alt: Odoo Community Association
84+
:target: https://odoo-community.org
85+
86+
OCA, or the Odoo Community Association, is a nonprofit organization whose
87+
mission is to support the collaborative development of Odoo features and
88+
promote its widespread use.
89+
90+
This module is part of the `OCA/repair <https://github.com/OCA/repair/tree/17.0/repair_kit_to_sale>`_ project on GitHub.
91+
92+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

repair_kit_to_sale/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
2+
3+
from . import models

repair_kit_to_sale/__manifest__.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
3+
4+
{
5+
"name": "Repair Kit to Sale",
6+
"summary": """
7+
Handles the creation of a kit sale line with the moves added to a repair order
8+
""",
9+
"version": "17.0.1.0.0",
10+
"category": "Repair",
11+
"website": "https://github.com/OCA/repair",
12+
"author": "ForgeFlow, Odoo Community Association (OCA)",
13+
"license": "AGPL-3",
14+
"depends": ["mrp_repair"],
15+
}

repair_kit_to_sale/models/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
2+
3+
from . import stock_move
4+
from . import sale_order_line
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from odoo import api, models
2+
3+
4+
class SaleOrderLine(models.Model):
5+
_inherit = "sale.order.line"
6+
7+
@api.depends("move_ids", "move_ids.move_line_ids", "move_ids.move_line_ids.lot_id")
8+
def _compute_lot_ids(self):
9+
for line in self:
10+
if any(m.bom_line_id for m in line.move_ids):
11+
line.lot_ids = self.env["stock.lot"]
12+
else:
13+
related_move_lines = line.mapped("move_ids.move_line_ids").filtered(
14+
lambda ml: ml.product_id == line.product_id # noqa: B023
15+
)
16+
line.lot_ids = related_move_lines.mapped("lot_id")
17+
18+
def _compute_qty_delivered(self):
19+
remaining_so_lines = self
20+
for so_line in self:
21+
moves = so_line.move_ids.sudo().filtered(
22+
lambda m: m.repair_id and m.state == "done"
23+
)
24+
if not moves:
25+
continue
26+
product = so_line.product_id
27+
bom = (
28+
self.env["mrp.bom"]._bom_find(product, bom_type="phantom").get(product)
29+
if product
30+
else None
31+
)
32+
33+
if moves[0].kit_original_qty and bom:
34+
bom_requirements = {
35+
line.product_id: line.product_qty for line in bom.bom_line_ids
36+
}
37+
min_possible_kits = None
38+
for move in moves:
39+
required_qty = bom_requirements.get(move.product_id, 0)
40+
if required_qty > 0:
41+
possible_kits = move.quantity // required_qty
42+
min_possible_kits = (
43+
possible_kits
44+
if min_possible_kits is None
45+
else min(min_possible_kits, possible_kits)
46+
)
47+
qty_delivered = (
48+
int(min_possible_kits) if min_possible_kits is not None else 0
49+
)
50+
else:
51+
if len(moves) == 1:
52+
qty_delivered = moves.quantity
53+
else:
54+
continue
55+
remaining_so_lines -= so_line
56+
so_line.qty_delivered = qty_delivered
57+
return super(SaleOrderLine, remaining_so_lines)._compute_qty_delivered()
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from odoo import Command, fields, models
2+
3+
4+
class StockMove(models.Model):
5+
_inherit = "stock.move"
6+
7+
kit_original_qty = fields.Float(string="Original Kit Quantity", required=False)
8+
9+
def write(self, vals):
10+
"""
11+
When increasing `product_uom_qty` beyond the BoM limit, create a new move
12+
manually instead of using `_split`, since core Odoo restricts splitting
13+
for repair moves.
14+
"""
15+
for move in self:
16+
if "product_uom_qty" in vals and move.bom_line_id:
17+
new_qty = vals["product_uom_qty"]
18+
max_qty = move.bom_line_id.product_qty * move.kit_original_qty
19+
if new_qty > max_qty:
20+
surplus_qty = new_qty - max_qty
21+
new_move_vals = move.copy_data(
22+
{
23+
"product_uom_qty": surplus_qty,
24+
"origin": move.origin,
25+
"repair_id": move.repair_id.id,
26+
"sale_line_id": False,
27+
"bom_line_id": False,
28+
"kit_original_qty": False,
29+
}
30+
)[0]
31+
self.env["stock.move"].create(new_move_vals)
32+
vals["product_uom_qty"] = max_qty
33+
return super(
34+
StockMove, self.with_context(not_update_or_create_repair_so_line=True)
35+
).write(vals)
36+
37+
def _can_form_full_kit(self, moves, bom):
38+
"""
39+
Determines whether we can form a full kit for the given moves and BoM.
40+
"""
41+
kit_qty = moves[0].kit_original_qty or 0.0
42+
relevant_moves = [m for m in moves if m.bom_line_id.bom_id == bom]
43+
return all(
44+
sum(
45+
m.product_uom_qty
46+
for m in relevant_moves
47+
if m.product_id == line.product_id
48+
)
49+
>= line.product_qty * kit_qty
50+
for line in bom.bom_line_ids
51+
)
52+
53+
def _create_repair_sale_order_line(self):
54+
if self.env.context.get("not_update_or_create_repair_so_line"):
55+
return False
56+
57+
so_line_vals = []
58+
grouped_boms = {}
59+
standard_moves = self.filtered(lambda m: not m.bom_line_id)
60+
61+
# 1) Group BoM kit-related moves
62+
for move in self - standard_moves:
63+
bom_id = move.bom_line_id.bom_id
64+
grouped_boms.setdefault(bom_id, []).append(move)
65+
66+
# 2) Process kit-based moves
67+
for bom, moves in grouped_boms.items():
68+
is_full = self._can_form_full_kit(moves, bom)
69+
70+
if not is_full:
71+
# Partial kit => treat as standard moves
72+
standard_moves |= self.env["stock.move"].browse(
73+
[m.id for m in moves]
74+
) # Add these moves to the standard logic
75+
else:
76+
# Full kit => create a single sale order line
77+
first_move = moves[0]
78+
kit_qty = first_move.kit_original_qty
79+
so_line_vals.append(
80+
{
81+
"order_id": first_move.repair_id.sale_order_id.id,
82+
"name": bom.product_tmpl_id.display_name or "Kit",
83+
"product_id": bom.product_tmpl_id.product_variant_id.id,
84+
"product_uom_qty": kit_qty,
85+
"move_ids": [Command.link(m.id) for m in moves],
86+
"price_unit": 0.0
87+
if first_move.repair_id.under_warranty
88+
else first_move.price_unit,
89+
}
90+
)
91+
92+
# 3) Process standard (non-kit and partial kit) moves using Odoo's logic
93+
if standard_moves:
94+
super(StockMove, standard_moves)._create_repair_sale_order_line()
95+
96+
# 4) Create all sale order lines at once
97+
if so_line_vals:
98+
self.env["sale.order.line"].create(so_line_vals)
99+
100+
return True

repair_kit_to_sale/pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- [ForgeFlow](https://forgeflow.com):
2+
3+
> - Andreu Orensanz \<<[email protected]>\>
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
This module ensures that when a kit product (with a BoM) is added to a repair order, it is correctly reflected in the corresponding sale order as a kit.
2+
3+
Key Features:
4+
5+
- When a kit product is added to a repair order, its Bill of Materials (BoM) components are automatically exploded into separate stock moves.
6+
- Instead of creating individual sale order lines for each component, this module groups them into a single sale order line for the original kit product when the sale order is generated.
7+
- Ensures that delivered quantities in the sale order are correctly updated upon repair completion, counting only fully completed kits (where all required components are done in the repair).

0 commit comments

Comments
 (0)