Lot invoicing + fee padding
This commit is contained in:
@@ -130,13 +130,43 @@ class Invoice(metaclass=PoolMeta):
|
||||
lots[getattr(lot, 'id', id(lot))] = lot
|
||||
return list(lots.values())
|
||||
|
||||
@classmethod
|
||||
def _fee_updates_on_delete(cls, invoices):
|
||||
reset_fees = {}
|
||||
clear_dn_cn_fees = {}
|
||||
for invoice in invoices:
|
||||
if getattr(invoice, 'number', None):
|
||||
continue
|
||||
invoice_id = getattr(invoice, 'id', None)
|
||||
for line in getattr(invoice, 'lines', None) or []:
|
||||
fee = getattr(line, 'fee', None)
|
||||
if not fee:
|
||||
continue
|
||||
fee_key = getattr(fee, 'id', None) or id(fee)
|
||||
dn_cn = getattr(fee, 'dn_cn', None)
|
||||
dn_cn_id = getattr(dn_cn, 'id', dn_cn)
|
||||
if invoice_id and dn_cn_id == invoice_id:
|
||||
clear_dn_cn_fees[fee_key] = fee
|
||||
else:
|
||||
reset_fees[fee_key] = fee
|
||||
return list(reset_fees.values()), list(clear_dn_cn_fees.values())
|
||||
|
||||
@classmethod
|
||||
def delete(cls, invoices):
|
||||
Lot = Pool().get('lot.lot')
|
||||
Fee = Pool().get('fee.fee')
|
||||
padding_lots = cls._sale_padding_lots_to_clear_on_delete(invoices)
|
||||
reset_fees, clear_dn_cn_fees = cls._fee_updates_on_delete(invoices)
|
||||
super().delete(invoices)
|
||||
if padding_lots:
|
||||
Lot.write(padding_lots, {'sale_invoice_padding': Decimal(0)})
|
||||
if reset_fees:
|
||||
Fee.write(reset_fees, {
|
||||
'state': 'not invoiced',
|
||||
'dn_cn': None,
|
||||
})
|
||||
if clear_dn_cn_fees:
|
||||
Fee.write(clear_dn_cn_fees, {'dn_cn': None})
|
||||
|
||||
def do_lot_invoicing(self):
|
||||
super().do_lot_invoicing()
|
||||
|
||||
@@ -4916,11 +4916,9 @@ class LotInvoice(Wizard):
|
||||
|
||||
def default_message(self, fields):
|
||||
fee_count = getattr(self, '_invoiced_fee_count', 0)
|
||||
message = 'The invoice has been successfully created.'
|
||||
message = 'Invoice created.'
|
||||
if fee_count:
|
||||
message += (
|
||||
' %s selected fee invoice(s) have also been processed.'
|
||||
% fee_count)
|
||||
message += ' Fee invoices: %s.' % fee_count
|
||||
return {
|
||||
'message': message,
|
||||
}
|
||||
@@ -5031,10 +5029,83 @@ class LotInvoiceStart(ModelView):
|
||||
amount += l.lot_amount
|
||||
amount += padding_share * Decimal(str(l.lot_price or 0))
|
||||
return amount
|
||||
|
||||
@classmethod
|
||||
def default_action(cls):
|
||||
return 'prov'
|
||||
|
||||
def _get_sale_padding_by_lot(self):
|
||||
lots = [
|
||||
line.lot for line in (self.lot_s or [])
|
||||
if getattr(line, 'lot', None)]
|
||||
return self._split_sale_padding(self.sale_padding, lots)
|
||||
|
||||
@staticmethod
|
||||
def _record_key(record):
|
||||
return getattr(record, 'id', None) or id(record)
|
||||
|
||||
def _fee_wizard_padding_quantity(self, fee, padding_by_lot):
|
||||
if not getattr(fee, 'add_padding', False):
|
||||
return Decimal(0)
|
||||
quantity = Decimal(0)
|
||||
for lot in fee._get_effective_fee_lots():
|
||||
padding = padding_by_lot.get(self._record_key(lot), Decimal(0))
|
||||
if not padding:
|
||||
continue
|
||||
source_line = (
|
||||
getattr(fee, 'sale_line', None)
|
||||
or getattr(lot, 'sale_line', None)
|
||||
or getattr(fee, 'line', None)
|
||||
or getattr(lot, 'line', None))
|
||||
source_unit = getattr(source_line, 'unit', None)
|
||||
if not source_unit:
|
||||
source_unit = getattr(lot, 'lot_unit_line', None)
|
||||
quantity += fee._convert_padding_quantity(padding, source_unit)
|
||||
return quantity
|
||||
|
||||
def _fee_wizard_billing_quantity(self, fee, padding_by_lot):
|
||||
quantity = fee._get_effective_fee_lots_quantity()
|
||||
if quantity is None:
|
||||
quantity = getattr(fee, 'quantity', None)
|
||||
if quantity is None:
|
||||
quantity = fee.get_quantity()
|
||||
return (
|
||||
Decimal(quantity or 0)
|
||||
+ self._fee_wizard_padding_quantity(fee, padding_by_lot))
|
||||
|
||||
@staticmethod
|
||||
def _fee_wizard_unit_price(fee):
|
||||
line = getattr(fee, 'sale_line', None) or getattr(fee, 'line', None)
|
||||
if line:
|
||||
return Decimal(str(getattr(line, 'unit_price', 0) or 0))
|
||||
for lot in fee._get_effective_fee_lots():
|
||||
if getattr(lot, 'sale_line', None):
|
||||
return Decimal(str(
|
||||
getattr(lot.sale_line, 'unit_price', 0) or 0))
|
||||
if getattr(lot, 'line', None):
|
||||
return Decimal(str(getattr(lot.line, 'unit_price', 0) or 0))
|
||||
return Decimal(0)
|
||||
|
||||
@fields.depends('type', 'sale_padding', 'lot_s', 'fee_sale')
|
||||
def on_change_sale_padding(self):
|
||||
if self.type != 'sale' or not self.fee_sale:
|
||||
return
|
||||
padding_by_lot = self._get_sale_padding_by_lot()
|
||||
for fee_line in self.fee_sale:
|
||||
fee = getattr(fee_line, 'fee', None)
|
||||
if not fee or not getattr(fee, 'add_padding', False):
|
||||
continue
|
||||
if fee.mode not in {'perqt', 'pprice'}:
|
||||
continue
|
||||
quantity = self._fee_wizard_billing_quantity(fee, padding_by_lot)
|
||||
fee_line.fee_quantity = quantity
|
||||
if fee.mode == 'perqt':
|
||||
fee_line.fee_amount = round(
|
||||
quantity * Decimal(str(fee.price or 0)), 2)
|
||||
elif fee.mode == 'pprice':
|
||||
fee_line.fee_amount = round(
|
||||
Decimal(str(fee.price or 0)) / Decimal(100)
|
||||
* quantity * self._fee_wizard_unit_price(fee), 2)
|
||||
|
||||
@classmethod
|
||||
def default_action(cls):
|
||||
return 'prov'
|
||||
|
||||
@classmethod
|
||||
def default_type(cls):
|
||||
|
||||
@@ -3238,6 +3238,66 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(invoice_line.quantity, Decimal('7'))
|
||||
fee.get_billing_quantity.assert_called_once_with()
|
||||
|
||||
def test_lot_invoice_sale_padding_refreshes_per_quantity_fee_amount(self):
|
||||
'sale padding refreshes displayed per quantity fee amount'
|
||||
start = lot_module.LotInvoiceStart()
|
||||
unit = SimpleNamespace(id=1)
|
||||
lot = SimpleNamespace(id=10, sale_line=SimpleNamespace(unit=unit))
|
||||
fee = SimpleNamespace(
|
||||
add_padding=True,
|
||||
mode='perqt',
|
||||
price=Decimal('10'),
|
||||
quantity=Decimal('5'),
|
||||
_get_effective_fee_lots=Mock(return_value=[lot]),
|
||||
_get_effective_fee_lots_quantity=Mock(return_value=Decimal('5')),
|
||||
_convert_padding_quantity=Mock(
|
||||
side_effect=lambda quantity, unit: Decimal(str(quantity))),
|
||||
get_quantity=Mock(return_value=Decimal('5')))
|
||||
fee_line = SimpleNamespace(
|
||||
fee=fee,
|
||||
fee_quantity=Decimal('5'),
|
||||
fee_amount=Decimal('50'))
|
||||
start.type = 'sale'
|
||||
start.sale_padding = Decimal('2')
|
||||
start.lot_s = [SimpleNamespace(lot=lot)]
|
||||
start.fee_sale = [fee_line]
|
||||
|
||||
start.on_change_sale_padding()
|
||||
|
||||
self.assertEqual(fee_line.fee_quantity, Decimal('7'))
|
||||
self.assertEqual(fee_line.fee_amount, Decimal('70.00'))
|
||||
|
||||
def test_lot_invoice_sale_padding_refreshes_percent_price_fee_amount(self):
|
||||
'sale padding refreshes displayed percent price fee amount'
|
||||
start = lot_module.LotInvoiceStart()
|
||||
unit = SimpleNamespace(id=1)
|
||||
sale_line = SimpleNamespace(unit=unit, unit_price=Decimal('100'))
|
||||
lot = SimpleNamespace(id=10, sale_line=sale_line)
|
||||
fee = SimpleNamespace(
|
||||
add_padding=True,
|
||||
mode='pprice',
|
||||
price=Decimal('2'),
|
||||
quantity=Decimal('5'),
|
||||
sale_line=sale_line,
|
||||
_get_effective_fee_lots=Mock(return_value=[lot]),
|
||||
_get_effective_fee_lots_quantity=Mock(return_value=Decimal('5')),
|
||||
_convert_padding_quantity=Mock(
|
||||
side_effect=lambda quantity, unit: Decimal(str(quantity))),
|
||||
get_quantity=Mock(return_value=Decimal('5')))
|
||||
fee_line = SimpleNamespace(
|
||||
fee=fee,
|
||||
fee_quantity=Decimal('5'),
|
||||
fee_amount=Decimal('10'))
|
||||
start.type = 'sale'
|
||||
start.sale_padding = Decimal('2')
|
||||
start.lot_s = [SimpleNamespace(lot=lot)]
|
||||
start.fee_sale = [fee_line]
|
||||
|
||||
start.on_change_sale_padding()
|
||||
|
||||
self.assertEqual(fee_line.fee_quantity, Decimal('7'))
|
||||
self.assertEqual(fee_line.fee_amount, Decimal('14.00'))
|
||||
|
||||
def test_fee_get_non_cog_returns_zero_without_move_lines(self):
|
||||
'fee non-cog amount is zero before any accounting move line exists'
|
||||
fee = fee_module.Fee()
|
||||
@@ -7939,6 +7999,36 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
Invoice._sale_padding_lots_to_clear_on_delete([invoice]),
|
||||
[lot])
|
||||
|
||||
def test_fee_invoice_delete_collects_fee_to_reset(self):
|
||||
'deleting a draft fee invoice resets the linked fee to not invoiced'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
fee = SimpleNamespace(id=20, dn_cn=None)
|
||||
invoice = SimpleNamespace(
|
||||
id=10,
|
||||
number=None,
|
||||
lines=[SimpleNamespace(fee=fee)])
|
||||
|
||||
reset_fees, clear_dn_cn_fees = Invoice._fee_updates_on_delete(
|
||||
[invoice])
|
||||
|
||||
self.assertEqual(reset_fees, [fee])
|
||||
self.assertEqual(clear_dn_cn_fees, [])
|
||||
|
||||
def test_fee_dn_cn_delete_collects_fee_to_clear_dn_cn_only(self):
|
||||
'deleting a draft fee DN/CN clears only the fee DN/CN link'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
invoice = SimpleNamespace(id=10, number=None, lines=[])
|
||||
fee = SimpleNamespace(id=20, dn_cn=invoice)
|
||||
invoice.lines = [SimpleNamespace(fee=fee)]
|
||||
|
||||
reset_fees, clear_dn_cn_fees = Invoice._fee_updates_on_delete(
|
||||
[invoice])
|
||||
|
||||
self.assertEqual(reset_fees, [])
|
||||
self.assertEqual(clear_dn_cn_fees, [fee])
|
||||
|
||||
def test_lot_invoice_message_mentions_processed_fees(self):
|
||||
'lot invoice success message mentions selected fee invoicing'
|
||||
wizard = lot_module.LotInvoice()
|
||||
@@ -7946,8 +8036,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
|
||||
self.assertEqual(
|
||||
wizard.default_message(None)['message'],
|
||||
'The invoice has been successfully created. '
|
||||
'2 selected fee invoice(s) have also been processed.')
|
||||
'Invoice created. Fee invoices: 2.')
|
||||
|
||||
def test_invoice_report_net_sums_signed_invoice_lines(self):
|
||||
'invoice report net uses the signed differential from invoice lines'
|
||||
|
||||
Reference in New Issue
Block a user