# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from collections import defaultdict from trytond.i18n import gettext from trytond.model import ModelView, Workflow, fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval from .exceptions import LotUnitQuantityError class LotUnitMixin: __slots__ = () unit = fields.Many2One( 'product.uom', "Unit", domain=[ ('category', '=', Eval('product_default_uom_category', -1)), ], help="The biggest unit for the lot.") unit_quantity = fields.Float( "Unit Quantity", digits='unit', states={ 'required': Bool(Eval('unit')), 'invisible': ~Eval('unit'), }, help="The maximal quantity for the lot.") product_default_uom_category = fields.Function( fields.Many2One( 'product.uom.category', "Default Product UoM Category"), 'on_change_with_product_default_uom_category') @fields.depends('product', methods=['on_change_unit']) def on_change_product(self): try: super().on_change_product() except AttributeError: pass if self.product and self.product.lot_uom: self.unit = self.product.lot_uom self.on_change_unit() @fields.depends('unit') def on_change_unit(self): if self.unit: self.unit_quantity = self.unit.rounding @fields.depends('product') def on_change_with_product_default_uom_category(self, name=None): if self.product: return self.product.default_uom_category class Lot(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.lot' @classmethod def __setup__(cls): super().__setup__() cls._modify_no_move += [ ('unit', 'done', 'stock_lot_unit.msg_change_unit'), ('unit_quantity', 'done', 'stock_lot_unit.msg_change_unit_quantity'), ] class MoveAddLotsStartLot(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.move.add.lots.start.lot' @fields.depends( 'unit', 'unit_quantity', 'quantity', 'parent', '_parent_parent.unit') def _set_lot_values(self, lot): pool = Pool() Uom = pool.get('product.uom') super()._set_lot_values(lot) self.unit = lot.unit self.unit_quantity = lot.unit_quantity if self.parent.unit: maximum_quantity = Uom.compute_qty( lot.unit, lot.unit_quantity, self.parent.unit, round=False) if self.quantity > maximum_quantity: self.quantity = self.parent.unit.round(maximum_quantity) def _get_lot_values(self, move): values = super()._get_lot_values(move) values['unit'] = self.unit values['unit_quantity'] = self.unit_quantity return values class Move(metaclass=PoolMeta): __name__ = 'stock.move' def check_lot(self): pool = Pool() UoM = pool.get('product.uom') super().check_lot() if (self.state == 'done' and self.lot and self.lot.unit): quantity = UoM.compute_qty( self.unit, self.quantity, self.lot.unit, round=False) if quantity > self.lot.unit_quantity: raise LotUnitQuantityError( gettext('stock_lot_unit.msg_lot_unit_quantity_greater', quantity=self.lot.unit_quantity, unit=self.lot.unit.symbol, lot=self.lot.rec_name, name=self.rec_name)) class Inventory(metaclass=PoolMeta): __name__ = 'stock.inventory' @classmethod @ModelView.button @Workflow.transition('done') def confirm(cls, inventories): for inventory in inventories: for line in inventory.lines: if (line.quantity is not None and line.lot and line.lot.unit and line.quantity > line.lot.unit_quantity): raise LotUnitQuantityError( gettext('stock_lot_unit.msg_lot_unit_quantity_greater', quantity=line.lot.unit_quantity, unit=line.lot.unit.symbol, lot=line.lot.rec_name, name=line.rec_name)) super().confirm(inventories) class InventoryCount(metaclass=PoolMeta): __name__ = 'stock.inventory.count' def default_quantity(self, fields): pool = Pool() InventoryLine = pool.get('stock.inventory.line') UoM = pool.get('product.uom') values = super().default_quantity(fields) line = InventoryLine(values['line']) if line.lot and line.lot.unit: values['quantity'] = UoM.compute_qty( line.lot.unit, line.lot.unit_quantity, line.unit) return values class LotUnitMixin(object): __slots__ = () _lot_unit_moves = None @classmethod def validate(cls, shipments): pool = Pool() UoM = pool.get('product.uom') super().validate(shipments) for shipment in shipments: for move_attribute in cls._lot_unit_moves: lot_quantities = defaultdict(int) for move in getattr(shipment, move_attribute): if move.state != 'done': continue if not move.lot or not move.lot.unit: continue lot_quantities[move.lot] += UoM.compute_qty( move.unit, move.quantity, move.lot.unit, round=False) for lot, quantity in lot_quantities.items(): if quantity > lot.unit_quantity: raise LotUnitQuantityError( gettext('stock_lot_unit' '.msg_lot_unit_quantity_greater', quantity=lot.unit_quantity, unit=lot.unit.symbol, lot=lot.rec_name, name=shipment.rec_name)) class ShipmentIn(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.in' _lot_unit_moves = ['incoming_moves', 'inventory_moves'] class ShipmentInReturn(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.in.return' _lot_unit_moves = ['moves'] class ShipmentOut(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.out' _lot_unit_moves = ['outgoing_moves', 'inventory_moves'] class ShipmentOutReturn(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.out.return' _lot_unit_moves = ['incoming_moves', 'inventory_moves'] class ShipmentInternal(LotUnitMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.internal' _lot_unit_moves = ['incoming_moves', 'outgoing_moves'] class Production(LotUnitMixin, metaclass=PoolMeta): __name__ = 'production' _lot_unit_moves = ['inputs', 'outputs']