From 23cd459b36962d31b9b4888f1107f086741dcd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 4 Mar 2016 00:23:47 +0100 Subject: [PATCH] Improve get_reconcile_lines_for_amount Test all combinations of both payment_lines and lines_to_pay and keep the one with the smaller remainder. Use a namedtuple as result to ease its usage to callers. issue5327 review16001003 --- invoice.py | 72 +++++++++++-------------------------- tests/scenario_invoice.rst | 74 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 51 deletions(-) diff --git a/invoice.py b/invoice.py index e6f079b838..5b2f0cf7e9 100644 --- a/invoice.py +++ b/invoice.py @@ -1,7 +1,8 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from decimal import Decimal -from collections import defaultdict +from collections import defaultdict, namedtuple +from itertools import combinations import base64 import itertools @@ -1051,58 +1052,26 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): self.raise_user_error('customer_invoice_cancel_move', self.rec_name) - def get_reconcile_lines_for_amount(self, amount, exclude_lines=None): + def get_reconcile_lines_for_amount(self, amount): ''' Return list of lines and the remainder to make reconciliation. ''' - if exclude_lines is None: - exclude_lines = [] - payment_amount = Decimal('0.0') - remainder = self.total_amount - lines = [] - payment_lines = [] + Result = namedtuple('Result', ['lines', 'remainder']) - for line in self.payment_lines: - if line.reconciliation: - continue - payment_amount += line.debit - line.credit - payment_lines.append(line) + lines = [l for l in self.payment_lines + self.lines_to_pay + if not l.reconciliation] - for line in self.lines_to_pay: - - if line.reconciliation: - continue - if line in exclude_lines: - continue - - test_amount = (line.debit - line.credit) - amount - if self.currency.is_zero(test_amount): - return ([line], Decimal('0.0')) - if abs(test_amount) < abs(remainder): - lines = [line] - remainder = test_amount - - test_amount = payment_amount - amount - test_amount += (line.debit - line.credit) - if self.currency.is_zero(test_amount): - return ([line] + payment_lines, Decimal('0.0')) - if abs(test_amount) < abs(remainder): - lines = [line] + payment_lines - remainder = test_amount - - exclude_lines2 = exclude_lines[:] - exclude_lines2.append(line) - lines2, remainder2 = self.get_reconcile_lines_for_amount( - ((line.debit - line.credit) - amount), - exclude_lines=exclude_lines2) - if remainder2 == Decimal('0.0'): - lines2.append(line) - return lines2, remainder2 - if abs(remainder2) < abs(remainder): - lines2.append(line) - lines, remainder = lines2, remainder2 - - return (lines, remainder) + best = Result([], self.total_amount) + for n in xrange(len(lines), 0, -1): + for comb_lines in combinations(lines, n): + remainder = sum((l.debit - l.credit) for l in comb_lines) + remainder -= amount + result = Result(list(comb_lines), remainder) + if self.currency.is_zero(remainder): + return result + if abs(remainder) < abs(best.remainder): + best = result + return result def pay_invoice(self, amount, journal, date, description, amount_second_currency=None, second_currency=None): @@ -2506,10 +2475,13 @@ class PayInvoice(Wizard): if line_id not in default['lines_to_pay']: default['lines'].remove(line_id) + default['payment_lines'] = [x.id for x in invoice.payment_lines + if not x.reconciliation] + default['amount_writeoff'] = Decimal('0.0') for line in Line.browse(default['lines']): default['amount_writeoff'] += line.debit - line.credit - for line in invoice.payment_lines: + for line in Line.browse(default['payment_lines']): default['amount_writeoff'] += line.debit - line.credit if invoice.type in ('in_invoice', 'out_credit_note'): default['amount_writeoff'] = - default['amount_writeoff'] - amount @@ -2519,8 +2491,6 @@ class PayInvoice(Wizard): default['currency_writeoff'] = invoice.company.currency.id default['currency_digits_writeoff'] = invoice.company.currency.digits default['invoice'] = invoice.id - default['payment_lines'] = [x.id for x in invoice.payment_lines - if not x.reconciliation] if (amount > invoice.amount_to_pay or invoice.company.currency.is_zero(amount)): diff --git a/tests/scenario_invoice.rst b/tests/scenario_invoice.rst index 5aef8c90a9..d3933af7f3 100644 --- a/tests/scenario_invoice.rst +++ b/tests/scenario_invoice.rst @@ -49,6 +49,7 @@ Create chart of accounts:: >>> revenue = accounts['revenue'] >>> expense = accounts['expense'] >>> account_tax = accounts['tax'] + >>> account_cash = accounts['cash'] Create tax:: @@ -59,6 +60,23 @@ Create tax:: >>> credit_note_base_code = tax.credit_note_base_code >>> credit_note_tax_code = tax.credit_note_tax_code +Set Cash journal:: + + >>> Journal = Model.get('account.journal') + >>> journal_cash, = Journal.find([('type', '=', 'cash')]) + >>> journal_cash.credit_account = account_cash + >>> journal_cash.debit_account = account_cash + >>> journal_cash.save() + +Create Write-Off journal:: + + >>> Sequence = Model.get('ir.sequence') + >>> sequence_journal, = Sequence.find([('code', '=', 'account.journal')]) + >>> journal_writeoff = Journal(name='Write-Off', type='write-off', + ... sequence=sequence_journal, + ... credit_account=revenue, debit_account=expense) + >>> journal_writeoff.save() + Create party:: >>> Party = Model.get('party.party') @@ -204,6 +222,62 @@ Credit invoice with refund:: >>> credit_note_tax_code.sum Decimal('20.00') +Pay invoice:: + + >>> invoice, = invoice.duplicate() + >>> invoice.click('post') + + >>> pay = Wizard('account.invoice.pay', [invoice]) + >>> pay.form.amount + Decimal('240.00') + >>> pay.form.amount = Decimal('120.00') + >>> pay.form.journal = journal_cash + >>> pay.execute('choice') + >>> pay.state + 'end' + + >>> pay = Wizard('account.invoice.pay', [invoice]) + >>> pay.form.amount + Decimal('120.00') + >>> pay.form.amount = Decimal('20.00') + >>> pay.form.journal = journal_cash + >>> pay.execute('choice') + >>> pay.form.type = 'partial' + >>> pay.form.amount + Decimal('20.00') + >>> len(pay.form.lines_to_pay) + 1 + >>> len(pay.form.payment_lines) + 0 + >>> len(pay.form.lines) + 1 + >>> pay.form.amount_writeoff + Decimal('100.00') + >>> pay.execute('pay') + + >>> pay = Wizard('account.invoice.pay', [invoice]) + >>> pay.form.amount + Decimal('-20.00') + >>> pay.form.amount = Decimal('99.00') + >>> pay.form.journal = journal_cash + >>> pay.execute('choice') + >>> pay.form.type = 'writeoff' + >>> pay.form.journal_writeoff = journal_writeoff + >>> pay.form.amount + Decimal('99.00') + >>> len(pay.form.lines_to_pay) + 1 + >>> len(pay.form.payment_lines) + 1 + >>> len(pay.form.lines) + 1 + >>> pay.form.amount_writeoff + Decimal('1.00') + >>> pay.execute('pay') + + >>> invoice.state + u'paid' + Create empty invoice:: >>> invoice = Invoice()