This commit is contained in:
2026-04-09 21:23:27 +02:00
parent b39607d987
commit 9c8d7f11ae
3 changed files with 276 additions and 25 deletions

View File

@@ -131,6 +131,45 @@ class Invoice(metaclass=PoolMeta):
return lots[0].lot_unit_line
return getattr(line, 'unit', None)
@staticmethod
def _get_report_lbs_unit():
Uom = Pool().get('product.uom')
for domain in (
[('symbol', '=', 'LBS')],
[('rec_name', '=', 'LBS')],
[('name', '=', 'LBS')],
[('symbol', '=', 'LB')],
[('rec_name', '=', 'LB')],
[('name', '=', 'LB')]):
units = Uom.search(domain, limit=1)
if units:
return units[0]
return None
@classmethod
def _convert_report_quantity_to_lbs(cls, quantity, unit):
value = Decimal(str(quantity or 0))
if value == 0:
return Decimal('0.00')
if not unit:
return (value * Decimal('2204.62')).quantize(Decimal('0.01'))
label = (
getattr(unit, 'symbol', None)
or getattr(unit, 'rec_name', None)
or getattr(unit, 'name', None)
or ''
).strip().upper()
if label in {'LBS', 'LB', 'POUND', 'POUNDS'}:
return value.quantize(Decimal('0.01'))
lbs_unit = cls._get_report_lbs_unit()
if lbs_unit:
converted = Pool().get('product.uom').compute_qty(
unit, float(value), lbs_unit) or 0
return Decimal(str(converted)).quantize(Decimal('0.01'))
if label in {'KG', 'KGS', 'KILOGRAM', 'KILOGRAMS'}:
return (value * Decimal('2.20462')).quantize(Decimal('0.01'))
return (value * Decimal('2204.62')).quantize(Decimal('0.01'))
@staticmethod
def _clean_report_description(value):
text = (value or '').strip()
@@ -517,7 +556,7 @@ class Invoice(metaclass=PoolMeta):
quantity, keep_trailing_decimal=True)
unit = self._get_report_invoice_line_unit(line)
unit_name = unit.rec_name.upper() if unit and unit.rec_name else ''
lbs = round(Decimal(quantity) * Decimal('2204.62'), 2)
lbs = self._convert_report_quantity_to_lbs(quantity, unit)
parts = [quantity_text, unit_name]
if lbs != '':
parts.append(
@@ -746,7 +785,9 @@ class Invoice(metaclass=PoolMeta):
net = self.report_net
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
invoice_line = self._get_report_invoice_line()
unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
return self._convert_report_quantity_to_lbs(net, unit)
@property
def report_weight_unit_upper(self):
@@ -996,7 +1037,8 @@ class InvoiceLine(metaclass=PoolMeta):
net = self.report_net
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
unit = Invoice._get_report_invoice_line_unit(self)
return Invoice._convert_report_quantity_to_lbs(net, unit)
class ReportTemplateMixin:

View File

@@ -474,31 +474,107 @@ class Sale(metaclass=PoolMeta):
return lots[0].lot_unit_line
return getattr(line, 'unit', None)
@property
def report_gross(self):
def _get_report_total_unit(self):
virtual_units = []
for line in self._get_report_lines():
for lot in self._get_report_line_lots(line):
if (
getattr(lot, 'lot_type', None) == 'virtual'
and getattr(lot, 'lot_unit_line', None)):
virtual_units.append(lot.lot_unit_line)
if len(virtual_units) == 1:
return virtual_units[0]
line = self._get_report_first_line()
if line:
return self._get_report_line_unit(line)
return None
@staticmethod
def _get_report_unit_wording(unit):
label = ''
for attr in ('symbol', 'rec_name', 'name'):
value = getattr(unit, attr, None)
if value:
label = str(value).strip().upper()
break
mapping = {
'MT': ('METRIC TON', 'METRIC TONS'),
'METRIC TON': ('METRIC TON', 'METRIC TONS'),
'METRIC TONS': ('METRIC TON', 'METRIC TONS'),
'KG': ('KILOGRAM', 'KILOGRAMS'),
'KGS': ('KILOGRAM', 'KILOGRAMS'),
'KILOGRAM': ('KILOGRAM', 'KILOGRAMS'),
'KILOGRAMS': ('KILOGRAM', 'KILOGRAMS'),
'LB': ('POUND', 'POUNDS'),
'LBS': ('POUND', 'POUNDS'),
'POUND': ('POUND', 'POUNDS'),
'POUNDS': ('POUND', 'POUNDS'),
'BALE': ('BALE', 'BALES'),
'BALES': ('BALE', 'BALES'),
}
if label in mapping:
return mapping[label]
if label.endswith('S') and len(label) > 1:
return label[:-1], label
return label, label
@classmethod
def _report_quantity_to_words(cls, quantity, unit):
singular, plural = cls._get_report_unit_wording(unit)
return quantity_to_words(
quantity,
unit_singular=singular,
unit_plural=plural,
)
@staticmethod
def _convert_report_quantity(quantity, from_unit, to_unit):
value = Decimal(str(quantity or 0))
if not from_unit or not to_unit:
return value
if getattr(from_unit, 'id', None) == getattr(to_unit, 'id', None):
return value
from_name = getattr(from_unit, 'rec_name', None)
to_name = getattr(to_unit, 'rec_name', None)
if from_name and to_name and from_name == to_name:
return value
Uom = Pool().get('product.uom')
converted = Uom.compute_qty(from_unit, float(value), to_unit) or 0
return Decimal(str(converted))
def _get_report_total_weight(self, index):
lines = self._get_report_lines()
if lines:
if not lines:
return None
total_unit = self._get_report_total_unit()
total = Decimal(0)
for line in lines:
total += self._get_report_line_weights(line)[1]
quantity = self._get_report_line_weights(line)[index]
total += self._convert_report_quantity(
quantity,
self._get_report_line_unit(line),
total_unit,
)
return total
@property
def report_gross(self):
total = self._get_report_total_weight(1)
if total is not None:
return total
return ''
@property
def report_net(self):
lines = self._get_report_lines()
if lines:
total = Decimal(0)
for line in lines:
total += self._get_report_line_weights(line)[0]
total = self._get_report_total_weight(0)
if total is not None:
return total
return ''
@property
def report_total_quantity(self):
lines = self._get_report_lines()
if lines:
total = sum(self._get_report_line_weights(line)[0] for line in lines)
total = self._get_report_total_weight(0)
if total is not None:
return self._format_report_number(total, keep_trailing_decimal=True)
return '0.0'
@@ -515,10 +591,10 @@ class Sale(metaclass=PoolMeta):
@property
def report_qt(self):
lines = self._get_report_lines()
if lines:
total = sum(self._get_report_line_quantity(line) for line in lines)
return quantity_to_words(total)
total = self._get_report_total_weight(0)
if total is not None:
return self._report_quantity_to_words(
total, self._get_report_total_unit())
return ''
@property
@@ -536,7 +612,7 @@ class Sale(metaclass=PoolMeta):
line_unit.rec_name.upper()
if line_unit and line_unit.rec_name else ''
)
words = quantity_to_words(current_quantity)
words = self._report_quantity_to_words(current_quantity, line_unit)
period = line.del_period.description if getattr(line, 'del_period', None) else ''
detail = ' '.join(
part for part in [
@@ -623,8 +699,12 @@ class Sale(metaclass=PoolMeta):
current_quantity = self._get_report_line_quantity(line)
quantity = self._format_report_number(
current_quantity, keep_trailing_decimal=True)
unit = line.unit.rec_name.upper() if line.unit and line.unit.rec_name else ''
words = quantity_to_words(current_quantity)
line_unit = self._get_report_line_unit(line)
unit = (
line_unit.rec_name.upper()
if line_unit and line_unit.rec_name else ''
)
words = self._report_quantity_to_words(current_quantity, line_unit)
period = line.del_period.description if getattr(line, 'del_period', None) else ''
quantity_line = ' '.join(
part for part in [

View File

@@ -789,6 +789,106 @@ class PurchaseTradeTestCase(ModuleTestCase):
'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026',
])
def test_sale_report_converts_mixed_units_for_total_and_words(self):
'sale report totals prefer the virtual lot unit as common unit'
Sale = Pool().get('sale.sale')
mt = Mock(id=1, rec_name='MT')
kg = Mock(id=2, rec_name='KILOGRAM')
line_mt = Mock()
line_mt.type = 'line'
line_mt.quantity = Decimal('1000')
line_mt.unit = mt
line_mt.del_period = Mock(description='MARCH 2026')
line_mt.lots = []
virtual = Mock(lot_type='virtual', lot_unit_line=kg)
virtual.get_hist_quantity.return_value = (
Decimal('1000000'),
Decimal('1000000'),
)
line_kg = Mock()
line_kg.type = 'line'
line_kg.quantity = Decimal('1000')
line_kg.unit = mt
line_kg.del_period = Mock(description='MAY 2026')
line_kg.lots = [virtual]
sale = Sale()
sale.lines = [line_mt, line_kg]
uom_model = Mock()
uom_model.compute_qty.side_effect = (
lambda from_unit, qty, to_unit: (
qty * 1000
if getattr(from_unit, 'rec_name', None) == 'MT'
and getattr(to_unit, 'rec_name', None) == 'KILOGRAM'
else (
qty / 1000
if getattr(from_unit, 'rec_name', None) == 'KILOGRAM'
and getattr(to_unit, 'rec_name', None) == 'MT'
else qty
)
))
with patch('trytond.modules.purchase_trade.sale.Pool') as PoolMock:
PoolMock.return_value.get.return_value = uom_model
self.assertEqual(sale.report_total_quantity, '2000000.0')
self.assertEqual(sale.report_quantity_unit_upper, 'KILOGRAM')
self.assertEqual(sale.report_qt, 'TWO MILLION KILOGRAMS')
self.assertEqual(
sale.report_quantity_lines.splitlines(),
[
'1000.0 MT (ONE THOUSAND METRIC TONS) - MARCH 2026',
'1000000.0 KILOGRAM (ONE MILLION KILOGRAMS) - MAY 2026',
])
def test_sale_report_total_unit_falls_back_when_multiple_virtual_lots(self):
'sale report common unit uses virtual only when there is a single one'
Sale = Pool().get('sale.sale')
mt = Mock(id=1, rec_name='MT')
kg = Mock(id=2, rec_name='KILOGRAM')
line_mt = Mock(type='line', quantity=Decimal('1000'), unit=mt)
line_mt.del_period = Mock(description='MARCH 2026')
line_mt.lots = []
virtual_a = Mock(lot_type='virtual', lot_unit_line=kg)
virtual_a.get_hist_quantity.return_value = (
Decimal('1000000'),
Decimal('1000000'),
)
virtual_b = Mock(lot_type='virtual', lot_unit_line=kg)
virtual_b.get_hist_quantity.return_value = (
Decimal('1000000'),
Decimal('1000000'),
)
line_kg = Mock(type='line', quantity=Decimal('1000'), unit=mt)
line_kg.del_period = Mock(description='MAY 2026')
line_kg.lots = [virtual_a, virtual_b]
sale = Sale()
sale.lines = [line_mt, line_kg]
uom_model = Mock()
uom_model.compute_qty.side_effect = (
lambda from_unit, qty, to_unit: (
qty / 1000
if getattr(from_unit, 'rec_name', None) == 'KILOGRAM'
and getattr(to_unit, 'rec_name', None) == 'MT'
else qty
))
with patch('trytond.modules.purchase_trade.sale.Pool') as PoolMock:
PoolMock.return_value.get.return_value = uom_model
self.assertEqual(sale.report_quantity_unit_upper, 'MT')
self.assertEqual(sale.report_total_quantity, '2000.0')
self.assertEqual(sale.report_qt, 'TWO THOUSAND METRIC TONS')
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')
@@ -1015,7 +1115,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
'invoice net and gross weights come from the current lot hist entry'
Invoice = Pool().get('account.invoice')
unit = Mock(rec_name='LBS')
unit = Mock(rec_name='LBS', symbol='LBS')
lot = Mock(lot_unit_line=unit)
lot.get_hist_quantity.return_value = (
Decimal('950'),
@@ -1030,7 +1130,36 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(invoice.report_weight_unit_upper, 'LBS')
self.assertEqual(
invoice.report_quantity_lines,
'950.0 LBS (2094389.00 LBS)')
'950.0 LBS (950.00 LBS)')
def test_invoice_report_lbs_converts_kilogram_to_lbs(self):
'invoice lbs helper converts kilogram quantities with the proper uom ratio'
Invoice = Pool().get('account.invoice')
kg = Mock(id=1, rec_name='KILOGRAM', symbol='KG')
lbs = Mock(id=2, rec_name='LBS', symbol='LBS')
lot = Mock(lot_unit_line=kg)
lot.get_hist_quantity.return_value = (
Decimal('999995'),
Decimal('999995'),
)
line = Mock(type='line', quantity=Decimal('999995'), lot=lot, unit=kg)
invoice = Invoice()
invoice.lines = [line]
uom_model = Mock()
uom_model.search.return_value = [lbs]
uom_model.compute_qty.side_effect = (
lambda from_unit, qty, to_unit: qty * 2.20462
)
with patch('trytond.modules.purchase_trade.invoice.Pool') as PoolMock:
PoolMock.return_value.get.return_value = uom_model
self.assertEqual(invoice.report_lbs, Decimal('2204608.98'))
self.assertEqual(
invoice.report_quantity_lines,
'999995.0 KILOGRAM (2204608.98 LBS)')
def test_invoice_report_weights_keep_line_sign_with_lot_hist_values(self):
'invoice lot hist values keep the invoice line sign for final notes'