diff --git a/account_advanced_reconcile/__openerp__.py b/account_advanced_reconcile/__openerp__.py index 726fe5e2..5ca17767 100644 --- a/account_advanced_reconcile/__openerp__.py +++ b/account_advanced_reconcile/__openerp__.py @@ -29,6 +29,21 @@ 'description': """ Advanced reconciliation methods for the module account_easy_reconcile. +account_easy_reconcile, which is a dependency, is available in the branch: +lp:~openerp-community-committers/+junk/account-extra-addons +This branch is temporary and will soon be merged with the Akretion master +branch, but the master branch does not already exist. Sorry for the +inconvenience. + +In addition to the features implemented in account_easy_reconcile, which are: + - reconciliation facilities for big volume of transactions + - setup different profiles of reconciliation by account + - each profile can use many methods of reconciliation + - this module is also a base to create others reconciliation methods + which can plug in the profiles + - a profile a reconciliation can be run manually or by a cron + - monitoring of reconcilation runs with a few logs + It implements a basis to created advanced reconciliation methods in a few lines of code. @@ -38,6 +53,11 @@ Typically, such a method can be: or name - Reconcile entries if the partner is equal and the ref match with a pattern +And they allows: + - Reconciliations with multiple credit / multiple debit lines + - Partial reconciliations + - Write-off amount as well + A method is already implemented in this module, it matches on entries: * Partner * Ref on credit move lines should be case insensitive equals to the ref or @@ -46,9 +66,6 @@ A method is already implemented in this module, it matches on entries: The base class to find the reconciliations is built to be as efficient as possible. -Reconciliations with multiple credit / debit lines is possible. -Partial reconciliation are generated. -You can choose a write-off amount as well. So basically, if you have an invoice with 3 payments (one per month), the first month, it will partial reconcile the debit move line with the first payment, the second @@ -56,12 +73,13 @@ month, it will partial reconcile the debit move line with 2 first payments, the third month, it will make the full reconciliation. This module is perfectly adapted for E-Commerce business where a big volume of -move lines and so, reconciliations, is involved and payments often come from +move lines and so, reconciliations, are involved and payments often come from many offices. + """, 'website': 'http://www.camptocamp.com', 'init_xml': [], - 'update_xml': [], + 'update_xml': ['easy_reconcile_view.xml'], 'demo_xml': [], 'test': [], 'images': [], diff --git a/account_advanced_reconcile/base_advanced_reconciliation.py b/account_advanced_reconcile/base_advanced_reconciliation.py index 299ffab2..df26708c 100644 --- a/account_advanced_reconcile/base_advanced_reconciliation.py +++ b/account_advanced_reconcile/base_advanced_reconciliation.py @@ -263,11 +263,11 @@ class easy_reconcile_advanced(AbstractModel): lines_by_id = dict([(l['id'], l) for l in credit_lines + debit_lines]) for reconcile_group_ids in reconcile_groups: group_lines = [lines_by_id[lid] for lid in reconcile_group_ids] - reconciled, partial = self._reconcile_lines( + reconciled, full = self._reconcile_lines( cr, uid, rec, group_lines, allow_partial=True, context=context) - if reconciled and partial: + if reconciled and full: reconciled_ids += reconcile_group_ids - elif partial: + elif reconciled: partial_reconciled_ids += reconcile_group_ids return reconciled_ids, partial_reconciled_ids diff --git a/account_advanced_reconcile/easy_reconcile.py b/account_advanced_reconcile/easy_reconcile.py index 8204a8c1..747a2e3c 100644 --- a/account_advanced_reconcile/easy_reconcile.py +++ b/account_advanced_reconcile/easy_reconcile.py @@ -31,7 +31,7 @@ class account_easy_reconcile_method(Model): _get_all_rec_method(cr, uid, context=context) methods += [ ('easy.reconcile.advanced.ref', - 'Advanced method, payment ref matches with ref or name'), + 'Advanced. Partner and Ref.'), ] return methods diff --git a/account_advanced_reconcile/easy_reconcile_view.xml b/account_advanced_reconcile/easy_reconcile_view.xml new file mode 100644 index 00000000..c5e81ebd --- /dev/null +++ b/account_advanced_reconcile/easy_reconcile_view.xml @@ -0,0 +1,18 @@ + + + + + account.easy.reconcile.form + account.easy.reconcile + form + + + + + + + + + diff --git a/account_statement_base_completion/__init__.py b/account_statement_base_completion/__init__.py new file mode 100644 index 00000000..f6c46966 --- /dev/null +++ b/account_statement_base_completion/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Joel Grand-Guillaume +# Copyright 2011-2012 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 . +# +############################################################################## + +import statement \ No newline at end of file diff --git a/account_statement_base_completion/__openerp__.py b/account_statement_base_completion/__openerp__.py new file mode 100644 index 00000000..d9f61447 --- /dev/null +++ b/account_statement_base_completion/__openerp__.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Joel Grand-Guillaume +# Copyright 2011-2012 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 . +# +############################################################################## + +{'name': "Bank statement base completion", + 'version': '1.0', + 'author': 'Camptocamp', + 'maintainer': 'Camptocamp', + 'category': 'Finance', + 'complexity': 'normal', #easy, normal, expert + 'depends': ['account_statement_ext'], + 'description': """ + The goal of this module is to improve the basic bank statement, help dealing with huge volume of + reconciliation by providing basic rules to identify the partner of a bank statement line. + Each bank statement profile can have his own rules to apply respecting a sequence order. + + Some basic rules are provided in this module: + + 1) Match from statement line label (based on partner field 'Bank Statement Label') + 2) Match from statement line label (based on partner name) + 3) Match from statement line reference (based on SO number) + 3) Match from statement line reference (based on Invoice number) + + You can easily override this module and add your own rules in your own one. The basic rules only + fullfill the partner, but you can use them to complete all values of the line (in the future, we'll + add rule to automatically match and reconcile the line). + + It add as well a label on the bank statement line (on which the pre-define rules can match) and + a char field on the partner called 'Bank Statement Label'. Using the pre-define rules, you'll be + able to match various label for a partner. + + The reference of the line is always used by the reconciliation process. We're supposed to copy + there (or write manually) the matching string. That can be : the order Number or an invoice number, + or anything that will be found in the invoice accounting entry part to make the match. + + You can use it with our account_advanced_reconcile module to automatize the reconciliation process. + + """, + 'website': 'http://www.camptocamp.com', + 'init_xml': [], + 'update_xml': [ + 'statement_view.xml', + 'partner_view.xml', + 'data.xml', + ], + 'demo_xml': [], + 'test': [], + 'installable': True, + 'images': [], + 'auto_install': False, + 'license': 'AGPL-3', + 'active': False, +} diff --git a/account_statement_base_completion/data.xml b/account_statement_base_completion/data.xml new file mode 100644 index 00000000..3757e4fd --- /dev/null +++ b/account_statement_base_completion/data.xml @@ -0,0 +1,32 @@ + + + + + + Match from line label (based on partner field 'Bank Statement Label') + 60 + get_from_label_and_partner_field + + + + Match from line label (based on partner name) + 70 + get_from_label_and_partner_name + + + + Match from line reference (based on SO number) + 50 + get_from_ref_and_so + + + + Match from line reference (based on Invoice number) + 40 + get_from_ref_and_invoice + + + + + + diff --git a/account_statement_base_completion/partner.py b/account_statement_base_completion/partner.py new file mode 100644 index 00000000..978de421 --- /dev/null +++ b/account_statement_base_completion/partner.py @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- +################################################################################# +# # +# Copyright (C) 2011 Akretion & Camptocamp +# Author : Sébastien BEAU, Joel Grand-Guillaume # +# # +# 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 . # +# # +################################################################################# + +from osv import fields, osv + +class res_partner(osv.osv): + """ + Add a bank label on the partner so that we can use it to match + this partner when we found this in a statement line. + """ + _inherit = 'res.partner' + + _columns = { + 'bank_statement_label':fields.char('Bank Statement Label', size=100, + help="Enter the various label found on your bank statement separated by a ; If \ + one of this label is include in the bank statement line, the partner will be automatically \ + filled (as long as you use this method/rules in your statement profile)."), + } + +res_partner() diff --git a/account_statement_base_completion/partner_view.xml b/account_statement_base_completion/partner_view.xml new file mode 100644 index 00000000..c7ec3f1a --- /dev/null +++ b/account_statement_base_completion/partner_view.xml @@ -0,0 +1,22 @@ + + + + + + + + account_bank_statement_import.view.partner.form + res.partner + form + 20 + + + + + + + + + + + diff --git a/account_statement_base_completion/statement.py b/account_statement_base_completion/statement.py new file mode 100644 index 00000000..551707ac --- /dev/null +++ b/account_statement_base_completion/statement.py @@ -0,0 +1,361 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Nicolas Bessi, Joel Grand-Guillaume +# Copyright 2011-2012 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 . +# +############################################################################## +from tools.translate import _ +import netsvc +logger = netsvc.Logger() +from openerp.osv.orm import Model, fields +from openerp.osv import fields, osv +from operator import itemgetter, attrgetter + +class ErrorTooManyPartner(Exception): + """ + New Exception definition that is raised when more than one partner is matched by + the completion rule. + """ + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + + +class AccountStatementProfil(Model): + """ + Extend the class to add rules per profil that will match at least the partner, + but it could also be used to match other values as well. + """ + + _inherit = "account.statement.profil" + + _columns={ + # @Akretion : For now, we don't implement this features, but this would probably be there: + # 'auto_completion': fields.text('Auto Completion'), + # 'transferts_account_id':fields.many2one('account.account', 'Transferts Account'), + # => You can implement it in a module easily, we design it with your needs in mind + # as well ! + + 'rule_ids':fields.many2many('account.statement.completion.rule', + string='Related statement profiles', + rel='as_rul_st_prof_rel', + ), + } + + def find_values_from_rules(self, cr, uid, id, line_id, context=None): + """ + This method will execute all related rules, in their sequence order, + to retrieve all the values returned by the first rules that will match. + + :param int/long line_id: id of the concerned account.bank.statement.line + :return: + A dict of value that can be passed directly to the write method of + the statement line or {} + {'partner_id': value, + 'account_id' : value, + + ...} + """ + if not context: + context={} + res = {} + rule_obj = self.pool.get('account.statement.completion.rule') + profile = self.browse(cr, uid, id, context=context) + # We need to respect the sequence order + sorted_array = sorted(profile.rule_ids, key=attrgetter('sequence')) + for rule in sorted_array: + method_to_call = getattr(rule_obj, rule.function_to_call) + result = method_to_call(cr,uid,line_id,context) + if result: + return result + return res + + +class AccountStatementCompletionRule(Model): + """ + This will represent all the completion method that we can have to + fullfill the bank statement lines. You'll be able to extend them in you own module + and choose those to apply for every statement profile. + The goal of a rule is to fullfill at least the partner of the line, but + if possible also the reference because we'll use it in the reconciliation + process. The reference should contain the invoice number or the SO number + or any reference that will be matched by the invoice accounting move. + """ + + _name = "account.statement.completion.rule" + _order = "sequence asc" + + def _get_functions(self, cr, uid, context=None): + """ + List of available methods for rules. Override this to add you own. + """ + return [ + ('get_from_ref_and_invoice', 'From line reference (based on invoice number)'), + ('get_from_ref_and_so', 'From line reference (based on SO number)'), + ('get_from_label_and_partner_field', 'From line label (based on partner field)'), + ('get_from_label_and_partner_name', 'From line label (based on partner name)'), + ] + + _columns={ + 'sequence': fields.integer('Sequence', help="Lower means paresed first."), + 'name': fields.char('Name', size=128), + 'profile_ids': fields.many2many('account.statement.profil', + rel='as_rul_st_prof_rel', + string='Related statement profiles'), + 'function_to_call': fields.selection(_get_functions, 'Method'), + } + + def get_from_ref_and_invoice(self, cursor, uid, line_id, context=None): + """ + Match the partner based on the invoice number and the reference of the statement + line. Then, call the generic get_values_for_line method to complete other values. + If more than one partner matched, raise the ErrorTooManyPartner error. + + :param int/long line_id: id of the concerned account.bank.statement.line + :return: + A dict of value that can be passed directly to the write method of + the statement line or {} + {'partner_id': value, + 'account_id' : value, + + ...} + """ + st_obj = self.pool.get('account.bank.statement.line') + st_line = st_obj.browse(cursor,uid,line_id) + res = {} + if st_line: + inv_obj = self.pool.get('account.invoice') + inv_id = inv_obj.search(cursor, uid, [('number', '=', st_line.ref)]) + if inv_id: + if inv_id and len(inv_id) == 1: + inv = inv_obj.browse(cursor, uid, inv_id[0]) + res['partner_id'] = inv.partner_id.id + elif inv_id and len(inv_id) > 1: + raise ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id)) + st_vals = st_obj.get_values_for_line(cursor, uid, profile_id = st_line.statement_id.profile_id.id, + partner_id = res.get('partner_id',False), line_type = st_line.type, amount = st_line.amount, context = context) + res.update(st_vals) + return res + + def get_from_ref_and_so(self, cursor, uid, line_id, context=None): + """ + Match the partner based on the SO number and the reference of the statement + line. Then, call the generic get_values_for_line method to complete other values. + If more than one partner matched, raise the ErrorTooManyPartner error. + + :param int/long line_id: id of the concerned account.bank.statement.line + :return: + A dict of value that can be passed directly to the write method of + the statement line or {} + {'partner_id': value, + 'account_id' : value, + + ...} + """ + st_obj = self.pool.get('account.bank.statement.line') + st_line = st_obj.browse(cursor,uid,line_id) + res = {} + if st_line: + so_obj = self.pool.get('sale.order') + so_id = so_obj.search(cursor, uid, [('name', '=', st_line.ref)]) + if so_id: + if so_id and len(so_id) == 1: + so = so_obj.browse(cursor, uid, so_id[0]) + res['partner_id'] = so.partner_id.id + elif so_id and len(so_id) > 1: + raise ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id)) + st_vals = st_obj.get_values_for_line(cursor, uid, profile_id = st_line.statement_id.profile_id.id, + partner_id = res.get('partner_id',False), line_type = st_line.type, amount = st_line.amount, context = context) + res.update(st_vals) + return res + + + def get_from_label_and_partner_field(self, cursor, uid, line_id, context=None): + """ + Match the partner based on the label field of the statement line + and the text defined in the 'bank_statement_label' field of the partner. + Remember that we can have values separated with ; Then, call the generic + get_values_for_line method to complete other values. + If more than one partner matched, raise the ErrorTooManyPartner error. + + :param int/long line_id: id of the concerned account.bank.statement.line + :return: + A dict of value that can be passed directly to the write method of + the statement line or {} + {'partner_id': value, + 'account_id' : value, + + ...} + """ + partner_obj = self.pool.get('res.partner') + st_obj = self.pool.get('account.bank.statement.line') + st_line = st_obj.browse(cursor,uid,line_id) + res = {} + compt = 0 + if st_line: + ids = partner_obj.search(cursor, uid, [['bank_statement_label', '!=', False]], context=context) + for partner in self.browse(cursor, uid, ids, context=context): + for partner_label in partner.bank_statement_label.split(';'): + if partner_label in st_line.label: + compt += 1 + res['partner_id'] = partner.id + if compt > 1: + raise ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id)) + if res: + st_vals = st_obj.get_values_for_line(cursor, uid, profile_id = st_line.statement_id.profile_id.id, + partner_id = res.get('partner_id',False), line_type = st_line.type, amount = st_line.amount, context = context) + res.update(st_vals) + return res + + def get_from_label_and_partner_name(self, cursor, uid, line_id, context=None): + """ + Match the partner based on the label field of the statement line + and the name of the partner. + Then, call the generic get_values_for_line method to complete other values. + If more than one partner matched, raise the ErrorTooManyPartner error. + + :param int/long line_id: id of the concerned account.bank.statement.line + :return: + A dict of value that can be passed directly to the write method of + the statement line or {} + {'partner_id': value, + 'account_id' : value, + + ...} + """ + # This Method has not been tested yet ! + res = {} + st_obj = self.pool.get('account.bank.statement.line') + st_line = st_obj.browse(cursor,uid,line_id) + if st_line: + sql = "SELECT id FROM res_partner WHERE name ~* '.*%s.*'" + cursor.execute(sql, (st_line.label,)) + result = cursor.fetchall() + if len(result) > 1: + raise ErrorTooManyPartner(_('Line named "%s" was matched by more than one partner.')%(st_line.name,st_line.id)) + for id in result: + res['partner_id'] = id + if res: + st_vals = st_obj.get_values_for_line(cursor, uid, profile_id = st_line.statement_id.profile_id.id, + partner_id = res.get('partner_id',False), line_type = st_line.type, amount = st_line.amount, context = context) + res.update(st_vals) + return res + + +class AccountStatementLine(Model): + """ + Add sparse field on the statement line to allow to store all the + bank infos that are given by a bank/office. You can then add you own in your + module. The idea here is to store all bank/office infos in the additionnal_bank_fields + serialized field when importing the file. If many values, add a tab in the bank + statement line to store your specific one. Have a look in account_statement_base_import + module to see how we've done it. + """ + _inherit = "account.bank.statement.line" + + _columns={ + 'additionnal_bank_fields' : fields.serialized('Additionnal infos from bank', + help="Used by completion and import system. Adds every field that is present in your bank/office \ + statement file"), + 'label': fields.sparse(type='char', string='Label', + serialization_field='additionnal_bank_fields', + help="Generiy field to store a label given from the bank/office on which we can \ + base the default/standard providen rule."), + 'already_completed': fields.boolean("Auto-Completed", + help="When this checkbox is ticked, the auto-completion process/button will ignore this line."), + } + _defaults = { + 'already_completed': False, + } + + + def get_line_values_from_rules(self, cr, uid, ids, context=None): + """ + We'll try to find out the values related to the line based on rules setted on + the profile.. We will ignore line for which already_completed is ticked. + + :return: + A dict of value that can be passed directly to the write method of + the statement line or {} + {'partner_id': value, + 'account_id' : value, + + ...} + """ + profile_obj = self.pool.get('account.statement.profil') + st_obj = self.pool.get('account.bank.statement.line') + res={} + errors_stack = [] + for line in self.browse(cr,uid, ids, context): + if not line.already_completed: + try: + # Take the default values + res[line.id] = st_obj.get_values_for_line(cr, uid, profile_id = line.statement_id.profile_id.id, + line_type = line.type, amount = line.amount, context = context) + # Ask the rule + vals = profile_obj.find_values_from_rules(cr, uid, line.statement_id.profile_id.id, line.id, context) + # Merge the result + res[line.id].update(vals) + except ErrorTooManyPartner, exc: + msg = "Line ID %s had following error: %s" % (line.id, str(exc)) + errors_stack.append(msg) + if errors_stack: + msg = u"\n".join(errors_stack) + raise ErrorTooManyPartner(msg) + return res + +class AccountBankSatement(Model): + """ + We add a basic button and stuff to support the auto-completion + of the bank statement once line have been imported or manually fullfill. + """ + _inherit = "account.bank.statement" + + def button_auto_completion(self, cr, uid, ids, context=None): + """ + Complete line with values given by rules and tic the already_completed + checkbox so we won't compute them again unless the user untick them ! + """ + # TODO: Test the errors system, we should be able to complete all line that + # passed, and raise an error for all other at once.. + if not context: + context={} + stat_line_obj = self.pool.get('account.bank.statement.line') + errors_msg=False + for stat in self.browse(cr, uid, ids, context=context): + ctx = context.copy() + line_ids = map(lambda x:x.id, stat.line_ids) + try: + res = stat_line_obj.get_line_values_from_rules(cr, uid, line_ids, context=ctx) + except ErrorTooManyPartner, exc: + errors_msg = str(exc) + for id in line_ids: + vals = res.get(id, False) + if vals: + vals['already_completed'] = True + stat_line_obj.write(cr, uid, id, vals, context=ctx) + # cr.commit() + # TOTEST: I don't know if this is working... + if errors_msg: + # raise osv.except_osv(_('Error'), errors_msg) + warning = { + 'title': _('Error!'), + 'message' : errors_msg, + } + return {'warning': warning} + return True diff --git a/account_statement_base_completion/statement_view.xml b/account_statement_base_completion/statement_view.xml new file mode 100644 index 00000000..4c1fb2b3 --- /dev/null +++ b/account_statement_base_completion/statement_view.xml @@ -0,0 +1,88 @@ + + + + + + account_bank_statement_import_base.bank_statement.view_form + account.bank.statement + + + form + + + + + + + + + + + + + 10 + + + +