02.04.26
This commit is contained in:
@@ -3154,37 +3154,55 @@ class CreateContracts(Wizard):
|
||||
def transition_start(self):
|
||||
return 'ct'
|
||||
|
||||
def default_ct(self, fields):
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Lot = Pool().get('lot.lot')
|
||||
context = Transaction().context
|
||||
ids = context.get('active_ids')
|
||||
def default_ct(self, fields):
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Lot = Pool().get('lot.lot')
|
||||
context = Transaction().context
|
||||
ids = context.get('active_ids')
|
||||
unit = None
|
||||
product = None
|
||||
sh_in = None
|
||||
sh_int = None
|
||||
sh_out = None
|
||||
lot = None
|
||||
qt = None
|
||||
type = None
|
||||
for i in ids:
|
||||
val = {}
|
||||
if i < 10000000:
|
||||
raise UserError("You must create contract from an open quantity !")
|
||||
l = LotQt(i - 10000000)
|
||||
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
||||
type = "Sale" if l.lot_p else "Purchase"
|
||||
unit = l.lot_unit.id
|
||||
qt = l.lot_quantity
|
||||
product = ll.lot_product.id
|
||||
sh_in = l.lot_shipment_in.id if l.lot_shipment_in else None
|
||||
sh_int = l.lot_shipment_internal.id if l.lot_shipment_internal else None
|
||||
sh_out = l.lot_shipment_out.id if l.lot_shipment_out else None
|
||||
lot = ll.id
|
||||
|
||||
return {
|
||||
'quantity': qt,
|
||||
'unit': unit,
|
||||
sh_out = None
|
||||
lot = None
|
||||
qt = Decimal(0)
|
||||
type = None
|
||||
shipment_in_values = set()
|
||||
shipment_internal_values = set()
|
||||
shipment_out_values = set()
|
||||
for i in ids:
|
||||
if i < 10000000:
|
||||
raise UserError("You must create contract from an open quantity !")
|
||||
l = LotQt(i - 10000000)
|
||||
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
||||
current_type = "Sale" if l.lot_p else "Purchase"
|
||||
if type and current_type != type:
|
||||
raise UserError("You must select open quantities from the same side.")
|
||||
type = current_type
|
||||
if product and ll.lot_product.id != product:
|
||||
raise UserError("You must select open quantities with the same product.")
|
||||
if unit and l.lot_unit.id != unit:
|
||||
raise UserError("You must select open quantities with the same unit.")
|
||||
unit = l.lot_unit.id
|
||||
qt += abs(Decimal(str(l.lot_quantity or 0)))
|
||||
product = ll.lot_product.id
|
||||
shipment_in_values.add(l.lot_shipment_in.id if l.lot_shipment_in else None)
|
||||
shipment_internal_values.add(
|
||||
l.lot_shipment_internal.id if l.lot_shipment_internal else None)
|
||||
shipment_out_values.add(l.lot_shipment_out.id if l.lot_shipment_out else None)
|
||||
if lot is None:
|
||||
lot = ll.id
|
||||
|
||||
if len(shipment_in_values) == 1:
|
||||
sh_in = next(iter(shipment_in_values))
|
||||
if len(shipment_internal_values) == 1:
|
||||
sh_int = next(iter(shipment_internal_values))
|
||||
if len(shipment_out_values) == 1:
|
||||
sh_out = next(iter(shipment_out_values))
|
||||
|
||||
return {
|
||||
'quantity': qt,
|
||||
'unit': unit,
|
||||
'product': product,
|
||||
'shipment_in': sh_in,
|
||||
'shipment_internal': sh_int,
|
||||
@@ -3359,4 +3377,4 @@ class ContractDetail(ModelView):
|
||||
if self.del_period:
|
||||
self.from_del = self.del_period.beg_date
|
||||
self.to_del = self.del_period.end_date
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.exceptions import UserError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,16 +36,11 @@ class ContractFactory:
|
||||
Date = pool.get('ir.date')
|
||||
|
||||
created = []
|
||||
|
||||
base_contract = (
|
||||
ct.lot.sale_line.sale
|
||||
if type_ == 'Purchase'
|
||||
else ct.lot.line.purchase
|
||||
)
|
||||
sources = cls._get_sources(ct, type_)
|
||||
base_contract = cls._get_base_contract(sources, ct, type_)
|
||||
|
||||
for c in contracts:
|
||||
contract = Purchase() if type_ == 'Purchase' else Sale()
|
||||
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
|
||||
|
||||
# ---------- CONTRACT ----------
|
||||
parts = c.currency_unit.split("_")
|
||||
@@ -79,33 +75,34 @@ class ContractFactory:
|
||||
|
||||
contract.save()
|
||||
|
||||
# ---------- LINE ----------
|
||||
line.quantity = c.quantity
|
||||
line.quantity_theorical = c.quantity
|
||||
line.product = ct.product
|
||||
line.unit = ct.unit
|
||||
line.price_type = c.price_type
|
||||
line.created_by_code = ct.matched
|
||||
line.premium = Decimal(0)
|
||||
line_sources = cls._get_line_sources(c, sources, ct)
|
||||
for source in line_sources:
|
||||
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
|
||||
|
||||
if type_ == 'Purchase':
|
||||
line.purchase = contract.id
|
||||
else:
|
||||
line.sale = contract.id
|
||||
# ---------- LINE ----------
|
||||
line.quantity = source['quantity']
|
||||
line.quantity_theorical = source['quantity']
|
||||
line.product = ct.product
|
||||
line.unit = ct.unit
|
||||
line.price_type = c.price_type
|
||||
line.created_by_code = ct.matched
|
||||
line.premium = Decimal(0)
|
||||
|
||||
cls._apply_price(line, c, parts)
|
||||
if type_ == 'Purchase':
|
||||
line.purchase = contract.id
|
||||
else:
|
||||
line.sale = contract.id
|
||||
|
||||
line.del_period = c.del_period
|
||||
line.from_del = c.from_del
|
||||
line.to_del = c.to_del
|
||||
cls._apply_price(line, c, parts)
|
||||
cls._apply_delivery(line, c, source)
|
||||
|
||||
line.save()
|
||||
line.save()
|
||||
|
||||
logger.info("CREATE_ID:%s", contract.id)
|
||||
logger.info("CREATE_LINE_ID:%s", line.id)
|
||||
logger.info("CREATE_ID:%s", contract.id)
|
||||
logger.info("CREATE_LINE_ID:%s", line.id)
|
||||
|
||||
if ct.matched:
|
||||
cls._create_lot(line, c, ct, type_)
|
||||
if ct.matched:
|
||||
cls._create_lot(line, c, source, type_)
|
||||
|
||||
created.append(contract)
|
||||
|
||||
@@ -155,12 +152,124 @@ class ContractFactory:
|
||||
else:
|
||||
line.unit_price = c.price if c.price else Decimal(0)
|
||||
|
||||
@staticmethod
|
||||
def _apply_delivery(line, c, source):
|
||||
source_line = source.get('trade_line')
|
||||
if source.get('use_source_delivery') and source_line:
|
||||
line.del_period = getattr(source_line, 'del_period', None)
|
||||
line.from_del = getattr(source_line, 'from_del', None)
|
||||
line.to_del = getattr(source_line, 'to_del', None)
|
||||
return
|
||||
line.del_period = c.del_period
|
||||
line.from_del = c.from_del
|
||||
line.to_del = c.to_del
|
||||
|
||||
@staticmethod
|
||||
def _normalize_quantity(quantity):
|
||||
return abs(Decimal(str(quantity or 0))).quantize(Decimal('0.00001'))
|
||||
|
||||
@classmethod
|
||||
def _get_base_contract(cls, sources, ct, type_):
|
||||
if sources:
|
||||
source_lot = sources[0]['lot']
|
||||
return (
|
||||
source_lot.sale_line.sale
|
||||
if type_ == 'Purchase'
|
||||
else source_lot.line.purchase
|
||||
)
|
||||
return (
|
||||
ct.lot.sale_line.sale
|
||||
if type_ == 'Purchase'
|
||||
else ct.lot.line.purchase
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_sources(cls, ct, type_):
|
||||
pool = Pool()
|
||||
LotQt = pool.get('lot.qt')
|
||||
context = Transaction().context or {}
|
||||
active_ids = context.get('active_ids') or []
|
||||
sources = []
|
||||
if active_ids:
|
||||
for record_id in active_ids:
|
||||
if record_id < 10000000:
|
||||
continue
|
||||
lqt = LotQt(record_id - 10000000)
|
||||
lot = lqt.lot_p or lqt.lot_s
|
||||
if not lot:
|
||||
continue
|
||||
trade_line = (
|
||||
lot.sale_line if type_ == 'Purchase' else lot.line
|
||||
)
|
||||
sources.append({
|
||||
'lqt': lqt,
|
||||
'lot': lot,
|
||||
'trade_line': trade_line,
|
||||
'quantity': cls._normalize_quantity(lqt.lot_quantity),
|
||||
'shipment_origin': lqt.lot_shipment_origin,
|
||||
})
|
||||
elif getattr(ct, 'lot', None):
|
||||
lot = ct.lot
|
||||
trade_line = (
|
||||
lot.sale_line if type_ == 'Purchase' else lot.line
|
||||
)
|
||||
sources.append({
|
||||
'lqt': None,
|
||||
'lot': lot,
|
||||
'trade_line': trade_line,
|
||||
'quantity': cls._normalize_quantity(getattr(ct, 'quantity', 0)),
|
||||
'shipment_origin': cls._get_shipment_origin(ct),
|
||||
})
|
||||
|
||||
cls._validate_sources(sources, type_)
|
||||
return sources
|
||||
|
||||
@classmethod
|
||||
def _validate_sources(cls, sources, type_):
|
||||
if not sources:
|
||||
return
|
||||
first_line = sources[0]['trade_line']
|
||||
for source in sources[1:]:
|
||||
line = source['trade_line']
|
||||
if bool(getattr(line, 'sale', None)) != bool(getattr(first_line, 'sale', None)):
|
||||
raise UserError('Selected lots must all come from the same side.')
|
||||
if getattr(line.product, 'id', None) != getattr(first_line.product, 'id', None):
|
||||
raise UserError('Selected lots must share the same product.')
|
||||
if getattr(line.unit, 'id', None) != getattr(first_line.unit, 'id', None):
|
||||
raise UserError('Selected lots must share the same unit.')
|
||||
|
||||
@classmethod
|
||||
def _get_line_sources(cls, contract_detail, sources, ct):
|
||||
if not ct.matched or len(sources) <= 1:
|
||||
quantity = cls._normalize_quantity(contract_detail.quantity)
|
||||
source = sources[0] if sources else {
|
||||
'lot': getattr(ct, 'lot', None),
|
||||
'trade_line': None,
|
||||
'shipment_origin': cls._get_shipment_origin(ct),
|
||||
}
|
||||
return [{
|
||||
**source,
|
||||
'quantity': quantity,
|
||||
'use_source_delivery': False,
|
||||
}]
|
||||
|
||||
selected_total = sum(source['quantity'] for source in sources)
|
||||
requested = cls._normalize_quantity(contract_detail.quantity)
|
||||
if requested != selected_total:
|
||||
raise UserError(
|
||||
'For multi-lot matched creation, quantity must equal the total selected open quantity.'
|
||||
)
|
||||
return [{
|
||||
**source,
|
||||
'use_source_delivery': True,
|
||||
} for source in sources]
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# LOT / MATCHING (repris tel quel du wizard)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def _create_lot(cls, line, c, ct, type_):
|
||||
def _create_lot(cls, line, c, source, type_):
|
||||
pool = Pool()
|
||||
Lot = pool.get('lot.lot')
|
||||
LotQtHist = pool.get('lot.qt.hist')
|
||||
@@ -192,10 +301,9 @@ class ContractFactory:
|
||||
|
||||
lot.save()
|
||||
|
||||
vlot = ct.lot
|
||||
shipment_origin = cls._get_shipment_origin(ct)
|
||||
|
||||
qt = c.quantity
|
||||
vlot = source['lot']
|
||||
shipment_origin = source.get('shipment_origin')
|
||||
qt = source['quantity']
|
||||
|
||||
if type_ == 'Purchase':
|
||||
if not lot.updateVirtualPart(qt, shipment_origin, vlot):
|
||||
|
||||
@@ -6,7 +6,9 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from trytond.pool import Pool
|
||||
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.service import ContractFactory
|
||||
|
||||
|
||||
class PurchaseTradeTestCase(ModuleTestCase):
|
||||
@@ -201,5 +203,35 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026',
|
||||
])
|
||||
|
||||
def test_contract_factory_uses_one_line_per_selected_source(self):
|
||||
'matched multi-lot contract creation keeps one generated line per source lot'
|
||||
contract_detail = Mock(quantity=Decimal('2000'))
|
||||
ct = Mock(matched=True)
|
||||
line_a = Mock()
|
||||
line_b = Mock()
|
||||
sources = [
|
||||
{'lot': Mock(), 'trade_line': line_a, 'quantity': Decimal('1000')},
|
||||
{'lot': Mock(), 'trade_line': line_b, 'quantity': Decimal('1000')},
|
||||
]
|
||||
|
||||
result = ContractFactory._get_line_sources(contract_detail, sources, ct)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertEqual([r['quantity'] for r in result], [
|
||||
Decimal('1000'), Decimal('1000')])
|
||||
self.assertTrue(all(r['use_source_delivery'] for r in result))
|
||||
|
||||
def test_contract_factory_rejects_multi_lot_quantity_mismatch(self):
|
||||
'matched multi-lot contract creation rejects totals that do not match the selection'
|
||||
contract_detail = Mock(quantity=Decimal('1500'))
|
||||
ct = Mock(matched=True)
|
||||
sources = [
|
||||
{'lot': Mock(), 'trade_line': Mock(), 'quantity': Decimal('1000')},
|
||||
{'lot': Mock(), 'trade_line': Mock(), 'quantity': Decimal('1000')},
|
||||
]
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
ContractFactory._get_line_sources(contract_detail, sources, ct)
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
||||
Reference in New Issue
Block a user