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..548df98d083 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): @@ -41,18 +44,61 @@ 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 - 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 - ) + dest_location = line.location_id + 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 + elif move.location_id == dest_location: + qty -= moved_qty line.qty_delivered = qty + @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 + 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: + 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") 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..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 @@ -1,8 +1,10 @@ # 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 +from odoo.tests import Form, TransactionCase class TestHRPersonalEquipment(TransactionCase): @@ -162,15 +164,132 @@ 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): + 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") + # 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] @@ -188,24 +307,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 @@ +