Holger Brunn
6 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 795 additions and 0 deletions
-
88account_bank_statement_import_auto_reconcile/README.rst
-
4account_bank_statement_import_auto_reconcile/__init__.py
-
29account_bank_statement_import_auto_reconcile/__openerp__.py
-
15account_bank_statement_import_auto_reconcile/demo/account_bank_statement_import_auto_reconcile_rule.xml
-
10account_bank_statement_import_auto_reconcile/models/__init__.py
-
47account_bank_statement_import_auto_reconcile/models/account_bank_statement_import.py
-
74account_bank_statement_import_auto_reconcile/models/account_bank_statement_import_auto_reconcile.py
-
75account_bank_statement_import_auto_reconcile/models/account_bank_statement_import_auto_reconcile_exact_amount.py
-
20account_bank_statement_import_auto_reconcile/models/account_bank_statement_import_auto_reconcile_odoo.py
-
156account_bank_statement_import_auto_reconcile/models/account_bank_statement_import_auto_reconcile_rule.py
-
37account_bank_statement_import_auto_reconcile/models/account_bank_statement_import_reapply_rules.py
-
15account_bank_statement_import_auto_reconcile/models/account_journal.py
-
3account_bank_statement_import_auto_reconcile/security/ir.model.access.csv
-
BINaccount_bank_statement_import_auto_reconcile/static/description/icon.png
-
4account_bank_statement_import_auto_reconcile/tests/__init__.py
-
103account_bank_statement_import_auto_reconcile/tests/test_account_bank_statement_import_auto_reconcile.py
-
13account_bank_statement_import_auto_reconcile/views/account_bank_statement.xml
-
15account_bank_statement_import_auto_reconcile/views/account_bank_statement_import.xml
-
26account_bank_statement_import_auto_reconcile/views/account_bank_statement_import_auto_reconcile_exact_amount.xml
-
24account_bank_statement_import_auto_reconcile/views/account_bank_statement_import_auto_reconcile_rule.xml
-
18account_bank_statement_import_auto_reconcile/views/account_bank_statement_import_reapply_rules.xml
-
14account_bank_statement_import_auto_reconcile/views/account_journal.xml
-
5oca_dependencies.txt
@ -0,0 +1,88 @@ |
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
|
|||
===================================== |
|||
Automatic reconciliation after import |
|||
===================================== |
|||
|
|||
This addon allows you have Odoo reconcile transactions from a bank statement import automatically in cases where the matching transaction can be determined unambigously. |
|||
|
|||
Configuration |
|||
============= |
|||
|
|||
To configure this module, you need to: |
|||
|
|||
#. go to the journal your bank account uses |
|||
#. in the field ``Automatic reconciliation rules``, add at least one rule |
|||
|
|||
Usage |
|||
===== |
|||
|
|||
After a journal is configured for automatic reconciliations, it simply happens during an import on this journal. If there were automatic reconciliations, you'll see a notification about that and the lines in question will also show up as reconciled. |
|||
|
|||
Reconciliation rules |
|||
-------------------- |
|||
|
|||
Odoo standard |
|||
Do exactly what Odoo does when proposing reconciliations. This searches for an exact match on amount and reference first, but falls back to less exact matches if none are found before. If there's only one match, do the reconciliation |
|||
Exact amount and reference |
|||
Strictly only match if we have the same partner, amount and reference. Check at least one field one the statement and one field on the move lines to tell the rule which fields to match with each other. |
|||
|
|||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas |
|||
:alt: Try me on Runbot |
|||
:target: https://runbot.odoo-community.org/runbot/174/8.0 |
|||
|
|||
Background |
|||
========== |
|||
|
|||
Mainly, this module is a framework for conveniently (for programmers) adding new custom automatic reconciliation rules. To do this, study the provided AbstractModels. |
|||
|
|||
Known issues / Roadmap |
|||
====================== |
|||
|
|||
* add more matching rules: |
|||
* AmountDiffuse (let the user configure the threshold) |
|||
* SameCompany (if A from company C bought it, but B from the same company/organization pays) |
|||
* AmountTransposedDigits (reconcile if only two digits are swapped. Dangerous and a special case of AmountDiffuse) |
|||
* whatever else we can think of |
|||
* add some helpers/examples for using the options field |
|||
* allow to fiddle with the parameters of configured rules during a specific import |
|||
|
|||
Bug Tracker |
|||
=========== |
|||
|
|||
Bugs are tracked on `GitHub Issues |
|||
<https://github.com/OCA/bank-statement-import/issues>`_. In case of trouble, please |
|||
check there if your issue has already been reported. If you spotted it first, |
|||
help us smashing it by providing a detailed and welcomed feedback. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Images |
|||
------ |
|||
|
|||
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
|||
|
|||
Contributors |
|||
------------ |
|||
|
|||
* Holger Brunn <hbrunn@therp.nl> |
|||
|
|||
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues. |
|||
|
|||
Maintainer |
|||
---------- |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://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 https://odoo-community.org. |
@ -0,0 +1,4 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from . import models |
@ -0,0 +1,29 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
{ |
|||
"name": "Automatic reconciliation after import", |
|||
"version": "8.0.1.0.0", |
|||
"author": "Therp BV,Odoo Community Association (OCA)", |
|||
"license": "AGPL-3", |
|||
"category": 'Banking addons', |
|||
"summary": "This module allows you to define automatic " |
|||
"reconciliation rules to be run after a bank statement is imported", |
|||
"depends": [ |
|||
'account_bank_statement_import', |
|||
'web_widget_one2many_tags', |
|||
'base_domain_operator', |
|||
], |
|||
"demo": [ |
|||
"demo/account_bank_statement_import_auto_reconcile_rule.xml", |
|||
], |
|||
"data": [ |
|||
"views/account_bank_statement_import_reapply_rules.xml", |
|||
"views/account_bank_statement.xml", |
|||
"views/account_bank_statement_import_auto_reconcile_exact_amount.xml", |
|||
"views/account_bank_statement_import.xml", |
|||
"views/account_journal.xml", |
|||
"views/account_bank_statement_import_auto_reconcile_rule.xml", |
|||
'security/ir.model.access.csv', |
|||
], |
|||
} |
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<record id="rule_amount_exact" model="account.bank.statement.import.auto.reconcile.rule"> |
|||
<field name="journal_id" ref="account.bank_journal" /> |
|||
<field name="rule_type">account.bank.statement.import.auto.reconcile.exact.amount</field> |
|||
<field name="match_st_name" eval="True" /> |
|||
<field name="match_st_ref" eval="True" /> |
|||
<field name="match_move_name" eval="True" /> |
|||
<field name="match_move_ref" eval="True" /> |
|||
<field name="match_line_name" eval="True" /> |
|||
<field name="match_line_ref" eval="True" /> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from . import account_bank_statement_import_auto_reconcile |
|||
from . import account_bank_statement_import_auto_reconcile_rule |
|||
from . import account_journal |
|||
from . import account_bank_statement_import_auto_reconcile_odoo |
|||
from . import account_bank_statement_import |
|||
from . import account_bank_statement_import_auto_reconcile_exact_amount |
|||
from . import account_bank_statement_import_reapply_rules |
@ -0,0 +1,47 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from openerp import _, api, fields, models |
|||
|
|||
|
|||
# pylint: disable=R7980 |
|||
class AccountBankStatementImport(models.TransientModel): |
|||
_inherit = 'account.bank.statement.import' |
|||
|
|||
auto_reconcile = fields.Boolean('Auto reconcile', default=True) |
|||
|
|||
@api.model |
|||
def _create_bank_statement(self, stmt_vals): |
|||
statement_id, notifications = super( |
|||
AccountBankStatementImport, self |
|||
)._create_bank_statement(stmt_vals) |
|||
if not statement_id: |
|||
return statement_id, notifications |
|||
statement = self.env['account.bank.statement'].browse(statement_id) |
|||
if ( |
|||
not statement.journal_id |
|||
.statement_import_auto_reconcile_rule_ids or |
|||
not self.auto_reconcile |
|||
): |
|||
return statement_id, notifications |
|||
reconcile_rules = statement.journal_id\ |
|||
.statement_import_auto_reconcile_rule_ids.get_rules() |
|||
auto_reconciled_ids = [] |
|||
for line in statement.line_ids: |
|||
for rule in reconcile_rules: |
|||
if rule.reconcile(line): |
|||
auto_reconciled_ids.append(line.id) |
|||
break |
|||
if auto_reconciled_ids: |
|||
notifications.append({ |
|||
'type': 'warning', |
|||
'message': |
|||
_("%d transactions were reconciled automatically.") % |
|||
len(auto_reconciled_ids), |
|||
'details': { |
|||
'name': _('Automatically reconciled'), |
|||
'model': 'account.bank.statement.line', |
|||
'ids': auto_reconciled_ids, |
|||
}, |
|||
}) |
|||
return statement_id, notifications |
@ -0,0 +1,74 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from openerp.tools import float_compare, float_round |
|||
from openerp import api, fields, models |
|||
|
|||
|
|||
class AccountBankStatementImportAutoReconcile(models.AbstractModel): |
|||
"""Inherit from this class and implement the reconcile function""" |
|||
|
|||
_name = 'account.bank.statement.import.auto.reconcile' |
|||
_description = 'Base class for automatic reconciliation rules' |
|||
|
|||
wizard_id = fields.Many2one('account.bank.statement.import', required=True) |
|||
# this is meant as a field to save options in and/or to carry state |
|||
# between different reconciliations |
|||
options = fields.Serialized('Options') |
|||
|
|||
@property |
|||
def _digits(self): |
|||
try: |
|||
return self.__digits |
|||
except: |
|||
self.__digits = self.env['decimal.precision'].precision_get( |
|||
'Account' |
|||
) |
|||
return self.__digits |
|||
|
|||
@api.model |
|||
def _round(self, value): |
|||
return float_round(value, precision_digits=self._digits) |
|||
|
|||
@api.model |
|||
def _matches_amount(self, statement_line, debit, credit): |
|||
"""helper to compare if an amount matches some move line data""" |
|||
return ( |
|||
float_compare( |
|||
debit, statement_line.amount, |
|||
precision_digits=self._digits |
|||
) == 0 or |
|||
float_compare( |
|||
-credit, statement_line.amount, |
|||
precision_digits=self._digits |
|||
) == 0 |
|||
) |
|||
|
|||
@api.model |
|||
def _reconcile_move_line(self, statement_line, move_line_id): |
|||
"""Helper to reconcile some move line with a bank statement. |
|||
This will create a move to reconcile with and assigns journal_entry_id |
|||
""" |
|||
move = self.env['account.move'].create( |
|||
self.env['account.bank.statement']._prepare_move( |
|||
statement_line, |
|||
( |
|||
statement_line.statement_id.name or statement_line.name |
|||
) + "/" + str(statement_line.sequence or '') |
|||
) |
|||
) |
|||
move_line_dict = self.env['account.bank.statement']\ |
|||
._prepare_bank_move_line( |
|||
statement_line, move.id, -statement_line.amount, |
|||
statement_line.statement_id.company_id.currency_id.id, |
|||
) |
|||
move_line_dict['counterpart_move_line_id'] = move_line_id |
|||
statement_line.process_reconciliation([move_line_dict]) |
|||
|
|||
@api.multi |
|||
def reconcile(self, statement_line): |
|||
"""Will be called on your model, with wizard_id pointing |
|||
to the currently open statement import wizard. If your rule consumes |
|||
any options or similar, get the values from there. |
|||
Return True if you reconciled this line, something Falsy otherwise""" |
|||
raise NotImplementedError() |
@ -0,0 +1,75 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from openerp import api, fields, models |
|||
|
|||
|
|||
# pylint: disable=R7980 |
|||
class AccountBankStatementImportAutoReconcileExactAmount(models.AbstractModel): |
|||
_inherit = 'account.bank.statement.import.auto.reconcile' |
|||
_name = 'account.bank.statement.import.auto.reconcile.exact.amount' |
|||
_description = 'Exact partner, amount and reference' |
|||
|
|||
substring_match = fields.Boolean('Match for substrings', default=False) |
|||
case_sensitive = fields.Boolean('Case sensitive matching', default=False) |
|||
match_st_ref = fields.Boolean('Reference from st. line', default=True) |
|||
match_st_name = fields.Boolean('Name from st. line', default=True) |
|||
match_move_ref = fields.Boolean('Move reference', default=True) |
|||
match_move_name = fields.Boolean('Move name', default=True) |
|||
match_line_ref = fields.Boolean('Move line reference', default=True) |
|||
match_line_name = fields.Boolean('Move line name', default=True) |
|||
|
|||
@api.multi |
|||
def reconcile(self, statement_line): |
|||
if not statement_line.partner_id or ( |
|||
not statement_line.ref and not statement_line.name |
|||
): |
|||
return |
|||
|
|||
operator = '=ilike' |
|||
if self.substring_match: |
|||
operator = 'substring_of' |
|||
elif self.case_sensitive: |
|||
operator = '=like' |
|||
|
|||
amount_field = 'debit' |
|||
sign = 1 |
|||
if statement_line.currency_id or statement_line.journal_id.currency: |
|||
amount_field = 'amount_currency' |
|||
elif statement_line.amount < 0: |
|||
amount_field = 'credit' |
|||
sign = -1 |
|||
|
|||
statement_fields = filter(None, [ |
|||
self.match_st_name and 'name' or None, |
|||
self.match_st_ref and 'ref' or None, |
|||
]) |
|||
|
|||
move_line_fields = filter(None, [ |
|||
self.match_move_ref and 'move_id.ref' or None, |
|||
self.match_move_name and 'move_id.name' or None, |
|||
self.match_line_ref and 'ref' or None, |
|||
self.match_line_name and 'name' or None, |
|||
]) |
|||
|
|||
domain = [ |
|||
('reconcile_id', '=', False), |
|||
('state', '=', 'valid'), |
|||
('account_id.reconcile', '=', True), |
|||
('partner_id', '=', statement_line.partner_id.id), |
|||
(amount_field, '=', self._round(sign * statement_line.amount)), |
|||
] |
|||
|
|||
domain += ( |
|||
len(statement_fields) * len(move_line_fields) - 1 |
|||
) * ['|'] |
|||
|
|||
for move_line_field in move_line_fields: |
|||
for statement_field in statement_fields: |
|||
value = statement_line[statement_field] |
|||
domain.append((move_line_field, operator, value)) |
|||
|
|||
move_lines = self.env['account.move.line'].search(domain, limit=2) |
|||
if move_lines and len(move_lines) == 1: |
|||
self._reconcile_move_line(statement_line, move_lines.id) |
|||
return True |
@ -0,0 +1,20 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from openerp import api, models |
|||
|
|||
|
|||
class AccountBankStatementImportAutoReconcileOdoo(models.AbstractModel): |
|||
_inherit = 'account.bank.statement.import.auto.reconcile' |
|||
_name = 'account.bank.statement.import.auto.reconcile.odoo' |
|||
_description = 'Odoo standard' |
|||
|
|||
@api.multi |
|||
def reconcile(self, statement_line): |
|||
"""Find an open invoice for the statement line's partner""" |
|||
matches = statement_line.get_reconciliation_proposition(statement_line) |
|||
if len(matches) == 1 and self._matches_amount( |
|||
statement_line, matches[0]['debit'], -matches[0]['credit'], |
|||
): |
|||
self._reconcile_move_line(statement_line, matches[0]['id']) |
|||
return True |
@ -0,0 +1,156 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from lxml import etree |
|||
from openerp import _, api, exceptions, fields, models, tools |
|||
from .account_bank_statement_import_auto_reconcile import\ |
|||
AccountBankStatementImportAutoReconcile as auto_reconcile_base |
|||
|
|||
|
|||
class AccountBankStatementImportAutoReconcileRule(models.Model): |
|||
_name = 'account.bank.statement.import.auto.reconcile.rule' |
|||
_description = 'Automatic reconciliation rule' |
|||
|
|||
rule_type = fields.Selection('_sel_rule_type', required=True) |
|||
journal_id = fields.Many2one('account.journal', 'Journal', required=True) |
|||
options = fields.Serialized('Options') |
|||
|
|||
@api.model |
|||
@tools.ormcache() |
|||
def _get_model_names(self): |
|||
return [ |
|||
model for model in self.env.registry |
|||
if self.env[model]._name != auto_reconcile_base._name and |
|||
issubclass(self.env[model].__class__, auto_reconcile_base) |
|||
] |
|||
|
|||
@api.model |
|||
def _sel_rule_type(self): |
|||
return self.env['ir.model'].search([ |
|||
('model', 'in', self._get_model_names()), |
|||
]).mapped(lambda x: (x.model, x.name)) |
|||
|
|||
@api.constrains('rule_type') |
|||
def _check_rule_type(self): |
|||
for this in self: |
|||
if this.mapped( |
|||
'journal_id.statement_import_auto_reconcile_rule_ids' |
|||
).filtered(lambda x: x != this and x.rule_type == this.rule_type): |
|||
raise exceptions.ValidationError( |
|||
_('Reconciliation rules must be unique per journal') |
|||
) |
|||
|
|||
@api.model |
|||
def create(self, values): |
|||
self._options_from_values(values) |
|||
return super(AccountBankStatementImportAutoReconcileRule, self).create( |
|||
values |
|||
) |
|||
|
|||
@api.multi |
|||
def write(self, values): |
|||
self._options_from_values(values) |
|||
return super(AccountBankStatementImportAutoReconcileRule, self).write( |
|||
values |
|||
) |
|||
|
|||
@api.multi |
|||
def read(self, fields=None, load='_classic_read'): |
|||
rule_type_fields = [] |
|||
self_fields = [] |
|||
for field in fields or []: |
|||
if field in self._fields: |
|||
self_fields.append(field) |
|||
else: |
|||
rule_type_fields.append(field) |
|||
if self_fields and rule_type_fields and 'options' not in self_fields: |
|||
self_fields.append('options') |
|||
result = super(AccountBankStatementImportAutoReconcileRule, self)\ |
|||
.read(fields=self_fields or None, load=load) |
|||
if not rule_type_fields: |
|||
return result |
|||
defaults = {} |
|||
for model_name in self._get_model_names(): |
|||
defaults.update(self.env[model_name].default_get(rule_type_fields)) |
|||
for res in result: |
|||
for field in rule_type_fields: |
|||
res[field] = res['options'].get(field, defaults.get(field)) |
|||
return result |
|||
|
|||
@api.model |
|||
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, |
|||
submenu=False): |
|||
"""Carve a view such that we can inject every field the view of the |
|||
currently selected matching rule into our form""" |
|||
# TODO: at a certain point, we'll have to namespace field names in |
|||
# order to avoid clashes |
|||
result = super(AccountBankStatementImportAutoReconcileRule, self)\ |
|||
.fields_view_get(view_id=view_id, view_type=view_type, |
|||
toolbar=toolbar, submenu=submenu) |
|||
standard_fields = set(self.env[auto_reconcile_base._name]._fields) |
|||
arch = etree.fromstring(result['arch']) |
|||
container = arch.xpath('//div[@name="rule_options"]')[0] |
|||
|
|||
for model_name in self._get_model_names(): |
|||
fields_view = self.env[model_name].fields_view_get() |
|||
if set(fields_view['fields']).issubset(standard_fields): |
|||
# this is an autogenerated form |
|||
continue |
|||
group = etree.SubElement( |
|||
container, |
|||
'div', |
|||
modifiers='{"invisible": [["rule_type", "!=", "%s"]]}' % ( |
|||
model_name, |
|||
) |
|||
) |
|||
form = etree.fromstring(fields_view['arch']) |
|||
for element in form: |
|||
group.append(element) |
|||
for field in group.xpath('descendant::field[@modifiers]'): |
|||
# TODO: merging modifiers would be better |
|||
del field.attrib['modifiers'] |
|||
|
|||
for key, value in fields_view['fields'].iteritems(): |
|||
result['fields'][key] = dict(value, readonly=False) |
|||
|
|||
result['arch'] = etree.tostring(arch) |
|||
return result |
|||
|
|||
@api.multi |
|||
def name_get(self): |
|||
return [ |
|||
(this.id, self.env[this.rule_type]._description) |
|||
for this in self |
|||
] |
|||
|
|||
@api.multi |
|||
def _options_from_values(self, values): |
|||
"""Write values we got from the user into options dict""" |
|||
if 'options' in values: |
|||
return |
|||
rule = values.get('rule_type', self and self[:1].rule_type or None) |
|||
if not rule or rule not in self.env.registry: |
|||
return |
|||
rule_model = self.env[rule] |
|||
options = self and self[:1].options or {} |
|||
for field_name in rule_model._fields: |
|||
if field_name in values: |
|||
options[field_name] = values.pop(field_name) |
|||
values['options'] = options |
|||
|
|||
@api.multi |
|||
def get_rules(self): |
|||
"""Return a NewId object for the configured rule""" |
|||
rules = self.mapped( |
|||
lambda x: self.env[x.rule_type].new(dict( |
|||
self.env[x.rule_type].default_get( |
|||
self.env[x.rule_type]._fields.keys() |
|||
), |
|||
wizard_id=self.id, |
|||
options=x.options, |
|||
)) |
|||
if x else None |
|||
) |
|||
for rule in rules: |
|||
rule.update(rule.options) |
|||
return rules |
@ -0,0 +1,37 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from openerp import _, api, fields, models |
|||
from openerp.exceptions import Warning as UserError |
|||
|
|||
|
|||
class AccountBankStatementImportReapplyRules(models.TransientModel): |
|||
_inherit = 'account.bank.statement.import' |
|||
_name = 'account.bank.statement.import.reapply.rules' |
|||
|
|||
data_file = fields.Binary(required=False) |
|||
|
|||
@api.multi |
|||
def action_reapply_rules(self): |
|||
statements = self.env['account.bank.statement'].browse( |
|||
self.env.context.get('active_ids', []) |
|||
) |
|||
journal = statements.mapped('journal_id') |
|||
if len(journal) != 1: |
|||
raise UserError(_( |
|||
'You can only reapply rules on statements with the same ' |
|||
'journal!' |
|||
)) |
|||
|
|||
self.write({'journal_id': journal.id}) |
|||
reconcile_rules = journal.statement_import_auto_reconcile_rule_ids\ |
|||
.get_rules() |
|||
|
|||
for line in self.env['account.bank.statement.line'].search([ |
|||
('statement_id', 'in', statements.ids), |
|||
('journal_entry_id', '=', False), |
|||
]): |
|||
for rule in reconcile_rules: |
|||
if rule.reconcile(line): |
|||
break |
|||
return {'type': 'ir.actions.act_window_close'} |
@ -0,0 +1,15 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from openerp import fields, models |
|||
|
|||
|
|||
class AccountJournal(models.Model): |
|||
_inherit = 'account.journal' |
|||
|
|||
statement_import_auto_reconcile_rule_ids = fields.One2many( |
|||
'account.bank.statement.import.auto.reconcile.rule', |
|||
'journal_id', string='Automatic reconciliation rules', |
|||
help='When importing a bank statement into this journal ,' |
|||
'apply the following rules for automatic reconciliation', |
|||
) |
@ -0,0 +1,3 @@ |
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" |
|||
account_bank_statement_import_auto_reconcile_rule,account.bank.statement.import.auto.reconcile.rule manager,model_account_bank_statement_import_auto_reconcile_rule,account.group_account_user,1,0,0,0 |
|||
account_bank_statement_import_auto_reconcile_rule_manager,account.bank.statement.import.auto.reconcile.rule manager,model_account_bank_statement_import_auto_reconcile_rule,account.group_account_manager,1,1,1,1 |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,4 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
from . import test_account_bank_statement_import_auto_reconcile |
@ -0,0 +1,103 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2017 Therp BV <http://therp.nl> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
import base64 |
|||
from datetime import timedelta |
|||
from openerp import fields |
|||
from openerp.tests.common import TransactionCase |
|||
from openerp.addons.account_bank_statement_import.models\ |
|||
.account_bank_statement_import import AccountBankStatementImport |
|||
|
|||
|
|||
class TestAccountBankStatementImportAutoReconcile(TransactionCase): |
|||
def setUp(self): |
|||
super(TestAccountBankStatementImportAutoReconcile, self).setUp() |
|||
# we don't really have something to import, so we patch the |
|||
# import routine to return what we want for our tests |
|||
self.original_parse_file = AccountBankStatementImport._parse_file |
|||
AccountBankStatementImport._parse_file = self._parse_file |
|||
self.invoice = self.env.ref('account.invoice_4') |
|||
self.rule = self.env.ref( |
|||
'account_bank_statement_import_auto_reconcile.rule_amount_exact' |
|||
) |
|||
|
|||
def tearDown(self): |
|||
super(TestAccountBankStatementImportAutoReconcile, self).tearDown() |
|||
AccountBankStatementImport._parse_file = self.original_parse_file |
|||
|
|||
def _parse_file(self, data): |
|||
date = self.invoice.date_invoice |
|||
return [ |
|||
{ |
|||
'currency_code': self.invoice.company_id.currency_id.name, |
|||
'account_number': False, |
|||
'name': 'Auto reconcile test', |
|||
'date': fields.Date.to_string( |
|||
fields.Date.from_string(date) + timedelta(days=5) |
|||
), |
|||
'transactions': [ |
|||
{ |
|||
'name': self.invoice.number, |
|||
'date': fields.Date.to_string( |
|||
fields.Date.from_string(date) + timedelta(days=5) |
|||
), |
|||
'amount': self.invoice.residual, |
|||
'unique_import_id': '42', |
|||
'account_number': |
|||
self.invoice.partner_id.bank_ids[:1].acc_number, |
|||
}, |
|||
], |
|||
}, |
|||
] |
|||
|
|||
def test_account_bank_statement_import_auto_reconcile(self): |
|||
# first, we do an import with auto reconciliation turned off |
|||
action = self.env['account.bank.statement.import'].create({ |
|||
'data_file': base64.b64encode('hello world'), |
|||
'journal_id': self.env.ref('account.bank_journal').id, |
|||
'auto_reconcile': False, |
|||
}).import_file() |
|||
# nothing should have happened |
|||
self.assertEqual(self.invoice.state, 'open') |
|||
self.env['account.bank.statement'].browse( |
|||
action['context']['statement_ids'] |
|||
).unlink() |
|||
# now we do matching, but manipulate the matching to work on the |
|||
# ref field only which is empty in our example |
|||
self.rule.write({'match_st_name': False}) |
|||
action = self.env['account.bank.statement.import'].create({ |
|||
'data_file': base64.b64encode('hello world'), |
|||
'journal_id': self.env.ref('account.bank_journal').id, |
|||
'auto_reconcile': True, |
|||
}).import_file() |
|||
# nothing should have happened |
|||
self.assertEqual(self.invoice.state, 'open') |
|||
self.env['account.bank.statement'].browse( |
|||
action['context']['statement_ids'] |
|||
).unlink() |
|||
# for exact amount matching, our first transaction should be matched |
|||
# to the invoice's move line, marking the invoice as paid, |
|||
# provided we allow matching by name |
|||
self.rule.write({'match_st_name': True}) |
|||
action = self.env['account.bank.statement.import'].create({ |
|||
'data_file': base64.b64encode('hello world'), |
|||
'journal_id': self.env.ref('account.bank_journal').id, |
|||
'auto_reconcile': True, |
|||
}).import_file() |
|||
self.assertEqual(self.invoice.state, 'paid') |
|||
|
|||
def test_rule_options(self): |
|||
self.rule.unlink() |
|||
rule = self.env[ |
|||
'account.bank.statement.import.auto.reconcile.rule' |
|||
].create({ |
|||
'journal_id': self.env.ref('account.bank_journal').id, |
|||
'rule_type': |
|||
'account.bank.statement.import.auto.reconcile.exact.amount', |
|||
'match_st_name': False, |
|||
}) |
|||
rules = rule.get_rules() |
|||
# explicitly written |
|||
self.assertFalse(rules.match_st_name) |
|||
# defaults must be used here too |
|||
self.assertTrue(rules.match_st_ref) |
@ -0,0 +1,13 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<act_window |
|||
id="action_reapply_rules" |
|||
src_model="account.bank.statement" |
|||
res_model="account.bank.statement.import.reapply.rules" |
|||
name="Reapply matching rules" |
|||
target="new" |
|||
view_id="account_bank_statement_import_reapply_rules_form" |
|||
/> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<record id="account_bank_statement_import_view" model="ir.ui.view"> |
|||
<field name="model">account.bank.statement.import</field> |
|||
<field name="inherit_id" ref="account_bank_statement_import.account_bank_statement_import_view" /> |
|||
<field name="arch" type="xml"> |
|||
<field name="journal_id" position="after"> |
|||
<field name="auto_reconcile" attrs="{'invisible': [('hide_journal_field', '=', True)]}" class="oe_inline" /> |
|||
<label for="auto_reconcile" /> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,26 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<record id="account_bank_statement_import_auto_reconcile_exact_amount_form" model="ir.ui.view"> |
|||
<field name="model">account.bank.statement.import.auto.reconcile.exact.amount</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<group name="statement_fields" string="On the statement, match..."> |
|||
<field name="match_st_ref" /> |
|||
<field name="match_st_name" /> |
|||
</group> |
|||
<group name="move_line_fields" string="... with the following fields"> |
|||
<field name="match_move_ref" /> |
|||
<field name="match_move_name" /> |
|||
<field name="match_line_ref" /> |
|||
<field name="match_line_name" /> |
|||
</group> |
|||
<group name="options" string="Options"> |
|||
<field name="substring_match" /> |
|||
<field name="case_sensitive" /> |
|||
</group> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,24 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<record id="account_bank_statement_import_auto_reconcile_rule_form" model="ir.ui.view"> |
|||
<field name="model">account.bank.statement.import.auto.reconcile.rule</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<group> |
|||
<field name="rule_type" /> |
|||
</group> |
|||
<div name="rule_options" attrs="{'invisible': [('rule_type', '=', False)]}" /> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
<record id="account_bank_statement_import_auto_reconcile_rule_tree" model="ir.ui.view"> |
|||
<field name="model">account.bank.statement.import.auto.reconcile.rule</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="display_name" /> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<record id="account_bank_statement_import_reapply_rules_form" model="ir.ui.view"> |
|||
<field name="model">account.bank.statement.import.reapply.rules</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<div>This wizard will reapply the journal's matching rules on the selected bank statement(s)</div> |
|||
<footer> |
|||
<button name="action_reapply_rules" string="Reapply rules" type="object" class="oe_highlight" /> |
|||
or |
|||
<button special="cancel" string="Cancel" class="oe_link" /> |
|||
</footer> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,14 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
<record id="view_account_journal_form" model="ir.ui.view"> |
|||
<field name="model">account.journal</field> |
|||
<field name="inherit_id" ref="account.view_account_journal_form" /> |
|||
<field name="arch" type="xml"> |
|||
<field name="default_credit_account_id" position="after"> |
|||
<field name="statement_import_auto_reconcile_rule_ids" widget="one2many_tags" /> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,5 @@ |
|||
# list the OCA project dependencies, one per line |
|||
# add a github url if you need a forked version |
|||
web |
|||
# TODO: change this back to server-tools once https://github.com/OCA/server-tools/pull/738 is merged |
|||
server-tools https://github.com/hbrunn/server-tools 8.0-base_domain_operator |
Write
Preview
Loading…
Cancel
Save
Reference in new issue