Fee CN/DN
This commit is contained in:
@@ -2057,10 +2057,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
||||
var_qt = sum([i.quantity for i in gl])
|
||||
logger.info("LOT_TO_PROCESS:%s",lot)
|
||||
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
|
||||
if (gl[0].fee and not gl[0].product.landed_cost):
|
||||
fee_correction = (
|
||||
gl[0].fee
|
||||
and any(Decimal(str(i.quantity or 0)) < 0 for i in gl)
|
||||
and any(Decimal(str(i.quantity or 0)) > 0 for i in gl))
|
||||
if (gl[0].fee and not gl[0].product.landed_cost
|
||||
and not fee_correction):
|
||||
diff = gl[0].fee.amount - gl[0].fee.get_non_cog(lot)
|
||||
account_move = gl[0].fee._get_account_move_fee(lot,'in',diff)
|
||||
Move.save([account_move])
|
||||
if account_move:
|
||||
Move.save([account_move])
|
||||
if (lot and not gl[0].fee) or (gl[0].fee and gl[0].product.landed_cost):
|
||||
adjust_move_lines = []
|
||||
mov = None
|
||||
|
||||
@@ -1016,16 +1016,18 @@ class Purchase(
|
||||
for purchase in purchases:
|
||||
logger.info("PROCESS_INVOICE:%s",action)
|
||||
invoice = purchase.create_invoice(lots,action)
|
||||
if not invoice:
|
||||
continue
|
||||
if action == 'prov':
|
||||
invoice.reference = 'Provisional'
|
||||
elif action == 'service':
|
||||
invoice.reference = 'Service'
|
||||
else:
|
||||
invoice.reference = 'Final'
|
||||
if invoice:
|
||||
invoices[purchase] = invoice
|
||||
invoices[purchase] = invoice
|
||||
|
||||
cls._save_invoice(invoices,prepayment)
|
||||
if invoices:
|
||||
cls._save_invoice(invoices,prepayment)
|
||||
|
||||
@classmethod
|
||||
def _save_invoice(cls, invoices,prepayment=None):
|
||||
@@ -1854,6 +1856,85 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
def on_change_with_currency(self, name=None):
|
||||
return self.purchase.currency if self.purchase else None
|
||||
|
||||
@staticmethod
|
||||
def _record_id(record):
|
||||
return getattr(record, 'id', record)
|
||||
|
||||
@classmethod
|
||||
def _get_last_fee_invoice_line(cls, fee, lot):
|
||||
fee_id = cls._record_id(fee)
|
||||
lot_id = cls._record_id(lot)
|
||||
if not fee_id or not lot_id:
|
||||
return
|
||||
InvoiceLine = Pool().get('account.invoice.line')
|
||||
lines = InvoiceLine.search([
|
||||
('fee', '=', fee_id),
|
||||
('lot', '=', lot_id),
|
||||
('quantity', '>', 0),
|
||||
('invoice.state', '!=', 'cancelled'),
|
||||
], order=[
|
||||
('invoice.invoice_date', 'DESC'),
|
||||
('invoice.id', 'DESC'),
|
||||
('id', 'DESC'),
|
||||
], limit=1)
|
||||
return lines[0] if lines else None
|
||||
|
||||
@staticmethod
|
||||
def _fee_invoice_line_changed(previous_line, invoice_line):
|
||||
if not previous_line:
|
||||
return True
|
||||
previous_quantity = Decimal(str(previous_line.quantity or 0))
|
||||
quantity = Decimal(str(invoice_line.quantity or 0))
|
||||
previous_price = Decimal(str(previous_line.unit_price or 0))
|
||||
price = Decimal(str(invoice_line.unit_price or 0))
|
||||
return previous_quantity != quantity or previous_price != price
|
||||
|
||||
@classmethod
|
||||
def _get_fee_reversal_invoice_line(cls, previous_line, origin):
|
||||
if not previous_line:
|
||||
return
|
||||
InvoiceLine = Pool().get('account.invoice.line')
|
||||
reversal_line, = InvoiceLine.copy([previous_line], default={
|
||||
'invoice': None,
|
||||
'quantity': -previous_line.quantity,
|
||||
'unit_price': previous_line.unit_price,
|
||||
'party': previous_line.invoice.party,
|
||||
'origin': str(origin),
|
||||
})
|
||||
return reversal_line
|
||||
|
||||
def _get_service_fee_invoice_lines(self, invoice_line, lot):
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee.search(['purchase','=',self.purchase.id])
|
||||
if not fee:
|
||||
return [invoice_line]
|
||||
|
||||
fee = fee[0]
|
||||
invoice_line.fee = fee
|
||||
if fee.mode == 'lumpsum':
|
||||
invoice_line.quantity = 1
|
||||
elif fee.mode == 'ppack':
|
||||
invoice_line.quantity = fee.quantity
|
||||
else:
|
||||
state_id = 0
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqt = LotQtType.search([('name','=','BL')])
|
||||
if lqt:
|
||||
state_id = lqt[0].id
|
||||
invoice_line.quantity = fee.get_fee_lots_qt(state_id)
|
||||
|
||||
if getattr(fee, 'state', None) != 'invoiced':
|
||||
return [invoice_line]
|
||||
|
||||
previous_line = self._get_last_fee_invoice_line(fee, lot)
|
||||
if not self._fee_invoice_line_changed(previous_line, invoice_line):
|
||||
return []
|
||||
reversal_line = self._get_fee_reversal_invoice_line(
|
||||
previous_line, self)
|
||||
if reversal_line:
|
||||
return [reversal_line, invoice_line]
|
||||
return [invoice_line]
|
||||
|
||||
def get_invoice_line(self,lots=None,action=None):
|
||||
'Return a list of invoice line for purchase line'
|
||||
pool = Pool()
|
||||
@@ -1933,21 +2014,9 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
invoice_line.unit_price = self.unit_price
|
||||
invoice_line.product = self.product
|
||||
invoice_line.stock_moves = []
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee.search(['purchase','=',self.purchase.id])
|
||||
if fee:
|
||||
invoice_line.fee = fee[0]
|
||||
if fee[0].mode == 'lumpsum':
|
||||
invoice_line.quantity = 1
|
||||
elif fee[0].mode == 'ppack':
|
||||
invoice_line.quantity = fee[0].quantity
|
||||
else:
|
||||
state_id = 0
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqt = LotQtType.search([('name','=','BL')])
|
||||
if lqt:
|
||||
state_id = lqt[0].id
|
||||
invoice_line.quantity = fee[0].get_fee_lots_qt(state_id)
|
||||
lines.extend(
|
||||
self._get_service_fee_invoice_lines(invoice_line, l))
|
||||
continue
|
||||
|
||||
lines.append(invoice_line)
|
||||
logger.info("GETINVLINE:%s",self.product.type)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user