From f2254fcc302a0d85b9e872be202ee6a4606c091d Mon Sep 17 00:00:00 2001 From: anusrinps96 Date: Wed, 4 Mar 2026 12:18:55 +0100 Subject: [PATCH 1/4] [IMP] hr_personal_equipment_stock: Enhance PPE view Display serial number under "Personal Equipment" Tab in Personal Equipment request form view when Personal Equipment Request is in "Accepted" state. --- hr_personal_equipment_stock/README.rst | 6 ++- .../models/hr_personal_equipment.py | 52 +++++++++++++++++++ .../static/description/index.html | 26 ++++++---- .../tests/test_hr_personal_equipment_stock.py | 32 +++++++++++- .../views/hr_personal_equipment_request.xml | 5 ++ 5 files changed, 109 insertions(+), 12 deletions(-) diff --git a/hr_personal_equipment_stock/README.rst b/hr_personal_equipment_stock/README.rst index 5b408ff1a8f..0fc56903959 100644 --- a/hr_personal_equipment_stock/README.rst +++ b/hr_personal_equipment_stock/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + =========================== Hr Personal Equipment Stock =========================== @@ -13,7 +17,7 @@ Hr Personal Equipment Stock .. |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/licence-AGPL--3-blue.png +.. |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%2Fhr-lightgray.png?logo=github diff --git a/hr_personal_equipment_stock/models/hr_personal_equipment.py b/hr_personal_equipment_stock/models/hr_personal_equipment.py index 3840031c47e..73144bc9297 100644 --- a/hr_personal_equipment_stock/models/hr_personal_equipment.py +++ b/hr_personal_equipment_stock/models/hr_personal_equipment.py @@ -30,6 +30,9 @@ class HrPersonalEquipment(models.Model): "stock.move", "personal_equipment_id", string="Stock Moves" ) skip_procurement = fields.Boolean(compute="_compute_skip_procurement") + lot_ids = fields.Many2many( + "stock.lot", compute="_compute_lot_ids", string="Serial Numbers" + ) @api.depends("state", "product_id", "product_id.type") def _compute_skip_procurement(self): @@ -53,6 +56,55 @@ def _compute_qty_delivered(self): ) line.qty_delivered = qty + @api.depends( + "move_ids.lot_ids", + ) + def _compute_lot_ids(self): + for line in self: + available_lot_ids = {} + returned_move_ids = line.move_ids.filtered( + lambda x: x.product_id == line.product_id + and x.quantity_done + and x.origin_returned_move_id + ) + moves_to_be_checked = line.move_ids - returned_move_ids + for move_id in moves_to_be_checked: + if move_id.lot_ids: + lot_infos = self.env["stock.quant"].read_group( + domain=[ + ("product_id", "=", line.product_id.id), + ("location_id", "child_of", line.location_id.id), + ("lot_id", "in", move_id.lot_ids.ids), + ], + fields=["lot_id", "quantity: sum"], + groupby=["lot_id"], + ) + available_lot_ids.update( + { + lot_info["lot_id"][0]: lot_info["quantity"] + for lot_info in lot_infos + if lot_info["lot_id"] + and lot_info["quantity"] not in (0.0, -1.0) + } + ) + lot_ids = available_lot_ids.keys() + if lot_ids: + returned_lots = returned_move_ids.mapped("lot_ids") + actual_move_lots = moves_to_be_checked.mapped("lot_ids") + # Remove returned lots from available lots in location + # when same lot_id used in different PPE request + if len(returned_move_ids) and ( + len(returned_lots) != len(actual_move_lots) + ): + lot_ids = [ + x + for x in available_lot_ids.keys() + if x not in returned_move_ids.mapped("lot_ids").ids + ] + line.lot_ids = [(6, 0, lot_ids)] + else: + line.lot_ids = False + def _skip_procurement(self): return self.product_id.type not in ("consu", "product") diff --git a/hr_personal_equipment_stock/static/description/index.html b/hr_personal_equipment_stock/static/description/index.html index 563e4bce53f..6e0c5a47090 100644 --- a/hr_personal_equipment_stock/static/description/index.html +++ b/hr_personal_equipment_stock/static/description/index.html @@ -3,7 +3,7 @@ -Hr Personal Equipment Stock +README.rst -
-

Hr Personal Equipment Stock

+
+ + +Odoo Community Association + +
+

Hr Personal Equipment Stock

-

Beta License: AGPL-3 OCA/hr Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/hr Translate me on Weblate Try me on Runboat

This module integrates stock with hr_personal_equipment_request module. When the equipment request is accepted, a stock request is generated and a “stock.move” is created for each request line. If the “stock.move” is marked as done, the corresponding allocations are marked as valid if the quantity_delivered is equal to the requested quantity. @@ -389,11 +394,11 @@

Hr Personal Equipment Stock

-

Installation

+

Installation

This module is auto installed if the modules “hr_personal_equipment_request” and “stock” are installed.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub 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 @@ -401,21 +406,21 @@

Bug Tracker

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

+
diff --git a/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py b/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py index 5e5a97010f3..7286433902d 100644 --- a/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py +++ b/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py @@ -1,6 +1,8 @@ # Copyright 2021 Creu Blanca # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from uuid import uuid4 + from odoo.exceptions import UserError from odoo.tests import TransactionCase @@ -162,14 +164,34 @@ def test_skip_procurement(self): self.personal_equipment_request.line_ids[1]._compute_skip_procurement() self.assertTrue(self.personal_equipment_request.line_ids[1].skip_procurement) + def _create_lot_ids(self, product, quantity): + product.tracking = "serial" + lot_ids = [] + for _qty in range(quantity): + serial_number = str(uuid4()) + lot_id = self.env["stock.lot"].create( + { + "name": f"Lots for tests {product.name} - {serial_number}", + "product_id": product.product_variant_id.id, + } + ) + lot_ids += lot_id + return lot_ids + def test_compute_qty_delivered(self): - self.personal_equipment_request.accept_request() allocation = self.personal_equipment_request.line_ids[0] + lot_ids = self._create_lot_ids( + self.product_personal_equipment_1, allocation.quantity + ) + self.personal_equipment_request.accept_request() move = allocation.move_ids[0] + move.lot_ids = [lot_id.id for lot_id in lot_ids] move.quantity_done = allocation.quantity picking = self.personal_equipment_request.picking_ids[0] picking._action_done() self.assertEqual(allocation.qty_delivered, allocation.quantity) + self.assertEqual(len(allocation.lot_ids), allocation.qty_delivered) + self.assertEqual(allocation.lot_ids, allocation.move_ids.lot_ids) self.assertEqual(allocation.state, "valid") def test_quantity_delivered_skip_procurement(self): @@ -188,24 +210,32 @@ def test_action_launch_procurement_rule_raise_error(self): def test_action_cancel_with_qty_delivered(self): allocation = self.personal_equipment_request.line_ids[0] + self._create_lot_ids(self.product_personal_equipment_1, allocation.quantity) self.personal_equipment_request.accept_request() self.assertEqual(allocation.state, "accepted") picking = self.personal_equipment_request.picking_ids[0] picking.action_cancel() self.assertEqual(allocation.qty_delivered, 0) + self.assertFalse(allocation.lot_ids) self.assertEqual(allocation.state, "cancelled") def test_action_cancel_without_qty_delivered(self): allocation = self.personal_equipment_request.line_ids[0] + lot_ids = self._create_lot_ids( + self.product_personal_equipment_1, allocation.quantity + ) self.personal_equipment_request.accept_request() self.assertEqual(allocation.state, "accepted") move = allocation.move_ids[0] + move.lot_ids = [lot_ids[index].id for index in range(allocation.quantity - 1)] move.quantity_done = allocation.quantity - 1 picking = self.personal_equipment_request.picking_ids[0] picking._action_done() back_order = self.personal_equipment_request.picking_ids[1] back_order.action_cancel() self.assertEqual(allocation.qty_delivered, allocation.quantity - 1) + self.assertEqual(len(allocation.lot_ids), allocation.qty_delivered) + self.assertEqual(allocation.lot_ids, allocation.move_ids.lot_ids) self.assertEqual(allocation.state, "valid") def test_action_view_pickings(self): diff --git a/hr_personal_equipment_stock/views/hr_personal_equipment_request.xml b/hr_personal_equipment_stock/views/hr_personal_equipment_request.xml index b4ee0483f6f..65a772b62cb 100644 --- a/hr_personal_equipment_stock/views/hr_personal_equipment_request.xml +++ b/hr_personal_equipment_stock/views/hr_personal_equipment_request.xml @@ -25,6 +25,11 @@ + From 936d66a59265bd93c07419ce43ff4969f5a8fba3 Mon Sep 17 00:00:00 2001 From: anusrinps96 Date: Mon, 20 Jul 2026 16:23:57 +0200 Subject: [PATCH 2/4] [IMP] hr_personal_equipment_stock: Fix qty_delivered field Using proper values for qty_delivered based on stock availability when a picking associated with allocation is validated/returned/scrapped. --- .../models/hr_personal_equipment.py | 93 ++++++++++++++++- .../tests/test_hr_personal_equipment_stock.py | 99 ++++++++++++++++++- 2 files changed, 186 insertions(+), 6 deletions(-) diff --git a/hr_personal_equipment_stock/models/hr_personal_equipment.py b/hr_personal_equipment_stock/models/hr_personal_equipment.py index 73144bc9297..1e68f18d648 100644 --- a/hr_personal_equipment_stock/models/hr_personal_equipment.py +++ b/hr_personal_equipment_stock/models/hr_personal_equipment.py @@ -44,16 +44,99 @@ def _compute_skip_procurement(self): "move_ids.scrapped", "move_ids.product_uom_qty", "move_ids.product_uom", + "equipment_request_id.picking_ids.move_ids.scrapped", ) def _compute_qty_delivered(self): for line in self: qty = 0.0 - for move in line.move_ids.filtered( - lambda r: r.state == "done" and line.product_id == r.product_id - ): - qty += move.product_uom._compute_quantity( - move.product_uom_qty, line.product_uom_id + if line.product_id.tracking == "none" and len(line.move_ids.lot_ids) == 0: + move_ids_to_be_processed = ( + line.equipment_request_id.picking_ids.filtered( + lambda x: x.state != "done" + ).move_ids ) + move_ids = line.move_ids.filtered( + lambda x: x.product_id == line.product_id + and x.quantity_done + and not x.origin_returned_move_id + ) + returned_move_ids = line.move_ids.filtered( + lambda x: x.product_id == line.product_id + and x.quantity_done + and x.origin_returned_move_id + ).sorted("create_date") + scrapped_move_ids = ( + line.equipment_request_id.picking_ids.move_ids.filtered( + lambda x: x.scrapped + ) + ).sorted("create_date") + move_ids = move_ids - scrapped_move_ids + if move_ids: + if len(move_ids) == 1 and ( + not returned_move_ids and not scrapped_move_ids + ): + line.qty_delivered = move_ids.quantity_done + # check for duplication moves and use latest one + elif len(move_ids) > 1 and ( + returned_move_ids and not scrapped_move_ids + ): + move_ids = move_ids.sorted("create_date") + line.qty_delivered = move_ids[-1].quantity_done + else: + if returned_move_ids: + returned_move_ids = returned_move_ids.sorted("create_date") + if not move_ids_to_be_processed or ( + move_ids_to_be_processed + and (returned_move_ids in move_ids_to_be_processed) + ): + line.qty_delivered = ( + line.qty_delivered + - returned_move_ids[-1].quantity_done + ) + elif scrapped_move_ids: + if scrapped_move_ids in move_ids_to_be_processed: + line.qty_delivered = ( + line.qty_delivered + - scrapped_move_ids[-1].quantity_done + ) + continue + # Filter scrapped move_ids + scrapped_lot_ids = line.equipment_request_id.picking_ids.move_ids.filtered( + lambda x: x.scrapped + ).lot_ids + if scrapped_lot_ids: + line.lot_ids = line.lot_ids.filtered( + lambda x: x not in scrapped_lot_ids + ) + lot_ids_qty_info = { + lot_id.id: self.env["stock.quant"]._get_available_quantity( + line.product_id, + line.location_id, + lot_id, + ) + for lot_id in line.lot_ids + } + # Filter moves which are moved to PPE location + moves_to_be_checked = line.move_ids.filtered( + lambda r: r.state == "done" + and line.product_id == r.product_id + and r.location_dest_id == line.location_id + and not r.origin_returned_move_id + and r.lot_ids not in scrapped_lot_ids + ) + # check and remove moves with same lots + # if same lots exists among filtered moves + lots_to_be_filtered = self.env["stock.move"] + for lot in moves_to_be_checked.mapped("lot_ids"): + move_with_same_lots = moves_to_be_checked.filtered( + lambda x: lot in x.lot_ids + ) + if len(move_with_same_lots) > 1: + lot_records = move_with_same_lots.sorted("create_date") + lots_to_be_filtered |= lot_records[:-1] + moves_to_be_checked -= lots_to_be_filtered + for move in moves_to_be_checked: + qty += sum([lot_ids_qty_info.get(lot.id, 0) for lot in move.lot_ids]) line.qty_delivered = qty @api.depends( diff --git a/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py b/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py index 7286433902d..c59378a493f 100644 --- a/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py +++ b/hr_personal_equipment_stock/tests/test_hr_personal_equipment_stock.py @@ -4,7 +4,7 @@ from uuid import uuid4 from odoo.exceptions import UserError -from odoo.tests import TransactionCase +from odoo.tests import Form, TransactionCase class TestHRPersonalEquipment(TransactionCase): @@ -193,6 +193,103 @@ def test_compute_qty_delivered(self): self.assertEqual(len(allocation.lot_ids), allocation.qty_delivered) self.assertEqual(allocation.lot_ids, allocation.move_ids.lot_ids) self.assertEqual(allocation.state, "valid") + # Return picking + return_form = Form( + self.env["stock.return.picking"].with_context( + active_id=picking.id, active_model="stock.picking" + ) + ) + return_wizard = return_form.save() + action = return_wizard.create_returns() + return_picking = self.env["stock.picking"].browse(action["res_id"]) + return_picking.move_line_ids.write({"qty_done": 1}) + return_picking.button_validate() + self.assertEqual(len(self.personal_equipment_request.picking_ids), 2) + self.assertEqual(allocation.qty_delivered, 0) + self.assertNotEqual(allocation.lot_ids, allocation.move_ids.lot_ids) + self.assertFalse(allocation.lot_ids) + # Duplicate picking + duplicate_picking = picking.copy() + lot_ids = self._create_lot_ids( + self.product_personal_equipment_1, allocation.quantity + ) + move = duplicate_picking.move_ids + move.lot_ids = [lot_id.id for lot_id in lot_ids] + move.quantity_done = allocation.quantity + duplicate_picking._action_done() + self.assertEqual(allocation.qty_delivered, allocation.quantity) + self.assertEqual(len(allocation.lot_ids), allocation.qty_delivered) + self.assertEqual(allocation.lot_ids, duplicate_picking.move_ids.lot_ids) + self.assertEqual(allocation.state, "valid") + # create scraps + self.assertEqual(allocation.qty_delivered, 3) + scrap = self.env["stock.scrap"].create( + { + "product_id": allocation.product_id.id, + "scrap_qty": 1.0, + "location_id": self.location_employee.id, + "picking_id": duplicate_picking.id, + "lot_id": lot_ids[0].id, + } + ) + scrap.action_validate() + self.assertEqual(scrap.state, "done") + self.assertTrue(duplicate_picking.has_scrap_move) + self.assertEqual(len(allocation.lot_ids), 2) + self.assertNotIn(lot_ids[0], allocation.lot_ids) + self.assertEqual(allocation.qty_delivered, 2) + + def test_compute_qty_delivered_without_lot_ids(self): + allocation = self.personal_equipment_request.line_ids[0] + self.personal_equipment_request.accept_request() + move = allocation.move_ids[0] + move.quantity_done = allocation.quantity + picking = self.personal_equipment_request.picking_ids[0] + picking._action_done() + self.assertEqual(allocation.qty_delivered, allocation.quantity) + self.assertFalse(allocation.lot_ids) + self.assertEqual(allocation.lot_ids, allocation.move_ids.lot_ids) + self.assertEqual(allocation.state, "valid") + # Return picking + return_form = Form( + self.env["stock.return.picking"].with_context( + active_id=picking.id, active_model="stock.picking" + ) + ) + return_wizard = return_form.save() + action = return_wizard.create_returns() + return_picking = self.env["stock.picking"].browse(action["res_id"]) + return_picking.move_line_ids.write({"qty_done": 3}) + return_picking.button_validate() + self.assertEqual(len(self.personal_equipment_request.picking_ids), 2) + self.assertEqual(allocation.qty_delivered, 0) + self.assertEqual(allocation.lot_ids, allocation.move_ids.lot_ids) + self.assertFalse(allocation.lot_ids) + # Duplicate picking + duplicate_picking = picking.copy() + move = duplicate_picking.move_ids + move.quantity_done = allocation.quantity + duplicate_picking._action_done() + self.assertEqual(allocation.qty_delivered, allocation.quantity) + self.assertFalse(allocation.lot_ids) + self.assertEqual(allocation.lot_ids, duplicate_picking.move_ids.lot_ids) + self.assertEqual(allocation.state, "valid") + # create scraps + self.assertEqual(allocation.qty_delivered, 3) + scrap = self.env["stock.scrap"].create( + { + "product_id": allocation.product_id.id, + "scrap_qty": 1.0, + "location_id": self.location_employee.id, + "picking_id": duplicate_picking.id, + } + ) + scrap.action_validate() + self.assertEqual(scrap.state, "done") + self.assertFalse(scrap.lot_id) + self.assertTrue(duplicate_picking.has_scrap_move) + self.assertFalse(allocation.lot_ids) + self.assertEqual(allocation.qty_delivered, 2) def test_quantity_delivered_skip_procurement(self): allocation = self.personal_equipment_request.line_ids[1] From 7b048017d1f8541760975d7c56bdb3eaf02b1a2a Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 31 Aug 2026 10:49:08 +0200 Subject: [PATCH 3/4] Loop --- .../models/hr_personal_equipment.py | 161 ++++-------------- 1 file changed, 29 insertions(+), 132 deletions(-) diff --git a/hr_personal_equipment_stock/models/hr_personal_equipment.py b/hr_personal_equipment_stock/models/hr_personal_equipment.py index 1e68f18d648..75c1441e482 100644 --- a/hr_personal_equipment_stock/models/hr_personal_equipment.py +++ b/hr_personal_equipment_stock/models/hr_personal_equipment.py @@ -44,99 +44,19 @@ def _compute_skip_procurement(self): "move_ids.scrapped", "move_ids.product_uom_qty", "move_ids.product_uom", - "equipment_request_id.picking_ids.move_ids.scrapped", ) def _compute_qty_delivered(self): for line in self: qty = 0.0 - if line.product_id.tracking == "none" and len(line.move_ids.lot_ids) == 0: - move_ids_to_be_processed = ( - line.equipment_request_id.picking_ids.filtered( - lambda x: x.state != "done" - ).move_ids - ) - move_ids = line.move_ids.filtered( - lambda x: x.product_id == line.product_id - and x.quantity_done - and not x.origin_returned_move_id - ) - returned_move_ids = line.move_ids.filtered( - lambda x: x.product_id == line.product_id - and x.quantity_done - and x.origin_returned_move_id - ).sorted("create_date") - scrapped_move_ids = ( - line.equipment_request_id.picking_ids.move_ids.filtered( - lambda x: x.scrapped - ) - ).sorted("create_date") - move_ids = move_ids - scrapped_move_ids - if move_ids: - if len(move_ids) == 1 and ( - not returned_move_ids and not scrapped_move_ids - ): - line.qty_delivered = move_ids.quantity_done - # check for duplication moves and use latest one - elif len(move_ids) > 1 and ( - returned_move_ids and not scrapped_move_ids - ): - move_ids = move_ids.sorted("create_date") - line.qty_delivered = move_ids[-1].quantity_done - else: - if returned_move_ids: - returned_move_ids = returned_move_ids.sorted("create_date") - if not move_ids_to_be_processed or ( - move_ids_to_be_processed - and (returned_move_ids in move_ids_to_be_processed) - ): - line.qty_delivered = ( - line.qty_delivered - - returned_move_ids[-1].quantity_done - ) - elif scrapped_move_ids: - if scrapped_move_ids in move_ids_to_be_processed: - line.qty_delivered = ( - line.qty_delivered - - scrapped_move_ids[-1].quantity_done - ) - continue - # Filter scrapped move_ids - scrapped_lot_ids = line.equipment_request_id.picking_ids.move_ids.filtered( - lambda x: x.scrapped - ).lot_ids - if scrapped_lot_ids: - line.lot_ids = line.lot_ids.filtered( - lambda x: x not in scrapped_lot_ids - ) - lot_ids_qty_info = { - lot_id.id: self.env["stock.quant"]._get_available_quantity( - line.product_id, - line.location_id, - lot_id, - ) - for lot_id in line.lot_ids - } - # Filter moves which are moved to PPE location - moves_to_be_checked = line.move_ids.filtered( - lambda r: r.state == "done" - and line.product_id == r.product_id - and r.location_dest_id == line.location_id - and not r.origin_returned_move_id - and r.lot_ids not in scrapped_lot_ids - ) - # check and remove moves with same lots - # if same lots exists among filtered moves - lots_to_be_filtered = self.env["stock.move"] - for lot in moves_to_be_checked.mapped("lot_ids"): - move_with_same_lots = moves_to_be_checked.filtered( - lambda x: lot in x.lot_ids - ) - if len(move_with_same_lots) > 1: - lot_records = move_with_same_lots.sorted("create_date") - lots_to_be_filtered |= lot_records[:-1] - moves_to_be_checked -= lots_to_be_filtered - for move in moves_to_be_checked: - qty += sum([lot_ids_qty_info.get(lot.id, 0) for lot in move.lot_ids]) + dest_location = line.location_id + for move in line.move_ids.filtered( + lambda move: move.state == "done" and move.product_id == line.product_id + ): + moved_qty = move.quantity_done + if move.location_dest_id == dest_location: + qty += moved_qty + elif move.location_id == dest_location: + qty -= moved_qty line.qty_delivered = qty @api.depends( @@ -144,49 +64,26 @@ def _compute_qty_delivered(self): ) def _compute_lot_ids(self): for line in self: - available_lot_ids = {} - returned_move_ids = line.move_ids.filtered( - lambda x: x.product_id == line.product_id - and x.quantity_done - and x.origin_returned_move_id - ) - moves_to_be_checked = line.move_ids - returned_move_ids - for move_id in moves_to_be_checked: - if move_id.lot_ids: - lot_infos = self.env["stock.quant"].read_group( - domain=[ - ("product_id", "=", line.product_id.id), - ("location_id", "child_of", line.location_id.id), - ("lot_id", "in", move_id.lot_ids.ids), - ], - fields=["lot_id", "quantity: sum"], - groupby=["lot_id"], - ) - available_lot_ids.update( - { - lot_info["lot_id"][0]: lot_info["quantity"] - for lot_info in lot_infos - if lot_info["lot_id"] - and lot_info["quantity"] not in (0.0, -1.0) - } - ) - lot_ids = available_lot_ids.keys() - if lot_ids: - returned_lots = returned_move_ids.mapped("lot_ids") - actual_move_lots = moves_to_be_checked.mapped("lot_ids") - # Remove returned lots from available lots in location - # when same lot_id used in different PPE request - if len(returned_move_ids) and ( - len(returned_lots) != len(actual_move_lots) - ): - lot_ids = [ - x - for x in available_lot_ids.keys() - if x not in returned_move_ids.mapped("lot_ids").ids - ] - line.lot_ids = [(6, 0, lot_ids)] - else: - line.lot_ids = False + qty_by_lot = {} + dest_location = line.location_id + for move in line.move_ids.filtered( + lambda move: move.state == "done" and move.product_id == line.product_id + ): + for move_line in move.move_line_ids: + lot = move_line.lot_id + if lot not in qty_by_lot: + qty_by_lot[lot] = 0 + moved_qty = move_line.qty_done + if move.location_dest_id == dest_location: + qty_by_lot[lot] += moved_qty + elif move.location_id == dest_location: + qty_by_lot[lot] -= moved_qty + + lots = self.env["stock.lot"].browse() + for lot, moved in qty_by_lot.items(): + if moved > 0: + lots |= lot + line.lot_ids = lots def _skip_procurement(self): return self.product_id.type not in ("consu", "product") From 3166678b0562db739364a605c2c99cacb4458c3f Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 31 Aug 2026 11:02:52 +0200 Subject: [PATCH 4/4] with scrap --- .../models/hr_personal_equipment.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/hr_personal_equipment_stock/models/hr_personal_equipment.py b/hr_personal_equipment_stock/models/hr_personal_equipment.py index 75c1441e482..548df98d083 100644 --- a/hr_personal_equipment_stock/models/hr_personal_equipment.py +++ b/hr_personal_equipment_stock/models/hr_personal_equipment.py @@ -44,14 +44,21 @@ def _compute_skip_procurement(self): "move_ids.scrapped", "move_ids.product_uom_qty", "move_ids.product_uom", + "equipment_request_id.picking_ids.move_ids.scrapped", + "equipment_request_id.picking_ids.move_ids.state", ) def _compute_qty_delivered(self): for line in self: qty = 0.0 dest_location = line.location_id - for move in line.move_ids.filtered( + moves = line.move_ids.filtered( lambda move: move.state == "done" and move.product_id == line.product_id - ): + ) + line.equipment_request_id.picking_ids.move_ids.filtered( + lambda move: move.scrapped + and move.state == "done" + and move.product_id == line.product_id + ) + for move in moves: moved_qty = move.quantity_done if move.location_dest_id == dest_location: qty += moved_qty @@ -61,14 +68,21 @@ def _compute_qty_delivered(self): @api.depends( "move_ids.lot_ids", + "equipment_request_id.picking_ids.move_ids.scrapped", + "equipment_request_id.picking_ids.move_ids.state", ) def _compute_lot_ids(self): for line in self: qty_by_lot = {} dest_location = line.location_id - for move in line.move_ids.filtered( + moves = line.move_ids.filtered( lambda move: move.state == "done" and move.product_id == line.product_id - ): + ) + line.equipment_request_id.picking_ids.move_ids.filtered( + lambda move: move.scrapped + and move.state == "done" + and move.product_id == line.product_id + ) + for move in moves: for move_line in move.move_line_ids: lot = move_line.lot_id if lot not in qty_by_lot: