Lot invoicing + fee padding
This commit is contained in:
@@ -130,13 +130,43 @@ class Invoice(metaclass=PoolMeta):
|
|||||||
lots[getattr(lot, 'id', id(lot))] = lot
|
lots[getattr(lot, 'id', id(lot))] = lot
|
||||||
return list(lots.values())
|
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
|
@classmethod
|
||||||
def delete(cls, invoices):
|
def delete(cls, invoices):
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
padding_lots = cls._sale_padding_lots_to_clear_on_delete(invoices)
|
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)
|
super().delete(invoices)
|
||||||
if padding_lots:
|
if padding_lots:
|
||||||
Lot.write(padding_lots, {'sale_invoice_padding': Decimal(0)})
|
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):
|
def do_lot_invoicing(self):
|
||||||
super().do_lot_invoicing()
|
super().do_lot_invoicing()
|
||||||
|
|||||||
@@ -4916,11 +4916,9 @@ class LotInvoice(Wizard):
|
|||||||
|
|
||||||
def default_message(self, fields):
|
def default_message(self, fields):
|
||||||
fee_count = getattr(self, '_invoiced_fee_count', 0)
|
fee_count = getattr(self, '_invoiced_fee_count', 0)
|
||||||
message = 'The invoice has been successfully created.'
|
message = 'Invoice created.'
|
||||||
if fee_count:
|
if fee_count:
|
||||||
message += (
|
message += ' Fee invoices: %s.' % fee_count
|
||||||
' %s selected fee invoice(s) have also been processed.'
|
|
||||||
% fee_count)
|
|
||||||
return {
|
return {
|
||||||
'message': message,
|
'message': message,
|
||||||
}
|
}
|
||||||
@@ -5031,10 +5029,83 @@ class LotInvoiceStart(ModelView):
|
|||||||
amount += l.lot_amount
|
amount += l.lot_amount
|
||||||
amount += padding_share * Decimal(str(l.lot_price or 0))
|
amount += padding_share * Decimal(str(l.lot_price or 0))
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
@classmethod
|
def _get_sale_padding_by_lot(self):
|
||||||
def default_action(cls):
|
lots = [
|
||||||
return 'prov'
|
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
|
@classmethod
|
||||||
def default_type(cls):
|
def default_type(cls):
|
||||||
|
|||||||
@@ -3238,6 +3238,66 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(invoice_line.quantity, Decimal('7'))
|
self.assertEqual(invoice_line.quantity, Decimal('7'))
|
||||||
fee.get_billing_quantity.assert_called_once_with()
|
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):
|
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 non-cog amount is zero before any accounting move line exists'
|
||||||
fee = fee_module.Fee()
|
fee = fee_module.Fee()
|
||||||
@@ -7939,6 +7999,36 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
Invoice._sale_padding_lots_to_clear_on_delete([invoice]),
|
Invoice._sale_padding_lots_to_clear_on_delete([invoice]),
|
||||||
[lot])
|
[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):
|
def test_lot_invoice_message_mentions_processed_fees(self):
|
||||||
'lot invoice success message mentions selected fee invoicing'
|
'lot invoice success message mentions selected fee invoicing'
|
||||||
wizard = lot_module.LotInvoice()
|
wizard = lot_module.LotInvoice()
|
||||||
@@ -7946,8 +8036,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
wizard.default_message(None)['message'],
|
wizard.default_message(None)['message'],
|
||||||
'The invoice has been successfully created. '
|
'Invoice created. Fee invoices: 2.')
|
||||||
'2 selected fee invoice(s) have also been processed.')
|
|
||||||
|
|
||||||
def test_invoice_report_net_sums_signed_invoice_lines(self):
|
def test_invoice_report_net_sums_signed_invoice_lines(self):
|
||||||
'invoice report net uses the signed differential from invoice lines'
|
'invoice report net uses the signed differential from invoice lines'
|
||||||
|
|||||||
Reference in New Issue
Block a user