Skip to content
Open
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
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,24 @@ jobs:
matrix:
include:
- container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest
include: "repair_stock_move"
name: test with Odoo
- container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest
include: "repair_stock_move"
name: test with OCB
makepot: "true"
- container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest
include: "repair_stock_consumption_step"
name: test with Odoo
- container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest
include: "repair_stock_consumption_step"
name: test with OCB
makepot: "true"
- container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest
exclude: "repair_stock_move,repair_stock_consumption_step"
name: test with Odoo
- container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest
exclude: "repair_stock_move,repair_stock_consumption_step"
name: test with OCB
makepot: "true"
services:
Expand All @@ -49,6 +65,9 @@ jobs:
POSTGRES_DB: odoo
ports:
- 5432:5432
env:
INCLUDE: "${{ matrix.include }}"
EXCLUDE: "${{ matrix.exclude }}"
steps:
- uses: actions/checkout@v4
with:
Expand Down
144 changes: 144 additions & 0 deletions repair_stock_consumption_step/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

=============================
Repair Stock Consumption Step
=============================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:93fc83002290646de5793f5425fca96699172ab4f9487b4196a3e8481bbaead6
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frepair-lightgray.png?logo=github
:target: https://github.com/OCA/repair/tree/16.0/repair_stock_consumption_step
:alt: OCA/repair
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/repair-16-0/repair-16-0-repair_stock_consumption_step
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/repair&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module introduces an optional intermediate step:

- When enabled at warehouse level, repair consumption moves are grouped
into a stock picking.
- The repair order is set to a new **Consumption** state until the
picking is validated.
- Users can process the picking manually, assign lots/serials, and only
then complete the repair.

**Table of contents**

.. contents::
:local:

Use Cases / Context
===================

In the base repair module, consumption moves for spare parts are created
and immediately validated when the repair order is marked as done. This
prevents user interaction such as choosing lot/serial numbers.

Configuration
=============

1. Go to **Inventory / Configuration / Warehouses**.

2. Open the warehouse you want to use for repairs.

3.

2. In **Technical info** :

- Enable **Repair Consumption Step**.
- Select the **Repair Consumption Picking Type** (an internal
transfer type from the repair location to a production/virtual
location).

Usage
=====

1. Create a repair order with spare part lines.
2. Confirm the repair order.
3. Click **End Repair**:

- If the warehouse setting is disabled:

- Consumption moves are validated immediately, repair goes directly
to **Done**.

- If the setting is enabled:

- The repair order moves to the **Consumption** state.
- A stock picking is created for the spare part moves.

4. Open the **Consumption Picking** from the repair order.
5. Process the picking:

- Assign quantities and lots/serials.
- Validate the picking.

6. Once the picking is validated, the repair order automatically moves
to **Done**.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/repair/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <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**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* ACSONE SA/NV

Contributors
------------

- Souheil Bejaoui souheil.bejaoui@acsone.eu

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-sbejaoui| image:: https://github.com/sbejaoui.png?size=40px
:target: https://github.com/sbejaoui
:alt: sbejaoui

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-sbejaoui|

This module is part of the `OCA/repair <https://github.com/OCA/repair/tree/16.0/repair_stock_consumption_step>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions repair_stock_consumption_step/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizards
22 changes: 22 additions & 0 deletions repair_stock_consumption_step/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Repair Stock Consumption Step",
"summary": """Adds a warehouse-configurable step to process repair consumption
moves in a picking""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/repair",
"maintainers": ["sbejaoui"],
"depends": ["repair", "repair_warehouse"],
"excludes": ["repair_stock_move"],
"data": [
"security/ir.model.access.csv",
"wizards/repair_consumption_partial_wizard.xml",
"views/repair_order.xml",
"views/stock_warehouse.xml",
],
"demo": [],
}
4 changes: 4 additions & 0 deletions repair_stock_consumption_step/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import stock_warehouse
from . import repair_order
from . import stock_move
from . import stock_picking
142 changes: 142 additions & 0 deletions repair_stock_consumption_step/models/repair_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import Command, fields, models
from odoo.tools import float_is_zero


class RepairOrder(models.Model):
_inherit = "repair.order"

consumption_picking_id = fields.Many2one(
"stock.picking", string="Consumption Picking", readonly=True, copy=False
)
repair_consumption_step = fields.Boolean(
related="warehouse_id.repair_consumption_step"
)
state = fields.Selection(
# Put "consumption" step before "done" step (for UI)
selection_add=[("consumption", "Waiting Consumption"), ("done",)],
ondelete={"consumption": "set done"},
)

def action_view_consumption_picking(self):
self.ensure_one()
return {
"name": "Repair Consumption Picking",
"type": "ir.actions.act_window",
"res_model": "stock.picking",
"view_mode": "form",
"res_id": self.consumption_picking_id.id,
}

def action_repair_done(self):
need_consumption_step = self.filtered("repair_consumption_step")
res = super(RepairOrder, self - need_consumption_step).action_repair_done()
for rec in need_consumption_step:
rec_res = super(
RepairOrder, rec.with_context(dont_validate_repair_move=True)
).action_repair_done()
res.update(rec_res)
moves = self.env["stock.move"].search(
[("repair_id", "=", rec.id), ("state", "!=", "cancel")]
)
if not moves:
continue
picking = self.env["stock.picking"].create(
{
"picking_type_id": rec.warehouse_id.repair_consumption_picking_type_id.id,
"origin": rec.name,
"move_ids": [Command.set(moves.ids)],
}
)

# Preserve lot info before unlink the move lines
move_id_lots_ids_map = {}
for move in moves:
move_id_lots_ids_map[move.id] = move.move_line_ids.mapped("lot_id").ids

moves.move_line_ids.unlink()
moves._action_confirm()
# need exists because confirm() may merge moves
moves.exists()._action_assign()

# Reset lot_id on the new move lines
for move in moves:
lot_ids = move_id_lots_ids_map.get(move.id)
if not lot_ids:
continue
for line in move.move_line_ids:
if not line.lot_id:
line.lot_id = lot_ids.pop(0)

rec.consumption_picking_id = picking
rec.state = "consumption"
return res

def action_repair_cancel(self):
res = super().action_repair_cancel()
if self.consumption_picking_id and self.consumption_picking_id.state not in (
"done",
"cancel",
):
self.consumption_picking_id.action_cancel()
return res

def action_repair_end(self):
super().action_repair_end()
need_consumption_step = self.filtered("consumption_picking_id")
need_consumption_step.state = "consumption"
return True

def _action_consumption_done(self):
for rec in self:
state = "done"
if not rec.invoice_id and rec.invoice_method == "after_repair":
state = "2binvoiced"
rec.state = state

def _update_parts(self, return_consumption_moves):
"""Update Repair Order operations based on unconsumed quantities.

This method reduces the demand (product_uom_qty) on the repair order
lines by matching them against moves that were cancelled/returned
during the partial consumption process. It uses a 'pop' logic across
multiple lines of the same product until the returned quantity
is fully accounted for.

Complexity Note:
The pop logic here is needed because there might be more than one repair.line
per product.
"""
self.ensure_one()
operations_to_delete = self.env["repair.line"]
for return_move in return_consumption_moves:
remaining_qty = return_move.product_uom_qty
operations = self.operations.filtered(
lambda o: o.product_id == return_move.product_id
)
while (
not float_is_zero(
remaining_qty, precision_rounding=return_move.product_uom.rounding
)
and operations
):
# TODO: find a better solution to extract the "best matching" operation
# instead of simply taking the first
operation = operations[0]
qty_before_update = operation.product_uom_qty
operation.product_uom_qty -= min(
operation.product_uom_qty, remaining_qty
)
if float_is_zero(
operation.product_uom_qty,
precision_rounding=operation.product_uom.rounding,
):
operations_to_delete |= operation

remaining_qty -= qty_before_update - operation.product_uom_qty
operations -= operation

if operations_to_delete:
operations_to_delete.unlink()
18 changes: 18 additions & 0 deletions repair_stock_consumption_step/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class StockMove(models.Model):

_inherit = "stock.move"

def _action_done(self, cancel_backorder=False):
repair_moves = self.browse()
if self.env.context.get("dont_validate_repair_move"):
repair_moves = self.filtered("repair_id")

return super(StockMove, self - repair_moves)._action_done(
cancel_backorder=cancel_backorder
)
Loading
Loading