759 lines
30 KiB
Python
759 lines
30 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
|
|
from decimal import Decimal
|
|
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):
|
|
'Test purchase_trade module'
|
|
module = 'purchase_trade'
|
|
|
|
@with_transaction()
|
|
def test_get_totals_without_rows(self):
|
|
'get_totals returns zeros when the query has no row'
|
|
Valuation = Pool().get('valuation.valuation')
|
|
cursor = Mock()
|
|
cursor.fetchone.return_value = None
|
|
connection = Mock()
|
|
connection.cursor.return_value = cursor
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.valuation.Transaction'
|
|
) as Transaction, patch.object(
|
|
Valuation, '__table__', return_value='valuation_valuation'):
|
|
Transaction.return_value.connection = connection
|
|
|
|
self.assertEqual(
|
|
Valuation.get_totals(), (Decimal(0), Decimal(0)))
|
|
|
|
@with_transaction()
|
|
def test_get_totals_without_previous_total(self):
|
|
'get_totals converts null variation to zero'
|
|
Valuation = Pool().get('valuation.valuation')
|
|
cursor = Mock()
|
|
cursor.fetchone.return_value = (Decimal('42.50'), None)
|
|
connection = Mock()
|
|
connection.cursor.return_value = cursor
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.valuation.Transaction'
|
|
) as Transaction, patch.object(
|
|
Valuation, '__table__', return_value='valuation_valuation'):
|
|
Transaction.return_value.connection = connection
|
|
|
|
self.assertEqual(
|
|
Valuation.get_totals(), (Decimal('42.50'), Decimal(0)))
|
|
|
|
@with_transaction()
|
|
def test_get_mtm_applies_component_ratio_as_percentage(self):
|
|
'get_mtm treats component ratio as a percentage'
|
|
Strategy = Pool().get('mtm.strategy')
|
|
strategy = Strategy()
|
|
strategy.scenario = Mock(
|
|
valuation_date='2026-03-29',
|
|
use_last_price=True,
|
|
)
|
|
strategy.currency = Mock()
|
|
strategy.components = [Mock(
|
|
price_source_type='curve',
|
|
price_index=Mock(get_price=Mock(return_value=Decimal('100'))),
|
|
price_matrix=None,
|
|
ratio=Decimal('25'),
|
|
)]
|
|
line = Mock(unit=Mock())
|
|
|
|
self.assertEqual(
|
|
strategy.get_mtm(line, Decimal('10')),
|
|
Decimal('250.00'))
|
|
|
|
def test_get_strategy_mtm_price_returns_unit_price(self):
|
|
'strategy mtm price exposes the raw unit valuation price'
|
|
strategy = Mock(
|
|
scenario=Mock(
|
|
valuation_date='2026-03-29',
|
|
use_last_price=True,
|
|
),
|
|
currency=Mock(),
|
|
)
|
|
strategy.components = [Mock(
|
|
price_source_type='curve',
|
|
price_index=Mock(get_price=Mock(return_value=Decimal('100'))),
|
|
price_matrix=None,
|
|
ratio=Decimal('25'),
|
|
)]
|
|
line = Mock(unit=Mock())
|
|
|
|
self.assertEqual(
|
|
valuation_module.Valuation._get_strategy_mtm_price(strategy, line),
|
|
Decimal('100.0000'))
|
|
|
|
def test_sale_line_is_unmatched_checks_lot_links(self):
|
|
'sale line unmatched helper ignores empty matches and detects linked purchases'
|
|
sale_line = Mock()
|
|
sale_line.get_matched_lines.return_value = []
|
|
self.assertTrue(
|
|
valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line))
|
|
|
|
linked = Mock(lot_p=Mock(line=Mock()))
|
|
sale_line.get_matched_lines.return_value = [linked]
|
|
self.assertFalse(
|
|
valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line))
|
|
|
|
def test_parse_numbers_supports_inline_and_legacy_separators(self):
|
|
'parse_numbers keeps supporting inline entry and legacy separators'
|
|
self.assertEqual(
|
|
valuation_module.ValuationProcess._parse_numbers(
|
|
'PUR-001 PUR-002, PUR-003\nPUR-004;PUR-005'
|
|
),
|
|
['PUR-001', 'PUR-002', 'PUR-003', 'PUR-004', 'PUR-005'])
|
|
|
|
def test_get_generate_types_maps_business_groups(self):
|
|
'valuation type groups map to the expected stored valuation types'
|
|
Valuation = Pool().get('valuation.valuation')
|
|
|
|
self.assertEqual(
|
|
Valuation._get_generate_types('fees'),
|
|
{'line fee', 'pur. fee', 'sale fee', 'shipment fee'})
|
|
self.assertEqual(
|
|
Valuation._get_generate_types('derivatives'),
|
|
{'derivative'})
|
|
self.assertIn('pur. priced', Valuation._get_generate_types('goods'))
|
|
|
|
def test_filter_values_by_types_keeps_matching_entries_only(self):
|
|
'type filtering keeps only the requested valuation entries'
|
|
Valuation = Pool().get('valuation.valuation')
|
|
|
|
values = [
|
|
{'type': 'pur. fee', 'amount': Decimal('10')},
|
|
{'type': 'pur. priced', 'amount': Decimal('20')},
|
|
{'type': 'derivative', 'amount': Decimal('30')},
|
|
]
|
|
|
|
self.assertEqual(
|
|
Valuation._filter_values_by_types(
|
|
values, {'pur. fee', 'derivative'}),
|
|
[
|
|
{'type': 'pur. fee', 'amount': Decimal('10')},
|
|
{'type': 'derivative', 'amount': Decimal('30')},
|
|
])
|
|
|
|
def test_sale_report_crop_name_handles_missing_crop(self):
|
|
'sale report crop name returns an empty string when crop is missing'
|
|
Sale = Pool().get('sale.sale')
|
|
|
|
sale = Sale()
|
|
sale.crop = None
|
|
self.assertEqual(sale.report_crop_name, '')
|
|
|
|
sale.crop = Mock(name='crop')
|
|
sale.crop.name = 'Main Crop'
|
|
self.assertEqual(sale.report_crop_name, 'Main Crop')
|
|
|
|
def test_sale_line_default_pricing_rule_comes_from_configuration(self):
|
|
'sale line pricing_rule defaults to the purchase_trade singleton value'
|
|
SaleLine = Pool().get('sale.line')
|
|
config = Mock(pricing_rule='Default pricing rule')
|
|
configuration_model = Mock()
|
|
configuration_model.search.return_value = [config]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.sale.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = configuration_model
|
|
self.assertEqual(
|
|
SaleLine.default_pricing_rule(), 'Default pricing rule')
|
|
|
|
def test_purchase_line_default_pricing_rule_comes_from_configuration(self):
|
|
'purchase line pricing_rule defaults to the purchase_trade singleton value'
|
|
PurchaseLine = Pool().get('purchase.line')
|
|
config = Mock(pricing_rule='Default pricing rule')
|
|
configuration_model = Mock()
|
|
configuration_model.search.return_value = [config]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.purchase.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = configuration_model
|
|
self.assertEqual(
|
|
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
|
|
|
|
def test_party_execution_achieved_percent_uses_real_area_statistics(self):
|
|
'party execution achieved percent reflects the controller share in its area'
|
|
PartyExecution = Pool().get('party.execution')
|
|
execution = PartyExecution()
|
|
execution.party = Mock(id=1)
|
|
execution.area = Mock(id=10)
|
|
|
|
shipments = [
|
|
Mock(controller=Mock(id=1)),
|
|
Mock(controller=Mock(id=2)),
|
|
Mock(controller=Mock(id=1)),
|
|
Mock(controller=Mock(id=2)),
|
|
Mock(controller=Mock(id=1)),
|
|
]
|
|
shipment_model = Mock()
|
|
shipment_model.search.return_value = shipments
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.party.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = shipment_model
|
|
|
|
self.assertEqual(
|
|
execution.get_percent('achieved_percent'),
|
|
Decimal('60.00'))
|
|
|
|
def test_get_controller_prioritizes_controller_farthest_from_target(self):
|
|
'shipment controller selection prioritizes the most under-target rule'
|
|
Shipment = Pool().get('stock.shipment.in')
|
|
Party = Pool().get('party.party')
|
|
PartyExecution = Pool().get('party.execution')
|
|
|
|
shipment = Shipment()
|
|
shipment.to_location = Mock(
|
|
country=Mock(region=Mock(id=20, parent=Mock(id=10, parent=None))))
|
|
|
|
party_a = Party()
|
|
party_a.id = 1
|
|
rule_a = PartyExecution()
|
|
rule_a.party = party_a
|
|
rule_a.area = Mock(id=10)
|
|
rule_a.percent = Decimal('80')
|
|
rule_a.compute_achieved_percent = Mock(return_value=Decimal('40'))
|
|
party_a.execution = [rule_a]
|
|
|
|
party_b = Party()
|
|
party_b.id = 2
|
|
rule_b = PartyExecution()
|
|
rule_b.party = party_b
|
|
rule_b.area = Mock(id=10)
|
|
rule_b.percent = Decimal('50')
|
|
rule_b.compute_achieved_percent = Mock(return_value=Decimal('45'))
|
|
party_b.execution = [rule_b]
|
|
|
|
category_model = Mock()
|
|
category_model.search.return_value = [Mock(id=99)]
|
|
party_category_model = Mock()
|
|
party_category_model.search.return_value = [
|
|
Mock(party=party_b),
|
|
Mock(party=party_a),
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.stock.Pool'
|
|
) as PoolMock:
|
|
def get_model(name):
|
|
return {
|
|
'party.category': category_model,
|
|
'party.party-party.category': party_category_model,
|
|
}[name]
|
|
|
|
PoolMock.return_value.get.side_effect = get_model
|
|
|
|
self.assertIs(shipment.get_controller(), party_a)
|
|
|
|
def test_weight_report_get_source_shipment_rejects_multiple_shipments(self):
|
|
'weight report export must not guess when the same WR is linked twice'
|
|
WeightReport = Pool().get('weight.report')
|
|
report = WeightReport()
|
|
report.id = 7
|
|
|
|
shipment_wr_model = Mock()
|
|
shipment_wr_model.search.return_value = [
|
|
Mock(shipment_in=Mock(id=1)),
|
|
Mock(shipment_in=Mock(id=2)),
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.weight_report.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = shipment_wr_model
|
|
with self.assertRaises(UserError):
|
|
report.get_source_shipment()
|
|
|
|
def test_weight_report_remote_context_requires_controller_and_returned_id(self):
|
|
'weight report export checks the shipment prerequisites before calling FastAPI'
|
|
WeightReport = Pool().get('weight.report')
|
|
report = WeightReport()
|
|
report.bales = 100
|
|
report.report_date = Mock(strftime=Mock(return_value='20260406'))
|
|
report.weight_date = Mock(strftime=Mock(return_value='20260406'))
|
|
|
|
shipment = Mock(
|
|
controller=None,
|
|
returned_id='RET-001',
|
|
agent=Mock(),
|
|
to_location=Mock(),
|
|
)
|
|
|
|
with self.assertRaises(UserError):
|
|
report.validate_remote_weight_report_context(shipment)
|
|
|
|
shipment.controller = Mock()
|
|
shipment.returned_id = None
|
|
with self.assertRaises(UserError):
|
|
report.validate_remote_weight_report_context(shipment)
|
|
|
|
def test_invoice_report_uses_invoice_template_from_configuration(self):
|
|
'invoice report path is resolved from purchase_trade configuration'
|
|
report_class = Pool().get('account.invoice', type='report')
|
|
config_model = Mock()
|
|
config_model.search.return_value = [
|
|
Mock(
|
|
sale_report_template='sale_melya.fodt',
|
|
sale_bill_report_template='bill_melya.fodt',
|
|
sale_final_report_template='sale_final_melya.fodt',
|
|
invoice_report_template='invoice_melya.fodt',
|
|
invoice_cndn_report_template='invoice_ict_final.fodt',
|
|
invoice_prepayment_report_template='prepayment.fodt',
|
|
purchase_report_template='purchase_melya.fodt',
|
|
)
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.invoice.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = config_model
|
|
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Invoice',
|
|
'report': 'account_invoice/invoice.fodt',
|
|
}),
|
|
'account_invoice/invoice_melya.fodt')
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Prepayment',
|
|
'report': 'account_invoice/prepayment.fodt',
|
|
}),
|
|
'account_invoice/prepayment.fodt')
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'CN/DN',
|
|
'report': 'account_invoice/invoice_ict_final.fodt',
|
|
}),
|
|
'account_invoice/invoice_ict_final.fodt')
|
|
|
|
def test_invoice_report_raises_when_template_is_missing(self):
|
|
'invoice report must fail clearly when no template is configured'
|
|
report_class = Pool().get('account.invoice', type='report')
|
|
config_model = Mock()
|
|
config_model.search.return_value = [
|
|
Mock(
|
|
invoice_report_template='',
|
|
invoice_cndn_report_template='',
|
|
invoice_prepayment_report_template='',
|
|
)
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.invoice.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = config_model
|
|
with self.assertRaises(UserError):
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Invoice',
|
|
'report': 'account_invoice/invoice.fodt',
|
|
})
|
|
|
|
def test_sale_report_uses_templates_from_configuration(self):
|
|
'sale report paths are resolved from purchase_trade configuration'
|
|
report_class = Pool().get('sale.sale', type='report')
|
|
config_model = Mock()
|
|
config_model.search.return_value = [
|
|
Mock(
|
|
sale_report_template='sale_melya.fodt',
|
|
sale_bill_report_template='bill_melya.fodt',
|
|
sale_final_report_template='sale_final_melya.fodt',
|
|
)
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.invoice.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = config_model
|
|
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Sale',
|
|
'report': 'sale/sale.fodt',
|
|
}),
|
|
'sale/sale_melya.fodt')
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Bill',
|
|
'report': 'sale/bill.fodt',
|
|
}),
|
|
'sale/bill_melya.fodt')
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Sale (final)',
|
|
'report': 'sale/sale_final.fodt',
|
|
}),
|
|
'sale/sale_final_melya.fodt')
|
|
|
|
def test_purchase_report_uses_template_from_configuration(self):
|
|
'purchase report path is resolved from purchase_trade configuration'
|
|
report_class = Pool().get('purchase.purchase', type='report')
|
|
config_model = Mock()
|
|
config_model.search.return_value = [
|
|
Mock(purchase_report_template='purchase_melya.fodt')
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.invoice.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = config_model
|
|
|
|
self.assertEqual(
|
|
report_class._resolve_configured_report_path({
|
|
'name': 'Purchase',
|
|
'report': 'purchase/purchase.fodt',
|
|
}),
|
|
'purchase/purchase_melya.fodt')
|
|
|
|
def test_shipment_reports_use_templates_from_configuration(self):
|
|
'shipment report paths are resolved from purchase_trade configuration'
|
|
shipping_report = Pool().get('stock.shipment.in.shipping', type='report')
|
|
insurance_report = Pool().get('stock.shipment.in.insurance', type='report')
|
|
config_model = Mock()
|
|
config_model.search.return_value = [
|
|
Mock(
|
|
shipment_shipping_report_template='si_custom.fodt',
|
|
shipment_insurance_report_template='insurance_custom.fodt',
|
|
)
|
|
]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.stock.Pool'
|
|
) as PoolMock:
|
|
PoolMock.return_value.get.return_value = config_model
|
|
|
|
self.assertEqual(
|
|
shipping_report._resolve_configured_report_path({
|
|
'name': 'Shipping instructions',
|
|
'report': 'stock/si.fodt',
|
|
}),
|
|
'stock/si_custom.fodt')
|
|
self.assertEqual(
|
|
insurance_report._resolve_configured_report_path({
|
|
'name': 'Insurance',
|
|
'report': 'stock/insurance.fodt',
|
|
}),
|
|
'stock/insurance_custom.fodt')
|
|
|
|
def test_shipment_insurance_helpers_use_fee_and_controller(self):
|
|
'shipment insurance helpers read insurance fee and shipment context'
|
|
ShipmentIn = Pool().get('stock.shipment.in')
|
|
shipment = ShipmentIn()
|
|
shipment.number = 'IN/0001'
|
|
shipment.bl_number = 'BL-001'
|
|
shipment.from_location = Mock(name='LIVERPOOL')
|
|
shipment.to_location = Mock(name='LE HAVRE')
|
|
shipment.vessel = Mock(vessel_name='MV ATLANTIC')
|
|
shipment.controller = Mock(rec_name='CONTROL UNION')
|
|
shipment.supplier = Mock(rec_name='MELYA SA')
|
|
|
|
sale_party = Mock(rec_name='SGT FR')
|
|
sale = Mock(party=sale_party)
|
|
product = Mock(name='COTTON UPLAND', description='RAW WHITE COTTON')
|
|
line = Mock(product=product, sale=sale)
|
|
lot = Mock(sale_line=line, line=None)
|
|
move = Mock(lot=lot, product=product)
|
|
shipment.incoming_moves = [move]
|
|
shipment.moves = [move]
|
|
|
|
insurance_fee = Mock()
|
|
insurance_fee.product = Mock(name='Insurance')
|
|
insurance_fee.currency = Mock(rec_name='USD')
|
|
insurance_fee.get_amount.return_value = Decimal('1234.56')
|
|
insurance_fee.supplier = Mock(rec_name='HELVETIA')
|
|
shipment.fees = [insurance_fee]
|
|
|
|
with patch(
|
|
'trytond.modules.purchase_trade.stock.Pool'
|
|
) as PoolMock:
|
|
date_model = Mock()
|
|
date_model.today.return_value = datetime.date(2026, 4, 6)
|
|
config_model = Mock()
|
|
PoolMock.return_value.get.side_effect = lambda name: {
|
|
'ir.date': date_model,
|
|
'purchase_trade.configuration': config_model,
|
|
}[name]
|
|
shipment.company = Mock(
|
|
party=Mock(
|
|
rec_name='MELYA SA',
|
|
address_get=Mock(return_value=Mock(city='GENEVA')),
|
|
)
|
|
)
|
|
|
|
self.assertEqual(
|
|
shipment.report_insurance_certificate_number, 'BL-001')
|
|
self.assertEqual(
|
|
shipment.report_insurance_account_of, 'SGT FR')
|
|
self.assertEqual(
|
|
shipment.report_insurance_goods_description,
|
|
'COTTON UPLAND - RAW WHITE COTTON')
|
|
self.assertEqual(
|
|
shipment.report_insurance_amount, 'USD 1234.56')
|
|
self.assertEqual(
|
|
shipment.report_insurance_surveyor, 'CONTROL UNION')
|
|
self.assertEqual(
|
|
shipment.report_insurance_issue_place_and_date,
|
|
'GENEVA, 06-04-2026')
|
|
|
|
def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
|
|
'sale report helpers aggregate quantity, price lines and shipment periods'
|
|
Sale = Pool().get('sale.sale')
|
|
|
|
def make_line(quantity, period, linked_price):
|
|
line = Mock()
|
|
line.type = 'line'
|
|
line.quantity = quantity
|
|
line.note = ''
|
|
line.price_type = 'priced'
|
|
line.unit_price = Decimal('0')
|
|
line.linked_price = Decimal(linked_price)
|
|
line.linked_currency = Mock(rec_name='USC')
|
|
line.linked_unit = Mock(rec_name='POUND')
|
|
line.unit = Mock(rec_name='MT')
|
|
line.del_period = Mock(description=period)
|
|
line.get_pricing_text = f'ON ICE Cotton #2 {period}'
|
|
line.lots = []
|
|
return line
|
|
|
|
sale = Sale()
|
|
sale.currency = Mock(rec_name='USD')
|
|
sale.lines = [
|
|
make_line('1000', 'MARCH 2026', '72.5000'),
|
|
make_line('1000', 'MAY 2026', '70.2500'),
|
|
]
|
|
|
|
self.assertEqual(sale.report_total_quantity, '2000.0')
|
|
self.assertEqual(sale.report_quantity_unit_upper, 'MT')
|
|
self.assertEqual(sale.report_qt, 'TWO THOUSAND METRIC TONS')
|
|
self.assertEqual(sale.report_nb_bale, '')
|
|
self.assertEqual(
|
|
sale.report_quantity_lines.splitlines(),
|
|
[
|
|
'1000.0 MT (ONE THOUSAND METRIC TONS) - MARCH 2026',
|
|
'1000.0 MT (ONE THOUSAND METRIC TONS) - MAY 2026',
|
|
])
|
|
self.assertEqual(
|
|
sale.report_shipment_periods.splitlines(),
|
|
['MARCH 2026', 'MAY 2026'])
|
|
self.assertEqual(
|
|
sale.report_price_lines.splitlines(),
|
|
[
|
|
'USC 72.5000 PER POUND (SEVENTY TWO USC AND FIFTY CENTS) ON ICE Cotton #2 MARCH 2026',
|
|
'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026',
|
|
])
|
|
|
|
def test_report_product_fields_expose_name_and_description(self):
|
|
'sale and invoice templates use stable product name/description helpers'
|
|
Sale = Pool().get('sale.sale')
|
|
Invoice = Pool().get('account.invoice')
|
|
|
|
product = Mock(name='COTTON UPLAND', description='RAW WHITE COTTON')
|
|
sale_line = Mock(product=product)
|
|
invoice_line = Mock(product=product)
|
|
|
|
sale = Sale()
|
|
sale.lines = [sale_line]
|
|
|
|
invoice = Invoice()
|
|
invoice.lines = [invoice_line]
|
|
invoice._get_report_trade_line = Mock(return_value=sale_line)
|
|
|
|
self.assertEqual(sale.report_product_name, 'COTTON UPLAND')
|
|
self.assertEqual(sale.report_product_description, 'RAW WHITE COTTON')
|
|
self.assertEqual(invoice.report_product_name, 'COTTON UPLAND')
|
|
self.assertEqual(invoice.report_product_description, 'RAW WHITE COTTON')
|
|
|
|
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)
|
|
|
|
def test_sale_report_price_lines_basis_displays_premium_only(self):
|
|
'basis report pricing displays only the premium in templates'
|
|
Sale = Pool().get('sale.sale')
|
|
|
|
line = Mock()
|
|
line.type = 'line'
|
|
line.price_type = 'basis'
|
|
line.enable_linked_currency = True
|
|
line.linked_currency = Mock(rec_name='USC')
|
|
line.linked_unit = Mock(rec_name='POUND')
|
|
line.unit = Mock(rec_name='MT')
|
|
line.unit_price = Decimal('1598.3495')
|
|
line.linked_price = Decimal('72.5000')
|
|
line.premium = Decimal('8.3000')
|
|
line.get_pricing_text = 'ON ICE Cotton #2 MARCH 2026'
|
|
|
|
sale = Sale()
|
|
sale.currency = Mock(rec_name='USD')
|
|
sale.lines = [line]
|
|
|
|
self.assertEqual(
|
|
sale.report_price_lines,
|
|
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026')
|
|
|
|
def test_sale_report_net_and_gross_sum_all_lines(self):
|
|
'sale report totals aggregate every line instead of the first one only'
|
|
Sale = Pool().get('sale.sale')
|
|
|
|
def make_lot(quantity):
|
|
lot = Mock()
|
|
lot.lot_type = 'physic'
|
|
lot.get_current_quantity.return_value = Decimal(quantity)
|
|
lot.get_current_gross_quantity.return_value = Decimal(quantity)
|
|
return lot
|
|
|
|
line_a = Mock(type='line', quantity=Decimal('1000'))
|
|
line_a.lots = [make_lot('1000')]
|
|
line_b = Mock(type='line', quantity=Decimal('1000'))
|
|
line_b.lots = [make_lot('1000')]
|
|
|
|
sale = Sale()
|
|
sale.lines = [line_a, line_b]
|
|
|
|
self.assertEqual(sale.report_net, Decimal('2000'))
|
|
self.assertEqual(sale.report_gross, Decimal('2000'))
|
|
|
|
def test_sale_report_trade_blocks_use_lot_current_quantity(self):
|
|
'sale trade blocks use current lot quantity for quantity display'
|
|
Sale = Pool().get('sale.sale')
|
|
|
|
lot = Mock()
|
|
lot.lot_type = 'physic'
|
|
lot.get_current_quantity.return_value = Decimal('950')
|
|
line = Mock()
|
|
line.type = 'line'
|
|
line.lots = [lot]
|
|
line.quantity = Decimal('1000')
|
|
line.unit = Mock(rec_name='MT')
|
|
line.del_period = Mock(description='MARCH 2026')
|
|
line.price_type = 'priced'
|
|
line.linked_currency = Mock(rec_name='USC')
|
|
line.linked_unit = Mock(rec_name='POUND')
|
|
line.linked_price = Decimal('8.3000')
|
|
line.unit_price = Decimal('0')
|
|
line.get_pricing_text = 'ON ICE Cotton #2 MARCH 2026'
|
|
|
|
sale = Sale()
|
|
sale.currency = Mock(rec_name='USD')
|
|
sale.lines = [line]
|
|
|
|
self.assertEqual(
|
|
sale.report_trade_blocks,
|
|
[(
|
|
'950.0 MT (NINE HUNDRED AND FIFTY METRIC TONS) - MARCH 2026',
|
|
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026',
|
|
)])
|
|
|
|
def test_invoice_report_note_title_uses_total_amount_sign(self):
|
|
'final invoice title switches between credit and debit note'
|
|
Invoice = Pool().get('account.invoice')
|
|
|
|
credit = Invoice()
|
|
credit.total_amount = Decimal('10')
|
|
self.assertEqual(credit.report_note_title, 'Credit Note')
|
|
|
|
debit = Invoice()
|
|
debit.total_amount = Decimal('-10')
|
|
self.assertEqual(debit.report_note_title, 'Debit Note')
|
|
|
|
def test_invoice_report_net_sums_signed_invoice_lines(self):
|
|
'invoice report net uses the signed differential from invoice lines'
|
|
Invoice = Pool().get('account.invoice')
|
|
|
|
line_a = Mock(type='line', quantity=Decimal('1000'))
|
|
line_b = Mock(type='line', quantity=Decimal('-200'))
|
|
invoice = Invoice()
|
|
invoice.lines = [line_a, line_b]
|
|
|
|
self.assertEqual(invoice.report_net, Decimal('800'))
|
|
|
|
def test_invoice_report_nb_bale_sums_signed_line_lot_quantities(self):
|
|
'invoice reports packaging from the signed sum of line lot_qt values'
|
|
Invoice = Pool().get('account.invoice')
|
|
|
|
lot = Mock(lot_qt=Decimal('350'), lot_unit=Mock(symbol='bale'))
|
|
negative = Mock(type='line', quantity=Decimal('-1000'), lot=lot)
|
|
positive = Mock(type='line', quantity=Decimal('1000'), lot=lot)
|
|
invoice = Invoice()
|
|
invoice.lines = [negative, positive]
|
|
|
|
self.assertEqual(invoice.report_nb_bale, 'NB BALES: 0')
|
|
|
|
def test_invoice_report_positive_rate_lines_keep_positive_components(self):
|
|
'invoice final note pricing section keeps only positive component lines'
|
|
Invoice = Pool().get('account.invoice')
|
|
sale = Mock()
|
|
sale.report_price_lines = (
|
|
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026\n'
|
|
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MAY 2026'
|
|
)
|
|
|
|
invoice = Invoice()
|
|
invoice.sales = [sale]
|
|
invoice.lines = []
|
|
|
|
self.assertEqual(
|
|
invoice.report_positive_rate_lines.splitlines(),
|
|
[
|
|
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026',
|
|
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MAY 2026',
|
|
])
|
|
|
|
def test_lot_invoice_sale_uses_sale_invoice_line_reference(self):
|
|
'sale invoicing must resolve the generated invoice from sale invoice links'
|
|
sale_invoice = Mock()
|
|
sale_invoice_line = Mock(invoice=sale_invoice)
|
|
lot = Mock(
|
|
sale_invoice_line=sale_invoice_line,
|
|
sale_invoice_line_prov=None,
|
|
invoice_line=None,
|
|
invoice_line_prov=None,
|
|
)
|
|
|
|
invoice_line = lot.sale_invoice_line or lot.sale_invoice_line_prov
|
|
|
|
self.assertIs(invoice_line.invoice, sale_invoice)
|
|
|
|
|
|
del ModuleTestCase
|