diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index e2f82e4..50ae76b 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -417,11 +417,15 @@ class Purchase(metaclass=PoolMeta): 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( + physical_lots = [ + lot for lot in getattr(line, 'lots', None) or [] + if getattr(lot, 'lot_type', None) == 'physic'] + if physical_lots: + return sum( + Decimal(str( lot.get_current_quantity_converted(unit=unit) or 0)) - continue + for lot in physical_lots) + for lot in getattr(line, 'lots', None) or []: if getattr(lot, 'lot_type', None) != 'virtual': continue for lqt in LotQt.search([('lot_p', '=', lot.id)]): @@ -1354,10 +1358,16 @@ class Line(metaclass=PoolMeta): tol_max_qt = fields.Numeric("Tol +",states={ 'readonly': (Eval('inherit_tol')), }) - inherit_tol = fields.Boolean("Inherit tolerance") - tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min') - tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max') - # certification = fields.Selection([ + inherit_tol = fields.Boolean("Inherit tolerance") + tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min') + tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max') + 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') + # certification = fields.Selection([ # (None, ''), # ('bci', 'BCI'), # ],"Certification",states={'readonly': (Eval('inherit_cer')),}) @@ -1496,14 +1506,74 @@ class Line(metaclass=PoolMeta): if self.tol_min and self.quantity_theorical: return round((1-(self.tol_min/100))*Decimal(self.quantity_theorical),3) - def get_tol_max(self,name): - if self.inherit_tol: - if self.purchase.tol_max and self.quantity_theorical: - return round((1+(self.purchase.tol_max/100))*Decimal(self.quantity_theorical),3) - else: - if self.tol_max and self.quantity_theorical: - return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3) - + def get_tol_max(self,name): + if self.inherit_tol: + if self.purchase.tol_max and self.quantity_theorical: + return round((1+(self.purchase.tol_max/100))*Decimal(self.quantity_theorical),3) + else: + if self.tol_max and self.quantity_theorical: + return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3) + + 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 _tolerance_theoretical_quantity(self): + return Decimal(str( + getattr(self, 'quantity_theorical', None) + or getattr(self, 'quantity', None) + or 0)) + + def _tolerance_actual_quantity(self): + LotQt = Pool().get('lot.qt') + unit = getattr(self, 'unit', None) + physical_lots = [ + lot for lot in getattr(self, 'lots', None) or [] + if getattr(lot, 'lot_type', None) == 'physic'] + if physical_lots: + return sum( + Decimal(str( + lot.get_current_quantity_converted(unit=unit) or 0)) + for lot in physical_lots) + quantity = Decimal(0) + for lot in getattr(self, 'lots', None) or []: + 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 get_tolerance_used(self, name): + theoretical = self._tolerance_theoretical_quantity() + if not theoretical: + return Decimal(0) + return round( + ((self._tolerance_actual_quantity() - theoretical) + / theoretical) * Decimal(100), 5) + + def get_tolerance_min(self, name): + purchase = getattr(self, 'purchase', None) + if self.inherit_tol and purchase: + return -Decimal(str(purchase.tol_min or 0)) + return -Decimal(str(self.tol_min or 0)) + + def get_tolerance_max(self, name): + purchase = getattr(self, 'purchase', None) + if self.inherit_tol and purchase: + return Decimal(str(purchase.tol_max or 0)) + return Decimal(str(self.tol_max or 0)) + def get_progress(self,name): PS = Pool().get('purchase.pricing.summary') ps = PS.search(['line','=',self.id]) diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 84b1546..db5ea13 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -381,11 +381,15 @@ class Sale(metaclass=PoolMeta): 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( + physical_lots = [ + lot for lot in getattr(line, 'lots', None) or [] + if getattr(lot, 'lot_type', None) == 'physic'] + if physical_lots: + return sum( + abs(Decimal(str( lot.get_current_quantity_converted(unit=unit) or 0))) - continue + for lot in physical_lots) + for lot in getattr(line, 'lots', None) or []: if getattr(lot, 'lot_type', None) != 'virtual': continue for lqt in LotQt.search([('lot_s', '=', lot.id)]): @@ -1339,6 +1343,12 @@ class SaleLine(metaclass=PoolMeta): inherit_tol = fields.Boolean("Inherit tolerance") tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min') tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max') + 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",states={'readonly': (Eval('inherit_cer')),}) # certification = fields.Selection([ # (None, ''), @@ -1494,6 +1504,66 @@ class SaleLine(metaclass=PoolMeta): if self.tol_max and self.quantity_theorical: return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3) + 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 _tolerance_theoretical_quantity(self): + return abs(Decimal(str( + getattr(self, 'quantity_theorical', None) + or getattr(self, 'quantity', None) + or 0))) + + def _tolerance_actual_quantity(self): + LotQt = Pool().get('lot.qt') + unit = getattr(self, 'unit', None) + physical_lots = [ + lot for lot in getattr(self, 'lots', None) or [] + if getattr(lot, 'lot_type', None) == 'physic'] + if physical_lots: + return sum( + abs(Decimal(str( + lot.get_current_quantity_converted(unit=unit) or 0))) + for lot in physical_lots) + quantity = Decimal(0) + for lot in getattr(self, 'lots', None) or []: + 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 get_tolerance_used(self, name): + theoretical = self._tolerance_theoretical_quantity() + if not theoretical: + return Decimal(0) + return round( + ((self._tolerance_actual_quantity() - theoretical) + / theoretical) * Decimal(100), 5) + + def get_tolerance_min(self, name): + sale = getattr(self, 'sale', None) + if self.inherit_tol and sale: + return -Decimal(str(sale.tol_min or 0)) + return -Decimal(str(self.tol_min or 0)) + + def get_tolerance_max(self, name): + sale = getattr(self, 'sale', None) + if self.inherit_tol and sale: + return Decimal(str(sale.tol_max or 0)) + return Decimal(str(self.tol_max or 0)) + def get_progress(self,name): PS = Pool().get('sale.pricing.summary') ps = PS.search(['sale_line','=',self.id]) diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index db86cb2..d193b65 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2837,29 +2837,32 @@ 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' + def test_purchase_tolerance_used_is_weighted_line_average(self): + 'purchase tolerance gauge averages line gauges by theoretical quantity' 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( + physical.get_current_quantity_converted.return_value = Decimal('105') + physical_line = Mock( type='line', unit=unit, quantity_theorical=Decimal('100'), - lots=[virtual, physical]) + lots=[physical]) + virtual = Mock(id=10, lot_type='virtual') + open_line = Mock( + type='line', unit=unit, quantity_theorical=Decimal('300'), + lots=[virtual]) purchase = purchase_module.Purchase() - purchase.lines = [line] + purchase.lines = [physical_line, open_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)] + Mock(lot_quantity=Decimal('270'), 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')) + Decimal('-6.25000')) self.assertEqual( purchase.get_tolerance_min('tolerance_min'), Decimal('-5')) @@ -2867,20 +2870,78 @@ class PurchaseTradeTestCase(ModuleTestCase): 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' + def test_sale_tolerance_used_is_weighted_line_average(self): + 'sale tolerance gauge averages line gauges by theoretical quantity' 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( + physical.get_current_quantity_converted.return_value = Decimal('105') + physical_line = Mock( type='line', unit=unit, quantity_theorical=Decimal('100'), - lots=[virtual, physical]) + lots=[physical]) + virtual = Mock(id=20, lot_type='virtual') + open_line = Mock( + type='line', unit=unit, quantity_theorical=Decimal('300'), + lots=[virtual]) sale = sale_module.Sale() - sale.lines = [line] + sale.lines = [physical_line, open_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('-270'), 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('-6.25000')) + self.assertEqual( + sale.get_tolerance_min('tolerance_min'), + Decimal('-5')) + self.assertEqual( + sale.get_tolerance_max('tolerance_max'), + Decimal('10')) + + def test_purchase_line_tolerance_uses_physical_lots_when_present(self): + 'purchase line tolerance ignores open lot.qt when physical lots exist' + unit = Mock() + virtual = Mock(id=10, lot_type='virtual') + physical = Mock(lot_type='physic') + physical.get_current_quantity_converted.return_value = Decimal('105') + line = purchase_module.Line() + line.unit = unit + line.quantity_theorical = Decimal('100') + line.quantity = Decimal('100') + line.lots = [virtual, physical] + line.inherit_tol = False + line.tol_min = Decimal('5') + line.tol_max = Decimal('10') + lot_qt_model = Mock() + lot_qt_model.search.return_value = [ + Mock(lot_quantity=Decimal('80'), lot_unit=unit)] + pool = Mock() + pool.get.return_value = lot_qt_model + + with patch.object(purchase_module, 'Pool', return_value=pool): + self.assertEqual( + line.get_tolerance_used('tolerance_used'), + Decimal('5.00000')) + lot_qt_model.search.assert_not_called() + + def test_sale_line_tolerance_uses_open_lot_qt_without_physical_lots(self): + 'sale line tolerance uses absolute lot.qt when no physical lot exists' + unit = Mock() + virtual = Mock(id=20, lot_type='virtual') + line = sale_module.SaleLine() + line.unit = unit + line.quantity_theorical = Decimal('100') + line.quantity = Decimal('100') + line.lots = [virtual] + line.inherit_tol = False + line.tol_min = Decimal('5') + line.tol_max = Decimal('10') + lot_qt_model = Mock() lot_qt_model.search.return_value = [ Mock(lot_quantity=Decimal('-95'), lot_unit=unit)] pool = Mock() @@ -2888,14 +2949,8 @@ class PurchaseTradeTestCase(ModuleTestCase): 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')) + line.get_tolerance_used('tolerance_used'), + Decimal('-5.00000')) def test_lot_context_group_defaults_to_all(self): 'lots management opens without grouping by physical lot' diff --git a/modules/purchase_trade/view/purchase_form.xml b/modules/purchase_trade/view/purchase_form.xml index 2919131..8d8dd5e 100755 --- a/modules/purchase_trade/view/purchase_form.xml +++ b/modules/purchase_trade/view/purchase_form.xml @@ -32,12 +32,12 @@ this repository contains the full copyright notices and license terms. -->