diff --git a/ir/ui/form.rnc b/ir/ui/form.rnc index 82e04ca..6e34bb0 100755 --- a/ir/ui/form.rnc +++ b/ir/ui/form.rnc @@ -73,6 +73,7 @@ attlist.field &= | "text" | "time" | "timedelta" + | "tolerance_gauge" | "url" }? attlist.field &= attribute fill { "0" | "1" }? @@ -122,6 +123,12 @@ attlist.field &= attribute symbol { text }? attlist.field &= [a:defaultValue = "1"] attribute grouping { "0" | "1" }? attlist.field &= [a:defaultValue = "square"] attribute border { "square" | "circle" | "rounded" }? attlist.field &= attribute loading { "lazy" | "eager" }? +attlist.field &= attribute min { text }? +attlist.field &= attribute max { text }? +attlist.field &= attribute min_field { text }? +attlist.field &= attribute max_field { text }? +attlist.field &= attribute center { text }? +attlist.field &= attribute digits { text }? image = element image { attlist.image, empty } attlist.image &= attribute name { text } attlist.image &= [a:defaultValue = "icon"] attribute type { "icon" | "url" }? diff --git a/ir/ui/form.rng b/ir/ui/form.rng index aca91af..7abb2f9 100755 --- a/ir/ui/form.rng +++ b/ir/ui/form.rng @@ -251,6 +251,7 @@ text time timedelta + tolerance_gauge url @@ -577,6 +578,54 @@ + + + + min + + + + + + + + max + + + + + + + + min_field + + + + + + + + max_field + + + + + + + + center + + + + + + + + digits + + + + diff --git a/ir/ui/tree.rnc b/ir/ui/tree.rnc index 9024f9b..1f10fb6 100755 --- a/ir/ui/tree.rnc +++ b/ir/ui/tree.rnc @@ -42,6 +42,7 @@ attlist.field &= | "text" | "time" | "timedelta" + | "tolerance_gauge" | "url" }? attlist.field &= @@ -75,6 +76,12 @@ attlist.field &= attribute help_field { text }? attlist.field &= attribute view_ids { text }? attlist.field &= attribute symbol { text }? attlist.field &= [a:defaultValue = "1"] attribute grouping { "0" | "1" }? +attlist.field &= attribute min { text }? +attlist.field &= attribute max { text }? +attlist.field &= attribute min_field { text }? +attlist.field &= attribute max_field { text }? +attlist.field &= attribute center { text }? +attlist.field &= attribute digits { text }? prefix = element prefix { attlist.affix, empty } suffix = element suffix { attlist.affix, empty } attlist.affix &= attribute string { text }? diff --git a/ir/ui/tree.rng b/ir/ui/tree.rng index 96be576..4e15e90 100755 --- a/ir/ui/tree.rng +++ b/ir/ui/tree.rng @@ -139,6 +139,7 @@ text time timedelta + tolerance_gauge url @@ -330,6 +331,54 @@ + + + + min + + + + + + + + max + + + + + + + + min_field + + + + + + + + max_field + + + + + + + + center + + + + + + + + digits + + + + prefix diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 611c68b..e2f82e4 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -273,11 +273,17 @@ class Purchase(metaclass=PoolMeta): to_location = fields.Many2One('stock.location', 'To location', required=True,domain=[('type', "!=", 'supplier')]) shipment_in = fields.Many2One('stock.shipment.in','Purchases') broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])]) - tol_min = fields.Numeric("Tol - in %", required=True) - tol_max = fields.Numeric("Tol + in %", required=True) - tol_min_qt = fields.Numeric("Tol -") - tol_max_qt = fields.Numeric("Tol +") - certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),}) + tol_min = fields.Numeric("Tol - in %", required=True) + tol_max = fields.Numeric("Tol + in %", required=True) + tol_min_qt = fields.Numeric("Tol -") + tol_max_qt = fields.Numeric("Tol +") + tolerance_used = fields.Function( + fields.Numeric("Tolerance used"), 'get_tolerance_used') + tolerance_min = fields.Function( + fields.Numeric("Tolerance min"), 'get_tolerance_min') + tolerance_max = fields.Function( + fields.Numeric("Tolerance max"), 'get_tolerance_max') + certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),}) wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True) association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),}) crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),}) @@ -395,6 +401,62 @@ class Purchase(metaclass=PoolMeta): def default_tol_max(cls): return 0 + def _line_quantity_in_unit(self, quantity, from_unit, to_unit): + if not from_unit or not to_unit or from_unit == to_unit: + return Decimal(str(quantity or 0)) + Uom = Pool().get('product.uom') + factor = None + rate = None + if from_unit.category.id != to_unit.category.id: + factor = 1 + rate = 1 + return Decimal(str(Uom.compute_qty( + from_unit, float(quantity or 0), to_unit, True, factor, rate))) + + def _line_lot_quantity_for_tolerance(self, line): + LotQt = Pool().get('lot.qt') + quantity = Decimal(0) + unit = getattr(line, 'unit', None) + for lot in getattr(line, 'lots', None) or []: + if getattr(lot, 'lot_type', None) == 'physic': + quantity += Decimal(str( + lot.get_current_quantity_converted(unit=unit) or 0)) + continue + if getattr(lot, 'lot_type', None) != 'virtual': + continue + for lqt in LotQt.search([('lot_p', '=', lot.id)]): + quantity += self._line_quantity_in_unit( + getattr(lqt, 'lot_quantity', 0), + getattr(lqt, 'lot_unit', None), + unit) + return quantity + + def _tolerance_totals(self): + theoretical = Decimal(0) + actual = Decimal(0) + for line in self.lines or []: + if getattr(line, 'type', None) != 'line': + continue + theoretical += Decimal(str( + getattr(line, 'quantity_theorical', None) + or getattr(line, 'quantity', None) + or 0)) + actual += self._line_lot_quantity_for_tolerance(line) + return theoretical, actual + + def get_tolerance_used(self, name): + theoretical, actual = self._tolerance_totals() + if not theoretical: + return Decimal(0) + return round( + ((actual - theoretical) / theoretical) * Decimal(100), 5) + + def get_tolerance_min(self, name): + return -Decimal(str(self.tol_min or 0)) + + def get_tolerance_max(self, name): + return Decimal(str(self.tol_max or 0)) + @staticmethod def _has_matched_physical_lots(purchase): for line in purchase.lines or []: diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index b6d40de..84b1546 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -249,6 +249,12 @@ class Sale(metaclass=PoolMeta): tol_max = fields.Numeric("Tol + in %", required=True) tol_min_qt = fields.Numeric("Tol -") tol_max_qt = fields.Numeric("Tol +") + tolerance_used = fields.Function( + fields.Numeric("Tolerance used"), 'get_tolerance_used') + tolerance_min = fields.Function( + fields.Numeric("Tolerance min"), 'get_tolerance_min') + tolerance_max = fields.Function( + fields.Numeric("Tolerance max"), 'get_tolerance_max') certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),}) wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True) association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),}) @@ -359,6 +365,62 @@ class Sale(metaclass=PoolMeta): def default_tol_max(cls): return 0 + def _line_quantity_in_unit(self, quantity, from_unit, to_unit): + if not from_unit or not to_unit or from_unit == to_unit: + return abs(Decimal(str(quantity or 0))) + Uom = Pool().get('product.uom') + factor = None + rate = None + if from_unit.category.id != to_unit.category.id: + factor = 1 + rate = 1 + return abs(Decimal(str(Uom.compute_qty( + from_unit, float(quantity or 0), to_unit, True, factor, rate)))) + + def _line_lot_quantity_for_tolerance(self, line): + LotQt = Pool().get('lot.qt') + quantity = Decimal(0) + unit = getattr(line, 'unit', None) + for lot in getattr(line, 'lots', None) or []: + if getattr(lot, 'lot_type', None) == 'physic': + quantity += abs(Decimal(str( + lot.get_current_quantity_converted(unit=unit) or 0))) + continue + if getattr(lot, 'lot_type', None) != 'virtual': + continue + for lqt in LotQt.search([('lot_s', '=', lot.id)]): + quantity += self._line_quantity_in_unit( + getattr(lqt, 'lot_quantity', 0), + getattr(lqt, 'lot_unit', None), + unit) + return quantity + + def _tolerance_totals(self): + theoretical = Decimal(0) + actual = Decimal(0) + for line in self.lines or []: + if getattr(line, 'type', None) != 'line': + continue + theoretical += abs(Decimal(str( + getattr(line, 'quantity_theorical', None) + or getattr(line, 'quantity', None) + or 0))) + actual += self._line_lot_quantity_for_tolerance(line) + return theoretical, actual + + def get_tolerance_used(self, name): + theoretical, actual = self._tolerance_totals() + if not theoretical: + return Decimal(0) + return round( + ((actual - theoretical) / theoretical) * Decimal(100), 5) + + def get_tolerance_min(self, name): + return -Decimal(str(self.tol_min or 0)) + + def get_tolerance_max(self, name): + return Decimal(str(self.tol_max or 0)) + @staticmethod def _has_matched_physical_lots(sale): for line in sale.lines or []: diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index c831d10..db86cb2 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -11,6 +11,8 @@ from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.exceptions import UserError from trytond.modules.purchase_trade import valuation as valuation_module from trytond.modules.purchase_trade import lot as lot_module +from trytond.modules.purchase_trade import purchase as purchase_module +from trytond.modules.purchase_trade import sale as sale_module from trytond.modules.purchase_trade import stock as stock_module from trytond.modules.purchase_trade import fee as fee_module from trytond.modules.purchase_trade.service import ContractFactory @@ -2835,6 +2837,66 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(result['lot_s'][0]['lot_qt_max'], Decimal('80.00000')) self.assertEqual(result['lot_s'][0]['lot_cp'], 40) + def test_purchase_tolerance_used_sums_open_and_physical_quantities(self): + 'purchase tolerance gauge uses lot.qt plus physical lot quantities' + unit = Mock() + virtual = Mock(id=10, lot_type='virtual') + physical = Mock(lot_type='physic') + physical.get_current_quantity_converted.return_value = Decimal('10') + line = Mock( + type='line', unit=unit, quantity_theorical=Decimal('100'), + lots=[virtual, physical]) + purchase = purchase_module.Purchase() + purchase.lines = [line] + purchase.tol_min = Decimal('5') + purchase.tol_max = Decimal('10') + lot_qt_model = Mock() + lot_qt_model.search.return_value = [ + Mock(lot_quantity=Decimal('95'), lot_unit=unit)] + pool = Mock() + pool.get.return_value = lot_qt_model + + with patch.object(purchase_module, 'Pool', return_value=pool): + self.assertEqual( + purchase.get_tolerance_used('tolerance_used'), + Decimal('5.00000')) + self.assertEqual( + purchase.get_tolerance_min('tolerance_min'), + Decimal('-5')) + self.assertEqual( + purchase.get_tolerance_max('tolerance_max'), + Decimal('10')) + + def test_sale_tolerance_used_sums_open_and_physical_quantities(self): + 'sale tolerance gauge uses absolute lot.qt plus physical quantities' + unit = Mock() + virtual = Mock(id=20, lot_type='virtual') + physical = Mock(lot_type='physic') + physical.get_current_quantity_converted.return_value = Decimal('10') + line = Mock( + type='line', unit=unit, quantity_theorical=Decimal('100'), + lots=[virtual, physical]) + sale = sale_module.Sale() + sale.lines = [line] + sale.tol_min = Decimal('5') + sale.tol_max = Decimal('10') + lot_qt_model = Mock() + lot_qt_model.search.return_value = [ + Mock(lot_quantity=Decimal('-95'), lot_unit=unit)] + pool = Mock() + pool.get.return_value = lot_qt_model + + with patch.object(sale_module, 'Pool', return_value=pool): + self.assertEqual( + sale.get_tolerance_used('tolerance_used'), + Decimal('5.00000')) + self.assertEqual( + sale.get_tolerance_min('tolerance_min'), + Decimal('-5')) + self.assertEqual( + sale.get_tolerance_max('tolerance_max'), + Decimal('10')) + def test_lot_context_group_defaults_to_all(self): 'lots management opens without grouping by physical lot' self.assertEqual(lot_module.LotContext.default_group(), 'all') diff --git a/modules/purchase_trade/view/purchase_form.xml b/modules/purchase_trade/view/purchase_form.xml index af76af5..2919131 100755 --- a/modules/purchase_trade/view/purchase_form.xml +++ b/modules/purchase_trade/view/purchase_form.xml @@ -32,6 +32,13 @@ this repository contains the full copyright notices and license terms. -->