Go to matching

This commit is contained in:
2026-05-26 18:54:07 +02:00
parent 8b4daeac84
commit 0fffb341fe
8 changed files with 667 additions and 26 deletions

View File

@@ -1328,8 +1328,13 @@ class LotQt(
qt_p = Decimal(str(lp.lot_matched_qt or 0))
if qt_p <= 0:
continue
lqt = LotQt(lp.lot_r_id)
available = cls._matching_tolerated_available(lqt, 'purchase')
lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
if lqt:
available = cls._matching_tolerated_available(lqt, 'purchase')
else:
lot = Lot(lp.lot_id)
available = Decimal(str(
lot.get_current_quantity_converted() or 0))
if qt_p > available:
raise UserError(
"Quantity to match exceeds the available purchase "
@@ -1348,8 +1353,8 @@ class LotQt(
qt_p = lp.lot_matched_qt
if qt_p == 0:
continue
lqt = LotQt(lp.lot_r_id)
if not lqt.lot_p or lqt.lot_s:
lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
if lqt and (not lqt.lot_p or lqt.lot_s):
raise UserError(
"Please select only unmatched purchase quantities.")
for ls in lot_s:
@@ -1368,28 +1373,29 @@ class LotQt(
raise UserError(
"Please select only unmatched sale quantities.")
if l.lot_type == 'physic':
Sale = Pool().get('sale.sale')
s = Sale(ls.lot_sale)
ll = s.lines[0].lots[0]
#Decrease forecasted virtual parts matched
vlot_p = l.getVlot_p()
vlot_s = ll.getVlot_s()
logger.info("SALEVIRTUALLOT:%s",vlot_p)
logger.info("SALEVIRTUALLOT2:%s",vlot_s)
ll = lqt2.lot_s
#Decrease forecasted virtual parts matched
vlot_p = l.getVlot_p()
vlot_s = ll.getVlot_s()
logger.info("SALEVIRTUALLOT:%s",vlot_p)
logger.info("SALEVIRTUALLOT2:%s",vlot_s)
#l.updateVirtualPart(-qt,l.lot_shipment_origin,l.getVlot_s())
#Increase forecasted virtual part non matched
#if not l.updateVirtualPart(qt,l.lot_shipment_origin,None):
# l.createVirtualPart(qt,l.lot_shipment_origin,None)
l.updateVirtualPart(-qt,None,ll,'only sale')
lqt.lot_s = ll
lqt.lot_av = 'reserved'
LotQt.save([lqt])
l.sale_line = s.lines[0]
Lot.save([l])
#ll.lot_gross_quantity -= qt
ll.lot_quantity -= qt
ll.lot_qt -= float(qt)
Lot.save([ll])
#Increase forecasted virtual part non matched
#if not l.updateVirtualPart(qt,l.lot_shipment_origin,None):
# l.createVirtualPart(qt,l.lot_shipment_origin,None)
l.updateVirtualPart(-qt,None,ll,'only sale')
if lqt:
lqt.lot_s = ll
lqt.lot_av = 'reserved'
LotQt.save([lqt])
else:
l.createVirtualPart(qt, None, ll)
l.sale_line = ll.sale_line
Lot.save([l])
#ll.lot_gross_quantity -= qt
ll.lot_quantity -= qt
ll.lot_qt -= float(qt)
Lot.save([ll])
else:
#Increase forecasted virtual part matched
if not l.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s):
@@ -2938,13 +2944,45 @@ class LotMatching(Wizard):
values.update(LotQt._matching_line_tolerance_info(trade_line))
return values
@classmethod
def _physical_purchase_matching_value(cls, lot):
line = getattr(lot, 'line', None)
purchase = getattr(line, 'purchase', None)
quantity = Decimal(str(lot.get_current_quantity_converted() or 0))
values = {
'lot_id': cls._record_id(lot),
'lot_r_id': None,
'lot_purchase': cls._record_id(purchase),
'lot_sale': None,
'lot_shipment_in': cls._record_id(
getattr(lot, 'lot_shipment_in', None)),
'lot_shipment_internal': cls._record_id(
getattr(lot, 'lot_shipment_internal', None)),
'lot_shipment_out': cls._record_id(
getattr(lot, 'lot_shipment_out', None)),
'lot_type': getattr(lot, 'lot_type', None),
'lot_product': cls._record_id(getattr(lot, 'lot_product', None)),
'lot_quantity': quantity,
'lot_matched_qt': 0,
'lot_already_matched_qt': 0,
'lot_cp': cls._record_id(getattr(purchase, 'party', None)),
}
values.update(LotQt._matching_line_tolerance_info(line))
return values
@classmethod
def _selected_matching_defaults(cls, active_ids):
LotQt = Pool().get('lot.qt')
Lot = Pool().get('lot.lot')
lot_p = []
lot_s = []
for active_id in active_ids or []:
if active_id <= 10000000:
lot = Lot(active_id)
if (getattr(lot, 'lot_type', None) == 'physic'
and getattr(lot, 'line', None)
and not getattr(lot, 'sale_line', None)):
lot_p.append(cls._physical_purchase_matching_value(lot))
continue
lqt = LotQt(active_id - 10000000)
if lqt.lot_p and lqt.lot_s:
@@ -2961,12 +2999,12 @@ class LotMatching(Wizard):
def transition_matching(self):
Warning = Pool().get('res.user.warning')
LotQt = Pool().get('lot.qt')
Lot = Pool().get('lot.lot')
if self.match.tot_p != self.match.tot_s:
warning_name = Warning.format("Quantity's issue", [])
if Warning.check(warning_name):
raise QtWarning(warning_name,
"Quantities not compatibles")
Lot = Pool().get('lot.lot')
with Lot.skip_quantity_consistency():
LotQt.match_lots(self.match.lot_p,self.match.lot_s)
affected_lines = []
@@ -3011,10 +3049,18 @@ class LotGoMatching(Wizard):
context = Transaction().context
values = LotMatching._selected_matching_defaults(
context.get('active_ids') or [])
if not values.get('lot_p') or not values.get('lot_s'):
raise UserError(
"Please select at least one purchase line and one sale line "
"before using Go to matching.")
values['qt_type'] = 'all'
return values
def transition_matching(self):
if not self.match.lot_p or not self.match.lot_s:
raise UserError(
"Please select at least one purchase line and one sale line "
"before using Go to matching.")
return LotMatching.transition_matching(self)
def end(self):

View File

@@ -3417,6 +3417,77 @@ 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_go_matching_defaults_selected_physical_purchase_lot(self):
'go to matching preloads selected unmatched physical purchase lots'
purchase = Mock(id=10, party=Mock(id=20))
sale = Mock(id=30, party=Mock(id=40))
product = Mock(id=50)
physical_lot = Mock(
id=101,
line=Mock(
purchase=purchase, inherit_tol=False,
quantity_theorical=Decimal('100'),
targeted_qt=Decimal('96'), tol_min=0, tol_max=0),
sale_line=None,
lot_type='physic',
lot_product=product,
lot_shipment_in=None,
lot_shipment_internal=None,
lot_shipment_out=None)
physical_lot.get_current_quantity_converted.return_value = (
Decimal('70'))
sale_lot = Mock(
id=102, line=None,
sale_line=Mock(
sale=sale, inherit_tol=False,
quantity_theorical=Decimal('80'),
targeted_qt=Decimal('84'), tol_min=0, tol_max=0),
lot_type='virtual', lot_product=product)
sale_lqt = Mock(
id=2, lot_p=None, lot_s=sale_lot,
lot_quantity=Decimal('80'),
lot_shipment_in=None, lot_shipment_internal=None,
lot_shipment_out=None)
class LotQtMock:
def __call__(self, _id):
return sale_lqt
class LotMock:
def __call__(self, _id):
return physical_lot
pool = Mock()
pool.get.side_effect = (
lambda name: LotQtMock() if name == 'lot.qt' else LotMock())
transaction = Mock()
transaction.context = {
'active_ids': [101, 10000002],
}
with patch.object(lot_module, 'Pool', return_value=pool):
with patch.object(
lot_module, 'Transaction', return_value=transaction):
result = lot_module.LotGoMatching().default_match([])
self.assertEqual(len(result['lot_p']), 1)
self.assertEqual(len(result['lot_s']), 1)
self.assertEqual(result['lot_p'][0]['lot_id'], 101)
self.assertIsNone(result['lot_p'][0]['lot_r_id'])
self.assertEqual(result['lot_p'][0]['lot_type'], 'physic')
self.assertEqual(result['lot_p'][0]['lot_quantity'], Decimal('70'))
self.assertEqual(result['lot_p'][0]['lot_targeted_qt'], Decimal('96'))
self.assertEqual(result['lot_p'][0]['lot_cp'], 20)
self.assertEqual(result['lot_s'][0]['lot_r_id'], 2)
def test_go_matching_requires_purchase_and_sale_selection(self):
'go to matching requires one purchase and one sale row'
wizard = lot_module.LotGoMatching()
wizard.match = Mock(lot_p=[], lot_s=[Mock()])
with self.assertRaises(UserError):
wizard.transition_matching()
def test_purchase_tolerance_used_is_weighted_line_average(self):
'purchase tolerance gauge averages line gauges by theoretical quantity'
unit = Mock()