ICT bug padding + template
This commit is contained in:
@@ -1 +0,0 @@
|
||||
,PC_LO/baron,PC_LO,28.05.2026 13:23,file:///C:/Users/baron/AppData/Roaming/LibreOffice/4;
|
||||
@@ -230,12 +230,13 @@ def register():
|
||||
Pool.register(
|
||||
account.PhysicalTradeIFRS,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
configuration.AccountConfiguration,
|
||||
configuration.AccountConfigurationDefaultAccount,
|
||||
invoice.InvoicePaddingReport,
|
||||
invoice.InvoicePaddingContext,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
configuration.AccountConfiguration,
|
||||
configuration.AccountConfigurationDefaultAccount,
|
||||
invoice.InvoiceLineLotWeight,
|
||||
invoice.InvoicePaddingReport,
|
||||
invoice.InvoicePaddingContext,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
invoice.Invoice,
|
||||
invoice.InvoiceLine,
|
||||
|
||||
@@ -444,6 +444,9 @@ class Fee(ModelSQL,ModelView):
|
||||
return sum(quantities)
|
||||
|
||||
def sync_quantity_from_lots(self):
|
||||
if self.mode == 'ppack' and self.auto_calculation:
|
||||
self.assert_quantity_consistency()
|
||||
return
|
||||
quantity = self._get_effective_fee_lots_quantity()
|
||||
if quantity is None:
|
||||
self.assert_quantity_consistency()
|
||||
|
||||
@@ -17,13 +17,106 @@ from trytond.modules.purchase.purchase import (
|
||||
PurchaseReport as BasePurchaseReport)
|
||||
|
||||
|
||||
class InvoiceLineLotWeight(ModelSQL, ModelView):
|
||||
"Invoice Line Lot Weight"
|
||||
__name__ = 'account.invoice.line.lot.weight'
|
||||
|
||||
invoice_line = fields.Many2One(
|
||||
'account.invoice.line', "Invoice Line", required=True,
|
||||
ondelete='CASCADE')
|
||||
lot = fields.Many2One('lot.lot', "Lot", required=True, ondelete='CASCADE')
|
||||
lot_qt_hist = fields.Many2One('lot.qt.hist', "Quantity History")
|
||||
quantity_type = fields.Many2One('lot.qt.type', "Quantity Type")
|
||||
net_quantity = fields.Numeric("Net Weight", digits=(1, 5), required=True)
|
||||
gross_quantity = fields.Numeric(
|
||||
"Gross Weight", digits=(1, 5), required=True)
|
||||
unit = fields.Many2One('product.uom', "Unit")
|
||||
lot_qt = fields.Integer("Packing Quantity")
|
||||
lot_unit = fields.Many2One('product.uom', "Packing Unit")
|
||||
|
||||
@classmethod
|
||||
def _get_lot_hist_entry(cls, lot):
|
||||
state = getattr(lot, 'lot_state', None)
|
||||
state_id = getattr(state, 'id', None)
|
||||
hist = list(getattr(lot, 'lot_hist', []) or [])
|
||||
if state_id is not None:
|
||||
for entry in hist:
|
||||
quantity_type = getattr(entry, 'quantity_type', None)
|
||||
if getattr(quantity_type, 'id', None) == state_id:
|
||||
return entry
|
||||
if len(hist) == 1:
|
||||
return hist[0]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_lot_weights(cls, lot):
|
||||
hist = cls._get_lot_hist_entry(lot)
|
||||
if hist:
|
||||
net = Decimal(str(getattr(hist, 'quantity', 0) or 0))
|
||||
gross = Decimal(str(
|
||||
getattr(hist, 'gross_quantity', None)
|
||||
if getattr(hist, 'gross_quantity', None) not in (None, '')
|
||||
else net))
|
||||
return hist, net, gross
|
||||
if hasattr(lot, 'get_hist_quantity'):
|
||||
net, gross = lot.get_hist_quantity()
|
||||
net = Decimal(str(net or 0))
|
||||
gross = Decimal(str(gross if gross not in (None, '') else net))
|
||||
return None, net, gross
|
||||
net = Decimal(str(getattr(lot, 'lot_quantity', 0) or 0))
|
||||
gross = Decimal(str(
|
||||
getattr(lot, 'lot_gross_quantity', None)
|
||||
if getattr(lot, 'lot_gross_quantity', None) not in (None, '')
|
||||
else net))
|
||||
return None, net, gross
|
||||
|
||||
@classmethod
|
||||
def create_for_invoice_line(cls, line, lot):
|
||||
if not getattr(line, 'id', None) or not getattr(lot, 'id', None):
|
||||
return
|
||||
existing = cls.search([
|
||||
('invoice_line', '=', line.id),
|
||||
('lot', '=', lot.id),
|
||||
])
|
||||
if existing:
|
||||
cls.delete(existing)
|
||||
hist, net, gross = cls._get_lot_weights(lot)
|
||||
snapshot = cls()
|
||||
snapshot.invoice_line = line
|
||||
snapshot.lot = lot
|
||||
snapshot.lot_qt_hist = hist
|
||||
snapshot.quantity_type = (
|
||||
getattr(hist, 'quantity_type', None)
|
||||
or getattr(lot, 'lot_state', None))
|
||||
snapshot.net_quantity = net
|
||||
snapshot.gross_quantity = gross
|
||||
snapshot.unit = (
|
||||
getattr(lot, 'lot_unit_line', None)
|
||||
or getattr(lot, 'lot_unit', None)
|
||||
or getattr(line, 'unit', None))
|
||||
snapshot.lot_qt = getattr(lot, 'lot_qt', None)
|
||||
snapshot.lot_unit = getattr(lot, 'lot_unit', None)
|
||||
cls.save([snapshot])
|
||||
|
||||
|
||||
class Invoice(metaclass=PoolMeta):
|
||||
__name__ = 'account.invoice'
|
||||
|
||||
def do_lot_invoicing(self):
|
||||
super().do_lot_invoicing()
|
||||
self._create_lot_weight_snapshots()
|
||||
self._create_sale_padding_moves()
|
||||
|
||||
def _create_lot_weight_snapshots(self):
|
||||
Snapshot = Pool().get('account.invoice.line.lot.weight')
|
||||
Lot = Pool().get('lot.lot')
|
||||
for line in self._get_report_invoice_lines():
|
||||
if (getattr(line, 'description', None) != 'Pro forma'
|
||||
or Decimal(str(getattr(line, 'quantity', 0) or 0)) <= 0
|
||||
or not getattr(line, 'lot', None)):
|
||||
continue
|
||||
Snapshot.create_for_invoice_line(line, Lot(line.lot.id))
|
||||
|
||||
@classmethod
|
||||
def _post(cls, invoices):
|
||||
pool = Pool()
|
||||
@@ -292,6 +385,20 @@ class Invoice(metaclass=PoolMeta):
|
||||
return None, None
|
||||
|
||||
def _get_report_invoice_line_weights(self, line):
|
||||
snapshots = self._get_report_invoice_line_weight_snapshots(line)
|
||||
if snapshots:
|
||||
sign = self._get_report_line_sign(line)
|
||||
net_total = sum(
|
||||
Decimal(str(snapshot.net_quantity or 0))
|
||||
for snapshot in snapshots)
|
||||
gross_total = sum(
|
||||
Decimal(str(snapshot.gross_quantity or 0))
|
||||
for snapshot in snapshots)
|
||||
unit = self._get_report_invoice_line_unit(line)
|
||||
padding = self._get_report_invoice_line_padding(line, unit)
|
||||
net_total += padding
|
||||
gross_total += padding
|
||||
return net_total * sign, gross_total * sign
|
||||
lots = self._get_report_preferred_lots(line)
|
||||
if lots and self._report_invoice_line_reuses_lot(line):
|
||||
quantity = self._get_report_invoice_line_quantity_from_line(line)
|
||||
@@ -311,6 +418,33 @@ class Invoice(metaclass=PoolMeta):
|
||||
quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||||
return quantity, quantity
|
||||
|
||||
@staticmethod
|
||||
def _get_report_invoice_line_weight_snapshots(line):
|
||||
snapshots = getattr(line, 'lot_weight_snapshots', None)
|
||||
if snapshots and isinstance(snapshots, (list, tuple)):
|
||||
return list(snapshots)
|
||||
line_id = getattr(line, 'id', None)
|
||||
if not isinstance(line_id, int):
|
||||
return []
|
||||
Snapshot = Pool().get('account.invoice.line.lot.weight')
|
||||
return Snapshot.search([('invoice_line', '=', line_id)])
|
||||
|
||||
@classmethod
|
||||
def _get_report_invoice_line_padding(cls, line, to_unit):
|
||||
if getattr(line, 'description', None) != 'Pro forma':
|
||||
return Decimal(0)
|
||||
invoice_type = getattr(line, 'invoice_type', None)
|
||||
invoice = getattr(line, 'invoice', None)
|
||||
if invoice_type != 'out' and getattr(invoice, 'type', None) != 'out':
|
||||
return Decimal(0)
|
||||
lot = getattr(line, 'lot', None)
|
||||
padding = Decimal(str(
|
||||
getattr(lot, 'sale_invoice_padding', 0) or 0))
|
||||
if not padding:
|
||||
return Decimal(0)
|
||||
return cls._convert_report_quantity(
|
||||
padding, getattr(line, 'unit', None), to_unit)
|
||||
|
||||
@staticmethod
|
||||
def _get_report_line_lot_keys(line):
|
||||
keys = []
|
||||
@@ -416,6 +550,9 @@ class Invoice(metaclass=PoolMeta):
|
||||
|
||||
@staticmethod
|
||||
def _get_report_invoice_line_unit(line):
|
||||
snapshots = Invoice._get_report_invoice_line_weight_snapshots(line)
|
||||
if snapshots and getattr(snapshots[0], 'unit', None):
|
||||
return snapshots[0].unit
|
||||
lots = Invoice._get_report_preferred_lots(line)
|
||||
if lots and getattr(lots[0], 'lot_unit_line', None):
|
||||
return lots[0].lot_unit_line
|
||||
@@ -1556,6 +1693,10 @@ class InvoicePaddingContext(ModelView):
|
||||
class InvoiceLine(metaclass=PoolMeta):
|
||||
__name__ = 'account.invoice.line'
|
||||
|
||||
lot_weight_snapshots = fields.One2Many(
|
||||
'account.invoice.line.lot.weight', 'invoice_line',
|
||||
"Lot Weight Snapshots")
|
||||
|
||||
def _get_report_trade(self):
|
||||
origin = getattr(self, 'origin', None)
|
||||
if not origin:
|
||||
@@ -1648,6 +1789,15 @@ class InvoiceLine(metaclass=PoolMeta):
|
||||
def report_net(self):
|
||||
if self.type == 'line':
|
||||
invoice = getattr(self, 'invoice', None)
|
||||
snapshots = Invoice._get_report_invoice_line_weight_snapshots(self)
|
||||
if snapshots:
|
||||
sign = Invoice._get_report_line_sign(self)
|
||||
net = sum(
|
||||
Decimal(str(snapshot.net_quantity or 0))
|
||||
for snapshot in snapshots)
|
||||
unit = Invoice._get_report_invoice_line_unit(self)
|
||||
net += Invoice._get_report_invoice_line_padding(self, unit)
|
||||
return net * sign
|
||||
if invoice and invoice._report_invoice_line_reuses_lot(self):
|
||||
return Invoice._get_report_invoice_line_quantity_from_line(self)
|
||||
lot = getattr(self, 'lot', None)
|
||||
|
||||
@@ -132,31 +132,37 @@ class Lot(metaclass=PoolMeta):
|
||||
"more than one virtual lot exists",
|
||||
[('virtual_lots', len(virtual_lots))]))
|
||||
|
||||
vlot = virtual_lots[0]
|
||||
physical_quantity = sum(
|
||||
cls._get_lot_current_quantity_for_consistency(lot, unit)
|
||||
for lot in physical_lots)
|
||||
virtual_quantity = cls._get_lot_current_quantity_for_consistency(
|
||||
vlot, unit)
|
||||
theorical_quantity = Decimal(str(theorical or 0))
|
||||
expected_diff = (
|
||||
cls._round_consistency_quantity(physical_quantity)
|
||||
+ cls._round_consistency_quantity(virtual_quantity)
|
||||
- cls._round_consistency_quantity(theorical_quantity))
|
||||
if expected_diff:
|
||||
raise UserError(
|
||||
cls._format_consistency_error(
|
||||
line,
|
||||
"physical lots + virtual lot must equal theoretical "
|
||||
"quantity",
|
||||
[
|
||||
('physical', physical_quantity),
|
||||
('virtual', virtual_quantity),
|
||||
('theoretical', theorical_quantity),
|
||||
('diff', expected_diff),
|
||||
]))
|
||||
|
||||
cls._assert_virtual_lot_open_quantity_consistency(vlot, line)
|
||||
# Temporarily suspended: repesages successifs are no longer tied
|
||||
# reliably to the original lot.qt forecast split. We keep the helper
|
||||
# calls in place, but do not enforce the two historical invariants:
|
||||
# 1) physical lots + virtual lot must equal quantity_theorical
|
||||
# 2) sum(non-zero lot.qt) must cover the positive virtual lot quantity
|
||||
#
|
||||
# vlot = virtual_lots[0]
|
||||
# physical_quantity = sum(
|
||||
# cls._get_lot_current_quantity_for_consistency(lot, unit)
|
||||
# for lot in physical_lots)
|
||||
# virtual_quantity = cls._get_lot_current_quantity_for_consistency(
|
||||
# vlot, unit)
|
||||
# theorical_quantity = Decimal(str(theorical or 0))
|
||||
# expected_diff = (
|
||||
# cls._round_consistency_quantity(physical_quantity)
|
||||
# + cls._round_consistency_quantity(virtual_quantity)
|
||||
# - cls._round_consistency_quantity(theorical_quantity))
|
||||
# if expected_diff:
|
||||
# raise UserError(
|
||||
# cls._format_consistency_error(
|
||||
# line,
|
||||
# "physical lots + virtual lot must equal theoretical "
|
||||
# "quantity",
|
||||
# [
|
||||
# ('physical', physical_quantity),
|
||||
# ('virtual', virtual_quantity),
|
||||
# ('theoretical', theorical_quantity),
|
||||
# ('diff', expected_diff),
|
||||
# ]))
|
||||
#
|
||||
# cls._assert_virtual_lot_open_quantity_consistency(vlot, line)
|
||||
|
||||
@classmethod
|
||||
def _assert_virtual_lot_open_quantity_consistency(cls, vlot, line=None):
|
||||
|
||||
@@ -355,6 +355,29 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(fee.quantity, Decimal('40.00000'))
|
||||
save.assert_called_once_with([fee])
|
||||
|
||||
def test_fee_ppack_auto_sync_keeps_auto_quantity(self):
|
||||
'per packing auto fee keeps the on-change quantity on save'
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee()
|
||||
fee.id = 1
|
||||
fee.mode = 'ppack'
|
||||
fee.auto_calculation = True
|
||||
fee.quantity = Decimal('3')
|
||||
fee.unit = Mock()
|
||||
lot = Mock(lot_type='physic', lot_qt=Decimal('990'))
|
||||
fee_lots = Mock()
|
||||
fee_lots.search.return_value = [Mock(lot=lot)]
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.fee.Pool'
|
||||
) as PoolMock, patch.object(Fee, 'save') as save:
|
||||
PoolMock.return_value.get.return_value = fee_lots
|
||||
|
||||
fee.sync_quantity_from_lots()
|
||||
|
||||
self.assertEqual(fee.quantity, Decimal('3'))
|
||||
save.assert_not_called()
|
||||
|
||||
def test_fee_pnl_regeneration_uses_shipment_lotqt_purchase_line(self):
|
||||
'fee pnl regeneration follows shipment lot.qt back to purchase line'
|
||||
Fee = Pool().get('fee.fee')
|
||||
@@ -4206,6 +4229,106 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
invoice.report_quantity_lines,
|
||||
'950.00 LBS (950.00 LBS)')
|
||||
|
||||
def test_invoice_report_weights_prefer_invoice_line_snapshot(self):
|
||||
'invoice report weights use the invoicing snapshot when available'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
unit = Mock(rec_name='LBS', symbol='LBS')
|
||||
lot = Mock(lot_unit_line=unit)
|
||||
lot.get_hist_quantity.return_value = (
|
||||
Decimal('950'),
|
||||
Decimal('980'),
|
||||
)
|
||||
snapshot = Mock(
|
||||
net_quantity=Decimal('900'),
|
||||
gross_quantity=Decimal('920'),
|
||||
unit=unit,
|
||||
)
|
||||
line = Mock(
|
||||
type='line',
|
||||
quantity=Decimal('1000'),
|
||||
lot=lot,
|
||||
unit=Mock(rec_name='MT'),
|
||||
lot_weight_snapshots=[snapshot],
|
||||
)
|
||||
invoice = Invoice()
|
||||
invoice.lines = [line]
|
||||
|
||||
self.assertEqual(invoice.report_net, Decimal('900'))
|
||||
self.assertEqual(invoice.report_gross, Decimal('920'))
|
||||
self.assertEqual(invoice.report_weight_unit_upper, 'LBS')
|
||||
self.assertEqual(
|
||||
invoice.report_quantity_lines,
|
||||
'900.00 LBS (900.00 LBS)')
|
||||
|
||||
def test_invoice_report_proforma_snapshot_adds_sale_padding(self):
|
||||
'ICT provisional report displays snapshot weights plus sale padding'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
unit = Mock(id=1, rec_name='LBS', symbol='LBS')
|
||||
lot = Mock(lot_unit_line=unit, sale_invoice_padding=Decimal('10'))
|
||||
snapshot = Mock(
|
||||
net_quantity=Decimal('900'),
|
||||
gross_quantity=Decimal('920'),
|
||||
unit=unit,
|
||||
)
|
||||
line = Mock(
|
||||
type='line',
|
||||
description='Pro forma',
|
||||
invoice_type='out',
|
||||
quantity=Decimal('1010'),
|
||||
lot=lot,
|
||||
unit=unit,
|
||||
lot_weight_snapshots=[snapshot],
|
||||
)
|
||||
invoice = Invoice()
|
||||
invoice.lines = [line]
|
||||
|
||||
self.assertEqual(invoice.report_net, Decimal('910'))
|
||||
self.assertEqual(invoice.report_gross, Decimal('930'))
|
||||
self.assertEqual(
|
||||
invoice.report_quantity_lines,
|
||||
'910.00 LBS (910.00 LBS)')
|
||||
|
||||
def test_invoice_validation_creates_weight_snapshot_for_proforma_lines(self):
|
||||
'invoice validation snapshots lot weights for pro forma invoice lines'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
lot_ref = Mock(id=10)
|
||||
lot = Mock(id=10)
|
||||
proforma = Mock(
|
||||
type='line',
|
||||
description='Pro forma',
|
||||
quantity=Decimal('1000'),
|
||||
lot=lot_ref,
|
||||
)
|
||||
final = Mock(
|
||||
type='line',
|
||||
description='Final',
|
||||
quantity=Decimal('1000'),
|
||||
lot=lot_ref,
|
||||
)
|
||||
invoice = Invoice()
|
||||
invoice.lines = [proforma, final]
|
||||
|
||||
snapshot_model = Mock()
|
||||
lot_model = Mock(return_value=lot)
|
||||
|
||||
def get_model(name):
|
||||
if name == 'account.invoice.line.lot.weight':
|
||||
return snapshot_model
|
||||
if name == 'lot.lot':
|
||||
return lot_model
|
||||
raise AssertionError(name)
|
||||
|
||||
with patch('trytond.modules.purchase_trade.invoice.Pool') as PoolMock:
|
||||
PoolMock.return_value.get.side_effect = get_model
|
||||
|
||||
invoice._create_lot_weight_snapshots()
|
||||
|
||||
snapshot_model.create_for_invoice_line.assert_called_once_with(
|
||||
proforma, lot)
|
||||
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user