From 914d87ff78435a632b0505d33afc4d657d0cddc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 23 Aug 2023 19:04:20 +0200 Subject: [PATCH] Check inventory lot quantities of supplier shipment Closes #4545 --- modules/stock_lot/CHANGELOG | 1 + modules/stock_lot/stock.py | 56 ++++++++++- .../tests/scenario_stock_lot_shipment_in.rst | 97 +++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 modules/stock_lot/tests/scenario_stock_lot_shipment_in.rst diff --git a/modules/stock_lot/CHANGELOG b/modules/stock_lot/CHANGELOG index 8b2ffbe81b..d7d162b97e 100644 --- a/modules/stock_lot/CHANGELOG +++ b/modules/stock_lot/CHANGELOG @@ -1,3 +1,4 @@ +* Check inventory lot quantities of supplier shipment Version 6.8.0 - 2023-05-01 -------------------------- diff --git a/modules/stock_lot/stock.py b/modules/stock_lot/stock.py index 4088c7e5ae..2aa60a6118 100644 --- a/modules/stock_lot/stock.py +++ b/modules/stock_lot/stock.py @@ -1,6 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime +from collections import defaultdict from copy import copy from functools import wraps @@ -13,6 +14,7 @@ from trytond.model import ( from trytond.model.exceptions import ( AccessError, RequiredValidationError, ValidationError) from trytond.modules.stock import StockMixin +from trytond.modules.stock.exceptions import ShipmentCheckQuantityWarning from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Len from trytond.tools import grouped_slice @@ -653,7 +655,59 @@ class MoveAddLotsStartLot(ModelView, LotMixin): } -class ShipmentIn(metaclass=PoolMeta): +class ShipmentCheckQuantity: + "Check quantities per lot between source and target moves" + __slots__ = () + + def check_quantity(self): + pool = Pool() + Warning = pool.get('res.user.warning') + Lang = pool.get('ir.lang') + lang = Lang.get() + + super().check_quantity() + + products_with_lot = set() + source_qties = defaultdict(float) + for move in self._check_quantity_source_moves: + if move.lot: + products_with_lot.add(move.product) + source_qties[move.lot] += move.internal_quantity + + target_qties = defaultdict(float) + for move in self._check_quantity_target_moves: + if move.lot: + target_qties[move.lot] += move.internal_quantity + + diffs = {} + for lot, incoming_qty in target_qties.items(): + if (lot not in source_qties + and lot.product not in products_with_lot): + continue + unit = lot.product.default_uom + incoming_qty = unit.round(incoming_qty) + inventory_qty = unit.round(source_qties.pop(lot, 0)) + diff = inventory_qty - incoming_qty + if diff: + diffs[lot] = diff + + if diffs: + warning_name = Warning.format( + 'check_quantity_lot', [self]) + if Warning.check(warning_name): + quantities = [] + for lot, quantity in diffs.items(): + quantity = lang.format_number_symbol( + quantity, lot.product.default_uom) + quantities.append(f"{lot.rec_name}: {quantity}") + raise ShipmentCheckQuantityWarning(warning_name, + gettext( + 'stock.msg_shipment_check_quantity', + shipment=self.rec_name, + quantities=', '.join(quantities))) + + +class ShipmentIn(ShipmentCheckQuantity, metaclass=PoolMeta): __name__ = 'stock.shipment.in' def _get_inventory_move(self, incoming_move): diff --git a/modules/stock_lot/tests/scenario_stock_lot_shipment_in.rst b/modules/stock_lot/tests/scenario_stock_lot_shipment_in.rst new file mode 100644 index 0000000000..160d6d7434 --- /dev/null +++ b/modules/stock_lot/tests/scenario_stock_lot_shipment_in.rst @@ -0,0 +1,97 @@ +============================== +Stock Lot Shipment In Scenario +============================== + +Imports:: + + >>> from decimal import Decimal + >>> from proteus import Model + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import ( + ... create_company, get_company) + +Activate modules:: + + >>> config = activate_modules('stock_lot') + + >>> Location = Model.get('stock.location') + >>> Lot = Model.get('stock.lot') + >>> Party = Model.get('party.party') + >>> ProductTemplate = Model.get('product.template') + >>> ProductUom = Model.get('product.uom') + >>> ShipmentIn = Model.get('stock.shipment.in') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create supplier:: + + >>> supplier = Party(name='Supplier') + >>> supplier.save() + +Create product:: + + >>> unit, = ProductUom.find([('name', '=', "Unit")]) + >>> template = ProductTemplate() + >>> template.name = "Product" + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.list_price = Decimal('20') + >>> template.save() + >>> product, = template.products + +Get stock locations:: + + >>> supplier_loc, = Location.find([('code', '=', 'SUP')]) + >>> input_loc, = Location.find([('code', '=', 'IN')]) + >>> storage_loc, = Location.find([('code', '=', 'STO')]) + +Set 2 lots:: + + >>> lot1 = Lot(number="1", product=product) + >>> lot1.save() + >>> lot2 = Lot(number="2", product=product) + >>> lot2.save() + +Create a shipment:: + + >>> shipment = ShipmentIn() + >>> shipment.supplier = supplier + >>> move = shipment.incoming_moves.new() + >>> move.product = product + >>> move.quantity = 10 + >>> move.from_location = supplier_loc + >>> move.to_location = input_loc + >>> move.unit_price = Decimal('5') + >>> move.currency = company.currency + >>> shipment.save() + +Receive the shipment with one lot:: + + >>> incoming_move, = shipment.incoming_moves + >>> incoming_move.lot = lot1 + >>> shipment.click('receive') + >>> shipment.state + 'received' + +Change lot and try to finish:: + + >>> incoming_move, = shipment.inventory_moves + >>> incoming_move.lot == lot1 + True + >>> incoming_move.lot = lot2 + >>> shipment.click('done') # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ShipmentCheckQuantityWarning: ... + >>> incoming_move.reload() + >>> incoming_move.lot = lot1 + >>> incoming_move.save() + +Finish the shipment:: + + >>> shipment.click('done') + >>> shipment.state + 'done'