← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~therp-nl/account-invoicing/7.0-account_cash_discount into lp:account-invoicing

 

Holger Brunn (Therp) has proposed merging lp:~therp-nl/account-invoicing/7.0-account_cash_discount into lp:account-invoicing.

Requested reviews:
  Account Core Editors (account-core-editors)

For more details, see:
https://code.launchpad.net/~therp-nl/account-invoicing/7.0-account_cash_discount/+merge/203359

This started as a port of http://bazaar.launchpad.net/~camptocamp/c2c-rd-addons/7.0/files/head:/account_cash_discount_61

I've been unhappy with the way the original module did its work (too much SQL, deleting moves and recreating them), so by now, it's actually a rewrite.
-- 
https://code.launchpad.net/~therp-nl/account-invoicing/7.0-account_cash_discount/+merge/203359
Your team Account Core Editors is requested to review the proposed merge of lp:~therp-nl/account-invoicing/7.0-account_cash_discount into lp:account-invoicing.
=== added directory 'account_cash_discount'
=== added file 'account_cash_discount/__init__.py'
--- account_cash_discount/__init__.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/__init__.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#    Copyright (C) 2010-2012 Camptocamp Austria (<http://www.camptocamp.at>)
+#    Copyright (C) 2014 Therp BV (<http://www.therp.nl>)
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import model
+import wizard
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

=== added file 'account_cash_discount/__openerp__.py'
--- account_cash_discount/__openerp__.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/__openerp__.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#    Copyright (C) 2010-2012 Camptocamp Austria (<http://www.camptocamp.at>)
+#    Copyright (C) 2014 Therp BV (<http://www.therp.nl>)
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+{
+    'name': 'Cash discount',
+    'version': '0.9',
+    'category': 'Accounting & Finance',
+    'description': """
+Cash Discount (Austria and Germany style)
+=========================================
+
+Usage
+-----
+
+Define your discounts as part of your payment terms. Keep in mind that other
+computation lines than 'balance' won't make much sense, so only fill in one
+computation line of type 'balance' indicating your payment date.
+
+When paying your invoice, fill in the exact amount for the discounted invoice.
+If the payment date and the amount matches a discount, the invoice will be
+marked as paid and some correction move lines will be created to reflect the
+invoice being paid with a discount.
+
+If you manually reconcile move lines, you'll be offered a button to book the
+writeoff amount as cash discount if a matching cash discount can be found.
+    
+Example
+-------
+
+On 01/20/2014, you charged EUR 80 to a customer, with 20% tax, totalling in
+EUR 100. Further, you set up a payment term for this invoice that includes
+5% discount if paid within a week.
+
+If you fill in EUR 95 and payment date 01/22/2014 when paying the invoice, the
+following happens:
+
+- the invoice is marked as paid
+- on the invoice's 'Other info' tag, you'll find a field 'Cash discount
+  correction' with the following lines:
+
+    - EUR 1 as debit on your tax account
+    - EUR 4 as debit on your income account
+    - EUR 5 as credit on your customer's account
+
+TODO
+----
+
+- cash discounts don't show up on invoice report
+- multi currency not tested
+- no automatic reconciliation
+""",
+    'author': 'Therp BV',
+    'depends': [ 'account_voucher' ],
+    'data': [
+        'view/account_move_line_reconcile.xml',
+        'view/account_payment_term.xml',
+        'view/account_invoice.xml',
+        'security/ir.model.access.csv',
+           ],
+    'installable': True,
+    'auto_install': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

=== added directory 'account_cash_discount/i18n'
=== added file 'account_cash_discount/i18n/account_cash_discount_61.pot'
--- account_cash_discount/i18n/account_cash_discount_61.pot	1970-01-01 00:00:00 +0000
+++ account_cash_discount/i18n/account_cash_discount_61.pot	2014-01-27 15:58:21 +0000
@@ -0,0 +1,89 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+#	* account_cash_discount_61
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 6.1rc1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-01-23 22:02+0000\n"
+"PO-Revision-Date: 2012-01-23 22:02+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: account_cash_discount_61
+#: constraint:account.payment.term.line:0
+msgid "Percentages for Payment Term Line must be between 0 and 1, Example: 0.02 for 2% "
+msgstr ""
+
+#. module: account_cash_discount_61
+#: view:account.payment.term.line:0
+msgid "Discount Computation"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: constraint:account.move:0
+msgid "You can not create more than one move per period on centralized journal"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: model:ir.model,name:account_cash_discount_61.model_account_move
+msgid "Account Entry"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: help:account.payment.term.line,discount_income_account_id:0
+msgid "This account will be used to post the cash discount income"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: help:account.payment.term.line,discount_expense_account_id:0
+msgid "This account will be used to post the cash discount expense"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: model:ir.model,name:account_cash_discount_61.model_account_voucher
+msgid "Accounting Voucher"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: view:account.payment.term:0
+#: model:ir.model,name:account_cash_discount_61.model_account_payment_term
+msgid "Payment Term"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: field:account.payment.term,is_discount:0
+#: field:account.payment.term.line,is_discount:0
+msgid "Is Cash Discount"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: model:ir.model,name:account_cash_discount_61.model_account_payment_term_line
+msgid "Payment Term Line"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: help:account.payment.term,is_discount:0
+msgid "Check this box if this payment term is a cash discount. If cash discount is used the remaining amount of the invoice will not be paid"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: field:account.payment.term.line,discount_expense_account_id:0
+msgid "Discount Expense Account"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: field:account.payment.term.line,discount_income_account_id:0
+msgid "Discount Income Account"
+msgstr ""
+
+#. module: account_cash_discount_61
+#: field:account.payment.term.line,discount:0
+msgid "Discount (%)"
+msgstr ""
+

=== added file 'account_cash_discount/i18n/de.po'
--- account_cash_discount/i18n/de.po	1970-01-01 00:00:00 +0000
+++ account_cash_discount/i18n/de.po	2014-01-27 15:58:21 +0000
@@ -0,0 +1,84 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+#	* account_cash_discount_61
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 6.1rc1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-01-23 22:02+0000\n"
+"PO-Revision-Date: 2012-01-24 01:26+0000\n"
+"Last-Translator: Joël Grand-Guillaume @ CampToCamp "
+"<joel.grandguillaume@xxxxxxxxxxxxxx>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-10-23 05:13+0000\n"
+"X-Generator: Launchpad (build 16179)\n"
+
+#. module: account_cash_discount_61
+#: view:account.payment.term.line:0
+msgid "Discount Computation"
+msgstr "Skontoberechnung"
+
+#. module: account_cash_discount_61
+#: model:ir.model,name:account_cash_discount_61.model_account_move
+msgid "Account Entry"
+msgstr "Buchungssatz"
+
+#. module: account_cash_discount_61
+#: help:account.payment.term.line,discount_income_account_id:0
+msgid "This account will be used to post the cash discount income"
+msgstr "This account will be used to post the cash discount income"
+
+#. module: account_cash_discount_61
+#: help:account.payment.term.line,discount_expense_account_id:0
+msgid "This account will be used to post the cash discount expense"
+msgstr "This account will be used to post the cash discount expense"
+
+#. module: account_cash_discount_61
+#: model:ir.model,name:account_cash_discount_61.model_account_voucher
+msgid "Accounting Voucher"
+msgstr "Buchung Zahlungsbelege"
+
+#. module: account_cash_discount_61
+#: view:account.payment.term:0
+#: model:ir.model,name:account_cash_discount_61.model_account_payment_term
+msgid "Payment Term"
+msgstr "Zahlungsbedingung"
+
+#. module: account_cash_discount_61
+#: field:account.payment.term,is_discount:0
+#: field:account.payment.term.line,is_discount:0
+msgid "Is Cash Discount"
+msgstr "Skonto"
+
+#. module: account_cash_discount_61
+#: model:ir.model,name:account_cash_discount_61.model_account_payment_term_line
+msgid "Payment Term Line"
+msgstr "Zahlungsbedingungen"
+
+#. module: account_cash_discount_61
+#: help:account.payment.term,is_discount:0
+msgid ""
+"Check this box if this payment term is a cash discount. If cash discount is "
+"used the remaining amount of the invoice will not be paid"
+msgstr ""
+"Check this box if this payment term is a cash discount. If cash discount is "
+"used the remaining amount of the invoice will not be paid"
+
+#. module: account_cash_discount_61
+#: field:account.payment.term.line,discount_expense_account_id:0
+msgid "Discount Expense Account"
+msgstr "Skonto Aufwandskonto"
+
+#. module: account_cash_discount_61
+#: field:account.payment.term.line,discount_income_account_id:0
+msgid "Discount Income Account"
+msgstr "Skonto Ertragskonto"
+
+#. module: account_cash_discount_61
+#: field:account.payment.term.line,discount:0
+msgid "Discount (%)"
+msgstr "Skonto (%)"

=== added directory 'account_cash_discount/model'
=== added file 'account_cash_discount/model/__init__.py'
--- account_cash_discount/model/__init__.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/model/__init__.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    This module copyright (C) 2014 Therp BV (<http://therp.nl>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import account_voucher
+import account_invoice
+import account_payment_term
+import account_payment_term_cash_discount

=== added file 'account_cash_discount/model/account_invoice.py'
--- account_cash_discount/model/account_invoice.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/model/account_invoice.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    This module copyright (C) 2014 Therp BV (<http://therp.nl>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import datetime
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+from openerp.tools import float_round, float_is_zero,\
+        DEFAULT_SERVER_DATE_FORMAT
+
+
+class AccountInvoice(orm.Model):
+    _inherit = 'account.invoice'
+
+    _columns = {
+            'cash_discount_move_id': fields.many2one(
+                'account.move', string='Cash discount correction move'),
+            }
+
+    def create_cash_discount_move_lines(
+            self, cr, uid, ids, discount, payment_move_lines, date=None,
+            post_move=True, context=None):
+        '''create correction entries for invoices eligible to a cash discount,
+        given by the appropriate payment.term.line
+        
+        :param discount: the discount
+        :type discount: browse_record('payment.term.line')
+        :param payment_move_lines: the payment move lines to create correction
+        entries for
+        :type payment_move_lines: browse_record_list('account.move.line')
+        :param date: the date to post the move to
+        :type date: string
+
+        :returns: list of created account.move.lines' ids
+        '''
+        if date is None:
+            date = datetime.datetime.now().strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+        account_move_line = self.pool.get('account.move.line')
+        account_move = self.pool.get('account.move')
+        precision = self.pool.get('decimal.precision').precision_get(
+            cr, uid, 'Account')
+        result = []
+        for this in self.browse(cr, uid, ids, context=context):
+            discount_move_id = account_move.create(
+                    cr, uid,
+                    {
+                        'name': _('Cash discount for %s') % this.move_id.name,
+                        'period_id': self.pool.get('account.period').find(
+                            cr, uid, dt=date, context=context)[0],
+                        'journal_id': this.journal_id.id,
+                        'date': date,
+                    },
+                    context=context)
+
+            #prepare to correct potential rounding errors
+            total_debit_payment = total_credit_payment = 0
+            for payment_move_line in payment_move_lines:
+                total_debit_payment += payment_move_line.debit
+                total_credit_payment += payment_move_line.credit
+            total_debit_invoice = total_credit_invoice = 0
+            #create correction entries
+            for move_line in this.move_id.line_id:
+                line_data = self.create_cash_discount_move_line_dict(
+                    discount_move_id, move_line, discount, precision)
+                line_id = account_move_line.create(cr, uid, line_data,
+                                                   context=context)
+                result.append(line_id)
+                total_debit_payment += line_data['debit']
+                total_credit_payment += line_data['credit']
+                total_debit_invoice += move_line.debit
+                total_credit_invoice += move_line.credit
+            #add rounding error to a matching correction entry
+            if not float_is_zero(total_debit_payment - total_debit_invoice, 
+                                 precision) and\
+               not float_is_zero(total_credit_payment - total_credit_invoice,
+                                 precision):
+                error_debit = float_round(
+                    total_debit_payment - total_debit_invoice, precision)
+                error_credit = float_round(
+                    total_credit_payment - total_credit_invoice, precision)
+
+                for correction_line in account_move_line.browse(
+                        cr, uid, result, context=context):
+                    if correction_line.credit and error_credit:
+                        correction_line.write(
+                            {'credit': correction_line.credit - error_credit})
+                        error_credit = 0
+                    if correction_line.debit and error_debit:
+                        correction_line.write(
+                            {'debit': correction_line.debit - error_debit})
+                        error_debit = 0
+                    if not error_credit and not error_debit:
+                        break
+
+            this.write({'cash_discount_move_id': discount_move_id})
+            if post_move:
+                account_move.post(cr, uid, [discount_move_id], context=context)
+        return result
+
+    def create_cash_discount_move_line_dict(self, move_id, move_line,
+                                            discount, precision):
+        '''return a dict suitable to create a correction entry for specified
+        cash discount
+        
+        :param move_id: the move to append correction entries to
+        :type move: int
+        :param move_line: the move to append correction entries to
+        :type move_line: browse_record('account.move')
+        :param discount: the discount
+        :type discount: browse_record('payment.term.line')
+
+        :returns: dict that can be fed to account_move_line.create
+        '''
+
+        data = self.pool.get('account.move.line').copy_data(
+            move_line._cr, move_line._uid, move_line.id,
+            default={
+                'name': _('Cash discount for %s') % move_line.name,
+                'debit': float_round(
+                    move_line.credit * discount.discount / 100,
+                    precision),
+                'credit': float_round(
+                    move_line.debit * discount.discount / 100,
+                    precision),
+                'tax_amount': float_round(
+                    -move_line.tax_amount * discount.discount / 100,
+                    precision),
+                'amount_currency': float_round(
+                    -move_line.amount_currency * discount.discount / 100,
+                    precision),
+                'move_id': move_id,
+            },
+            context=move_line._context)
+        return data
+
+    def copy_data(self, cr, uid, id, default=None, context=None):
+        '''reset cash_discount_move_id'''
+        if default is None:
+            default = {}
+        default.setdefault('cash_discount_move_id', False)
+
+        return super(AccountInvoice, self).copy_data(
+            cr, uid, id, default=default, context=None)
+
+    def get_matching_cash_discount(self, cr, uid, ids, amount, 
+                                   payment_date=None, context=None):
+        '''return a cash discount that matches the amount paid and payment
+        date'''
+        result = False
+        for this in self.browse(cr, uid, ids, context=context):
+            if not this.payment_term or\
+                    not this.payment_term.cash_discount_ids:
+                continue
+            for discount in this.payment_term.cash_discount_ids:
+                if discount.matches(amount, this.date_invoice,
+                                    this.amount_total,
+                                    payment_date=payment_date):
+                    return discount
+        return result

=== added file 'account_cash_discount/model/account_payment_term.py'
--- account_cash_discount/model/account_payment_term.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/model/account_payment_term.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#    Copyright (C) 2012-2012 Camptocamp Austria (<http://www.camptocamp.at>)
+#    Copyright (C) 2014 Therp BV (<http://www.therp.nl>)
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import fields, orm
+from openerp.tools.translate import _
+
+
+class AccountPaymentTerm(orm.Model):
+    _inherit = "account.payment.term"
+    _columns = {
+        'cash_discount_ids': fields.one2many(
+            'account.payment.term.cash.discount', 'payment_term_id',
+            'Cash discount'),
+    }
+
+    def _check_validity(self, cr, uid, ids, context=None):
+        for this in self.browse(
+                cr, uid, ids if isinstance(ids, list) else [ids],
+                context=context):
+            if not this.cash_discount_ids:
+                continue
+            for payment_term_line in this.line_ids:
+                if payment_term_line.value == 'balance':
+                    continue
+                raise orm.except_orm(
+                    _('Error'), _('When working with cash discounts, you can '
+                                  'only have one computation line of type '
+                                  '"balance"!'))
+
+    def create(self, cr, uid, vals, context=None):
+        result = super(AccountPaymentTerm, self).create(
+            cr, uid, vals, context=context)
+        self._check_validity(cr, uid, result, context=context)
+        return result
+
+    def write(self, cr, uid, ids, vals, context=None):
+        result = super(AccountPaymentTerm, self).write(
+            cr, uid, ids, vals, context=context)
+        self._check_validity(cr, uid, ids, context=context)
+        return result

=== added file 'account_cash_discount/model/account_payment_term_cash_discount.py'
--- account_cash_discount/model/account_payment_term_cash_discount.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/model/account_payment_term_cash_discount.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    This module copyright (C) 2014 Therp BV (<http://therp.nl>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import datetime
+from openerp.osv.orm import Model
+from openerp.osv import fields
+from openerp.tools import float_compare, DEFAULT_SERVER_DATE_FORMAT
+
+class AccountPaymentTermCashDiscount(Model):
+    _name = 'account.payment.term.cash.discount'
+    _description= 'Cash discount'
+    _rec_name = 'days'
+    _order = 'days'
+
+    _columns = {
+        'payment_term_id': fields.many2one(
+            'account.payment.term', 'Payment term', required=True),
+        'days': fields.integer('Days', required=True),
+        'discount': fields.float('Discount', required=True),
+        'discount_income_account_id': fields.property(
+            'account.account', type='many2one', relation='account.account',
+            string='Discount Income Account',  view_load=True,
+            help="This account will be used to post the cash discount income"),
+        'discount_expense_account_id': fields.property(
+            'account.account', type='many2one', relation='account.account',
+            string='Discount Expense Account',  view_load=True,
+            help="This account will be used to post the cash discount expense")
+    }
+
+    def matches(self, cr, uid, ids, amount, invoice_date, invoice_amount,
+                payment_date=None, context=None):
+        '''determine if an amount paid at a certain date matches an invoiced
+        amount from a certain date
+        
+        :param amount: the amount paid
+        :type amount: float
+        :param invoice_date: the invoice's date
+        :type invoice_date: string
+        :param invoice_amount: the invoiced amount
+        :type invoice_amount: float
+        :param payment_date: the date of payment, today if None
+        :type payment_date: string        
+        '''
+        precision = self.pool.get('decimal.precision').precision_get(
+            cr, uid, 'Account')
+
+        if not payment_date:
+            payment_date = datetime.datetime.now().strftime(
+                DEFAULT_SERVER_DATE_FORMAT)
+
+        for this in self.browse(cr, uid, ids, context=context):
+            date = (
+                datetime.datetime.strptime(
+                    invoice_date, DEFAULT_SERVER_DATE_FORMAT) +
+                datetime.timedelta(days=this.days))\
+                .strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+            return date >= payment_date and float_compare(
+                amount,
+                (1 - this.discount / 100) * invoice_amount,
+                precision) == 0

=== added file 'account_cash_discount/model/account_voucher.py'
--- account_cash_discount/model/account_voucher.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/model/account_voucher.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#    Copyright (C) 2012-2012 Camptocamp Austria (<http://www.camptocamp.at>)
+#    Copyright (C) 2014 Therp BV (<http://www.therp.nl>)
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv import fields, orm
+
+
+class AccountVoucher(orm.Model):
+    _inherit = 'account.voucher'
+
+    def voucher_move_line_create(
+            self, cr, uid, voucher_id, line_total, move_id, company_currency,
+            current_currency, context=None):
+        total, ids_list = super(AccountVoucher, self).voucher_move_line_create(
+                cr, uid, voucher_id, line_total, move_id, company_currency,
+                current_currency)
+        '''add correction entries to payment if the payment amount matches a
+        cash discount amount and is in time for that'''
+
+        account_move_line = self.pool.get('account.move.line')
+
+        precision = self.pool.get('decimal.precision').precision_get(
+                cr, uid, 'Account')
+        voucher = self.browse(cr, uid, voucher_id, context=context)
+        move = self.pool.get('account.move').browse(
+                cr, uid, move_id, context=context)
+
+        for ids in ids_list:
+            move_lines = account_move_line.browse(cr, uid, ids, 
+                                                  context=context)
+            for move_line in move_lines:
+                #only act on move liness with invoices whose payment term is a
+                #cash discount
+                if move_line.invoice and move_line.invoice.payment_term and\
+                        move_line.invoice.payment_term.cash_discount_ids:
+
+                    #find a cash discount that matches current payment
+                    discount = move_line.invoice.get_matching_cash_discount(
+                        voucher.amount, payment_date=voucher.date)
+                    if not discount:
+                        continue
+
+                    discount_move_line_ids = move_line.invoice\
+                            .create_cash_discount_move_lines(
+                                payment_move_lines=move.line_id,
+                                discount=discount, date=move.date)
+
+                    #put correction entries into list with matching
+                    #original ones to have them reconciled at once
+                    for discount_move_line in account_move_line.browse(
+                            cr, uid, discount_move_line_ids, context=context):
+                        if discount_move_line.account_id == \
+                                move_line.account_id:
+                            ids.append(discount_move_line.id)
+
+        return total, ids_list

=== added directory 'account_cash_discount/security'
=== added file 'account_cash_discount/security/ir.model.access.csv'
--- account_cash_discount/security/ir.model.access.csv	1970-01-01 00:00:00 +0000
+++ account_cash_discount/security/ir.model.access.csv	2014-01-27 15:58:21 +0000
@@ -0,0 +1,3 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+crud_cash_discount_line,"CRUD cash discount",model_account_payment_term_cash_discount,account.group_account_manager,1,1,1,1
+r_cash_discount_line,"R cash discount",model_account_payment_term_cash_discount,account.group_account_user,1,0,0,0

=== added directory 'account_cash_discount/view'
=== added file 'account_cash_discount/view/account_invoice.xml'
--- account_cash_discount/view/account_invoice.xml	1970-01-01 00:00:00 +0000
+++ account_cash_discount/view/account_invoice.xml	2014-01-27 15:58:21 +0000
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+    <data>
+        <record id="invoice_form" model="ir.ui.view">
+            <field name="model">account.invoice</field>
+            <field name="inherit_id" ref="account.invoice_form" />
+            <field name="arch" type="xml">
+                <data>
+                    <field name="move_id" position="after">
+                        <field name="cash_discount_move_id"
+                            groups="account.group_account_user"
+                            readonly="1"
+                            attrs="{'invisible': [('cash_discount_move_id', '=', False)]}"/>
+                    </field>
+                    <field name="residual" position="after">
+                        <div attrs="{'invisible': [('cash_discount_move_id', '=', False)]}" colspan="2">
+                            Note: This invoice was paid with a cash discount as detailed in the cash discount correction move on the 'Other info' tab
+                        </div>
+                    </field>
+                </data>
+            </field>
+        </record>
+    </data>
+</openerp>

=== added file 'account_cash_discount/view/account_move_line_reconcile.xml'
--- account_cash_discount/view/account_move_line_reconcile.xml	1970-01-01 00:00:00 +0000
+++ account_cash_discount/view/account_move_line_reconcile.xml	2014-01-27 15:58:21 +0000
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+    <data>
+        <record id="view_account_move_line_reconcile_full" model="ir.ui.view">
+            <field name="model">account.move.line.reconcile</field>
+            <field name="inherit_id" ref="account.view_account_move_line_reconcile_full" />
+            <field name="arch" type="xml">
+                <data>
+                    <footer position="before">
+                        <group attrs="{'invisible': [('invoice_has_cash_discount', '=', False)]}">
+                            <field name="invoice_id" readonly="1" />
+                            <field name="invoice_has_cash_discount" invisible="1" />
+                        </group>
+                    </footer>
+                    <button string="Reconcile" position="after">
+                        <button string="Book difference as cash discount"
+                            type="object"
+                            name="reconcile_with_cash_discount"
+                            attrs="{'invisible': [('invoice_has_cash_discount', '=', False)]}"
+                            />
+                    </button>
+                </data>
+            </field>
+        </record>
+    </data>
+</openerp>

=== added file 'account_cash_discount/view/account_payment_term.xml'
--- account_cash_discount/view/account_payment_term.xml	1970-01-01 00:00:00 +0000
+++ account_cash_discount/view/account_payment_term.xml	2014-01-27 15:58:21 +0000
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data>
+
+        <record id="view_payment_term_disc_tree" model="ir.ui.view">
+            <field name="name">account.payment.term.disc.tree</field>
+            <field name="model">account.payment.term</field>
+            <field name="type">tree</field>
+            <field name="arch" type="xml">
+                <tree string="Payment Term">
+                    <field name="name"/>
+                    <field name="active"/>
+                </tree>
+            </field>
+        </record>
+
+
+        <record id="view_payment_term_disc_form" model="ir.ui.view">
+            <field name="name">account.payment.term.disc.form</field>
+            <field name="model">account.payment.term</field>
+            <field name="inherit_id" ref="account.view_payment_term_form"/>
+            <field name="type">form</field>
+            <field name="arch" type="xml">
+                <field name="line_ids" position="after">
+                    <div class="oe_horizontal_separator oe_clear"><label for="cash_discount_ids" /></div>
+                    <field name="cash_discount_ids">
+                        <tree>
+                            <field name="days" />
+                            <field name="discount" />
+                        </tree>
+                        <form>
+                            <field name="days" />
+                            <field name="discount" />
+                            <field name="discount_expense_account_id" />
+                            <field name="discount_income_account_id" />
+                        </form>
+                    </field>
+                </field>
+            </field>
+        </record>
+    </data>
+</openerp>
+ 

=== added directory 'account_cash_discount/wizard'
=== added file 'account_cash_discount/wizard/__init__.py'
--- account_cash_discount/wizard/__init__.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/wizard/__init__.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    This module copyright (C) 2014 Therp BV (<http://therp.nl>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import account_move_line_reconcile
+import account_move_line_reconcile

=== added file 'account_cash_discount/wizard/account_move_line_reconcile.py'
--- account_cash_discount/wizard/account_move_line_reconcile.py	1970-01-01 00:00:00 +0000
+++ account_cash_discount/wizard/account_move_line_reconcile.py	2014-01-27 15:58:21 +0000
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    This module copyright (C) 2014 Therp BV (<http://therp.nl>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv.orm import Model
+from openerp.osv import fields
+
+class AccountMoveLineReconcile(Model):
+    _inherit = 'account.move.line.reconcile'
+
+    _columns = {
+        'invoice_id': fields.many2one('account.invoice', 'Invoice'),
+        'invoice_has_cash_discount': fields.boolean('Cash discount on invoice')
+    }
+
+    def default_get(self, cr, uid, fields, context=None):
+        result = super(AccountMoveLineReconcile, self).default_get(
+            cr, uid, fields, context=context)
+        for move_line in self.pool.get('account.move.line').browse(
+                cr, uid, context.get('active_ids', []), context=context):
+            if move_line.invoice:
+                if move_line.invoice.id != result.get('invoice_id'):
+                    result['invoice_id'] = move_line.invoice.id
+                    result['invoice_has_cash_discount'] = bool(
+                        move_line.invoice.get_matching_cash_discount(
+                            result.get('credit')) or
+                        move_line.invoice.get_matching_cash_discount(
+                            result.get('debit')))
+                else:
+                    result['invoice_id'] = False
+                    result['invoice_has_cash_discount'] = False
+        return result
+
+    def reconcile_with_cash_discount(self, cr, uid, ids, context=None):
+        account_move_line = self.pool.get('account.move.line')
+        for this in self.browse(cr, uid, ids, context=context):
+            discount = this.invoice_id.get_matching_cash_discount(this.credit)\
+                    or this.invoice_id.get_matching_cash_discount(this.debit)
+            if not discount:
+                continue
+            payment_move_lines = account_move_line.browse(
+                cr, uid, context.get('active_ids', []), context=context)
+            correction_move_line_ids = this.invoice_id\
+                    .create_cash_discount_move_lines(discount,
+                                                     payment_move_lines)
+            reconcile_ids = [l.id for l in payment_move_lines]
+            for correction_move_line in account_move_line.browse(
+                    cr, uid, correction_move_line_ids, context=context):
+                for payment_move_line in payment_move_lines:
+                    if payment_move_line.account_id ==\
+                            correction_move_line.account_id:
+                        reconcile_ids.append(correction_move_line.id)
+                        break
+            return self.trans_rec_reconcile_full(
+                cr, uid, [this.id], dict(context, active_ids=reconcile_ids))
+        pass


Follow ups