diff --git a/account_advanced_reconcile/__init__.py b/account_advanced_reconcile/__init__.py new file mode 100644 index 00000000..1c643cae --- /dev/null +++ b/account_advanced_reconcile/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 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 easy_reconcile +import base_advanced_reconciliation +import advanced_reconciliation diff --git a/account_advanced_reconcile/__openerp__.py b/account_advanced_reconcile/__openerp__.py new file mode 100644 index 00000000..d3f704b2 --- /dev/null +++ b/account_advanced_reconcile/__openerp__.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 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': "Advanced Reconcile", + 'version': '1.0', + 'author': 'Camptocamp', + 'maintainer': 'Camptocamp', + 'category': 'Finance', + 'complexity': 'normal', + 'depends': ['account_easy_reconcile'], + 'description': """ +Advanced reconciliation methods for the module account_easy_reconcile. +The latter is available on: lp:~akretion-team/+junk/account-extra-addons + +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. + +Typically, such a method can be: + - Reconcile entries if the partner and the ref are equal + - Reconcile entries if the partner is equal and the ref is the same than ref + or name + - Reconcile entries if the partner is equal and the ref match with a pattern + +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 + the name of the debit move line + +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 +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 +many offices. + """, + 'website': 'http://www.camptocamp.com', + 'init_xml': [], + 'update_xml': ['easy_reconcile_view.xml'], + 'demo_xml': [], + 'test': [], + 'images': [], + 'installable': True, + 'auto_install': False, + 'license': 'AGPL-3', + 'application': True, +} diff --git a/account_advanced_reconcile/advanced_reconciliation.py b/account_advanced_reconcile/advanced_reconciliation.py new file mode 100644 index 00000000..dfdb8883 --- /dev/null +++ b/account_advanced_reconcile/advanced_reconciliation.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 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 openerp.osv.orm import TransientModel + + +class easy_reconcile_advanced_ref(TransientModel): + + _name = 'easy.reconcile.advanced.ref' + _inherit = 'easy.reconcile.advanced' + _auto = True # False when inherited from AbstractModel + + def _skip_line(self, cr, uid, rec, move_line, context=None): + """ + When True is returned on some conditions, the credit move line + will be skipped for reconciliation. Can be inherited to + skip on some conditions. ie: ref or partner_id is empty. + """ + return not (move_line.get('ref') and move_line.get('partner_id')) + + def _matchers(self, cr, uid, rec, move_line, context=None): + """ + Return the values used as matchers to found the opposite lines + + All the matcher keys in the dict must have their equivalent in + the `_opposite_matchers`. + + The values of each matcher key will be searched in the + one returned by the `_opposite_matchers` + + Must be inherited to implement the matchers for one method + + As instance, it can returns: + return ('ref', move_line['rec']) + + or + return (('partner_id', move_line['partner_id']), + ('ref', "prefix_%s" % move_line['rec'])) + + All the matchers have to be found in the opposite lines + to consider them as "opposite" + + The matchers will be evaluated in the same order than declared + vs the the opposite matchers, so you can gain performance by + declaring first the partners with the less computation. + + All matchers should match with their opposite to be considered + as "matching". + So with the previous example, partner_id and ref have to be + equals on the opposite line matchers. + + :return: tuple of tuples (key, value) where the keys are + the matchers keys + (must be the same than `_opposite_matchers` returns, + and their values to match in the opposite lines. + A matching key can have multiples values. + """ + return (('partner_id', move_line['partner_id']), + ('ref', move_line['ref'].lower().strip())) + + def _opposite_matchers(self, cr, uid, rec, move_line, context=None): + """ + Return the values of the opposite line used as matchers + so the line is matched + + Must be inherited to implement the matchers for one method + It can be inherited to apply some formatting of fields + (strip(), lower() and so on) + + This method is the counterpart of the `_matchers()` method. + + Each matcher have to yield its value respecting the orders + of the `_matchers()`. + + When a matcher does not correspond, the next matchers won't + be evaluated so the ones which need the less computation + have to be executed first. + + If the `_matchers()` returns: + (('partner_id', move_line['partner_id']), + ('ref', move_line['ref'])) + + Here, you should yield : + yield ('partner_id', move_line['partner_id']) + yield ('ref', move_line['ref']) + + Note that a matcher can contain multiple values, as instance, + if for a move line, you want to search from its `ref` in the + `ref` or `name` fields of the opposite move lines, you have to + yield ('partner_id', move_line['partner_id']) + yield ('ref', (move_line['ref'], move_line['name']) + + An OR is used between the values for the same key. + An AND is used between the differents keys. + + :param dict move_line: values of the move_line + :yield: matchers as tuple ('matcher key', value(s)) + """ + yield ('partner_id', move_line['partner_id']) + yield ('ref', (move_line['ref'].lower().strip(), + move_line['name'].lower().strip())) + diff --git a/account_advanced_reconcile/base_advanced_reconciliation.py b/account_advanced_reconcile/base_advanced_reconciliation.py new file mode 100644 index 00000000..299ffab2 --- /dev/null +++ b/account_advanced_reconcile/base_advanced_reconciliation.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 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 itertools import groupby, product +from operator import itemgetter +from openerp.osv.orm import Model, AbstractModel, TransientModel +from openerp.osv import fields + + +class easy_reconcile_advanced(AbstractModel): + + _name = 'easy.reconcile.advanced' + _inherit = 'easy.reconcile.base' + + def _query_debit(self, cr, uid, rec, context=None): + """Select all move (debit>0) as candidate. Optional choice on invoice + will filter with an inner join on the related moves. + """ + select = self._select(rec) + sql_from = self._from(rec) + where, params = self._where(rec) + where += " AND account_move_line.debit > 0 " + + where2, params2 = self._get_filter(cr, uid, rec, context=context) + + query = ' '.join((select, sql_from, where, where2)) + + cr.execute(query, params + params2) + return cr.dictfetchall() + + def _query_credit(self, cr, uid, rec, context=None): + """Select all move (credit>0) as candidate. Optional choice on invoice + will filter with an inner join on the related moves. + """ + select = self._select(rec) + sql_from = self._from(rec) + where, params = self._where(rec) + where += " AND account_move_line.credit > 0 " + + where2, params2 = self._get_filter(cr, uid, rec, context=context) + + query = ' '.join((select, sql_from, where, where2)) + + cr.execute(query, params + params2) + return cr.dictfetchall() + + def _matchers(self, cr, uid, rec, move_line, context=None): + """ + Return the values used as matchers to found the opposite lines + + All the matcher keys in the dict must have their equivalent in + the `_opposite_matchers`. + + The values of each matcher key will be searched in the + one returned by the `_opposite_matchers` + + Must be inherited to implement the matchers for one method + + As instance, it can returns: + return ('ref', move_line['rec']) + + or + return (('partner_id', move_line['partner_id']), + ('ref', "prefix_%s" % move_line['rec'])) + + All the matchers have to be found in the opposite lines + to consider them as "opposite" + + The matchers will be evaluated in the same order than declared + vs the the opposite matchers, so you can gain performance by + declaring first the partners with the less computation. + + All matchers should match with their opposite to be considered + as "matching". + So with the previous example, partner_id and ref have to be + equals on the opposite line matchers. + + :return: tuple of tuples (key, value) where the keys are + the matchers keys + (must be the same than `_opposite_matchers` returns, + and their values to match in the opposite lines. + A matching key can have multiples values. + """ + raise NotImplementedError + + def _opposite_matchers(self, cr, uid, rec, move_line, context=None): + """ + Return the values of the opposite line used as matchers + so the line is matched + + Must be inherited to implement the matchers for one method + It can be inherited to apply some formatting of fields + (strip(), lower() and so on) + + This method is the counterpart of the `_matchers()` method. + + Each matcher have to yield its value respecting the orders + of the `_matchers()`. + + When a matcher does not correspond, the next matchers won't + be evaluated so the ones which need the less computation + have to be executed first. + + If the `_matchers()` returns: + (('partner_id', move_line['partner_id']), + ('ref', move_line['ref'])) + + Here, you should yield : + yield ('partner_id', move_line['partner_id']) + yield ('ref', move_line['ref']) + + Note that a matcher can contain multiple values, as instance, + if for a move line, you want to search from its `ref` in the + `ref` or `name` fields of the opposite move lines, you have to + yield ('partner_id', move_line['partner_id']) + yield ('ref', (move_line['ref'], move_line['name']) + + An OR is used between the values for the same key. + An AND is used between the differents keys. + + :param dict move_line: values of the move_line + :yield: matchers as tuple ('matcher key', value(s)) + """ + raise NotImplementedError + + @staticmethod + def _compare_values(key, value, opposite_value): + """Can be inherited to modify the equality condition + specifically according to the matcher key (maybe using + a like operator instead of equality on 'ref' as instance) + """ + # consider that empty vals are not valid matchers + # it can still be inherited for some special cases + # where it would be allowed + if not (value and opposite_value): + return False + + if value == opposite_value: + return True + return False + + @staticmethod + def _compare_matcher_values(key, values, opposite_values): + """ Compare every values from a matcher vs an opposite matcher + and return True if it matches + """ + for value, ovalue in product(values, opposite_values): + # we do not need to compare all values, if one matches + # we are done + if easy_reconcile_advanced._compare_values(key, value, ovalue): + return True + return False + + @staticmethod + def _compare_matchers(matcher, opposite_matcher): + """ + Prepare and check the matchers to compare + """ + mkey, mvalue = matcher + omkey, omvalue = opposite_matcher + assert mkey == omkey, "A matcher %s is compared with a matcher %s, " \ + " the _matchers and _opposite_matchers are probably wrong" % \ + (mkey, omkey) + if not isinstance(mvalue, (list, tuple)): + mvalue = mvalue, + if not isinstance(omvalue, (list, tuple)): + omvalue = omvalue, + return easy_reconcile_advanced._compare_matcher_values(mkey, mvalue, omvalue) + + def _compare_opposite(self, cr, uid, rec, move_line, opposite_move_line, + matchers, context=None): + opp_matchers = self._opposite_matchers(cr, uid, rec, opposite_move_line, + context=context) + for matcher in matchers: + try: + opp_matcher = opp_matchers.next() + except StopIteration: + # if you fall here, you probably missed to put a `yield` + # in `_opposite_matchers()` + raise ValueError("Missing _opposite_matcher: %s" % matcher[0]) + + if not self._compare_matchers(matcher, opp_matcher): + # if any of the matcher fails, the opposite line + # is not a valid counterpart + # directly returns so the next yield of _opposite_matchers + # are not evaluated + return False + + return True + + def _search_opposites(self, cr, uid, rec, move_line, opposite_move_lines, context=None): + """ + Search the opposite move lines for a move line + + :param dict move_line: the move line for which we search opposites + :param list opposite_move_lines: list of dict of move lines values, the move + lines we want to search for + :return: list of matching lines + """ + matchers = self._matchers(cr, uid, rec, move_line, context=context) + return [op for op in opposite_move_lines if \ + self._compare_opposite(cr, uid, rec, move_line, op, matchers, context=context)] + + def _action_rec(self, cr, uid, rec, context=None): + credit_lines = self._query_credit(cr, uid, rec, context=context) + debit_lines = self._query_debit(cr, uid, rec, context=context) + return self._rec_auto_lines_advanced( + cr, uid, rec, credit_lines, debit_lines, context=context) + + def _skip_line(self, cr, uid, rec, move_line, context=None): + """ + When True is returned on some conditions, the credit move line + will be skipped for reconciliation. Can be inherited to + skip on some conditions. ie: ref or partner_id is empty. + """ + return False + + def _rec_auto_lines_advanced(self, cr, uid, rec, credit_lines, debit_lines, context=None): + if context is None: + context = {} + + reconciled_ids = [] + partial_reconciled_ids = [] + reconcile_groups = [] + + for credit_line in credit_lines: + if self._skip_line(cr, uid, rec, credit_line, context=context): + continue + + opposite_lines = self._search_opposites( + cr, uid, rec, credit_line, debit_lines, context=context) + + if not opposite_lines: + continue + + opposite_ids = [l['id'] for l in opposite_lines] + line_ids = opposite_ids + [credit_line['id']] + for group in reconcile_groups: + if any([lid in group for lid in opposite_ids]): + group.update(line_ids) + break + else: + reconcile_groups.append(set(line_ids)) + + 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( + cr, uid, rec, group_lines, allow_partial=True, context=context) + if reconciled and partial: + reconciled_ids += reconcile_group_ids + elif partial: + 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 new file mode 100644 index 00000000..747a2e3c --- /dev/null +++ b/account_advanced_reconcile/easy_reconcile.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 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 openerp.osv.orm import Model + + +class account_easy_reconcile_method(Model): + + _inherit = 'account.easy.reconcile.method' + + def _get_all_rec_method(self, cr, uid, context=None): + methods = super(account_easy_reconcile_method, self).\ + _get_all_rec_method(cr, uid, context=context) + methods += [ + ('easy.reconcile.advanced.ref', + '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 + + + +