Skip to content

Commit d39f316

Browse files
committed
[16.0][ADD] repair_preparation
1 parent eb67345 commit d39f316

21 files changed

Lines changed: 1340 additions & 0 deletions

repair_preparation/README.rst

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
==================
2+
Repair Preparation
3+
==================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:b0bd07927e226fef44773f9c6e5e143ad34e65a9b69ff53deba5485ef9469399
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/16.0/repair_preparation
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-16-0/repair-16-0-repair_preparation
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=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
The repair module consumes spare parts at the end of the repair without
32+
considering the qunatity reservation. the more natural flow is to have a
33+
**staging step** to bring and reserve parts **before** the repair
34+
begins, improving planning and reducing delays at the workbench.
35+
36+
This addon introduces a **Preparation** flow:
37+
38+
- On **Confirm** (validate) of the repair, a procurement is run
39+
procurement for all eligible **Add** lines
40+
- When a repair line is **created/edited** while the repair is *Under
41+
Repair*:
42+
43+
- If any linked move is **Done** → editing raises a validation error
44+
- Otherwise, linked moves are **canceled** and procurement is
45+
**re-run** for the updated lines
46+
47+
- **Finishing** a repair is blocked if:
48+
49+
- There are consumed lines but **no preparation pickings**, or
50+
- Preparation pickings exist but are **not done**.
51+
52+
**Table of contents**
53+
54+
.. contents::
55+
:local:
56+
57+
Configuration
58+
=============
59+
60+
0) Company settings
61+
-------------------
62+
63+
1. Go to **Settings ▸ General Settings ▸ Repairs**:
64+
65+
- Enable Repair Preparation
66+
- Select the default picking type used for preparation
67+
68+
1) Create a Preparation area
69+
----------------------------
70+
71+
1. Go to **Inventory ▸ Configuration ▸ Locations**.
72+
2. Create an **internal** location, e.g. **WH/Preparation**, under your
73+
warehouse **Stock** location.
74+
75+
2) Create a Preparation picking type
76+
------------------------------------
77+
78+
1. Go to **Inventory ▸ Configuration ▸ Operation Types**.
79+
2. Create an **Internal Transfer** type named **Preparation**.
80+
3. Set:
81+
82+
- **Default Source Location** = *Your Warehouse / Stock*
83+
- **Default Destination Location** = *Your Warehouse / Preparation*
84+
85+
3) Route & rule (Stock → Preparation)
86+
-------------------------------------
87+
88+
1. Go to **Inventory ▸ Configuration ▸ Routes** and create **Route to
89+
Preparation**.
90+
2. Enable **Selectable on warehouse**.
91+
3. Add a **Pull Rule**:
92+
93+
- **Action**: *Pull From*
94+
- **Operation Type**: *Preparation* (created above)
95+
- **Source Location**: *Your Warehouse / Stock*
96+
- **Destination Location**: *Your Warehouse / Preparation*
97+
- **Warehouse**: your warehouse
98+
99+
Bug Tracker
100+
===========
101+
102+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/repair/issues>`_.
103+
In case of trouble, please check there if your issue has already been reported.
104+
If you spotted it first, help us to smash it by providing a detailed and welcomed
105+
`feedback <https://github.com/OCA/repair/issues/new?body=module:%20repair_preparation%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
106+
107+
Do not contact contributors directly about support or help with technical issues.
108+
109+
Credits
110+
=======
111+
112+
Authors
113+
-------
114+
115+
* ACSONE SA/NV
116+
117+
Contributors
118+
------------
119+
120+
- Souheil Bejaoui souheil.bejaoui@acsone.eu
121+
122+
Maintainers
123+
-----------
124+
125+
This module is maintained by the OCA.
126+
127+
.. image:: https://odoo-community.org/logo.png
128+
:alt: Odoo Community Association
129+
:target: https://odoo-community.org
130+
131+
OCA, or the Odoo Community Association, is a nonprofit organization whose
132+
mission is to support the collaborative development of Odoo features and
133+
promote its widespread use.
134+
135+
This module is part of the `OCA/repair <https://github.com/OCA/repair/tree/16.0/repair_preparation>`_ project on GitHub.
136+
137+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

repair_preparation/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

repair_preparation/__manifest__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
{
5+
"name": "Repair Preparation",
6+
"summary": """Procure & prepare parts before starting repairs""",
7+
"version": "16.0.1.0.0",
8+
"license": "AGPL-3",
9+
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
10+
"website": "https://github.com/OCA/repair",
11+
"depends": ["repair"],
12+
"data": [
13+
"views/res_config_settings.xml",
14+
"views/repair_order.xml",
15+
],
16+
"demo": [],
17+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from . import repair_order
2+
from . import stock_move
3+
from . import stock_rule
4+
from . import repair_line
5+
from . import res_company
6+
from . import res_config_settings
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2025 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import _, api, fields, models
5+
from odoo.exceptions import ValidationError
6+
7+
8+
class RepairLine(models.Model):
9+
10+
_inherit = "repair.line"
11+
12+
preparation_move_ids = fields.One2many(
13+
comodel_name="stock.move",
14+
inverse_name="repair_line_id",
15+
)
16+
17+
@api.depends("type", "repair_id.location_id")
18+
def _compute_location_id(self):
19+
res = super()._compute_location_id()
20+
for rec in self:
21+
if rec.type == "add" and rec.repair_id.location_id:
22+
rec.location_id = rec.repair_id.location_id
23+
return res
24+
25+
@api.model_create_multi
26+
def create(self, vals_list):
27+
records = super().create(vals_list)
28+
for repair in records.repair_id:
29+
if repair.state != "under_repair":
30+
# we manage procurement auto-run when repair is started
31+
continue
32+
new_lines = records.filtered(lambda line, ro=repair: line.repair_id == ro)
33+
repair._run_preparation_procurements(new_lines)
34+
return records
35+
36+
def write(self, vals):
37+
res = super().write(vals)
38+
fields_affecting = {"type", "product_id", "product_uom_qty", "product_uom"}
39+
if all(_field not in vals for _field in fields_affecting):
40+
return res
41+
for repair in self.repair_id:
42+
43+
updated_lines = self.filtered(lambda line, ro=repair: line.repair_id == ro)
44+
if not updated_lines:
45+
continue
46+
moves = updated_lines.preparation_move_ids
47+
if moves.filtered(lambda m: m.state == "done"):
48+
raise ValidationError(
49+
_(
50+
"You cannot modify product/quantity for preparation lines "
51+
"because some linked moves are already done.\n"
52+
"Repair: %(repair)s\nLines: %(lines)s"
53+
)
54+
% {
55+
"repair": repair.display_name,
56+
"lines": ", ".join(updated_lines.mapped("display_name")),
57+
}
58+
)
59+
if repair.state != "under_repair":
60+
# we manage procurement auto-run when repair is started
61+
continue
62+
moves_to_cancel = moves.filtered(lambda m: m.state != "cancel")
63+
moves_to_cancel._action_cancel()
64+
repair._run_preparation_procurements(updated_lines)
65+
66+
return res
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Copyright 2025 ACSONE SA/NV
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import _, api, fields, models
5+
from odoo.exceptions import ValidationError
6+
7+
8+
class RepairOrder(models.Model):
9+
10+
_inherit = "repair.order"
11+
12+
preparation_picking_type_id = fields.Many2one(
13+
"stock.picking.type",
14+
help="Picking type used to bring spare parts before the repair starts.",
15+
domain="[('warehouse_id.company_id', '=', company_id)]",
16+
compute="_compute_preparation_picking_type_id",
17+
readonly=False,
18+
store=True,
19+
)
20+
preparation_group_id = fields.Many2one(
21+
"procurement.group",
22+
string="Preparation Procurement Group",
23+
readonly=True,
24+
copy=False,
25+
help="Procurement group used to gather all preparation moves/pickings.",
26+
)
27+
stock_move_ids = fields.Many2many("stock.move", compute="_compute_stock_move_ids")
28+
preparation_picking_ids = fields.Many2many(
29+
"stock.picking", compute="_compute_preparation_picking_ids"
30+
)
31+
repair_preparation_enabled = fields.Boolean(
32+
related="company_id.repair_preparation_enabled", readonly=False
33+
)
34+
35+
@api.depends("company_id", "repair_preparation_enabled")
36+
def _compute_preparation_picking_type_id(self):
37+
for rec in self:
38+
if rec.repair_preparation_enabled and not rec.preparation_picking_type_id:
39+
rec.preparation_picking_type_id = (
40+
rec.company_id.repair_preparation_picking_type_id
41+
)
42+
43+
@api.depends("operations.preparation_move_ids", "operations.move_id")
44+
def _compute_stock_move_ids(self):
45+
for rec in self:
46+
rec.stock_move_ids = (
47+
rec.operations.preparation_move_ids + rec.operations.move_id
48+
)
49+
50+
@api.depends("operations.preparation_move_ids")
51+
def _compute_preparation_picking_ids(self):
52+
for rec in self:
53+
rec.preparation_picking_ids = rec.operations.preparation_move_ids.picking_id
54+
55+
def _ensure_preparation_group(self):
56+
self.ensure_one()
57+
if not self.preparation_group_id:
58+
self.preparation_group_id = self.env["procurement.group"].create(
59+
{
60+
"name": _("Preparation for") + self.name,
61+
"partner_id": self.partner_id.id,
62+
}
63+
)
64+
65+
@api.model
66+
def _get_consumed_lines_for_preparation(self, operations):
67+
return operations.filtered(
68+
lambda line: line.type == "add"
69+
and line.product_id
70+
and line.product_uom_qty > 0
71+
)
72+
73+
def _get_repair_line_procurement_values(self, line):
74+
return {
75+
"company_id": self.company_id,
76+
"group_id": self.preparation_group_id,
77+
"warehouse_id": self.preparation_picking_type_id.warehouse_id,
78+
"picking_type_id": self.preparation_picking_type_id.id,
79+
"repair_line_id": line.id,
80+
}
81+
82+
def _get_repair_line_procurement(self, line):
83+
return self.env["procurement.group"].Procurement(
84+
line.product_id,
85+
line.product_uom_qty,
86+
line.product_uom,
87+
self.preparation_picking_type_id.default_location_dest_id,
88+
f"{self.name} {line.product_id.display_name}",
89+
self.name,
90+
self.company_id,
91+
self._get_repair_line_procurement_values(line),
92+
)
93+
94+
def _run_preparation_procurements(self, operations):
95+
self.ensure_one()
96+
if not operations:
97+
return
98+
if not self.preparation_picking_type_id:
99+
return
100+
lines = self._get_consumed_lines_for_preparation(operations)
101+
if not lines:
102+
return
103+
self._ensure_preparation_group()
104+
self.env["procurement.group"].run(
105+
[self._get_repair_line_procurement(line) for line in lines]
106+
)
107+
108+
def action_validate(self):
109+
res = super().action_validate()
110+
for rec in self:
111+
if not rec.repair_preparation_enabled:
112+
continue
113+
rec._run_preparation_procurements(rec.operations)
114+
return res
115+
116+
def action_repair_end(self):
117+
for rec in self:
118+
if not rec.repair_preparation_enabled:
119+
continue
120+
consumed_lines = rec._get_consumed_lines_for_preparation(rec.operations)
121+
if consumed_lines and not rec.preparation_picking_ids:
122+
raise ValidationError(
123+
_(
124+
"Preparation picking not found. Please procure/prepare parts "
125+
"first."
126+
)
127+
)
128+
if consumed_lines and rec.preparation_picking_ids.filtered(
129+
lambda p: p.state != "done"
130+
):
131+
raise ValidationError(
132+
_(
133+
"Preparation picking is not done yet. Validate it before "
134+
"starting the repair."
135+
)
136+
)
137+
return super().action_repair_end()
138+
139+
def action_view_preparation_picking(self):
140+
self.ensure_one()
141+
action_xmlid = "stock.action_picking_tree_all"
142+
action = self.env["ir.actions.act_window"]._for_xml_id(action_xmlid)
143+
action["domain"] = [("id", "in", self.preparation_picking_ids.ids)]
144+
return action
145+
146+
def action_view_move(self):
147+
self.ensure_one()
148+
return {
149+
"type": "ir.actions.act_window",
150+
"name": _("Moves"),
151+
"res_model": self.stock_move_ids._name,
152+
"domain": [("id", "in", self.stock_move_ids.ids)],
153+
"view_mode": "tree,form",
154+
"context": self.env.context,
155+
}

0 commit comments

Comments
 (0)