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:
@@ -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
|
||||
|
||||
@@ -33,6 +33,7 @@ def register():
|
||||
Period,
|
||||
FiscalYear,
|
||||
Move,
|
||||
MoveLine,
|
||||
Reconciliation,
|
||||
module='account_invoice', type_='model')
|
||||
Pool.register(
|
||||
|
||||
+60
-2
@@ -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
@@ -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
@@ -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
@@ -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::
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user