Linkage report

This commit is contained in:
2026-07-07 18:42:34 +02:00
parent 1b292e4594
commit 82143ba87d
4 changed files with 329 additions and 3 deletions

View File

@@ -365,5 +365,6 @@ def register():
stock.ShipmentPackingListReport,
stock.ShipmentLinkageReport,
stock.LotQtLinkageReport,
stock.LotReportLinkageReport,
module='purchase_trade', type_='report')

View File

@@ -4470,6 +4470,226 @@ class LotQtLinkageReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
'shipment_linkage_report_template', 'stock')
class LotReportLinkageRecord:
def __init__(self, lot, lot_report=None):
self.lot = lot
self.lot_report = lot_report
self.id = getattr(lot_report, 'id', None) or getattr(lot, 'id', None)
@property
def rec_name(self):
return (
getattr(self.lot_report, 'rec_name', None)
or getattr(self.lot, 'rec_name', None)
or getattr(self.lot, 'lot_name', None)
or str(self.id))
def set_lang(self, language=None):
set_lang = getattr(self.lot, 'set_lang', None)
if callable(set_lang):
set_lang(language)
@staticmethod
def _report_record_key(record):
return ShipmentIn._report_record_key(record)
@classmethod
def _report_unique_records(cls, records):
return ShipmentIn._report_unique_records(records)
@staticmethod
def _report_linkage_lot_purchase_line(lot):
return ShipmentIn._report_linkage_lot_purchase_line(lot)
@staticmethod
def _report_linkage_lot_sale_line(lot):
return ShipmentIn._report_linkage_lot_sale_line(lot)
@staticmethod
def _report_rec_name(record):
return ShipmentIn._report_rec_name(record)
@classmethod
def _report_number(cls, record):
return ShipmentIn._report_number(record)
@staticmethod
def _report_date(value):
return ShipmentIn._report_date(value)
@staticmethod
def _report_amount(value):
return ShipmentIn._report_amount(value)
@staticmethod
def _report_price(value):
return ShipmentIn._report_price(value)
@classmethod
def _report_currency_code(cls, record):
return ShipmentIn._report_currency_code(record)
@classmethod
def _report_unit_symbol(cls, line):
return ShipmentIn._report_unit_symbol(line)
@classmethod
def _report_line_quantity(cls, line):
return ShipmentIn._report_line_quantity(line)
@classmethod
def _report_line_amount(cls, line):
return ShipmentIn._report_line_amount(line)
@classmethod
def _report_fee_amount(cls, fee):
return ShipmentIn._report_fee_amount(fee)
@classmethod
def _report_column(cls, rows, index, amount=False):
return ShipmentIn._report_column(rows, index, amount=amount)
@classmethod
def _report_table_rows(cls, rows, names, amount_names=()):
return ShipmentIn._report_table_rows(rows, names, amount_names)
@staticmethod
def _format_report_quantity(value, digits='0.001'):
return ShipmentIn._format_report_quantity(value, digits=digits)
@staticmethod
def _get_report_unit_text(unit):
return ShipmentIn._get_report_unit_text(unit)
def _get_report_primary_move(self):
return getattr(self.lot, 'move', None)
def _get_report_linkage_lots(self):
return [self.lot]
_get_report_linkage_purchase_lines = (
ShipmentIn._get_report_linkage_purchase_lines)
_get_report_linkage_sale_lines = ShipmentIn._get_report_linkage_sale_lines
_get_report_linkage_purchase = ShipmentIn._get_report_linkage_purchase
_get_report_linkage_sale = ShipmentIn._get_report_linkage_sale
_get_report_linkage_product = ShipmentIn._get_report_linkage_product
def _get_report_linkage_fees(self):
shipment = getattr(self.lot, 'lot_shipment_in', None)
return list(getattr(shipment, 'fees', []) or [])
_get_report_linkage_summary_rows = (
ShipmentIn._get_report_linkage_summary_rows)
_get_report_linkage_movement_rows = (
ShipmentIn._get_report_linkage_movement_rows)
_get_report_linkage_movement_row = (
ShipmentIn._get_report_linkage_movement_row)
_get_report_linkage_pricing_rows = (
ShipmentIn._get_report_linkage_pricing_rows)
_get_report_linkage_detail_rows = (
ShipmentIn._get_report_linkage_detail_rows)
_get_report_linkage_detail_row = (
ShipmentIn._get_report_linkage_detail_row)
_get_report_linkage_fee_detail_row = (
ShipmentIn._get_report_linkage_fee_detail_row)
report_linkage_title = ShipmentIn.report_linkage_title
report_linkage_desk = ShipmentIn.report_linkage_desk
report_linkage_book = ShipmentIn.report_linkage_book
report_linkage_strategy = ShipmentIn.report_linkage_strategy
@property
def report_linkage_bl_date(self):
shipment = getattr(self.lot, 'lot_shipment_in', None)
bl_date = getattr(shipment, 'bl_date', None)
if not bl_date:
move = self._get_report_primary_move()
bl_date = getattr(move, 'bldate', None)
return bl_date and bl_date.strftime('%A, %B %d, %Y') or ''
report_linkage_trading_unit = ShipmentIn.report_linkage_trading_unit
report_linkage_finance_user = ShipmentIn.report_linkage_finance_user
report_linkage_summary_groups = ShipmentIn.report_linkage_summary_groups
report_linkage_summary_cost_types = (
ShipmentIn.report_linkage_summary_cost_types)
report_linkage_summary_estimated = (
ShipmentIn.report_linkage_summary_estimated)
report_linkage_summary_validated = (
ShipmentIn.report_linkage_summary_validated)
report_linkage_summary_rows = ShipmentIn.report_linkage_summary_rows
report_linkage_movement_types = ShipmentIn.report_linkage_movement_types
report_linkage_movement_references = (
ShipmentIn.report_linkage_movement_references)
report_linkage_movement_counterparts = (
ShipmentIn.report_linkage_movement_counterparts)
report_linkage_movement_commodities = (
ShipmentIn.report_linkage_movement_commodities)
report_linkage_movement_quantities = (
ShipmentIn.report_linkage_movement_quantities)
report_linkage_movement_deliveries = (
ShipmentIn.report_linkage_movement_deliveries)
report_linkage_movement_basis = ShipmentIn.report_linkage_movement_basis
report_linkage_movement_periods = (
ShipmentIn.report_linkage_movement_periods)
report_linkage_movement_from_dates = (
ShipmentIn.report_linkage_movement_from_dates)
report_linkage_movement_to_dates = (
ShipmentIn.report_linkage_movement_to_dates)
report_linkage_movement_rows = ShipmentIn.report_linkage_movement_rows
report_linkage_bank_deal = ShipmentIn.report_linkage_bank_deal
report_linkage_bank_type = ShipmentIn.report_linkage_bank_type
report_linkage_bank_name = ShipmentIn.report_linkage_bank_name
report_linkage_lc_type = ShipmentIn.report_linkage_lc_type
report_linkage_lc_number = ShipmentIn.report_linkage_lc_number
report_linkage_lc_amount = ShipmentIn.report_linkage_lc_amount
report_linkage_lc_expiry_date = ShipmentIn.report_linkage_lc_expiry_date
report_linkage_pricing_deals = ShipmentIn.report_linkage_pricing_deals
report_linkage_pricing_sides = ShipmentIn.report_linkage_pricing_sides
report_linkage_pricing_types = ShipmentIn.report_linkage_pricing_types
report_linkage_pricing_symbols = ShipmentIn.report_linkage_pricing_symbols
report_linkage_pricing_periods = ShipmentIn.report_linkage_pricing_periods
report_linkage_pricing_input_prices = (
ShipmentIn.report_linkage_pricing_input_prices)
report_linkage_pricing_rows = ShipmentIn.report_linkage_pricing_rows
report_linkage_detail_groups = ShipmentIn.report_linkage_detail_groups
report_linkage_detail_cost_types = (
ShipmentIn.report_linkage_detail_cost_types)
report_linkage_detail_counterparts = (
ShipmentIn.report_linkage_detail_counterparts)
report_linkage_detail_sources = ShipmentIn.report_linkage_detail_sources
report_linkage_detail_unit_prices = (
ShipmentIn.report_linkage_detail_unit_prices)
report_linkage_detail_quantities = (
ShipmentIn.report_linkage_detail_quantities)
report_linkage_detail_estimated = (
ShipmentIn.report_linkage_detail_estimated)
report_linkage_detail_validated = (
ShipmentIn.report_linkage_detail_validated)
report_linkage_detail_rows = ShipmentIn.report_linkage_detail_rows
class LotReportLinkageReport(LotQtLinkageReport):
__name__ = 'lot.report.linkage'
@classmethod
def _get_records(cls, ids, model, data):
LotReport = Pool().get('lot.report')
records = LotReport.browse(ids)
linkage_records = []
for record in records:
if getattr(record, 'r_lot_type', None) != 'physic':
raise UserError(
'Linkage report is only available on physical lot lines.')
lot = getattr(record, 'r_lot_p', None) or getattr(
record, 'r_lot_s', None)
if not lot:
raise UserError(
'Linkage report needs a physical lot on the selected line.')
linkage_records.append(LotReportLinkageRecord(lot, record))
return linkage_records
class ShipmentCOOReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
__name__ = 'stock.shipment.in.coo'

View File

@@ -250,13 +250,13 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.action.report" id="report_shipment_in_linkage">
<field name="name">Linkage</field>
<field name="model">lot.qt</field>
<field name="report_name">lot.qt.linkage</field>
<field name="model">lot.report</field>
<field name="report_name">lot.report.linkage</field>
<field name="report">stock/linkage.fodt</field>
</record>
<record model="ir.action.keyword" id="report_shipment_in_linkage_keyword">
<field name="keyword">form_print</field>
<field name="model">lot.qt,-1</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="report_shipment_in_linkage"/>
</record>

View File

@@ -6584,6 +6584,111 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertIn('80.000 USD/MT', lotqt.report_linkage_pricing_input_prices)
self.assertIn('100.000 USD/MT', lotqt.report_linkage_pricing_input_prices)
def test_lot_report_linkage_rejects_non_physical_lines(self):
'lot.report linkage report is only available for physical lines'
linkage_report = Pool().get('lot.report.linkage', type='report')
lot_report_model = Mock()
lot_report_model.browse.return_value = [
SimpleNamespace(
id=10000001,
rec_name='Open quantity',
r_lot_type='virtual',
r_lot_p=None,
r_lot_s=None,
)
]
with patch('trytond.modules.purchase_trade.stock.Pool') as PoolMock:
PoolMock.return_value.get.return_value = lot_report_model
with self.assertRaises(UserError):
linkage_report._get_records([10000001], 'lot.report', {})
def test_lot_report_linkage_helpers_use_physical_lot(self):
'lot.report linkage report exposes data from the selected physical lot'
currency = SimpleNamespace(code='USD')
unit = SimpleNamespace(symbol='MT')
product = SimpleNamespace(
rec_name='Sulphuric Acid',
name='Sulphuric Acid',
code='H2SO4')
purchase = SimpleNamespace(
id=1,
number='P-10',
reference='26.0001',
currency=currency,
party=SimpleNamespace(rec_name='SUPPLIER SA'),
company=SimpleNamespace(party=SimpleNamespace(rec_name='TRADING SA')),
to_location=SimpleNamespace(rec_name='Odda'),
incoterm=SimpleNamespace(code='FOB'),
operator=SimpleNamespace(rec_name='Operator A'))
sale = SimpleNamespace(
id=2,
number='S-20',
reference='26.0001',
currency=currency,
party=SimpleNamespace(rec_name='CLIENT SA'),
to_location=SimpleNamespace(rec_name='Mejillones'),
incoterm=SimpleNamespace(code='CFR'))
period = SimpleNamespace(rec_name='Chile FY26')
purchase_line = SimpleNamespace(
id=11,
product=product,
purchase=purchase,
quantity_theorical=Decimal('100'),
quantity=Decimal('100'),
unit_price=Decimal('80'),
unit=unit,
price_type='fixed',
del_period=period,
period_at='laycan',
from_del=datetime.date(2026, 4, 30),
to_del=datetime.date(2026, 5, 8),
fees=[],
mtm=[])
sale_line = SimpleNamespace(
id=12,
product=product,
sale=sale,
quantity_theorical=Decimal('100'),
quantity=Decimal('100'),
unit_price=Decimal('100'),
unit=unit,
price_type='fixed',
del_period=period,
period_at='laycan',
from_del=datetime.date(2026, 4, 30),
to_del=datetime.date(2026, 4, 30),
fees=[],
mtm=[])
physical_lot = SimpleNamespace(
id=101,
rec_name='LOT-101',
line=purchase_line,
sale_line=sale_line,
lot_shipment_in=SimpleNamespace(
bl_date=datetime.date(2026, 4, 30),
fees=[]),
move=None)
lot_report = SimpleNamespace(
id=101,
rec_name='Physical lot',
r_lot_type='physic',
r_lot_p=physical_lot,
r_lot_s=None)
record = stock_module.LotReportLinkageRecord(
physical_lot, lot_report)
self.assertEqual(
record.report_linkage_title, 'Linkage P-10/S-20 26.0001')
self.assertEqual(record.report_linkage_bl_date, 'Thursday, April 30, 2026')
self.assertIn('Purchases', record.report_linkage_summary_groups)
self.assertIn('Sales', record.report_linkage_summary_groups)
self.assertIn('P-10', record.report_linkage_movement_references)
self.assertIn('S-20', record.report_linkage_movement_references)
self.assertIn('80.000 USD/MT', record.report_linkage_pricing_input_prices)
self.assertIn('100.000 USD/MT', record.report_linkage_pricing_input_prices)
def test_shipment_insurance_contact_surveyor_prefers_shipment_surveyor(self):
'insurance contact surveyor property uses shipment surveyor first'
ShipmentIn = Pool().get('stock.shipment.in')