diff --git a/CHANGELOG b/CHANGELOG
index be85262bf4..df854654cc 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
diff --git a/__init__.py b/__init__.py
index a65bb71c95..422837c506 100644
--- a/__init__.py
+++ b/__init__.py
@@ -33,6 +33,7 @@ def register():
Period,
FiscalYear,
Move,
+ MoveLine,
Reconciliation,
module='account_invoice', type_='model')
Pool.register(
diff --git a/account.py b/account.py
index 4754fc83e2..b81099ec38 100644
--- a/account.py
+++ b/account.py
@@ -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'
diff --git a/account.xml b/account.xml
index 3b7e8c1f56..7436924e62 100644
--- a/account.xml
+++ b/account.xml
@@ -9,6 +9,30 @@ this repository contains the full copyright notices and license terms. -->
fiscalyear_form
+
+ account.move.line
+
+ move_line_form
+
+
+
+ account.move.line
+
+ move_line_form
+
+
+
+ account.move.line
+
+ move_line_list
+
+
+
+ account.move.line
+
+ move_line_list
+
+
account.move.line
tree
diff --git a/invoice.py b/invoice.py
index 7531a3dc13..7eb4d80209 100644
--- a/invoice.py
+++ b/invoice.py
@@ -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):
diff --git a/tests/scenario_invoice.rst b/tests/scenario_invoice.rst
index e04bf5c2fa..ddde378aa8 100644
--- a/tests/scenario_invoice.rst
+++ b/tests/scenario_invoice.rst
@@ -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::
diff --git a/tests/scenario_invoice_supplier.rst b/tests/scenario_invoice_supplier.rst
index 08b3a56d0c..e7e9c9b64f 100644
--- a/tests/scenario_invoice_supplier.rst
+++ b/tests/scenario_invoice_supplier.rst
@@ -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::
diff --git a/view/move_line_form.xml b/view/move_line_form.xml
new file mode 100644
index 0000000000..10abad66c9
--- /dev/null
+++ b/view/move_line_form.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/view/move_line_list.xml b/view/move_line_list.xml
new file mode 100644
index 0000000000..1dd7127d5e
--- /dev/null
+++ b/view/move_line_list.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+