Fee CN/DN

This commit is contained in:
2026-06-16 23:47:35 +02:00
parent d0dd8965cd
commit ee1cb3e7fb
4 changed files with 225 additions and 39 deletions

View File

@@ -339,15 +339,12 @@ class Fee(ModelSQL,ModelView):
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
return Decimal(0)
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'invoice': {
'invisible': (Eval('state') == 'invoiced'),
'depends': ['state'],
},
})
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'invoice': {},
})
@classmethod
def default_state(cls):
@@ -368,12 +365,22 @@ class Fee(ModelSQL,ModelView):
@classmethod
@ModelView.button
@filter_state('not invoiced')
def invoice(cls, fees):
Purchase = Pool().get('purchase.purchase')
FeeLots = Pool().get('fee.lots')
Warning = Pool().get('res.user.warning')
fees_to_invoice = []
for fee in fees:
if fee.state == 'invoiced':
warning_name = Warning.format(
"Fee already invoiced", [fee])
if Warning.check(warning_name):
raise UserWarning(
warning_name,
"This fee has already been invoiced. Continuing will "
"create a debit note or credit note by reversing the "
"previous fee invoice line and adding a new line with "
"the current fee values. Do you want to continue?")
fee.ensure_ordered_purchase()
if fee.purchase:
fl = FeeLots.search([('fee','=',fee.id)])
@@ -420,10 +427,31 @@ class Fee(ModelSQL,ModelView):
return round(self.price * Decimal(sm[0].lot.get_lot_price()) / 100,4)
return price
def get_invoice(self,name):
if self.purchase:
if self.purchase.invoices:
return self.purchase.invoices[0]
def get_invoice(self,name):
InvoiceLine = Pool().get('account.invoice.line')
invoice_lines = InvoiceLine.search([
('fee', '=', self.id),
('quantity', '>', 0),
('invoice.state', '!=', 'cancelled'),
], order=[
('invoice.invoice_date', 'DESC'),
('invoice.id', 'DESC'),
('id', 'DESC'),
], limit=1)
if invoice_lines:
return invoice_lines[0].invoice
if self.purchase:
if self.purchase.invoices:
invoices = [
invoice for invoice in self.purchase.invoices
if getattr(invoice, 'state', None) != 'cancelled']
if invoices:
return sorted(
invoices,
key=lambda invoice: (
invoice.invoice_date or datetime.date.min,
invoice.id or 0),
reverse=True)[0]
def get_landed_status(self,name):
if self.product:
@@ -706,11 +734,11 @@ class Fee(ModelSQL,ModelView):
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
if self.mode == 'lumpsum':
if self.amount != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.amount
if self.type == 'ordered' and self.purchase:
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
if self.mode == 'lumpsum':
if self.amount != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.amount
elif self.mode == 'ppack':
if self.amount != self.purchase.lines[0].amount:
self.purchase.lines[0].unit_price = self.price

View File

@@ -13,6 +13,7 @@ from trytond.pyson import Eval
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.exceptions import UserError
from trytond.transaction import Transaction
from trytond.modules.purchase import purchase as base_purchase_module
from trytond.modules.purchase_trade import valuation as valuation_module
from trytond.modules.purchase_trade import lot as lot_module
from trytond.modules.purchase_trade import purchase as purchase_module
@@ -2607,6 +2608,88 @@ class PurchaseTradeTestCase(ModuleTestCase):
save.assert_called_once_with([fee])
create_accruals.assert_called_once_with()
def test_purchase_service_fee_invoice_reverses_previous_line(self):
're-invoiced service fee creates a reversal and a current line'
Line = base_purchase_module.Line
line = Line()
line.purchase = Mock(id=20)
line.unit_price = Decimal('15')
lot = Mock(id=30)
invoice_line = Mock(quantity=Decimal('12'), unit_price=Decimal('15'))
fee = Mock(
state='invoiced',
mode='perqt',
get_fee_lots_qt=Mock(return_value=Decimal('12')))
previous_line = Mock(
quantity=Decimal('10'),
unit_price=Decimal('11'),
invoice=Mock(party=Mock()))
reversal_line = Mock()
fee_model = Mock()
fee_model.search.return_value = [fee]
lqt_model = Mock()
lqt_model.search.return_value = []
invoice_line_model = Mock()
invoice_line_model.copy.return_value = [reversal_line]
with patch(
'trytond.modules.purchase.purchase.Pool'
) as PoolMock, patch.object(
Line, '_get_last_fee_invoice_line',
return_value=previous_line):
PoolMock.return_value.get.side_effect = lambda name: {
'fee.fee': fee_model,
'lot.qt.type': lqt_model,
'account.invoice.line': invoice_line_model,
}[name]
result = line._get_service_fee_invoice_lines(invoice_line, lot)
self.assertEqual(result, [reversal_line, invoice_line])
self.assertEqual(invoice_line.fee, fee)
self.assertEqual(invoice_line.quantity, Decimal('12'))
invoice_line_model.copy.assert_called_once_with(
[previous_line], default={
'invoice': None,
'quantity': Decimal('-10'),
'unit_price': Decimal('11'),
'party': previous_line.invoice.party,
'origin': str(line),
})
def test_purchase_service_fee_invoice_skips_unchanged_reinvoice(self):
'unchanged re-invoiced service fee does not create a zero note'
Line = base_purchase_module.Line
line = Line()
line.purchase = Mock(id=20)
lot = Mock(id=30)
invoice_line = Mock(quantity=Decimal('12'), unit_price=Decimal('15'))
fee = Mock(
state='invoiced',
mode='perqt',
get_fee_lots_qt=Mock(return_value=Decimal('12')))
previous_line = Mock(
quantity=Decimal('12'),
unit_price=Decimal('15'),
invoice=Mock(party=Mock()))
fee_model = Mock()
fee_model.search.return_value = [fee]
lqt_model = Mock()
lqt_model.search.return_value = []
with patch(
'trytond.modules.purchase.purchase.Pool'
) as PoolMock, patch.object(
Line, '_get_last_fee_invoice_line',
return_value=previous_line):
PoolMock.return_value.get.side_effect = lambda name: {
'fee.fee': fee_model,
'lot.qt.type': lqt_model,
}[name]
self.assertEqual(
line._get_service_fee_invoice_lines(invoice_line, lot), [])
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()