Merge pull request #67 from guewen/8.0-account_statement_operation_rule

Add module account_statement_operation_rule
This commit is contained in:
Pedro M. Baeza
2016-04-19 13:37:12 +02:00
28 changed files with 1829 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License
Bank Statement Operation Rules
==============================
This module complements the Reconciliation of the bank statements. When
the bank statement matches one or more journal entry for a line and
there is a remaining balance, Odoo proposes you to click on buttons that
will generate write-off entries according to pre-configured *Statement
Operation Templates*. The aim of this module is to automatically click
for you on these buttons (i.e. create the write-off journal entries)
when some rules are respected, rules that you can configure.
It contains 2 types of rules (but can be extended with additional rules),
described below:
Roundings
The most basic rule: when the remaining balance is within a range, 1
or more operations are applied.
Currencies
When the remaining balance is within a range and the currency of all
the lines is the same but different from the company's, and the amount
currency is the same, 1 or more operations are applied.
Configuration
-------------
As this module aims to automatize the ``Statement Operation Templates``,
you first want to ensure that you have at least one operation configured.
You can find them in ``Invoicing > Configuration > Miscellaneous >
Statement Operation Templates``. An example of a common operation is:
=================== ========================== ======= ========
Account Amount Type Amount Label
=================== ========================== ======= ========
Depends of the l10n Percentage of open balance 100.0 % Rounding
=================== ========================== ======= ========
The configuration of the rules themselves happens in ``Invoicing >
Configuration > Miscellaneous > Statement Operation Rules``. Refer to
the description of the types of rules above in case of doubt. The form
is divided in 2 parts: **Rule** and **Result**. The rule part is where
you will set the conditions and the result part is what operations will
be done if the conditions are valid.
For the **Roundings** rules, you will set a min. and a max. amount. It
can be negative or positive. The amount is compared to the remaining
balance when lines are matched in the bank statement. Example: if you
want to create a move line in a loss account when you received 1.- not
enough, you can create a rule with an min. amount of -1.0 and a max.
amount of 0.0.
For the **Currencies** rules, the min. and max. amount have the same
properties, but you will also set the currencies for which the rule
applies. Setting the currency allows to configure different amounts
according to the currencies.
Only the first rule matching the current situation is used, so if you
have several rules overlapping for some reason, be sure to order them
appropriately in the list view.
Usage
-----
When you use the *Reconcile* button of a bank statement, Odoo
automatically proposes you matching journal entries for each statement
line. This module automatically adds journal entries generated from the
*Statement Operation Templates* if a rule matches with the current
situation, so there is nothing special to do once the rules are
configured.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/98/8.0
Credits
=======
Contributors
------------
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
Maintainer
----------
.. image:: http://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: http://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization
whose mission is to support the collaborative development of Odoo
features and promote its widespread use.
To contribute to this module, please visit
http://odoo-community.org.

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import model

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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': 'Bank Statement Operation Rules',
'version': '8.0.1.0.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'license': 'AGPL-3',
'category': 'Accounting & Finance',
'depends': ['account',
],
'website': 'http://www.camptocamp.com',
'data': ['view/account_statement_operation_rule.xml',
'view/account_statement_operation_rule_view.xml',
'security/ir.model.access.csv',
],
'installable': True,
'auto_install': False,
}

View File

@@ -0,0 +1,152 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_operation_rule
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-03-16 14:46+0000\n"
"PO-Revision-Date: 2015-03-16 14:46+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_statement_operation_rule
#: model:ir.actions.act_window,help:account_statement_operation_rule.action_account_statement_operation_rule
msgid "<p class=\"oe_view_nocontent_create\">\n"
" Click to create a statement operation rule.\n"
" </p><p>\n"
" Those can be used to automatically create a move line when reconciling\n"
" your bank statements.\n"
" </p>\n"
" "
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "And"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "And the currency is one of"
msgstr ""
#. module: account_statement_operation_rule
#: model:ir.model,name:account_statement_operation_rule.model_account_bank_statement_line
msgid "Bank Statement Line"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,create_uid:0
msgid "Created by"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,create_date:0
msgid "Created on"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,currencies:0
#: selection:account.statement.operation.rule,rule_type:0
msgid "Currencies"
msgstr ""
#. module: account_statement_operation_rule
#: help:account.statement.operation.rule,currencies:0
msgid "For 'Currencies' rules, you can choose for which currencies the rule will be applicable."
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,id:0
msgid "ID"
msgstr ""
#. module: account_statement_operation_rule
#: help:account.statement.operation.rule,sequence:0
msgid "If several rules match, the first one is used."
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,write_uid:0
msgid "Last Updated by"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,write_date:0
msgid "Last Updated on"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,amount_max:0
msgid "Max. Amount"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,amount_min:0
msgid "Min. Amount"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,name:0
msgid "Name"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,operations:0
msgid "Operations"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Result"
msgstr ""
#. module: account_statement_operation_rule
#: selection:account.statement.operation.rule,rule_type:0
msgid "Roundings"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Rule"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,sequence:0
msgid "Sequence"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Statement Operation Rule"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_search
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_tree
#: model:ir.actions.act_window,name:account_statement_operation_rule.action_account_statement_operation_rule
#: model:ir.ui.menu,name:account_statement_operation_rule.menu_action_account_statement_operation_rule
msgid "Statement Operation Rules"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Then the following operations will be applied:"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,rule_type:0
msgid "Type"
msgstr ""
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "When the balance is between"
msgstr ""

View File

@@ -0,0 +1,158 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_operation_rule
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-03-16 14:46+0000\n"
"PO-Revision-Date: 2015-03-16 14:46+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_statement_operation_rule
#: model:ir.actions.act_window,help:account_statement_operation_rule.action_account_statement_operation_rule
msgid "<p class=\"oe_view_nocontent_create\">\n"
" Click to create a statement operation rule.\n"
" </p><p>\n"
" Those can be used to automatically create a move line when reconciling\n"
" your bank statements.\n"
" </p>\n"
" "
msgstr "<p class=\"oe_view_nocontent_create\">\n"
" Cliquer pour créer une nouvelle règle d'opération relevé.\n"
" </p><p>\n"
" Elles peuvent être utilisées pour automatiser la création de lignes "
" quand vous réconciliez des relevés bancaires."
" </p>\n"
" "
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "And"
msgstr "Et"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "And the currency is one of"
msgstr "Et la devise est une des suivantes"
#. module: account_statement_operation_rule
#: model:ir.model,name:account_statement_operation_rule.model_account_bank_statement_line
msgid "Bank Statement Line"
msgstr "Ligne de relevé bancaire"
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,create_uid:0
msgid "Created by"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,create_date:0
msgid "Created on"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,currencies:0
#: selection:account.statement.operation.rule,rule_type:0
msgid "Currencies"
msgstr "Devises"
#. module: account_statement_operation_rule
#: help:account.statement.operation.rule,currencies:0
msgid "For 'Currencies' rules, you can choose for which currencies the rule will be applicable."
msgstr "Pour les règles 'Devises', vous pouvez sélectionner les devises pour lesquelles la règle s'applique."
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,id:0
msgid "ID"
msgstr ""
#. module: account_statement_operation_rule
#: help:account.statement.operation.rule,sequence:0
msgid "If several rules match, the first one is used."
msgstr "Si plusieurs règles correspondent, la première est utilisée."
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,write_uid:0
msgid "Last Updated by"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,write_date:0
msgid "Last Updated on"
msgstr ""
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,amount_max:0
msgid "Max. Amount"
msgstr "Montant max."
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,amount_min:0
msgid "Min. Amount"
msgstr "Montant min."
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,name:0
msgid "Name"
msgstr "Nom"
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,operations:0
msgid "Operations"
msgstr "Opérations"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Result"
msgstr "Résultat"
#. module: account_statement_operation_rule
#: selection:account.statement.operation.rule,rule_type:0
msgid "Roundings"
msgstr "Arrondis"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Rule"
msgstr "Règle"
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,sequence:0
msgid "Sequence"
msgstr "Séquence"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Statement Operation Rule"
msgstr "Règle d'opération de relevé"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_search
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_tree
#: model:ir.actions.act_window,name:account_statement_operation_rule.action_account_statement_operation_rule
#: model:ir.ui.menu,name:account_statement_operation_rule.menu_action_account_statement_operation_rule
msgid "Statement Operation Rules"
msgstr "Règles d'opération de relevé"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "Then the following operations will be applied:"
msgstr "Alors l'opération suivant sera appliquée :"
#. module: account_statement_operation_rule
#: field:account.statement.operation.rule,rule_type:0
msgid "Type"
msgstr "Type"
#. module: account_statement_operation_rule
#: view:account.statement.operation.rule:account_statement_operation_rule.view_account_statement_operation_rule_form
msgid "When the balance is between"
msgstr "Quand la balance est entre"

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import account_statement_operation_rule
from . import account_statement_line

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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 import models, api
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
@api.multi
def currency_for_rules(self):
return self.currency_id or self.statement_id.currency

View File

@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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 import models, fields, api
from openerp.addons import decimal_precision as dp
class AccountStatementOperationRule(models.Model):
_name = 'account.statement.operation.rule'
_order = 'sequence ASC, id ASC'
name = fields.Char()
rule_type = fields.Selection(
selection=[('rounding', 'Roundings'),
('currency', 'Currencies')],
string='Type',
default='rounding',
required=True,
)
operations = fields.Many2many(
comodel_name='account.statement.operation.template',
relation='account_statement_oper_rule_rel',
)
amount_min = fields.Float(
string='Min. Amount',
digits=dp.get_precision('Account'),
)
amount_max = fields.Float(
string='Max. Amount',
digits=dp.get_precision('Account'),
)
currencies = fields.Many2many(
comodel_name='res.currency',
string='Currencies',
help="For 'Currencies' rules, you can choose for which currencies "
"the rule will be applicable.",
)
sequence = fields.Integer(
default=20,
help="If several rules match, the first one is used.",
)
@staticmethod
def _between_with_bounds(low, value, high, currency):
""" Equivalent to a three way comparison: ``min <= value <= high``
The comparisons are done with the currency to use the correct
precision.
"""
if currency.compare_amounts(value, low) == -1:
return False
if currency.compare_amounts(value, high) == 1:
return False
return True
@api.multi
def _balance_in_range(self, balance, currency):
amount_min = self.amount_min
amount_max = self.amount_max
return self._between_with_bounds(amount_min, balance,
amount_max, currency)
@api.model
def _is_multicurrency(self, statement_line):
currency = statement_line.currency_for_rules()
company_currency = statement_line.company_id.currency_id
return currency != company_currency
@api.multi
def _is_valid_balance(self, statement_line, move_lines, balance):
if self._is_multicurrency(statement_line):
return False
currency = statement_line.currency_for_rules()
return self._balance_in_range(balance, currency)
@api.multi
def _is_valid_multicurrency(self, statement_line, move_lines, balance):
""" Check if the multi-currency rule can be applied
The rule is applied if and only if:
* The currency is not company's one
* The currency of the statement line and all the lines is the same
* The balance of the amount currencies is 0
* The balance is between the bounds configured on the rule
"""
if not self._is_multicurrency(statement_line):
return False
currency = statement_line.currency_for_rules()
if currency not in self.currencies:
return False
amount_currency = statement_line.amount_currency
for move_line in move_lines:
if move_line.currency_id != statement_line.currency_id:
# use case not supported, no rule found
return False
amount_currency -= move_line.amount_currency
# amount in currency is the same, so the balance is
# a difference due to currency rates
if statement_line.currency_id.is_zero(amount_currency):
return self._balance_in_range(balance, currency)
return False
@api.multi
def is_valid(self, statement_line, move_lines, balance):
""" Returns True if a rule applies to a group of statement_line +
move lines.
This is the public method where the rule is evaluated whatever
its type is. When a rule returns True, it means that it is a
candidate for the current reconciliation. The rule with the lowest
number in the ``sequence`` field is chosen.
:param statement_line: the line to reconcile
:param move_lines: the selected move lines for reconciliation
:param balance: the balance between the statement_line and the
move_lines. It could be computed here but it is
computed before to avoid to compute it for each
rule when called on multiple rules.
"""
self.ensure_one()
if self.rule_type == 'rounding':
return self._is_valid_balance(statement_line, move_lines, balance)
elif self.rule_type == 'currency':
return self._is_valid_multicurrency(statement_line,
move_lines,
balance)
@api.model
def find_first_rule(self, statement_line, move_lines):
""" Find the rules that apply to a statement line and
a selection of move lines.
:param statement_line: the line to reconcile
:param move_lines: the selected move lines for reconciliation
"""
balance = statement_line.amount
for move_line in move_lines:
balance += move_line.credit - move_line.debit
currency = statement_line.currency_for_rules()
if currency.is_zero(balance):
return self.browse()
rules = self.search([])
# return the first applicable rule
for rule in rules:
if rule.is_valid(statement_line, move_lines, balance):
return rule
return self.browse()
@api.model
@api.returns('account.statement.operation.template')
def operations_for_reconciliation(self, statement_line_id, move_line_ids):
""" Find the rule for the current reconciliation and returns the
``account.statement.operation.template`` of the found rule.
Called from the javascript reconciliation view.
"""
line_obj = self.env['account.bank.statement.line']
move_line_obj = self.env['account.move.line']
statement_line = line_obj.browse(statement_line_id)
move_lines = move_line_obj.browse(move_line_ids)
rules = self.find_first_rule(statement_line, move_lines)
return rules.operations

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_statement_operation_rule,account.statement.operation.rule,model_account_statement_operation_rule,account.group_account_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_statement_operation_rule account.statement.operation.rule model_account_statement_operation_rule account.group_account_user 1 1 1 1

View File

@@ -0,0 +1,37 @@
openerp.account_statement_operation_rule = function (instance) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
instance.web.account_statement_operation_rule = instance.web.account_statement_operation_rule || {};
instance.web.account.bankStatementReconciliationLine.include({
operation_rules: function() {
var self = this;
var model_operation_rule = new instance.web.Model("account.statement.operation.rule");
model_operation_rule.call("operations_for_reconciliation",
[self.st_line.id,
_.pluck(self.get("mv_lines_selected"), 'id')])
.then(function (operations) {
_.each(operations, function(operation_id) {
preset_btn = self.$("button.preset[data-presetid='" + operation_id + "']");
preset_btn.click();
self.addLineBeingEdited();
});
});
},
render: function() {
deferred = this._super();
if (deferred) {
deferred.done(this.operation_rules());
}
return deferred;
},
restart: function() {
deferred = this._super();
deferred.done(this.operation_rules());
return deferred;
},
});
};

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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 . import test_rule_rounding
from . import test_rule_currency

View File

@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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/>.
#
##############################################################################
def prepare_statement(test, difference,
statement_line_currency=None,
move_line_currency=None,
amount_currency_difference=0):
""" Prepare a bank statement line and a move line
The difference is applied on the bank statement line relatively to
the move line.
"""
amount = 100
amount_currency = 120
statement_obj = test.env['account.bank.statement']
statement_line_obj = test.env['account.bank.statement.line']
move_obj = test.env['account.move']
move_line_obj = test.env['account.move.line']
statement = statement_obj.create({
'name': '/',
'journal_id': test.ref('account.cash_journal')
})
line_vals = {
'name': '001',
'amount': amount + difference,
'statement_id': statement.id,
}
if statement_line_currency:
line_vals.update({
'currency_id': statement_line_currency.id,
'amount_currency': amount_currency + amount_currency_difference,
})
statement_line = statement_line_obj.create(line_vals)
move = move_obj.create({
'journal_id': test.ref('account.sales_journal')
})
line_vals = {
'move_id': move.id,
'name': '001',
'account_id': test.ref('account.a_recv'),
'debit': amount,
}
if move_line_currency:
line_vals.update({
'currency_id': move_line_currency.id,
'amount_currency': amount_currency,
})
move_line = move_line_obj.create(line_vals)
line_vals = {
'move_id': move.id,
'name': '001',
'account_id': test.ref('account.a_sale'),
'credit': amount,
}
if move_line_currency:
line_vals['currency_id'] = move_line_currency.id
move_line_obj.create(line_vals)
return statement_line, move_line

View File

@@ -0,0 +1,164 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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.tests import common
from .common import prepare_statement
class TestRuleCurrency(common.TransactionCase):
def setUp(self):
super(TestRuleCurrency, self).setUp()
self.operation_obj = self.env['account.statement.operation.template']
self.rule_obj = self.env['account.statement.operation.rule']
self.aed = self.browse_ref('base.AED')
self.afn = self.browse_ref('base.AFN')
self.all = self.browse_ref('base.ALL')
self.amd = self.browse_ref('base.AMD')
self.aoa = self.browse_ref('base.AOA')
self.operation_currency_1 = self.operation_obj.create({
'name': 'Currency AED, AFR, ALL -1.0 to 0.0',
'label': 'Currency',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_currency_1 = self.rule_obj.create({
'name': 'Currency AED, AFR, ALL -1.0 to 0.0',
'rule_type': 'currency',
'operations': [(6, 0, (self.operation_currency_1.id, ))],
'amount_min': -1.0,
'amount_max': 0,
'sequence': 1,
'currencies': [(6, 0, [self.aed.id, self.afn.id, self.all.id])],
})
self.operation_currency_2 = self.operation_obj.create({
'name': 'Currency AED, AFR, ALL -2.0 to -1.0',
'label': 'Currency',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_currency_2 = self.rule_obj.create({
'name': 'Currency AED, AFR, ALL -2.0 to 1.0',
'rule_type': 'currency',
'operations': [(6, 0, (self.operation_currency_2.id, ))],
'amount_min': -2.0,
'amount_max': -1.0,
'sequence': 2,
'currencies': [(6, 0, [self.aed.id, self.afn.id, self.all.id])],
})
self.operation_currency_3 = self.operation_obj.create({
'name': 'Currency AMD, AOA -2.0 to 0.0',
'label': 'Currency',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_currency_3 = self.rule_obj.create({
'name': 'Currency AMD, AOA -2.0 to 0.0',
'rule_type': 'currency',
'operations': [(6, 0, (self.operation_currency_3.id, ))],
'amount_min': -2,
'amount_max': 0,
'sequence': 2,
'currencies': [(6, 0, [self.amd.id, self.aoa.id])],
})
def test_no_currency_match(self):
"""No rules for the current currency"""
sek = self.browse_ref('base.SEK')
statement_line, move_line = prepare_statement(
self, -0.5,
statement_line_currency=sek,
move_line_currency=sek)
ops = self.rule_obj.operations_for_reconciliation(statement_line.id,
move_line.ids)
self.assertFalse(ops)
def test_rounding_lines(self):
"""No Currencies rules on lines with company currency"""
statement_line, move_line = prepare_statement(self, -0.5)
ops = self.rule_obj.operations_for_reconciliation(statement_line.id,
move_line.ids)
self.assertFalse(ops)
def test_currency_rule_1(self):
"""Rule 1 is found with -0.5 AED"""
statement_line, move_line = prepare_statement(
self, -0.5,
statement_line_currency=self.aed,
move_line_currency=self.aed,
amount_currency_difference=0)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_currency_1)
def test_currency_rule_2(self):
"""Rule 2 is found with -2 AED"""
statement_line, move_line = prepare_statement(
self, -2,
statement_line_currency=self.aed,
move_line_currency=self.aed,
amount_currency_difference=0)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_currency_2)
def test_currency_rule_3(self):
"""Rule 3 is found with -2 AOA"""
statement_line, move_line = prepare_statement(
self, -2,
statement_line_currency=self.aoa,
move_line_currency=self.aoa,
amount_currency_difference=0)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_currency_3)
def test_currency_rule_not_in_bounds(self):
"""No rule is found with -3 AOA"""
statement_line, move_line = prepare_statement(
self, -3,
statement_line_currency=self.aoa,
move_line_currency=self.aoa,
amount_currency_difference=0)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_no_rule_amount_currency_different(self):
"""No rule when amount currency is different"""
statement_line, move_line = prepare_statement(
self, -0.5,
statement_line_currency=self.aed,
move_line_currency=self.aed,
amount_currency_difference=0.5)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_rule_amount_currency_difference_near_zero(self):
"""Apply the rule when the difference is near 0"""
statement_line, move_line = prepare_statement(
self, -0.5,
statement_line_currency=self.aed,
move_line_currency=self.aed,
amount_currency_difference=-0.001)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_currency_1)

View File

@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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.tests import common
from .common import prepare_statement
class TestRuleRounding(common.TransactionCase):
def setUp(self):
super(TestRuleRounding, self).setUp()
self.operation_obj = self.env['account.statement.operation.template']
self.rule_obj = self.env['account.statement.operation.rule']
self.operation_round_1 = self.operation_obj.create({
'name': 'Rounding -1.0 to 0.0',
'label': 'Rounding',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_round_1 = self.rule_obj.create({
'name': 'Rounding -1.0 to 0.0',
'rule_type': 'rounding',
'operations': [(6, 0, (self.operation_round_1.id, ))],
'amount_min': -1.0,
'amount_max': 0,
'sequence': 1,
})
self.operation_round_2 = self.operation_obj.create({
'name': 'Rounding -2.0 to -1.0',
'label': 'Rounding',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_round_2 = self.rule_obj.create({
'name': 'Rounding -1.0 to 0.0',
'rule_type': 'rounding',
'operations': [(6, 0, (self.operation_round_2.id, ))],
'amount_min': -2.0,
'amount_max': -1.0,
'sequence': 2,
})
self.operation_round_3 = self.operation_obj.create({
'name': 'Rounding 0.0 to 2.0',
'label': 'Rounding',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_round_3 = self.rule_obj.create({
'name': 'Rounding 0.0 to 2.0',
'rule_type': 'rounding',
'operations': [(6, 0, (self.operation_round_3.id, ))],
'amount_min': 0,
'amount_max': 2,
'sequence': 2,
})
def test_rule_round_1(self):
"""-0.5 => rule round 1"""
statement_line, move_line = prepare_statement(self, -0.5)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_1)
def test_rule_round_1_limit(self):
"""-1 => rule round 1"""
statement_line, move_line = prepare_statement(self, -1)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_1)
def test_rule_round_1_near_limit(self):
"""-1.0001 => rule round 1"""
statement_line, move_line = prepare_statement(self, -1.0001)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_1)
def test_rule_round_2(self):
"""-1.01 => rule round 2"""
statement_line, move_line = prepare_statement(self, -1.01)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_2)
def test_rule_round_2_limit(self):
"""-2 => rule round 2"""
statement_line, move_line = prepare_statement(self, -2)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_2)
def test_rule_round_3(self):
"""+1.5 => rule round 3"""
statement_line, move_line = prepare_statement(self, 1.5)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_3)
def test_rule_round_3_limit(self):
"""+2 => rule round 3"""
statement_line, move_line = prepare_statement(self, 2)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_3)
def test_rule_no_round_below(self):
"""-3 => no rule"""
statement_line, move_line = prepare_statement(self, -3)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_rule_no_round_above(self):
"""+3 => no rule"""
statement_line, move_line = prepare_statement(self, 3)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_rule_no_round_zero(self):
"""0 => no rule"""
statement_line, move_line = prepare_statement(self, 0)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_rule_no_round_near_zero(self):
"""0.0001 => no rule"""
statement_line, move_line = prepare_statement(self, 0.0001)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_operations(self):
"""test operations_for_reconciliation()"""
statement_line, move_line = prepare_statement(self, -0.5)
ops = self.rule_obj.operations_for_reconciliation(statement_line.id,
move_line.ids)
self.assertEquals(ops, self.operation_round_1)
def test_multicurrency_lines(self):
"""No rounding rules on multi-currency lines"""
currency = self.browse_ref('base.AED')
statement_line, move_line = prepare_statement(
self,
-0.5,
statement_line_currency=currency,
move_line_currency=currency
)
ops = self.rule_obj.operations_for_reconciliation(statement_line.id,
move_line.ids)
self.assertFalse(ops)

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets_backend" name="account assets" inherit_id="account.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/account_statement_operation_rule/static/src/js/account_widgets.js"></script>
</xpath>
</template>
</data>
</openerp>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_account_statement_operation_rule_form" model="ir.ui.view">
<field name="name">account.statement.operation.rule.form</field>
<field name="model">account.statement.operation.rule</field>
<field name="arch" type="xml">
<form string="Statement Operation Rule">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
</div>
<group name="options">
<field name="rule_type"/>
</group>
<group name="rule" string="Rule">
<group name="amount" attrs="{'invisible': [('rule_type', 'not in', ('rounding', 'currency'))]}">
<label for="amount" string="When the balance is between"/>
<div>
<field name="amount_min" class="oe_inline" /> And
<field name="amount_max" class="oe_inline" />
</div>
<label for="currencies" string="And the currency is one of" attrs="{'invisible': [('rule_type', '!=', 'currency')]}"/>
<div attrs="{'invisible': [('rule_type', '!=', 'currency')]}">
<field name="currencies" class="oe_inline" widget="many2many_tags"/>
</div>
</group>
</group>
<group name="operations" string="Result">
<label for="operations" string="Then the following operations will be applied:" colspan="2"/>
<field name="operations" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_account_statement_operation_rule_tree" model="ir.ui.view">
<field name="name">account.statement.operation.rule.tree</field>
<field name="model">account.statement.operation.rule</field>
<field name="arch" type="xml">
<tree string="Statement Operation Rules">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="rule_type"/>
<field name="amount_min"/>
<field name="amount_max"/>
<field name="operations"/>
</tree>
</field>
</record>
<record id="view_account_statement_operation_rule_search" model="ir.ui.view">
<field name="name">account.statement.operation.rule.search</field>
<field name="model">account.statement.operation.rule</field>
<field name="arch" type="xml">
<search string="Statement Operation Rules">
<field name="name"/>
<field name="rule_type"/>
<field name="amount_min"/>
<field name="amount_max"/>
<field name="operations"/>
</search>
</field>
</record>
<record id="action_account_statement_operation_rule" model="ir.actions.act_window">
<field name="name">Statement Operation Rules</field>
<field name="res_model">account.statement.operation.rule</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_account_statement_operation_rule_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a statement operation rule.
</p><p>
Those can be used to automatically create a move line when reconciling
your bank statements.
</p>
</field>
</record>
<menuitem action="action_account_statement_operation_rule"
id="menu_action_account_statement_operation_rule"
parent="account.menu_configuration_misc"
name="Statement Operation Rules"
sequence="22"/>
</data>
</openerp>

View File

@@ -0,0 +1,85 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License
Bank Statement Operation Rules with Dunning Fees
================================================
Extends the *Bank Statement Operation Rules* with a new rule, the
**Dunning Fees** rule. It allows to automatically create a write-off
entry for the amount paid by the customers when they received dunning
fees (using the **Account Credit Control** module).
Configuration
-------------
As this module aims to automatize the ``Statement Operation Templates``,
you first want to ensure that you have an operation configured for the
dunning fees.
You can find them in ``Invoicing > Configuration > Miscellaneous >
Statement Operation Templates``. An example of operation is (the account
is where the amount received for the dunning fees will be input):
=================== ========================== ======= ============
Account Amount Type Amount Label
=================== ========================== ======= ============
Depends of the l10n Percentage of open balance 100.0 % Dunning Fees
=================== ========================== ======= ============
The configuration of the rules themselves happens in ``Invoicing >
Configuration > Miscellaneous > Statement Operation Rules``.
There is no conditions to setup on this rule. It will be applied if the
amount in the bank statement line is above the journal entries amount
and if the difference is comprised in the amount of the dunning fees for
the journal entries.
Example:
======================= ======
Document Amount
======================= ======
Journal Entry (invoice) 100.-
Dunning Fees no1 5.-
Dunning Fees no2 10.-
Dunning Fees no3 15.-
======================= ======
The customer received 3 times dunning fees, with a increasing amount.
The customer might pay from 100.- to 115.-. The difference between
100.- and what the customer paid above goes to the write-off account
configured on the operation. If the customer pays 99.- or 116.-, the
Dunning Fees rule is not valid and the other rules will be evaluated.
.. note:: The Dunning Fees rule must be placed before the Roundings
rules, otherwise the fees might be confused with roundings.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/98/8.0
Dependencies
------------
This module only works with the ``account_credit_control_dunning_fees``
module in the project: https://github.com/OCA/account-financial-tools
Credits
=======
Contributors
------------
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
Maintainer
----------
.. image:: http://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: http://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import model

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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': 'Bank Statement Operation Rules with Dunning Fees',
'version': '8.0.1.0.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
'license': 'AGPL-3',
'category': 'Accounting & Finance',
'depends': ['account_statement_operation_rule',
# in https://github.com/OCA/account-financial-tools
'account_credit_control_dunning_fees',
],
'website': 'http://www.camptocamp.com',
'data': ['view/account_statement_operation_rule_view.xml',
],
'installable': True,
'auto_install': True,
}

View File

@@ -0,0 +1,32 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_operation_rule_dunning_fees
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-03-16 14:56+0000\n"
"PO-Revision-Date: 2015-03-16 14:56+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_statement_operation_rule_dunning_fees
#: code:addons/account_statement_operation_rule_dunning_fees/model/account_statement_operation_rule.py:97
#, python-format
msgid "The Dunning Fees rule must be before the Rounding Rules"
msgstr ""
#. module: account_statement_operation_rule_dunning_fees
#: view:account.statement.operation.rule:account_statement_operation_rule_dunning_fees.view_account_statement_operation_rule_form
msgid "This rule is applied when the invoice line has dunning fees and the payment includes the same amount of fees or a part of this amount."
msgstr ""
#. module: account_statement_operation_rule_dunning_fees
#: selection:account.statement.operation.rule,rule_type:0
msgid "Dunning Fees"
msgstr ""

View File

@@ -0,0 +1,32 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_statement_operation_rule_dunning_fees
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-03-16 14:56+0000\n"
"PO-Revision-Date: 2015-03-16 14:56+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_statement_operation_rule_dunning_fees
#: code:addons/account_statement_operation_rule_dunning_fees/model/account_statement_operation_rule.py:97
#, python-format
msgid "The Dunning Fees rule must be before the Rounding Rules"
msgstr "Les frais de rappel doivent être placés avant les règles d'arrondi"
#. module: account_statement_operation_rule_dunning_fees
#: view:account.statement.operation.rule:account_statement_operation_rule_dunning_fees.view_account_statement_operation_rule_form
msgid "This rule is applied when the invoice line has dunning fees and the payment includes the same amount of fees or a part of this amount."
msgstr "Cette règle est appliquée quand la ligne de facture a des frais de rappel que le paiement reçu inclu une partie ou le montant complet des frais de rappel."
#. module: account_statement_operation_rule_dunning_fees
#: selection:account.statement.operation.rule,rule_type:0
msgid "Dunning Fees"
msgstr "Frais de rappel"

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import account_statement_operation_rule

View File

@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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 import models, fields, api, exceptions, _
class AccountStatementOperationRule(models.Model):
_inherit = 'account.statement.operation.rule'
rule_type = fields.Selection(
selection_add=[('dunning_fees', 'Dunning Fees')],
)
@api.multi
def _is_valid_dunning_fees(self, statement_line, move_lines, balance):
control_line_obj = self.env['credit.control.line']
control_lines = None
for line in move_lines:
domain = [('move_line_id', '=', line.id),
('state', '=', 'sent')]
line_control_lines = control_line_obj.search(domain)
if line_control_lines and control_lines:
# Several lines have credit control lines, use case
# not covered, needs to be handled manually, dunning
# fees rules not applied
control_lines = None
break
elif line_control_lines:
control_lines = line_control_lines
if control_lines:
# If we have an amount of 100.- with 3 credit control
# lines, with the following fees amounts:
# * 1st level: 5.-
# * 2nd level: 10.-
# * 3rd level: 15.-
# The customer might pay from 100.- to 115.-, the rest
# goes to the writeoff account configured on the operation.
max_fees = max(control_lines.mapped('dunning_fees_amount'))
# only use the dunning rule if the balance is between -fees and 0
currency = statement_line.currency_for_rules()
return self._between_with_bounds(0, balance, max_fees, currency)
return False
@api.multi
def is_valid(self, statement_line, move_lines, balance):
""" Returns True if a rule applies to a group of statement_line +
move lines.
This is the public method where the rule is evaluated whatever
its type is. When a rule returns True, it means that it is a
candidate for the current reconciliation. The rule with the lowest
number in the ``sequence`` field is chosen.
:param statement_line: the line to reconcile
:param move_lines: the selected move lines for reconciliation
:param balance: the balance between the statement_line and the
move_lines. It could be computed here but it is
computed before to avoid to compute it for each
rule when called on multiple rules.
"""
if self.rule_type == 'dunning_fees':
return self._is_valid_dunning_fees(statement_line,
move_lines,
balance)
else:
_super = super(AccountStatementOperationRule, self)
return _super.is_valid(statement_line, move_lines, balance)
@api.constrains('sequence')
def check_dunning_before_rounding(self):
if self.rule_type == 'dunning_fees':
operator = '<='
other_type = 'rounding'
elif self.rule_type == 'rounding':
operator = '>'
other_type = 'dunning_fees'
else:
return
message = _('The Dunning Fees rule must be before the Rounding Rules')
if self.search([('sequence', operator, self.sequence),
('rule_type', '=', other_type)], limit=1):
raise exceptions.ValidationError(message)

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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 . import test_rule_dunning_fees
from . import test_rule_sequence

View File

@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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.tests import common
from openerp.addons.account_statement_operation_rule.tests.common import (
prepare_statement
)
def prepare_statement_with_dunning_fees(test, difference, fees):
""" Prepare a bank statement line and a move line
The difference is applied on the bank statement line relatively to
the move line.
The fees is a list of dunning fees (amounts) applied on the move line.
"""
statement_line, move_line = prepare_statement(test, difference)
control_lines = test.env['credit.control.line'].browse()
for fee in fees:
values = {
'date': move_line.date,
'date_due': move_line.date,
'state': 'sent',
'channel': 'letter',
'partner_id': test.ref('base.res_partner_1'),
'amount_due': move_line.credit,
'balance_due': move_line.credit,
'policy_level_id': test.ref('account_credit_control.3_time_1'),
'company_id': move_line.company_id.id,
'move_line_id': move_line.id,
'dunning_fees_amount': fee,
}
control_lines += test.env['credit.control.line'].create(values)
return statement_line, move_line, control_lines
class TestDunningRule(common.TransactionCase):
def setUp(self):
super(TestDunningRule, self).setUp()
self.operation_obj = self.env['account.statement.operation.template']
self.rule_obj = self.env['account.statement.operation.rule']
self.operation_dunning = self.operation_obj.create({
'name': 'Dunning Fees',
'label': 'Dunning Fees',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_dunning = self.rule_obj.create({
'name': 'Dunning Fees',
'rule_type': 'dunning_fees',
'operations': [(6, 0, (self.operation_dunning.id, ))],
'sequence': 1,
})
self.operation_round_1 = self.operation_obj.create({
'name': 'Rounding -1.0 to 0.0',
'label': 'Rounding',
'account_id': self.ref('account.rsa'),
'amount_type': 'percentage_of_total',
'amount': 100.0,
})
self.rule_round_1 = self.rule_obj.create({
'name': 'Rounding -1.0 to 0.0',
'rule_type': 'rounding',
'operations': [(6, 0, (self.operation_round_1.id, ))],
'amount_min': -1.0,
'amount_max': 0,
'sequence': 2,
})
def test_paid_dunning_fees(self):
"""Customer paid the dunning fees of 10.-"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, 10, [10]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_dunning)
def test_no_paid_dunning_fees(self):
"""Customer paid the dunning fees of 10.-"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, 0, [10]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_paid_part_of_dunning_fees(self):
"""Customer paid only 5.- of the dunning fees of 10.-"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, 5, [10]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_dunning)
def test_paid_too_much_dunning_fees(self):
"""Customer paid 15.- of the dunning fees of 10.-"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, 15, [10]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_paid_no_dunning_fees_and_less_amount(self):
"""Customer paid 0.- of the dunning fees of 10.- and 1.- less"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, -1, [10]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_round_1)
def test_paid_dunning_fees_several(self):
"""Customer paid 15.- of the dunning fees of 5.-, 10.- and 15.-"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, 15, [5, 10, 15]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_dunning)
def test_paid_too_much_dunning_fees_several(self):
"""Customer paid 16.- of the dunning fees of 5.-, 10.- and 15.-"""
statement_line, move_line, __ = prepare_statement_with_dunning_fees(
self, 16, [5, 10, 15]
)
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_ignored_credit_control_line(self):
"""Customer paid 15.- of the fees of 5.-, 10.- and draft 15.-"""
prepare = prepare_statement_with_dunning_fees
statement_line, move_line, control_lines = prepare(
self, 15, [5, 10, 15]
)
for control_line in control_lines:
if control_line.dunning_fees_amount == 15:
control_line.state = 'draft'
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertFalse(rule)
def test_ignored_credit_control_line_take_other(self):
"""Customer paid 10.- of the fees of 5.-, 10.- and draft 15.-"""
prepare = prepare_statement_with_dunning_fees
statement_line, move_line, control_lines = prepare(
self, 10, [5, 10, 15]
)
for control_line in control_lines:
if control_line.dunning_fees_amount == 15:
control_line.state = 'draft'
rule = self.rule_obj.find_first_rule(statement_line, [move_line])
self.assertEquals(rule, self.rule_dunning)

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
#
# 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 import exceptions
from openerp.tests import common
class TestRuleSequence(common.TransactionCase):
def setUp(self):
super(TestRuleSequence, self).setUp()
self.operation_obj = self.env['account.statement.operation.template']
self.rule_obj = self.env['account.statement.operation.rule']
self.rule_dunning = self.rule_obj.create({
'name': 'Dunning Fees',
'rule_type': 'dunning_fees',
'sequence': 5,
})
self.rule_round_1 = self.rule_obj.create({
'name': 'Rounding -1.0 to 0.0',
'rule_type': 'rounding',
'amount_min': -1.0,
'amount_max': 0,
'sequence': 10,
})
self.rule_round_2 = self.rule_obj.create({
'name': 'Rounding -2.0 to -1.0',
'rule_type': 'rounding',
'amount_min': -2.0,
'amount_max': -1.0,
'sequence': 15,
})
self.rule_currency = self.rule_obj.create({
'name': 'Currency',
'rule_type': 'currency',
'amount_min': -2.0,
'amount_max': -1.0,
'sequence': 20,
})
def test_dunning_first(self):
""" Dunning rule can be the first """
self.rule_dunning.sequence = 1
self.rule_round_1.sequence = 2
self.rule_round_2.sequence = 3
self.rule_currency.sequence = 4
def test_dunning_after_rounding(self):
""" Dunning rule cannot be after a rounding rule """
with self.assertRaises(exceptions.ValidationError):
self.rule_dunning.sequence = 30
def test_dunning_equal_rounding(self):
""" Dunning rule cannot be equal to a rounding rule """
with self.assertRaises(exceptions.ValidationError):
self.rule_dunning.sequence = 10
def test_rounding_before_dunning(self):
""" Rounding cannot be before dunning """
with self.assertRaises(exceptions.ValidationError):
self.rule_round_1.sequence = 1
def test_currency_before_dunning(self):
""" Currency can be before dunning"""
self.rule_currency.sequence = 1

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_account_statement_operation_rule_form" model="ir.ui.view">
<field name="name">account.statement.operation.rule.form</field>
<field name="model">account.statement.operation.rule</field>
<field name="inherit_id" ref="account_statement_operation_rule.view_account_statement_operation_rule_form"/>
<field name="arch" type="xml">
<group name="rule" position="inside">
<group name="dunning_fees" attrs="{'invisible': [('rule_type', '!=', 'dunning_fees')]}">
<label string="This rule is applied when the invoice line has dunning fees and the payment includes the same amount of fees or a part of this amount."/>
</group>
</group>
</field>
</record>
</data>
</openerp>

View File

@@ -1 +1,2 @@
account-financial-tools
bank-payment