Valuation open matché
This commit is contained in:
@@ -320,6 +320,51 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(target[1]['amount_prev'], Decimal('-180.00'))
|
||||
self.assertEqual(target[1]['mtm'], Decimal('-200.00'))
|
||||
|
||||
def test_purchase_pnl_uses_partial_lotqt_match_quantity(self):
|
||||
'open matched purchase and sale pnl use the matched lot.qt quantity'
|
||||
Valuation = valuation_module.Valuation
|
||||
unit = Mock(id=1)
|
||||
currency = Mock(id=2)
|
||||
company = Mock(currency=2)
|
||||
purchase = Mock(
|
||||
id=10, currency=currency, company=company, party=Mock(id=11))
|
||||
purchase_line = Mock(
|
||||
id=20, price_type='priced', mtm=[], purchase=purchase,
|
||||
product=Mock(id=21), unit=unit, lots=[])
|
||||
purchase_lot = Mock(
|
||||
id=30, lot_type='virtual', lot_price=Decimal('45'),
|
||||
sale_line=None)
|
||||
purchase_lot.get_current_quantity_converted.return_value = (
|
||||
Decimal('1000'))
|
||||
sale = Mock(
|
||||
id=40, currency=currency, company=company, party=Mock(id=41))
|
||||
sale_line = Mock(
|
||||
id=50, price_type='priced', mtm=[], sale=sale,
|
||||
product=Mock(id=51), unit=unit)
|
||||
sale_lot = Mock(
|
||||
id=60, lot_type='virtual', lot_price_sale=Decimal('100'),
|
||||
sale_line=sale_line)
|
||||
sale_lot.get_current_quantity_converted.return_value = Decimal('2000')
|
||||
purchase_line.lots = [purchase_lot]
|
||||
lotqt = Mock(
|
||||
lot_p=purchase_lot, lot_s=sale_lot,
|
||||
lot_quantity=Decimal('500'), lot_unit=unit)
|
||||
lotqt_model = Mock()
|
||||
lotqt_model.search.return_value = [lotqt]
|
||||
pool = Mock()
|
||||
pool.get.return_value = lotqt_model
|
||||
|
||||
with patch.object(valuation_module, 'Pool', return_value=pool):
|
||||
lines = Valuation.create_pnl_price_from_line(purchase_line)
|
||||
|
||||
self.assertEqual(len(lines), 2)
|
||||
self.assertEqual(lines[0]['type'], 'pur. priced')
|
||||
self.assertEqual(lines[0]['quantity'], Decimal('500.00000'))
|
||||
self.assertEqual(lines[0]['amount'], Decimal('-22500.00'))
|
||||
self.assertEqual(lines[1]['type'], 'sale priced')
|
||||
self.assertEqual(lines[1]['quantity'], Decimal('500.00000'))
|
||||
self.assertEqual(lines[1]['amount'], Decimal('50000.00'))
|
||||
|
||||
@with_transaction()
|
||||
def test_purchase_line_charter_conditions_inherit_header_when_empty(self):
|
||||
'purchase line uses header charter conditions when it has no line terms'
|
||||
|
||||
@@ -98,6 +98,77 @@ class ValuationBase(ModelSQL):
|
||||
def _lot_quantity(cls, lot):
|
||||
return Decimal(str(lot.get_current_quantity_converted() or 0))
|
||||
|
||||
@classmethod
|
||||
def _lotqt_quantity(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:
|
||||
return quantity
|
||||
lot_unit_id = cls._record_id(lot_unit)
|
||||
line_unit_id = cls._record_id(line_unit)
|
||||
if lot_unit_id == line_unit_id:
|
||||
return quantity
|
||||
Uom = Pool().get('product.uom')
|
||||
factor = None
|
||||
rate = None
|
||||
lot_category = getattr(lot_unit, 'category', None)
|
||||
line_category = getattr(line_unit, 'category', None)
|
||||
if (lot_category and line_category
|
||||
and cls._record_id(lot_category) != cls._record_id(line_category)):
|
||||
factor = 1
|
||||
rate = 1
|
||||
return Decimal(str(Uom.compute_qty(
|
||||
lot_unit, float(quantity), line_unit, True, factor, rate)))
|
||||
|
||||
@classmethod
|
||||
def _purchase_lot_segments(cls, line, lot, LotQt):
|
||||
if getattr(lot, 'lot_type', None) == 'physic':
|
||||
sale_lot = lot if getattr(lot, 'sale_line', None) else None
|
||||
return [{
|
||||
'quantity': None,
|
||||
'sale_quantity': None,
|
||||
'sale_lot': sale_lot,
|
||||
'sale_line': getattr(sale_lot, 'sale_line', None),
|
||||
}]
|
||||
|
||||
lot_id = getattr(lot, 'id', None)
|
||||
if not lot_id:
|
||||
return [{
|
||||
'quantity': None,
|
||||
'sale_quantity': None,
|
||||
'sale_lot': None,
|
||||
'sale_line': None,
|
||||
}]
|
||||
|
||||
lqts = LotQt.search([
|
||||
('lot_p', '=', lot_id),
|
||||
('lot_quantity', '>', 0),
|
||||
])
|
||||
if not lqts:
|
||||
matched_sale_line = cls._get_matched_sale_line_from_purchase_lot(
|
||||
lot, LotQt=LotQt)
|
||||
return [{
|
||||
'quantity': None,
|
||||
'sale_quantity': None,
|
||||
'sale_lot': None,
|
||||
'sale_line': matched_sale_line,
|
||||
}]
|
||||
|
||||
segments = []
|
||||
for lqt in lqts:
|
||||
sale_lot = getattr(lqt, 'lot_s', None)
|
||||
sale_line = getattr(sale_lot, 'sale_line', None)
|
||||
segments.append({
|
||||
'quantity': cls._lotqt_quantity(lqt, line),
|
||||
'sale_quantity': (
|
||||
cls._lotqt_quantity(lqt, sale_line)
|
||||
if sale_line else None),
|
||||
'sale_lot': sale_lot,
|
||||
'sale_line': sale_line,
|
||||
})
|
||||
return segments
|
||||
|
||||
@classmethod
|
||||
def _ignore_empty_open_fee_lot(cls, lot):
|
||||
return lot.lot_type != 'physic' and cls._lot_quantity(lot) == 0
|
||||
@@ -534,7 +605,8 @@ class ValuationBase(ModelSQL):
|
||||
return round(total - components, 4)
|
||||
|
||||
@classmethod
|
||||
def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign, extra_price=Decimal(0)):
|
||||
def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign,
|
||||
extra_price=Decimal(0), quantity=None):
|
||||
Currency = Pool().get('currency.currency')
|
||||
Date = Pool().get('ir.date')
|
||||
values = cls._base_pnl(
|
||||
@@ -545,7 +617,8 @@ class ValuationBase(ModelSQL):
|
||||
pnl_type='sale priced' if sale_line else 'pur. priced'
|
||||
)
|
||||
|
||||
qty = lot.get_current_quantity_converted()
|
||||
qty = (Decimal(str(quantity)) if quantity is not None
|
||||
else lot.get_current_quantity_converted())
|
||||
|
||||
price = pc.price
|
||||
logger.info("TERMS:%s",line.terms)
|
||||
@@ -597,7 +670,8 @@ class ValuationBase(ModelSQL):
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign, pnl_type):
|
||||
def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign,
|
||||
pnl_type, quantity=None):
|
||||
Currency = Pool().get('currency.currency')
|
||||
Date = Pool().get('ir.date')
|
||||
values = cls._base_pnl(
|
||||
@@ -608,7 +682,8 @@ class ValuationBase(ModelSQL):
|
||||
pnl_type=pnl_type
|
||||
)
|
||||
|
||||
qty = lot.get_current_quantity_converted()
|
||||
qty = (Decimal(str(quantity)) if quantity is not None
|
||||
else lot.get_current_quantity_converted())
|
||||
amount = round(price * qty * Decimal(sign), 2)
|
||||
base_amount = amount
|
||||
currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id
|
||||
@@ -648,114 +723,98 @@ class ValuationBase(ModelSQL):
|
||||
LotQt = Pool().get('lot.qt')
|
||||
|
||||
for lot in cls._valuation_lots(line):
|
||||
matched_sale_line = cls._get_matched_sale_line_from_purchase_lot(
|
||||
lot, LotQt=LotQt)
|
||||
for segment in cls._purchase_lot_segments(line, lot, LotQt):
|
||||
cls._create_pnl_price_from_purchase_segment(
|
||||
price_lines, line, lot, segment)
|
||||
|
||||
if line.price_type == 'basis':
|
||||
premium_delta = cls._get_basis_premium_delta(line)
|
||||
summaries = line.price_summary or []
|
||||
if not summaries:
|
||||
values = cls._build_simple_pnl(
|
||||
line=line,
|
||||
lot=lot,
|
||||
sale_line=None,
|
||||
price=Decimal(line.unit_price or 0) + premium_delta,
|
||||
state='unfixed',
|
||||
sign=-1,
|
||||
pnl_type='pur. priced'
|
||||
)
|
||||
cls._set_matched_sale_values(values, matched_sale_line)
|
||||
if line.mtm and cls._supports_strategy_mtm(values):
|
||||
for strat in line.mtm:
|
||||
cls._append_strategy_mtm_lines(
|
||||
price_lines, values, strat, line)
|
||||
else:
|
||||
if values:
|
||||
price_lines.append(values)
|
||||
continue
|
||||
return price_lines
|
||||
|
||||
@classmethod
|
||||
def _append_pnl_values(cls, price_lines, values, mtm_source):
|
||||
if (values and getattr(mtm_source, 'mtm', None)
|
||||
and cls._supports_strategy_mtm(values)):
|
||||
for strat in mtm_source.mtm:
|
||||
cls._append_strategy_mtm_lines(
|
||||
price_lines, values, strat, mtm_source)
|
||||
elif values:
|
||||
price_lines.append(values)
|
||||
|
||||
@classmethod
|
||||
def _create_pnl_price_from_purchase_segment(cls, price_lines, line, lot,
|
||||
segment):
|
||||
matched_sale_line = segment['sale_line']
|
||||
quantity = segment['quantity']
|
||||
|
||||
if line.price_type == 'basis':
|
||||
premium_delta = cls._get_basis_premium_delta(line)
|
||||
summaries = line.price_summary or []
|
||||
if not summaries:
|
||||
values = cls._build_simple_pnl(
|
||||
line=line,
|
||||
lot=lot,
|
||||
sale_line=None,
|
||||
price=Decimal(line.unit_price or 0) + premium_delta,
|
||||
state='unfixed',
|
||||
sign=-1,
|
||||
pnl_type='pur. priced',
|
||||
quantity=quantity,
|
||||
)
|
||||
cls._set_matched_sale_values(values, matched_sale_line)
|
||||
cls._append_pnl_values(price_lines, values, line)
|
||||
else:
|
||||
for pc in summaries:
|
||||
values = cls._build_basis_pnl(
|
||||
line=line, lot=lot, sale_line=None, pc=pc, sign=-1,
|
||||
extra_price=premium_delta)
|
||||
extra_price=premium_delta, quantity=quantity)
|
||||
cls._set_matched_sale_values(values, matched_sale_line)
|
||||
if line.mtm and cls._supports_strategy_mtm(values):
|
||||
for strat in line.mtm:
|
||||
cls._append_strategy_mtm_lines(
|
||||
price_lines, values, strat, line)
|
||||
else:
|
||||
if values:
|
||||
price_lines.append(values)
|
||||
cls._append_pnl_values(price_lines, values, line)
|
||||
|
||||
elif line.price_type in ('priced', 'efp') and lot.lot_price:
|
||||
values = cls._build_simple_pnl(
|
||||
line=line,
|
||||
lot=lot,
|
||||
sale_line=None,
|
||||
price=lot.lot_price,
|
||||
state='fixed' if line.price_type == 'priced' else 'not fixed',
|
||||
sign=-1,
|
||||
pnl_type=f'pur. {line.price_type}'
|
||||
)
|
||||
cls._set_matched_sale_values(values, matched_sale_line)
|
||||
if line.mtm and cls._supports_strategy_mtm(values):
|
||||
for strat in line.mtm:
|
||||
cls._append_strategy_mtm_lines(
|
||||
price_lines, values, strat, line)
|
||||
else:
|
||||
if values:
|
||||
price_lines.append(values)
|
||||
elif line.price_type in ('priced', 'efp') and lot.lot_price:
|
||||
values = cls._build_simple_pnl(
|
||||
line=line,
|
||||
lot=lot,
|
||||
sale_line=None,
|
||||
price=lot.lot_price,
|
||||
state='fixed' if line.price_type == 'priced' else 'not fixed',
|
||||
sign=-1,
|
||||
pnl_type=f'pur. {line.price_type}',
|
||||
quantity=quantity,
|
||||
)
|
||||
cls._set_matched_sale_values(values, matched_sale_line)
|
||||
cls._append_pnl_values(price_lines, values, line)
|
||||
|
||||
sale_lots = [lot] if lot.sale_line else [
|
||||
lqt.lot_s for lqt in LotQt.search([
|
||||
('lot_p', '=', lot.id),
|
||||
('lot_s', '>', 0),
|
||||
('lot_quantity', '>', 0),
|
||||
])
|
||||
]
|
||||
sale_lot = segment['sale_lot']
|
||||
if not sale_lot:
|
||||
return
|
||||
sale_line = sale_lot.sale_line
|
||||
if not sale_line:
|
||||
return
|
||||
if cls._ignore_finished_open_lot(line, sale_lot):
|
||||
return
|
||||
if cls._ignore_finished_open_lot(sale_line, sale_lot):
|
||||
return
|
||||
|
||||
for sl in sale_lots:
|
||||
sl_line = sl.sale_line
|
||||
if not sl_line:
|
||||
continue
|
||||
if cls._ignore_finished_open_lot(line, sl):
|
||||
continue
|
||||
if cls._ignore_finished_open_lot(sl_line, sl):
|
||||
continue
|
||||
if sale_line.price_type == 'basis':
|
||||
premium_delta = cls._get_basis_premium_delta(sale_line)
|
||||
for pc in sale_line.price_summary or []:
|
||||
values = cls._build_basis_pnl(
|
||||
line=line, lot=sale_lot, sale_line=sale_line, pc=pc,
|
||||
sign=+1, extra_price=premium_delta,
|
||||
quantity=segment.get('sale_quantity', quantity))
|
||||
cls._append_pnl_values(price_lines, values, sale_line)
|
||||
|
||||
if sl_line.price_type == 'basis':
|
||||
premium_delta = cls._get_basis_premium_delta(sl_line)
|
||||
for pc in sl_line.price_summary or []:
|
||||
values = cls._build_basis_pnl(
|
||||
line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1,
|
||||
extra_price=premium_delta)
|
||||
if sl_line.mtm and cls._supports_strategy_mtm(values):
|
||||
for strat in line.mtm:
|
||||
cls._append_strategy_mtm_lines(
|
||||
price_lines, values, strat, sl_line)
|
||||
else:
|
||||
if values:
|
||||
price_lines.append(values)
|
||||
|
||||
elif sl_line.price_type in ('priced', 'efp'):
|
||||
values = cls._build_simple_pnl(
|
||||
line=line,
|
||||
lot=sl,
|
||||
sale_line=sl_line,
|
||||
price=sl.lot_price_sale,
|
||||
state='fixed' if sl_line.price_type == 'priced' else 'not fixed',
|
||||
sign=+1,
|
||||
pnl_type=f'sale {sl_line.price_type}'
|
||||
)
|
||||
if sl_line.mtm and cls._supports_strategy_mtm(values):
|
||||
for strat in sl_line.mtm:
|
||||
cls._append_strategy_mtm_lines(
|
||||
price_lines, values, strat, sl_line)
|
||||
else:
|
||||
if values:
|
||||
price_lines.append(values)
|
||||
|
||||
return price_lines
|
||||
elif sale_line.price_type in ('priced', 'efp'):
|
||||
values = cls._build_simple_pnl(
|
||||
line=line,
|
||||
lot=sale_lot,
|
||||
sale_line=sale_line,
|
||||
price=sale_lot.lot_price_sale,
|
||||
state='fixed' if sale_line.price_type == 'priced' else 'not fixed',
|
||||
sign=+1,
|
||||
pnl_type=f'sale {sale_line.price_type}',
|
||||
quantity=segment.get('sale_quantity', quantity),
|
||||
)
|
||||
cls._append_pnl_values(price_lines, values, sale_line)
|
||||
|
||||
@classmethod
|
||||
def _build_basis_pnl_from_sale_line(cls, *, sale_line, lot, pc, extra_price=Decimal(0)):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<form col="8">
|
||||
<form col="8" col_widths="110,150,110,280,95,300,75,300">
|
||||
<label name="asof"/>
|
||||
<field name="asof"/>
|
||||
<field name="asof" xexpand="0"/>
|
||||
<label name="todate"/>
|
||||
<field name="todate"/>
|
||||
<field name="todate" xexpand="0"/>
|
||||
<label name="ps"/>
|
||||
<field name="ps"/>
|
||||
<newline/>
|
||||
|
||||
<label name="supplier"/>
|
||||
<field name="supplier"/>
|
||||
<label name="purchase"/>
|
||||
@@ -15,6 +16,7 @@
|
||||
<label name="sale"/>
|
||||
<field name="sale"/>
|
||||
<newline/>
|
||||
|
||||
<label name="dimension"/>
|
||||
<field name="dimension"/>
|
||||
<label name="product"/>
|
||||
@@ -22,6 +24,7 @@
|
||||
<label name="location"/>
|
||||
<field name="location"/>
|
||||
<newline/>
|
||||
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="shipping_status"/>
|
||||
@@ -31,16 +34,9 @@
|
||||
<label name="group"/>
|
||||
<field name="group"/>
|
||||
<newline/>
|
||||
<!--
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
-->
|
||||
<!--
|
||||
<label name="mode"/>
|
||||
<field name="mode"/>
|
||||
-->
|
||||
|
||||
<label name="origin"/>
|
||||
<field name="origin"/>
|
||||
<label name="finished"/>
|
||||
<field name="finished" xexpand="0" width="25"/>
|
||||
</form>
|
||||
</form>
|
||||
Reference in New Issue
Block a user