diff --git a/__init__.py b/__init__.py index ac48d54..930ac5c 100644 --- a/__init__.py +++ b/__init__.py @@ -1,13 +1,16 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from trytond.pool import Pool +from . import ir from . import stock from . import product def register(): Pool.register( + stock.Lot, stock.Move, product.Template, product.Product, + ir.Date, module='stock_lot_fifo', type_='model') diff --git a/ir.py b/ir.py new file mode 100644 index 0000000..b03f8a9 --- /dev/null +++ b/ir.py @@ -0,0 +1,17 @@ +# The COPYRIGHT file at the top level of this repository contains the full +# copyright notices and license terms. +from datetime import date + +from trytond.pool import PoolMeta +from trytond.transaction import Transaction + + +class Date(metaclass=PoolMeta): + __name__ = 'ir.date' + + @classmethod + def today(cls, timezone=None): + fifo_assign_date = Transaction().context.get('fifo_assign_date') + if isinstance(fifo_assign_date, date): + return fifo_assign_date + return super().today(timezone=timezone) diff --git a/stock.py b/stock.py index 19a830f..f64eefd 100644 --- a/stock.py +++ b/stock.py @@ -2,26 +2,10 @@ # copyright notices and license terms. from trytond.pool import Pool, PoolMeta from trytond.transaction import Transaction -from trytond.pyson import Id -class Move(metaclass=PoolMeta): - __name__ = 'stock.move' - - @property - def fifo_search_context(self): - return { - 'stock_date_end': self.effective_date or self.planned_date, - 'locations': [self.from_location.id], - 'stock_assign': True, - 'forecast': False, - } - - @property - def fifo_search_domain(self): - return [ - ('product', '=', self.product.id), - ] +class Lot(metaclass=PoolMeta): + __name__ = 'stock.lot' @staticmethod def _get_fifo_search_order_by(): @@ -37,6 +21,10 @@ class Move(metaclass=PoolMeta): order.append(('create_date', 'ASC')) return order + +class Move(metaclass=PoolMeta): + __name__ = 'stock.move' + def check_lot(self): if (not self.product.lot_force_assign or self.from_location.type == 'supplier'): @@ -44,65 +32,52 @@ class Move(metaclass=PoolMeta): @classmethod def assign_try(cls, moves, with_childs=True, grouping=('product',)): - ''' - If lots required assign lots in FIFO before assigning move. - ''' + not_lot_moves = [] + date2lot_moves = {} + for move in moves: + if move.lot or move.product.lot_is_required(move.from_location, + move.to_location): + date2lot_moves.setdefault( + move.effective_date or move.planned_date, []).append(move) + else: + not_lot_moves.append(move) + + not_lot_success, lot_success = True, True + if not_lot_moves: + not_lot_success = super().assign_try( + not_lot_moves, with_childs=with_childs, grouping=grouping) + + grouping = grouping + ('lot',) + # TODO 6.2: Grouping not necessary in context. + for date_, lot_moves in date2lot_moves.items(): + with Transaction().set_context( + assign_grouping=grouping, fifo_assign_date=date_): + lot_success &= super().assign_try(lot_moves, + with_childs=with_childs, grouping=grouping) + + return not_lot_success & lot_success + + def pick_product(self, quantities): + # TODO 6.2: Move this sorting to the method created for it + # def sort_quantities(self, quantities, locations, grouping): pool = Pool() - Uom = pool.get('product.uom') Lot = pool.get('stock.lot') - new_moves = [] - lots_by_product = {} - consumed_quantities = {} - order = cls._get_fifo_search_order_by() + transaction = Transaction() + grouping = transaction.context.get('assign_grouping') + if not quantities or not grouping: + return super().pick_product(quantities) - for move in moves: - if (not move.lot and move.product.lot_is_required( - move.from_location, move.to_location)): - if move.product.id not in lots_by_product: - with Transaction().set_context(move.fifo_search_context): - lots_by_product[move.product.id] = [x for x in - Lot.search(move.fifo_search_domain, order=order) - if x.quantity > 0] + lot_ids = set([key[2] for key, _ in quantities if key[2]]) + lot_id2order = {None: len(lot_ids)} + if lot_ids: + cursor = transaction.connection.cursor() + cursor.execute(*Lot.search([ + ('id', 'in', lot_ids) + ], order=Lot._get_fifo_search_order_by(), query=True)) + lot_id2order.update({row[0]: order + for order, row in enumerate(list(cursor.fetchall()))}) + quantities = sorted( + quantities, key=lambda key_qty: lot_id2order.get(key_qty[0][2])) - lots = lots_by_product[move.product.id] - remainder = move.internal_quantity - while lots and remainder > 0.0: - lot = lots.pop(0) - production_quantity = 0.0 - if getattr(move, 'production_input', False): - for production_input in move.production_input.inputs: - if (production_input.product == move.product - and production_input.state == 'draft' - and lot == production_input.lot): - production_quantity += production_input.quantity - consumed_quantities.setdefault(lot.id, production_quantity) - lot_quantity = lot.quantity - consumed_quantities[lot.id] - if not lot_quantity > 0.0: - continue - assigned_quantity = min(lot_quantity, remainder) - if assigned_quantity == remainder: - move.quantity = Uom.compute_qty( - move.product.default_uom, assigned_quantity, - move.uom) - move.lot = lot - move.save() - lots.insert(0, lot) - else: - quantity = Uom.compute_qty( - move.product.default_uom, assigned_quantity, - move.uom) - new_moves.extend(cls.copy([move], { - 'lot': lot.id, - 'quantity': quantity, - })) - - consumed_quantities[lot.id] += assigned_quantity - remainder -= assigned_quantity - if not lots: - move.quantity = Uom.compute_qty(move.product.default_uom, - remainder, move.uom) - move.save() - lots_by_product[move.product.id] = lots - return super(Move, cls).assign_try(new_moves + moves, - with_childs=with_childs, grouping=grouping) + return super().pick_product(quantities)