diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 7c631366..216c5b24 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -60,12 +60,17 @@ def _get_price_unit(self): [ ("product_id", "=", self.product_id.id), ("lot_id", "=", self.lot_ids.id), + "|", ("qty_consumed", ">", 0), + ("qty_remaining", ">", 0), ("company_id", "=", self.company_id.id), ], order="id desc", limit=1, ) if move_line: - return move_line.value_consumed / move_line.qty_consumed + if move_line.qty_consumed: + return move_line.value_consumed / move_line.qty_consumed + else: + return move_line.value_remaining / move_line.qty_remaining return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index f7725ccb..4ef0e97d 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -50,6 +50,21 @@ def _compute_remaining_value(self): "internal", "transit", ) or rec.location_dest_usage not in ("internal", "transit"): + layers = rec.move_id.stock_valuation_layer_ids + remaining_qty_layers = layers.filtered(lambda l: l.remaining_qty > 0) + if not remaining_qty_layers: + rec.qty_remaining = 0 + rec.value_remaining = 0 + continue + rec.qty_remaining = rec.product_uom_id._compute_quantity( + sum(remaining_qty_layers.mapped("remaining_qty")), + rec.product_id.uom_id, + ) + rec.value_remaining = ( + sum(remaining_qty_layers.mapped("remaining_value")) + * sum(remaining_qty_layers.mapped("remaining_qty")) + / rec.qty_remaining + ) continue rec.qty_remaining = rec.qty_done - rec.qty_consumed layers = rec.move_id.stock_valuation_layer_ids diff --git a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py index d0b32828..7c69f666 100644 --- a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -1,7 +1,6 @@ # Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - from odoo.tests.common import Form, TransactionCase @@ -30,111 +29,379 @@ def setUpClass(cls): cls.picking_type_in = cls.env.ref("stock.picking_type_in") cls.picking_type_out = cls.env.ref("stock.picking_type_out") - def create_picking(self, location, location_dest, picking_type): - return self.env["stock.picking"].create( + def create_picking( + self, + location, + location_dest, + picking_type, + lot_numbers, + price_unit=0.0, + is_receipt=True, + ): + picking = self.env["stock.picking"].create( { "location_id": location.id, "location_dest_id": location_dest.id, "picking_type_id": picking_type.id, } ) - - def create_stock_move(self, picking, product, price=0.0): move = self.env["stock.move"].create( { - "name": "Move", - "product_id": product.id, + "name": "Test", + "product_id": self.product.id, "location_id": picking.location_id.id, "location_dest_id": picking.location_dest_id.id, - "product_uom": product.uom_id.id, + "product_uom": self.product.uom_id.id, "product_uom_qty": 5.0, "picking_id": picking.id, } ) - if price: - move.write({"price_unit": price}) - return move + if price_unit: + move.write({"price_unit": price_unit}) - def create_stock_move_line(self, move, picking, lot_name=False): - move_line = self.env["stock.move.line"].create( - { - "move_id": move.id, - "picking_id": picking.id, - "product_id": move.product_id.id, - "location_id": move.location_id.id, - "location_dest_id": move.location_dest_id.id, - "product_uom_id": move.product_uom.id, - "qty_done": move.product_uom_qty, - "lot_name": lot_name, - } + for lot in lot_numbers: + move_line = self.env["stock.move.line"].create( + { + "move_id": move.id, + "picking_id": picking.id, + "product_id": self.product.id, + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + "product_uom_id": move.product_uom.id, + "qty_done": move.product_uom_qty, + } + ) + if is_receipt: + move_line.lot_name = lot + else: + lot = self.env["stock.lot"].search( + [("product_id", "=", self.product.id), ("name", "=", lot)], limit=1 + ) + move_line.lot_id = lot.id + picking.action_confirm() + picking.action_assign() + picking._action_done() + return picking, move + + def transfer_return(self, original_picking, return_qty): + """Handles product return for a given picking""" + return_picking_wizard_form = Form( + self.env["stock.return.picking"].with_context( + active_ids=original_picking.ids, + active_id=original_picking.id, + active_model="stock.picking", + ) + ) + return_picking_wizard = return_picking_wizard_form.save() + return_picking_wizard.product_return_moves.write({"quantity": return_qty}) + return_picking_wizard_action = return_picking_wizard.create_returns() + return_picking = self.env["stock.picking"].browse( + return_picking_wizard_action["res_id"] ) - return move_line + return_move = return_picking.move_ids + return_move.move_line_ids.qty_done = return_qty + return_picking.button_validate() + return return_move def test_stock_valuation_fifo_lot(self): - receipt_picking_1 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for the first receipt should be 500.0", ) - move = self.create_stock_move(receipt_picking_1, self.product, 100) - self.create_stock_move_line(move, receipt_picking_1, "11111") - receipt_picking_1.action_confirm() - receipt_picking_1.action_assign() - receipt_picking_1._action_done() - self.assertEqual(move.stock_valuation_layer_ids.value, 500) + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for the second receipt should be 1000.0", + ) + self.assertEqual( + self.product.standard_price, + 100.0, + "Standard price should be set to 100.0 after first receipt", + ) + + # Create delivery for lot 002 + _, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out.stock_valuation_layer_ids.value), + 1000.0, + "Stock valuation for the delivery should be 1000.0", + ) - receipt_picking_2 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + # Test return receipt + receipt_picking_3, move = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["003"], + 300.0, + ) + self.assertEqual( + move.stock_valuation_layer_ids.value, + 1500.0, + "Stock valuation for the third receipt should be 1500.0", ) - move = self.create_stock_move(receipt_picking_2, self.product, 200) - self.create_stock_move_line(move, receipt_picking_2, "22222") - receipt_picking_2.action_confirm() - receipt_picking_2.action_assign() - receipt_picking_2._action_done() - self.assertEqual(move.stock_valuation_layer_ids.value, 1000) - self.assertEqual(self.product.standard_price, 100) + return_move = self.transfer_return(receipt_picking_3, 5.0) + self.assertEqual( + abs(return_move.stock_valuation_layer_ids.value), + 1500.0, + "Stock valuation after return should be 1500.0", + ) - delivery_picking1 = self.create_picking( - self.stock_location, self.customer_location, self.picking_type_out + def test_receive_deliver_return_lot(self): + receipt_picking, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001", "002", "003"], + 100.0, ) - move = self.create_stock_move(delivery_picking1, self.product) - move_line = self.create_stock_move_line(move, delivery_picking1) - lot_id = self.env["stock.lot"].search( - [("name", "=", "22222"), ("product_id", "=", self.product.id)] + self.assertEqual(len(receipt_picking.move_line_ids), 3) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 1500.0, + "Stock valuation for multiple receipts should be 1500.0", ) - move_line.write({"lot_id": lot_id}) + self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 15.0) - delivery_picking1.action_confirm() - delivery_picking1.action_assign() - delivery_picking1._action_done() - self.assertEqual(abs(move.stock_valuation_layer_ids.value), 1000) + # Deliver lot 002 + delivery_picking, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for delivery of lot 002 should be 500.0", + ) + self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 10.0) - # Test return delivery - receipt_picking_3 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + # Return lot 002 + move_in_2 = self.transfer_return(delivery_picking, 5.0) + self.assertEqual( + move_in_2.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for returned lot 002 should be 500.0", ) - move = self.create_stock_move(receipt_picking_3, self.product, 300) - self.create_stock_move_line(move, receipt_picking_3, "33333") + self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 5.0) - receipt_picking_3.action_confirm() - receipt_picking_3.action_assign() - receipt_picking_3._action_done() - self.assertEqual(move.stock_valuation_layer_ids.value, 1500) + # Deliver lot 002 again + _, move_out_2 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out_2.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for second delivery of lot 002 should be 500.0", + ) + self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 0.0) - return_picking_wizard_form = Form( - self.env["stock.return.picking"].with_context( - active_ids=receipt_picking_3.ids, - active_id=receipt_picking_3.id, - active_model="stock.picking", - ) + def test_cost_tracking_by_lot(self): + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, ) - return_picking_wizard = return_picking_wizard_form.save() - return_picking_wizard.product_return_moves.write({"quantity": 5}) - return_picking_wizard_action = return_picking_wizard.create_returns() - return_picking = self.env["stock.picking"].browse( - return_picking_wizard_action["res_id"] + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for lot 001 should be 500.0", + ) + + _, move_in_2 = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + self.assertEqual( + move_in_2.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for lot 002 should be 1000.0", + ) + + # Deliver lot 001 + self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) + + # Deliver lot 002 + delivery_picking_2, _ = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + + # Return lot 002 + move_in = self.transfer_return(delivery_picking_2, 5.0) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for returned lot 002 should be 1000.0", + ) + + def test_change_qty_done_in_done_move_line(self): + receipt_picking, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 500.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 2500.0, + "Stock valuation for the first receipt should be 2500.0", + ) + # Change qty_done of the move line (increase) + move_line = receipt_picking.move_line_ids[0] + move_line.qty_done += 2.0 + self.assertEqual( + move_line.qty_done, + 7.0, + "The qty_done of the incoming move line should be increased to 7.0", + ) + self.assertEqual( + sum(move_in.stock_valuation_layer_ids.mapped("value")), + 3500.0, + "Stock valuation should reflect the increased qty_done", + ) + + # Change qty_done of the move line (decrease) + move_line.qty_done -= 1.0 + self.assertEqual( + move_line.qty_done, + 6.0, + "The qty_done of the incoming move line should be decreased to 6.0", + ) + self.assertEqual( + sum(move_in.stock_valuation_layer_ids.mapped("value")), + 3000.0, + "Stock valuation should reflect the decreased qty_done", + ) + + delivery_picking, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) + self.assertEqual( + abs(sum(move_out.stock_valuation_layer_ids.mapped("value"))), + 2500.0, + "Stock valuation for the outgoing should be 2500.0", + ) + + # Change qty_done of the move line (increase) + move_line = delivery_picking.move_line_ids[0] + move_line.qty_done -= 2.0 + self.assertEqual( + move_line.qty_done, + 3.0, + "The qty_done of the outgoing move line should be decreased to 3.0", + ) + self.assertEqual( + abs(sum(move_out.stock_valuation_layer_ids.mapped("value"))), + 1500.0, + "Stock valuation should reflect the decreased qty_done", + ) + + # Change qty_done of the move line (decrease) + move_line.qty_done += 1.0 + self.assertEqual( + move_line.qty_done, + 4.0, + "The qty_done of the outgoing move line should be increased to 4.0", + ) + self.assertEqual( + abs(sum(move_out.stock_valuation_layer_ids.mapped("value"))), + 2000.0, + "Stock valuation should reflect the increased qty_done", + ) + + def test_inventory_adjustment_after_multiple_receipts(self): + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for lot 0001 should be 500.0", + ) + + _, move_in_2 = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + self.assertEqual( + move_in_2.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for lot 0002 should be 1000.0", + ) + lot = self.env["stock.lot"].search( + [("name", "=", "002"), ("product_id", "=", self.product.id)], limit=1 + ) + inventory_quant = self.env["stock.quant"].search( + [ + ("location_id", "=", self.stock_location.id), + ("product_id", "=", self.product.id), + ("lot_id", "=", lot.id), + ] + ) + inventory_quant.inventory_quantity = 10.0 + inventory_quant.action_apply_inventory() + move = self.env["stock.move"].search( + [("product_id", "=", self.product.id), ("is_inventory", "=", True)], + limit=1, + ) + self.assertEqual( + move.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for lot 002 should be 1000.0", ) - return_move = return_picking.move_ids - return_move.move_line_ids.qty_done = 5 - return_picking.button_validate() - self.assertEqual(abs(return_move.stock_valuation_layer_ids.value), 1500)