Improve payment management

* Ensure payments on Invoice does not exceed invoice amount
* Add method to add payments on Invoice
* Update scenario to set period in context for Tax Code sum

issue7139
review44011002
This commit is contained in:
Cédric Krier
2018-03-24 15:29:47 +01:00
parent 058201c87f
commit 7b14095c22
9 changed files with 275 additions and 37 deletions
+5
View File
@@ -1,3 +1,8 @@
* Show invoice payment on Move Line
* Remove payment lines which actually do not pay the Invoice
* Ensure payment lines are linked to only one Invoice
* Ensure payments on Invoice does not exceed invoice amount
* Add methods to add and remove payments on Invoice
* Check pending invoice to erase party
* Add invoice usage to party's contact mechanisms
* Show reconciled date on invoice
+1
View File
@@ -33,6 +33,7 @@ def register():
Period,
FiscalYear,
Move,
MoveLine,
Reconciliation,
module='account_invoice', type_='model')
Pool.register(
+60 -2
View File
@@ -7,13 +7,15 @@ from sql.conditionals import Coalesce
from trytond.model import (fields, ModelView, ModelSQL, MatchMixin,
sequence_ordered)
from trytond.pyson import Eval
from trytond.pyson import Eval, If, Bool
from trytond.pool import Pool, PoolMeta
from trytond.tools import grouped_slice
from trytond.transaction import Transaction
from trytond import backend
__all__ = ['FiscalYear',
'Period', 'Move', 'Reconciliation', 'InvoiceSequence', 'RenewFiscalYear']
'Period', 'Move', 'MoveLine', 'Reconciliation', 'InvoiceSequence',
'RenewFiscalYear']
class FiscalYear:
@@ -195,6 +197,62 @@ class Move:
return super(Move, cls)._get_origin() + ['account.invoice']
class MoveLine:
__metaclass__ = PoolMeta
__name__ = 'account.move.line'
invoice_payment = fields.Function(fields.Many2One(
'account.invoice', "Invoice Payment",
domain=[
('account', '=', Eval('account', -1)),
If(Bool(Eval('party')),
('party', '=', Eval('party')),
(),
),
],
states={
'invisible': Bool(Eval('reconciliation')),
},
depends=['account', 'party', 'reconciliation']),
'get_invoice_payment',
setter='set_invoice_payment',
searcher='search_invoice_payment')
invoice_payments = fields.Many2Many(
'account.invoice-account.move.line', 'line', 'invoice',
"Invoice Payments", readonly=True)
@classmethod
def __setup__(cls):
super(MoveLine, cls).__setup__()
cls._check_modify_exclude.add('invoice_payment')
@classmethod
def get_invoice_payment(cls, lines, name):
pool = Pool()
InvoicePaymentLine = pool.get('account.invoice-account.move.line')
ids = map(int, lines)
result = dict.fromkeys(ids, None)
for sub_ids in grouped_slice(ids):
payment_lines = InvoicePaymentLine.search([
('line', 'in', list(sub_ids)),
])
result.update({p.line.id: p.invoice.id for p in payment_lines})
return result
@classmethod
def set_invoice_payment(cls, lines, name, value):
pool = Pool()
Invoice = pool.get('account.invoice')
Invoice.remove_payment_lines(lines)
if value:
Invoice.add_payment_lines({Invoice(value): lines})
@classmethod
def search_invoice_payment(cls, name, domain):
return [('invoice_payments',) + tuple(domain[1:])]
class Reconciliation:
__metaclass__ = PoolMeta
__name__ = 'account.move.reconciliation'
+24
View File
@@ -9,6 +9,30 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">fiscalyear_form</field>
</record>
<record model="ir.ui.view" id="move_line_view_form">
<field name="model">account.move.line</field>
<field name="inherit" ref="account.move_line_view_form"/>
<field name="name">move_line_form</field>
</record>
<record model="ir.ui.view" id="move_line_view_form_move">
<field name="model">account.move.line</field>
<field name="inherit" ref="account.move_line_view_form_move"/>
<field name="name">move_line_form</field>
</record>
<record model="ir.ui.view" id="move_line_view_list">
<field name="model">account.move.line</field>
<field name="inherit" ref="account.move_line_view_tree"/>
<field name="name">move_line_list</field>
</record>
<record model="ir.ui.view" id="move_line_view_list_move">
<field name="model">account.move.line</field>
<field name="inherit" ref="account.move_line_view_tree_move"/>
<field name="name">move_line_list</field>
</record>
<record model="ir.ui.view" id="move_line_view_list_payment">
<field name="model">account.move.line</field>
<field name="type">tree</field>
+128 -11
View File
@@ -12,7 +12,7 @@ from sql.conditionals import Coalesce, Case
from sql.functions import Round
from trytond.model import Workflow, ModelView, ModelSQL, fields, Check, \
sequence_ordered
sequence_ordered, Unique
from trytond.report import Report
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, \
Button
@@ -184,14 +184,27 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
lines_to_pay = fields.Function(fields.One2Many('account.move.line', None,
'Lines to Pay'), 'get_lines_to_pay')
payment_lines = fields.Many2Many('account.invoice-account.move.line',
'invoice', 'line', readonly=True, string='Payment Lines',
'invoice', 'line', string='Payment Lines',
domain=[
('move.company', '=', Eval('company', -1)),
('account', '=', Eval('account', -1)),
('party', 'in', [None, Eval('party', -1)]),
['OR',
('invoice_payment', '=', None),
('invoice_payment', '=', Eval('id', -1)),
],
If(Eval('type') == 'out',
If(Eval('total_amount', 0) >= 0,
('debit', '=', 0),
('credit', '=', 0)),
If(Eval('total_amount', 0) >= 0,
('credit', '=', 0),
('debit', '=', 0))),
],
states={
'invisible': (Eval('state') == 'paid') | ~Eval('payment_lines'),
'invisible': Eval('state') == 'paid',
'readonly': Eval('state') != 'posted',
},
depends=['state', 'company'])
depends=['state', 'account', 'party', 'id', 'type', 'total_amount'])
amount_to_pay_today = fields.Function(fields.Numeric('Amount to Pay Today',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits']), 'get_amount_to_pay')
@@ -248,6 +261,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
'customer_invoice_cancel_move': (
'Customer invoice/credit note '
'"%s" can not be cancelled once posted.'),
'payment_lines_greater_amount': (
'The payment lines on invoice "%(invoice)s" can not be '
'greater than the invoice amount.'),
'modify_payment_lines_invoice_paid': (
'Payment lines can not be modified '
'on a paid invoice "%(invoice)s"'),
})
cls._transitions |= set((
('draft', 'validated'),
@@ -1161,6 +1180,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
for invoice in invoices:
invoice.check_same_account()
invoice.check_cancel_move()
invoice.check_payment_lines()
def check_same_account(self):
for line in self.lines:
@@ -1177,6 +1197,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
self.raise_user_error('customer_invoice_cancel_move',
self.rec_name)
def check_payment_lines(self):
amount = sum(l.debit - l.credit for l in self.lines_to_pay)
payment_amount = sum(l.debit - l.credit for l in self.payment_lines)
if abs(amount) < abs(payment_amount):
self.raise_user_error('payment_lines_greater_amount', {
'invoice': self.rec_name,
})
def get_reconcile_lines_for_amount(self, amount):
'''
Return list of lines and the remainder to make reconciliation.
@@ -1257,12 +1285,50 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
for line in move.lines:
if line.account == self.account:
self.write([self], {
'payment_lines': [('add', [line.id])],
})
self.add_payment_lines({self: [line]})
return line
raise Exception('Missing account')
@classmethod
def add_payment_lines(cls, payments):
"Add value lines to the key invoice from the payment dictionary."
to_write = []
for invoice, lines in payments.iteritems():
if invoice.state == 'paid':
cls.raise_user_error('modify_payment_lines_invoice_paid', {
'invoice': invoice.rec_name,
})
to_write.append([invoice])
to_write.append({'payment_lines': [('add', lines)]})
if to_write:
cls.write(*to_write)
@classmethod
def remove_payment_lines(cls, lines):
"Remove payment lines from their invoices."
pool = Pool()
PaymentLine = pool.get('account.invoice-account.move.line')
payments = defaultdict(list)
ids = map(int, lines)
for sub_ids in grouped_slice(ids):
payment_lines = PaymentLine.search([
('line', 'in', list(sub_ids)),
])
for payment_line in payment_lines:
payments[payment_line.invoice].append(payment_line.line)
to_write = []
for invoice, lines in payments.iteritems():
if invoice.state == 'paid':
cls.raise_user_error('modify_payment_lines_invoice_paid', {
'invoice': invoice.rec_name,
})
to_write.append([invoice])
to_write.append({'payment_lines': [('remove', lines)]})
if to_write:
cls.write(*to_write)
def print_invoice(self):
'''
Generate invoice report and store it in invoice_report field.
@@ -1385,7 +1451,19 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
@classmethod
@Workflow.transition('paid')
def paid(cls, invoices):
pass
# Remove links to lines which actually do not pay the invoice
to_write = []
for invoice in invoices:
to_remove = []
reconciliations = [l.reconciliation for l in invoice.lines_to_pay]
for payment_line in invoice.payment_lines:
if payment_line.reconciliation not in reconciliations:
to_remove.append(payment_line)
if to_remove:
to_write.append([invoice])
to_write.append([('remove', to_remove)])
if to_write:
cls.write(*to_write)
@classmethod
@ModelView.button
@@ -1437,8 +1515,47 @@ class InvoicePaymentLine(ModelSQL):
__name__ = 'account.invoice-account.move.line'
invoice = fields.Many2One('account.invoice', 'Invoice', ondelete='CASCADE',
select=True, required=True)
line = fields.Many2One('account.move.line', 'Payment Line',
ondelete='CASCADE', select=True, required=True)
invoice_account = fields.Function(
fields.Many2One('account.account', "Invoice Account"),
'get_invoice')
invoice_party = fields.Function(
fields.Many2One('party.party', "Invoice Party"), 'get_invoice')
line = fields.Many2One(
'account.move.line', 'Payment Line', ondelete='CASCADE',
select=True, required=True,
domain=[
('account', '=', Eval('invoice_account')),
('party', '=', Eval('invoice_party')),
],
depends=['invoice_account', 'invoice_party'])
@classmethod
def __setup__(cls):
super(InvoicePaymentLine, cls).__setup__()
t = cls.__table__()
cls._sql_constraints = [
('line_unique', Unique(t, t.line),
"A payment line can be linked to only one invoice."),
]
@classmethod
def get_invoice(cls, records, names):
result = {}
for name in names:
result[name] = {}
invoice_account = 'invoice_account' in result
invoice_party = 'invoice_party' in result
for record in records:
if invoice_account:
result['invoice_account'][record.id] = (
record.invoice.account.id)
if invoice_party:
if record.invoice.account.party_required:
party = record.invoice.party.id
else:
party = None
result['invoice_party'][record.id] = party
return result
class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin):
+26 -16
View File
@@ -36,6 +36,7 @@ Create fiscal year::
... create_fiscalyear(company))
>>> fiscalyear.click('create_period')
>>> period = fiscalyear.periods[0]
>>> period_ids = [p.id for p in fiscalyear.periods]
Create chart of accounts::
@@ -49,6 +50,7 @@ Create chart of accounts::
Create tax::
>>> TaxCode = Model.get('account.tax.code')
>>> tax = set_tax_code(create_tax(Decimal('.10')))
>>> tax.save()
>>> invoice_base_code = tax.invoice_base_code
@@ -168,17 +170,21 @@ Post invoice::
Decimal('0.00')
>>> account_tax.credit
Decimal('20.00')
>>> invoice_base_code.reload()
>>> invoice_base_code.sum
>>> with config.set_context(periods=period_ids):
... invoice_base_code = TaxCode(invoice_base_code.id)
... invoice_base_code.sum
Decimal('200.00')
>>> invoice_tax_code.reload()
>>> invoice_tax_code.sum
>>> with config.set_context(periods=period_ids):
... invoice_tax_code = TaxCode(invoice_tax_code.id)
... invoice_tax_code.sum
Decimal('20.00')
>>> credit_note_base_code.reload()
>>> credit_note_base_code.sum
>>> with config.set_context(periods=period_ids):
... credit_note_base_code = TaxCode(credit_note_base_code.id)
... credit_note_base_code.sum
Decimal('0.00')
>>> credit_note_tax_code.reload()
>>> credit_note_tax_code.sum
>>> with config.set_context(periods=period_ids):
... credit_note_tax_code = TaxCode(credit_note_tax_code.id)
... credit_note_tax_code.sum
Decimal('0.00')
Credit invoice with refund::
@@ -206,17 +212,21 @@ Credit invoice with refund::
Decimal('20.00')
>>> account_tax.credit
Decimal('20.00')
>>> invoice_base_code.reload()
>>> invoice_base_code.sum
>>> with config.set_context(periods=period_ids):
... invoice_base_code = TaxCode(invoice_base_code.id)
... invoice_base_code.sum
Decimal('200.00')
>>> invoice_tax_code.reload()
>>> invoice_tax_code.sum
>>> with config.set_context(periods=period_ids):
... invoice_tax_code = TaxCode(invoice_tax_code.id)
... invoice_tax_code.sum
Decimal('20.00')
>>> credit_note_base_code.reload()
>>> credit_note_base_code.sum
>>> with config.set_context(periods=period_ids):
... credit_note_base_code = TaxCode(credit_note_base_code.id)
... credit_note_base_code.sum
Decimal('200.00')
>>> credit_note_tax_code.reload()
>>> credit_note_tax_code.sum
>>> with config.set_context(periods=period_ids):
... credit_note_tax_code = TaxCode(credit_note_tax_code.id)
... credit_note_tax_code.sum
Decimal('20.00')
Pay invoice::
+14 -8
View File
@@ -31,6 +31,7 @@ Create fiscal year::
>>> fiscalyear = set_fiscalyear_invoice_sequences(
... create_fiscalyear(company))
>>> fiscalyear.click('create_period')
>>> period_ids = [p.id for p in fiscalyear.periods]
Create chart of accounts::
@@ -43,6 +44,7 @@ Create chart of accounts::
Create tax::
>>> TaxCode = Model.get('account.tax.code')
>>> tax = set_tax_code(create_tax(Decimal('.10')))
>>> tax.save()
>>> invoice_base_code = tax.invoice_base_code
@@ -145,17 +147,21 @@ Create invoice::
Decimal('10.00')
>>> account_tax.credit
Decimal('0.00')
>>> invoice_base_code.reload()
>>> invoice_base_code.sum
>>> with config.set_context(periods=period_ids):
... invoice_base_code = TaxCode(invoice_base_code.id)
... invoice_base_code.sum
Decimal('100.00')
>>> invoice_tax_code.reload()
>>> invoice_tax_code.sum
>>> with config.set_context(periods=period_ids):
... invoice_tax_code = TaxCode(invoice_tax_code.id)
... invoice_tax_code.sum
Decimal('10.00')
>>> credit_note_base_code.reload()
>>> credit_note_base_code.sum
>>> with config.set_context(periods=period_ids):
... credit_note_base_code = TaxCode(credit_note_base_code.id)
... credit_note_base_code.sum
Decimal('0.00')
>>> credit_note_tax_code.reload()
>>> credit_note_tax_code.sum
>>> with config.set_context(periods=period_ids):
... credit_note_tax_code = TaxCode(credit_note_tax_code.id)
... credit_note_tax_code.sum
Decimal('0.00')
Credit invoice::
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='info']" position="inside">
<label name="invoice_payment"/>
<field name="invoice_payment"/>
</xpath>
</data>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//field[@name='reconciliation']" position="after">
<field name="invoice_payment"/>
</xpath>
</data>