diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index c8d34a3..1a7d0d8 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -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' diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index 21d7a57..fc5a4e3 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -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)): diff --git a/modules/purchase_trade/view/lot_report_context_form.xml b/modules/purchase_trade/view/lot_report_context_form.xml index f2e3af9..1888fd2 100755 --- a/modules/purchase_trade/view/lot_report_context_form.xml +++ b/modules/purchase_trade/view/lot_report_context_form.xml @@ -1,11 +1,12 @@ -
+ \ No newline at end of file