Skip to content

Commit 1659123

Browse files
[IMP] repair_stock_consumption_step: support partial consumption
1 parent f43d026 commit 1659123

14 files changed

Lines changed: 581 additions & 80 deletions

repair_stock_consumption_step/README.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
.. image:: https://odoo-community.org/readme-banner-image
2+
:target: https://odoo-community.org/get-involved?utm_source=readme
3+
:alt: Odoo Community Association
4+
15
=============================
26
Repair Stock Consumption Step
37
=============================
@@ -13,7 +17,7 @@ Repair Stock Consumption Step
1317
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
1418
:target: https://odoo-community.org/page/development-status
1519
:alt: Beta
16-
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
20+
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
1721
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
1822
:alt: License: AGPL-3
1923
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frepair-lightgray.png?logo=github
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import models
2+
from . import wizards

repair_stock_consumption_step/__manifest__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"maintainers": ["sbejaoui"],
1313
"depends": ["repair", "repair_warehouse"],
1414
"excludes": ["repair_stock_move"],
15-
"data": ["views/repair_order.xml", "views/stock_warehouse.xml"],
15+
"data": [
16+
"security/ir.model.access.csv",
17+
"wizards/repair_consumption_partial_wizard.xml",
18+
"views/repair_order.xml",
19+
"views/stock_warehouse.xml",
20+
],
1621
"demo": [],
1722
}

repair_stock_consumption_step/models/repair_order.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
33

44
from odoo import Command, fields, models
5+
from odoo.tools import float_is_zero
56

67

78
class RepairOrder(models.Model):
8-
99
_inherit = "repair.order"
1010

1111
consumption_picking_id = fields.Many2one(
@@ -84,3 +84,48 @@ def _action_consumption_done(self):
8484
if not rec.invoice_id and rec.invoice_method == "after_repair":
8585
state = "2binvoiced"
8686
rec.state = state
87+
88+
def _update_parts(self, return_consumption_moves):
89+
"""Update Repair Order operations based on unconsumed quantities.
90+
91+
This method reduces the demand (product_uom_qty) on the repair order
92+
lines by matching them against moves that were cancelled/returned
93+
during the partial consumption process. It uses a 'pop' logic across
94+
multiple lines of the same product until the returned quantity
95+
is fully accounted for.
96+
97+
Complexity Note:
98+
The pop logic here is needed because there might be more than one repair.line
99+
per product.
100+
"""
101+
self.ensure_one()
102+
operations_to_delete = self.env["repair.line"]
103+
for return_move in return_consumption_moves:
104+
remaining_qty = return_move.product_uom_qty
105+
operations = self.operations.filtered(
106+
lambda o: o.product_id == return_move.product_id
107+
)
108+
while (
109+
not float_is_zero(
110+
remaining_qty, precision_rounding=return_move.product_uom.rounding
111+
)
112+
and operations
113+
):
114+
# TODO: find a better solution to extract the "best matching" operation
115+
# instead of simply taking the first
116+
operation = operations[0]
117+
qty_before_update = operation.product_uom_qty
118+
operation.product_uom_qty -= min(
119+
operation.product_uom_qty, remaining_qty
120+
)
121+
if float_is_zero(
122+
operation.product_uom_qty,
123+
precision_rounding=operation.product_uom.rounding,
124+
):
125+
operations_to_delete |= operation
126+
127+
remaining_qty -= qty_before_update - operation.product_uom_qty
128+
operations -= operation
129+
130+
if operations_to_delete:
131+
operations_to_delete.unlink()

repair_stock_consumption_step/models/stock_picking.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
# Copyright 2025 ACSONE SA/NV
22
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
33

4-
from odoo import models
4+
from odoo import _, fields, models
5+
from odoo.exceptions import ValidationError
56

67

78
class StockPicking(models.Model):
8-
99
_inherit = "stock.picking"
1010

11+
consumption_repair_order_id = fields.One2many(
12+
"repair.order", "consumption_picking_id"
13+
)
14+
1115
def _action_done(self):
1216
res = super()._action_done()
1317
done_pickings = self.filtered(lambda p: p.state == "done")
@@ -19,3 +23,62 @@ def _action_done(self):
1923
)
2024
repair_orders._action_consumption_done()
2125
return res
26+
27+
def _check_no_partial_qties_on_repaired_product(self):
28+
"""
29+
Ensure the consumption pickings can not partially process the qties
30+
for the repaired product. (Partial quantities are only allowed on
31+
the spare parts).
32+
"""
33+
invalid_picks = self.env["stock.picking"]
34+
for pick in self.filtered("consumption_repair_order_id"):
35+
repair = pick.consumption_repair_order_id
36+
repaired_product_move = pick.move_ids.filtered(
37+
lambda m: m.product_id == repair.product_id
38+
)
39+
if (
40+
repaired_product_move
41+
and repaired_product_move.product_uom_qty
42+
!= repaired_product_move.quantity_done
43+
):
44+
invalid_picks |= pick
45+
46+
if invalid_picks:
47+
raise ValidationError(
48+
_(
49+
"Invalid partial quantities on repaired product."
50+
"You can only partially process spare parts.\n"
51+
"\n"
52+
"Picking(s):\n- %s",
53+
"\n- ".join(invalid_picks.mapped("name")),
54+
)
55+
)
56+
57+
def _pre_action_done_hook(self):
58+
partially_processed_pickings = self._check_backorder()
59+
if partial_consumption_picks := partially_processed_pickings.filtered(
60+
"consumption_repair_order_id"
61+
):
62+
partial_consumption_picks._check_no_partial_qties_on_repaired_product()
63+
return partial_consumption_picks._action_repair_consumption_partial_wizard(
64+
default_pick_ids=partial_consumption_picks.ids
65+
)
66+
return super()._pre_action_done_hook()
67+
68+
def _action_repair_consumption_partial_wizard(self, default_pick_ids):
69+
view = self.env.ref(
70+
"repair_stock_consumption_step.repair_consumption_backorder_wizard_form_view"
71+
)
72+
return {
73+
"name": _("Process Unused Spare Parts?"),
74+
"type": "ir.actions.act_window",
75+
"view_mode": "form",
76+
"res_model": "repair.consumption.partial.wizard",
77+
"views": [(view.id, "form")],
78+
"view_id": view.id,
79+
"target": "new",
80+
"context": dict(
81+
self.env.context,
82+
default_pick_ids=[(4, _id) for _id in default_pick_ids],
83+
),
84+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
access_repair_consumption_partial_wizard_user,repair.consumption.partial.wizard,model_repair_consumption_partial_wizard,stock.group_stock_user,1,1,1,1

repair_stock_consumption_step/static/description/index.html

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
55
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
6-
<title>Repair Stock Consumption Step</title>
6+
<title>README.rst</title>
77
<style type="text/css">
88

99
/*
@@ -360,16 +360,21 @@
360360
</style>
361361
</head>
362362
<body>
363-
<div class="document" id="repair-stock-consumption-step">
364-
<h1 class="title">Repair Stock Consumption Step</h1>
363+
<div class="document">
365364

365+
366+
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
367+
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
368+
</a>
369+
<div class="section" id="repair-stock-consumption-step">
370+
<h1>Repair Stock Consumption Step</h1>
366371
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
367372
!! This file is generated by oca-gen-addon-readme !!
368373
!! changes will be overwritten. !!
369374
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
370375
!! source digest: sha256:93fc83002290646de5793f5425fca96699172ab4f9487b4196a3e8481bbaead6
371376
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
372-
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/repair/tree/16.0/repair_stock_consumption_step"><img alt="OCA/repair" src="https://img.shields.io/badge/github-OCA%2Frepair-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/repair-16-0/repair-16-0-repair_stock_consumption_step"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/repair&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
377+
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/repair/tree/16.0/repair_stock_consumption_step"><img alt="OCA/repair" src="https://img.shields.io/badge/github-OCA%2Frepair-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/repair-16-0/repair-16-0-repair_stock_consumption_step"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/repair&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
373378
<p>This module introduces an optional intermediate step:</p>
374379
<ul class="simple">
375380
<li>When enabled at warehouse level, repair consumption moves are grouped
@@ -395,13 +400,13 @@ <h1 class="title">Repair Stock Consumption Step</h1>
395400
</ul>
396401
</div>
397402
<div class="section" id="use-cases-context">
398-
<h1><a class="toc-backref" href="#toc-entry-1">Use Cases / Context</a></h1>
403+
<h2><a class="toc-backref" href="#toc-entry-1">Use Cases / Context</a></h2>
399404
<p>In the base repair module, consumption moves for spare parts are created
400405
and immediately validated when the repair order is marked as done. This
401406
prevents user interaction such as choosing lot/serial numbers.</p>
402407
</div>
403408
<div class="section" id="configuration">
404-
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
409+
<h2><a class="toc-backref" href="#toc-entry-2">Configuration</a></h2>
405410
<ol class="arabic">
406411
<li><p class="first">Go to <strong>Inventory / Configuration / Warehouses</strong>.</p>
407412
</li>
@@ -420,7 +425,7 @@ <h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
420425
</ol>
421426
</div>
422427
<div class="section" id="usage">
423-
<h1><a class="toc-backref" href="#toc-entry-3">Usage</a></h1>
428+
<h2><a class="toc-backref" href="#toc-entry-3">Usage</a></h2>
424429
<ol class="arabic simple">
425430
<li>Create a repair order with spare part lines.</li>
426431
<li>Confirm the repair order.</li>
@@ -448,29 +453,29 @@ <h1><a class="toc-backref" href="#toc-entry-3">Usage</a></h1>
448453
</ol>
449454
</div>
450455
<div class="section" id="bug-tracker">
451-
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
456+
<h2><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h2>
452457
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/repair/issues">GitHub Issues</a>.
453458
In case of trouble, please check there if your issue has already been reported.
454459
If you spotted it first, help us to smash it by providing a detailed and welcomed
455460
<a class="reference external" href="https://github.com/OCA/repair/issues/new?body=module:%20repair_stock_consumption_step%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
456461
<p>Do not contact contributors directly about support or help with technical issues.</p>
457462
</div>
458463
<div class="section" id="credits">
459-
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
464+
<h2><a class="toc-backref" href="#toc-entry-5">Credits</a></h2>
460465
<div class="section" id="authors">
461-
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
466+
<h3><a class="toc-backref" href="#toc-entry-6">Authors</a></h3>
462467
<ul class="simple">
463468
<li>ACSONE SA/NV</li>
464469
</ul>
465470
</div>
466471
<div class="section" id="contributors">
467-
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
472+
<h3><a class="toc-backref" href="#toc-entry-7">Contributors</a></h3>
468473
<ul class="simple">
469474
<li>Souheil Bejaoui <a class="reference external" href="mailto:souheil.bejaoui&#64;acsone.eu">souheil.bejaoui&#64;acsone.eu</a></li>
470475
</ul>
471476
</div>
472477
<div class="section" id="maintainers">
473-
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
478+
<h3><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h3>
474479
<p>This module is maintained by the OCA.</p>
475480
<a class="reference external image-reference" href="https://odoo-community.org">
476481
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
@@ -485,5 +490,6 @@ <h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
485490
</div>
486491
</div>
487492
</div>
493+
</div>
488494
</body>
489495
</html>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from . import test_repair_stock_consumption_step
2+
from . import test_partial_consumption
3+
from . import common
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2026 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import Command
5+
from odoo.tests.common import TransactionCase
6+
7+
8+
class Common(TransactionCase):
9+
@classmethod
10+
def setUpClass(cls):
11+
super().setUpClass()
12+
cls.partner = cls.env["res.partner"].create({"name": "Test Partner"})
13+
cls.product = cls.env["product.product"].create(
14+
{"name": "Product to repair", "type": "product"}
15+
)
16+
cls.product_c = cls.env["product.product"].create(
17+
{"name": "product to consume", "type": "product"}
18+
)
19+
20+
cls.warehouse = cls.env["stock.warehouse"].create(
21+
{
22+
"name": "WH",
23+
"code": "wh_test",
24+
"repair_consumption_step": True,
25+
}
26+
)
27+
cls.repair_loc = cls.warehouse.lot_stock_id
28+
cls.production_location = cls.env["stock.location"].search(
29+
[("usage", "=", "production")], limit=1
30+
)
31+
cls.consumption_type = cls.env["stock.picking.type"].create(
32+
{
33+
"name": "Consumption",
34+
"warehouse_id": cls.warehouse.id,
35+
"code": "internal",
36+
"sequence_code": "PREP",
37+
"default_location_src_id": cls.repair_loc.id,
38+
"default_location_dest_id": cls.production_location.id,
39+
}
40+
)
41+
cls.warehouse.repair_consumption_picking_type_id = cls.consumption_type
42+
43+
cls.repair = cls.env["repair.order"].create(
44+
{
45+
"partner_id": cls.partner.id,
46+
"product_id": cls.product.id,
47+
"location_id": cls.repair_loc.id,
48+
"operations": [
49+
Command.create(
50+
{
51+
"name": "replace product",
52+
"type": "add",
53+
"price_unit": 100,
54+
"product_id": cls.product_c.id,
55+
"product_uom_qty": 2.0,
56+
"location_id": cls.repair_loc.id,
57+
"lot_id": cls.env["stock.lot"]
58+
.create(
59+
{"name": "Test Lot", "product_id": cls.product_c.id}
60+
)
61+
.id,
62+
}
63+
)
64+
],
65+
}
66+
)
67+
cls.env["stock.quant"]._update_available_quantity(
68+
cls.product, cls.repair_loc, 1.0
69+
)
70+
cls.env["stock.quant"]._update_available_quantity(
71+
cls.product_c, cls.repair_loc, 10.0
72+
)
73+
74+
@classmethod
75+
def _do_picking(cls, picking):
76+
for move in picking.move_ids:
77+
move.quantity_done = move.product_qty
78+
picking._action_done()

0 commit comments

Comments
 (0)