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. -->
+
+
+
+
+
diff --git a/modules/purchase_trade/view/sale_form.xml b/modules/purchase_trade/view/sale_form.xml
index 39b39a3..29ee55d 100755
--- a/modules/purchase_trade/view/sale_form.xml
+++ b/modules/purchase_trade/view/sale_form.xml
@@ -32,6 +32,13 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
+