Matching
This commit is contained in:
@@ -1217,13 +1217,85 @@ class LotQt(
|
||||
return (l.lot_type == 'virtual')
|
||||
return False
|
||||
|
||||
def isVirtualS(self):
|
||||
if self.lot_s:
|
||||
Lot = Pool().get('lot.lot')
|
||||
l = Lot(self.lot_s)
|
||||
return (l.lot_type == 'virtual')
|
||||
return False
|
||||
|
||||
def isVirtualS(self):
|
||||
if self.lot_s:
|
||||
Lot = Pool().get('lot.lot')
|
||||
l = Lot(self.lot_s)
|
||||
return (l.lot_type == 'virtual')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _matching_line(lqt, side):
|
||||
if side == 'purchase':
|
||||
lot = getattr(lqt, 'lot_p', None)
|
||||
return getattr(lot, 'line', None)
|
||||
lot = getattr(lqt, 'lot_s', None)
|
||||
return getattr(lot, 'sale_line', None)
|
||||
|
||||
@classmethod
|
||||
def _matching_line_tolerance_percent(cls, line, field):
|
||||
contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
||||
if getattr(line, 'inherit_tol', False) and contract:
|
||||
return Decimal(str(getattr(contract, field, None) or 0))
|
||||
return Decimal(str(getattr(line, field, None) or 0))
|
||||
|
||||
@classmethod
|
||||
def _matching_line_tolerance_info(cls, line):
|
||||
if not line:
|
||||
return {}
|
||||
theoretical = Decimal(str(
|
||||
getattr(line, 'quantity_theorical', None)
|
||||
or getattr(line, 'quantity', None)
|
||||
or 0))
|
||||
tol_min = cls._matching_line_tolerance_percent(line, 'tol_min')
|
||||
tol_max = cls._matching_line_tolerance_percent(line, 'tol_max')
|
||||
return {
|
||||
'lot_tol_min': tol_min,
|
||||
'lot_tol_max': tol_max,
|
||||
'lot_qt_min': round(
|
||||
theoretical * (Decimal(1) - tol_min / Decimal(100)), 5),
|
||||
'lot_qt_max': round(
|
||||
theoretical * (Decimal(1) + tol_max / Decimal(100)), 5),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _matching_quantity_in_line_unit(cls, lqt, line):
|
||||
quantity = abs(Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)))
|
||||
lot_unit = getattr(lqt, 'lot_unit', None)
|
||||
line_unit = getattr(line, 'unit', None)
|
||||
if not lot_unit or not line_unit or lot_unit == line_unit:
|
||||
return quantity
|
||||
Uom = Pool().get('product.uom')
|
||||
factor = None
|
||||
rate = None
|
||||
if lot_unit.category.id != line_unit.category.id:
|
||||
factor = 1
|
||||
rate = 1
|
||||
return Decimal(str(Uom.compute_qty(
|
||||
lot_unit, float(quantity), line_unit, True, factor, rate)))
|
||||
|
||||
@classmethod
|
||||
def _matching_tolerated_available(cls, lqt, side):
|
||||
available = abs(Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)))
|
||||
line = cls._matching_line(lqt, side)
|
||||
if not line:
|
||||
return available
|
||||
info = cls._matching_line_tolerance_info(line)
|
||||
qt_max = info.get('lot_qt_max')
|
||||
if qt_max is None:
|
||||
return available
|
||||
domain = [('lot_s', '>', 0)]
|
||||
if side == 'purchase':
|
||||
domain.append(('lot_p', '=', getattr(lqt.lot_p, 'id', lqt.lot_p)))
|
||||
else:
|
||||
domain = [('lot_p', '>', 0),
|
||||
('lot_s', '=', getattr(lqt.lot_s, 'id', lqt.lot_s))]
|
||||
matched = sum(
|
||||
cls._matching_quantity_in_line_unit(matched_lqt, line)
|
||||
for matched_lqt in cls.search(domain))
|
||||
tolerated_available = Decimal(str(qt_max)) - matched
|
||||
return max(available, tolerated_available, Decimal(0))
|
||||
|
||||
@classmethod
|
||||
def match_lots(cls,lot_p,lot_s):
|
||||
logger.info("MATCH_LOTS:%s",lot_p)
|
||||
@@ -1235,19 +1307,21 @@ class LotQt(
|
||||
if qt_p <= 0:
|
||||
continue
|
||||
lqt = LotQt(lp.lot_r_id)
|
||||
available = Decimal(str(lqt.lot_quantity or 0))
|
||||
available = cls._matching_tolerated_available(lqt, 'purchase')
|
||||
if qt_p > available:
|
||||
raise UserError(
|
||||
"Quantity to match exceeds the available purchase quantity.")
|
||||
"Quantity to match exceeds the available purchase "
|
||||
"quantity with tolerance.")
|
||||
for ls in lot_s:
|
||||
qt_s = Decimal(str(ls.lot_matched_qt or 0))
|
||||
if qt_s <= 0:
|
||||
continue
|
||||
lqt = LotQt(ls.lot_r_id)
|
||||
available = abs(Decimal(str(lqt.lot_quantity or 0)))
|
||||
available = cls._matching_tolerated_available(lqt, 'sale')
|
||||
if qt_s > available:
|
||||
raise UserError(
|
||||
"Quantity to match exceeds the available sale quantity.")
|
||||
"Quantity to match exceeds the available sale quantity "
|
||||
"with tolerance.")
|
||||
for lp in lot_p:
|
||||
qt_p = lp.lot_matched_qt
|
||||
if qt_p == 0:
|
||||
@@ -2821,7 +2895,7 @@ class LotMatching(Wizard):
|
||||
quantity = Decimal(str(lqt.lot_quantity or 0))
|
||||
if side == 'sale':
|
||||
quantity = abs(quantity)
|
||||
return {
|
||||
values = {
|
||||
'lot_id': cls._record_id(lot),
|
||||
'lot_r_id': cls._record_id(lqt),
|
||||
'lot_purchase': cls._record_id(purchase),
|
||||
@@ -2836,6 +2910,8 @@ class LotMatching(Wizard):
|
||||
'lot_matched_qt': 0,
|
||||
'lot_cp': cls._record_id(party),
|
||||
}
|
||||
values.update(LotQt._matching_line_tolerance_info(line))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _selected_matching_defaults(cls, active_ids):
|
||||
@@ -2996,15 +3072,18 @@ class LotMatchingStart(ModelView):
|
||||
val['lot_shipment_out'] = r['r_lot_shipment_out']
|
||||
val['lot_type'] = r['r_lot_type']
|
||||
val['lot_product'] = r['r_lot_product']
|
||||
val['lot_quantity'] = -r['r_lot_quantity']
|
||||
if r['r_sale'] and r['r_purchase']:
|
||||
val['lot_quantity'] = r['r_lot_matched']
|
||||
val['lot_matched_qt'] = 0
|
||||
val['lot_cp'] = r['r_client']
|
||||
lp.append(val)
|
||||
return lp
|
||||
|
||||
@fields.depends('cp_p','qt_type','purchase','product_p')
|
||||
val['lot_quantity'] = -r['r_lot_quantity']
|
||||
if r['r_sale'] and r['r_purchase']:
|
||||
val['lot_quantity'] = r['r_lot_matched']
|
||||
val['lot_matched_qt'] = 0
|
||||
val['lot_cp'] = r['r_client']
|
||||
lqt = LotQt(r['id'] - 10000000)
|
||||
val.update(LotQt._matching_line_tolerance_info(
|
||||
LotQt._matching_line(lqt, 'sale')))
|
||||
lp.append(val)
|
||||
return lp
|
||||
|
||||
@fields.depends('cp_p','qt_type','purchase','product_p')
|
||||
def on_change_with_lot_p(self, name=None):
|
||||
lp = []
|
||||
LotQt = Pool().get('lot.qt')
|
||||
@@ -3022,12 +3101,15 @@ class LotMatchingStart(ModelView):
|
||||
val['lot_shipment_internal'] = r['r_lot_shipment_internal']
|
||||
val['lot_shipment_out'] = r['r_lot_shipment_out']
|
||||
val['lot_type'] = r['r_lot_type']
|
||||
val['lot_product'] = r['r_lot_product']
|
||||
val['lot_quantity'] = r['r_lot_quantity']
|
||||
val['lot_matched_qt'] = 0
|
||||
val['lot_cp'] = r['r_supplier']
|
||||
lp.append(val)
|
||||
return lp
|
||||
val['lot_product'] = r['r_lot_product']
|
||||
val['lot_quantity'] = r['r_lot_quantity']
|
||||
val['lot_matched_qt'] = 0
|
||||
val['lot_cp'] = r['r_supplier']
|
||||
lqt = LotQt(r['id'] - 10000000)
|
||||
val.update(LotQt._matching_line_tolerance_info(
|
||||
LotQt._matching_line(lqt, 'purchase')))
|
||||
lp.append(val)
|
||||
return lp
|
||||
|
||||
class LotMatchingLot(ModelView):
|
||||
"Lots"
|
||||
@@ -3047,10 +3129,14 @@ class LotMatchingLot(ModelView):
|
||||
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
||||
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
||||
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
||||
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
||||
lot_quantity = fields.Numeric("Qt",readonly=True)
|
||||
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||
lot_matched_qt = fields.Numeric("Qt to match")
|
||||
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
||||
lot_quantity = fields.Numeric("Qt",readonly=True)
|
||||
lot_qt_min = fields.Numeric("Qt min", readonly=True)
|
||||
lot_qt_max = fields.Numeric("Qt max", readonly=True)
|
||||
lot_tol_min = fields.Numeric("Tol -", readonly=True)
|
||||
lot_tol_max = fields.Numeric("Tol +", readonly=True)
|
||||
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||
lot_matched_qt = fields.Numeric("Qt to match")
|
||||
lot_cp = fields.Many2One('party.party',"Counterparty")
|
||||
lot_shipment_origin = fields.Function(
|
||||
fields.Reference(
|
||||
|
||||
@@ -2722,6 +2722,46 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
with self.assertRaises(UserError):
|
||||
lot_module.LotQt.match_lots([purchase_lot], [])
|
||||
|
||||
def test_lot_matching_available_includes_tolerance_remaining(self):
|
||||
'matching can use remaining purchase tolerance above open quantity'
|
||||
purchase = Mock(tol_max=Decimal('10'))
|
||||
line = Mock(
|
||||
purchase=purchase, inherit_tol=True,
|
||||
quantity_theorical=Decimal('100'), unit=None)
|
||||
vlot = Mock(id=10, line=line)
|
||||
lqt = Mock(lot_p=vlot, lot_quantity=Decimal('60'))
|
||||
matched_lqt = Mock(lot_quantity=Decimal('40'), lot_unit=None)
|
||||
|
||||
with patch.object(
|
||||
lot_module.LotQt, 'search', return_value=[matched_lqt]):
|
||||
self.assertEqual(
|
||||
lot_module.LotQt._matching_tolerated_available(
|
||||
lqt, 'purchase'),
|
||||
Decimal('70.00000'))
|
||||
|
||||
def test_lot_matching_accepts_purchase_quantity_within_tolerance(self):
|
||||
'apply matching can consume purchase quantity up to tolerance max'
|
||||
purchase = Mock(tol_max=Decimal('10'))
|
||||
line = Mock(
|
||||
purchase=purchase, inherit_tol=True,
|
||||
quantity_theorical=Decimal('100'), unit=None)
|
||||
vlot = Mock(id=10, line=line)
|
||||
lqt = Mock(lot_p=vlot, lot_s=None, lot_quantity=Decimal('100'))
|
||||
|
||||
class LotQtMock:
|
||||
def __call__(self, _id):
|
||||
return lqt
|
||||
|
||||
pool = Mock()
|
||||
pool.get.side_effect = (
|
||||
lambda name: LotQtMock() if name == 'lot.qt' else Mock())
|
||||
purchase_lot = Mock(
|
||||
lot_matched_qt=Decimal('105'), lot_r_id=1)
|
||||
|
||||
with patch.object(lot_module, 'Pool', return_value=pool), \
|
||||
patch.object(lot_module.LotQt, 'search', return_value=[]):
|
||||
lot_module.LotQt.match_lots([purchase_lot], [])
|
||||
|
||||
def test_go_matching_defaults_selected_open_lot_qts(self):
|
||||
'go to matching only preloads selected unmatched open lot.qt rows'
|
||||
purchase = Mock(id=10, party=Mock(id=20))
|
||||
|
||||
@@ -7,5 +7,9 @@
|
||||
<field name="lot_cp"/>
|
||||
<field name="lot_product"/>
|
||||
<field name="lot_quantity"/>
|
||||
<field name="lot_qt_min"/>
|
||||
<field name="lot_qt_max"/>
|
||||
<field name="lot_tol_min"/>
|
||||
<field name="lot_tol_max"/>
|
||||
<field name="lot_matched_qt"/>
|
||||
</tree>
|
||||
|
||||
@@ -7,5 +7,9 @@
|
||||
<field name="lot_cp"/>
|
||||
<field name="lot_product"/>
|
||||
<field name="lot_quantity"/>
|
||||
<field name="lot_qt_min"/>
|
||||
<field name="lot_qt_max"/>
|
||||
<field name="lot_tol_min"/>
|
||||
<field name="lot_tol_max"/>
|
||||
<field name="lot_matched_qt"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user