diff --git a/account_advanced_reconcile/__init__.py b/account_advanced_reconcile/__init__.py index 1c643cae..d79c35ef 100644 --- a/account_advanced_reconcile/__init__.py +++ b/account_advanced_reconcile/__init__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA +# Author: Nicolas Bessi +# 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 @@ -19,6 +19,4 @@ # ############################################################################## -import easy_reconcile -import base_advanced_reconciliation -import advanced_reconciliation +import reconcile_method diff --git a/account_advanced_reconcile/__openerp__.py b/account_advanced_reconcile/__openerp__.py index 5ca17767..26e63b61 100644 --- a/account_advanced_reconcile/__openerp__.py +++ b/account_advanced_reconcile/__openerp__.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- -############################################################################## +# -*- coding: utf-8 -*- ############################################################################## # -# Author: Guewen Baconnier -# Copyright 2012 Camptocamp SA +# 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 @@ -25,66 +24,26 @@ 'maintainer': 'Camptocamp', 'category': 'Finance', 'complexity': 'normal', - 'depends': ['account_easy_reconcile'], + 'depends': ['base_transaction_id', 'account_easy_reconcile'], 'description': """ -Advanced reconciliation methods for the module account_easy_reconcile. +This module allows you auto reconcile entries with payment. +It is mostly used in E-Commerce, but could also be useful in other cases. -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. - -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 - -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 - the name of the debit move line - -The base class to find the reconciliations is built to be as efficient as -possible. - - -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, are involved and payments often come from -many offices. +The automatic reconciliation matches a transaction ID, if available, propagated from the Sale Order. +It can also search for the sale order name in the origin or description of the move line. +Basically, this module will match account move line with a matching reference on a same account. +It will make a partial reconciliation if more than one move has the same reference (like 3x payments) +Once all payment will be there, it will make a full reconciliation. +You can choose a write-off amount as well. """, 'website': 'http://www.camptocamp.com', 'init_xml': [], - 'update_xml': ['easy_reconcile_view.xml'], + 'update_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 deleted file mode 100644 index dfdb8883..00000000 --- a/account_advanced_reconcile/advanced_reconciliation.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- 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 deleted file mode 100644 index df26708c..00000000 --- a/account_advanced_reconcile/base_advanced_reconciliation.py +++ /dev/null @@ -1,274 +0,0 @@ -# -*- 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, full = self._reconcile_lines( - cr, uid, rec, group_lines, allow_partial=True, context=context) - if reconciled and full: - reconciled_ids += reconcile_group_ids - 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 deleted file mode 100644 index 747a2e3c..00000000 --- a/account_advanced_reconcile/easy_reconcile.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- 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 deleted file mode 100644 index 961add68..00000000 --- a/account_advanced_reconcile/easy_reconcile_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - account.easy.reconcile.form - account.easy.reconcile - form - - - - - - - - - - - diff --git a/account_statement_transactionid_completion/__init__.py b/account_advanced_reconcile/wizard/__init__.py similarity index 56% rename from account_statement_transactionid_completion/__init__.py rename to account_advanced_reconcile/wizard/__init__.py index a8ce7c24..f72fd976 100644 --- a/account_statement_transactionid_completion/__init__.py +++ b/account_advanced_reconcile/wizard/__init__.py @@ -1,22 +1,20 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Author: Joel Grand-Guillaume -# Copyright 2011-2012 Camptocamp SA +# Author Nicolas Bessi. Copyright 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. +# it under the terms of the GNU 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. +# GNU General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU General Public License # along with this program. If not, see . # ############################################################################## - -import statement +import statement_auto_reconcile diff --git a/account_advanced_reconcile/wizard/statement_auto_reconcile.py b/account_advanced_reconcile/wizard/statement_auto_reconcile.py new file mode 100644 index 00000000..732d5b55 --- /dev/null +++ b/account_advanced_reconcile/wizard/statement_auto_reconcile.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Nicolas Bessi, Guewen Baconnier +# 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 netsvc + +from osv import osv, fields +from tools.translate import _ +from operator import itemgetter, attrgetter +from itertools import groupby +import logging +logger = logging.getLogger('account.statement.reconcile') + +class AccountsStatementAutoReconcile(osv.osv_memory): + _name = 'account.statement.import.automatic.reconcile' + _description = 'Automatic Reconcile' + + _columns = { + 'account_ids': fields.many2many('account.account', + 'statement_reconcile_account_rel', + 'reconcile_id', + 'account_id', + 'Accounts to Reconcile', + domain=[('reconcile', '=', True)]), + 'partner_ids': fields.many2many('res.partner', + 'statement_reconcile_res_partner_rel', + 'reconcile_id', + 'res_partner_id', + 'Partners to Reconcile'), + 'invoice_ids': fields.many2many('account.invoice', + 'statement_account_invoice_rel', + 'reconcile_id', + 'invoice_id', + 'Invoices to Reconcile', + domain = [('type','=','out_invoice')]), + 'writeoff_acc_id': fields.many2one('account.account', 'Account'), + 'writeoff_amount_limit': fields.float('Max amount allowed for write off'), + 'journal_id': fields.many2one('account.journal', 'Journal'), + 'reconciled': fields.integer('Reconciled transactions', readonly=True), + 'allow_write_off': fields.boolean('Allow write off'), + } + + def _get_reconciled(self, cr, uid, context=None): + if context is None: + context = {} + return context.get('reconciled', 0) + + _defaults = { + 'reconciled': _get_reconciled, + } + + def return_stats(self, cr, uid, reconciled, context=None): + obj_model = self.pool.get('ir.model.data') + context = context or {} + context.update({'reconciled': reconciled}) + model_data_ids = obj_model.search( + cr, uid, + [('model','=','ir.ui.view'), + ('name','=','stat_account_automatic_reconcile_view1')] + ) + resource_id = obj_model.read( + cr, uid, model_data_ids, fields=['res_id'])[0]['res_id'] + return { + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'account.statement.import.automatic.reconcile', + 'views': [(resource_id,'form')], + 'type': 'ir.actions.act_window', + 'target': 'new', + 'context': context, + } + + def _below_write_off_limit(self, cr, uid, lines, + writeoff_limit, context=None): + + keys = ('debit', 'credit') + sums = reduce(lambda x, y: + dict((k, v + y[k]) for k, v in x.iteritems() if k in keys), + lines) + debit, credit = sums['debit'], sums['credit'] + writeoff_amount = debit - credit + return bool(writeoff_limit >= abs(writeoff_amount)) + + def _query_moves(self, cr, uid, form, context=None): + """Select all move (debit>0) as candidate. Optionnal choice on invoice + will filter with an inner join on the related moves. + """ + sql_params=[] + select_sql = ("SELECT " + "l.account_id, " + "l.ref as transaction_id, " + "l.name as origin, " + "l.id as invoice_id, " + "l.move_id as move_id, " + "l.id as move_line_id, " + "l.debit, l.credit, " + "l.partner_id " + "FROM account_move_line l " + "INNER JOIN account_move m " + "ON m.id = l.move_id ") + where_sql = ( + "WHERE " + # "AND l.move_id NOT IN %(invoice_move_ids)s " + "l.reconcile_id IS NULL " + # "AND NOT EXISTS (select id FROM account_invoice i WHERE i.move_id = m.id) " + "AND l.debit > 0 ") + if form.account_ids: + account_ids = [str(x.id) for x in form.account_ids] + sql_params = {'account_ids': tuple(account_ids)} + where_sql += "AND l.account_id in %(account_ids)s " + if form.invoice_ids: + invoice_ids = [str(x.id) for x in form.invoice_ids] + where_sql += "AND i.id IN %(invoice_ids)s " + select_sql += "INNER JOIN account_invoice i ON m.id = i.move_id " + sql_params['invoice_ids'] = tuple(invoice_ids) + if form.partner_ids: + partner_ids = [str(x.id) for x in form.partner_ids] + where_sql += "AND l.partner_id IN %(partner_ids)s " + sql_params['partner_ids'] = tuple(partner_ids) + sql = select_sql + where_sql + cr.execute(sql, sql_params) + return cr.dictfetchall() + + def _query_payments(self, cr, uid, account_id, invoice_move_ids, context=None): + sql_params = {'account_id': account_id, + 'invoice_move_ids': tuple(invoice_move_ids)} + sql = ("SELECT l.id, l.move_id, " + "l.ref, l.name, " + "l.debit, l.credit, " + "l.period_id as period_id, " + "l.partner_id " + "FROM account_move_line l " + "INNER JOIN account_move m " + "ON m.id = l.move_id " + "WHERE l.account_id = %(account_id)s " + "AND l.move_id NOT IN %(invoice_move_ids)s " + "AND l.reconcile_id IS NULL " + "AND NOT EXISTS (select id FROM account_invoice i WHERE i.move_id = m.id) " + "AND l.credit > 0") + cr.execute(sql, sql_params) + return cr.dictfetchall() + + @staticmethod + def _groupby_keys(keys, lines): + res = {} + key = keys.pop(0) + sorted_lines = sorted(lines, key=itemgetter(key)) + + for reference, iter_lines in groupby(sorted_lines, itemgetter(key)): + group_lines = list(iter_lines) + + if keys: + group_lines = (AccountsStatementAutoReconcile. + _groupby_keys(keys[:], group_lines)) + else: + # as we sort on all the keys, the last list + # is perforce alone in the list + group_lines = group_lines[0] + res[reference] = group_lines + + return res + + def _search_payment_ref(self, cr, uid, all_payments, + reference_key, reference, context=None): + def compare_key(payment, key, reference_patterns): + if not payment.get(key): + return False + if payment.get(key).lower() in reference_patterns: + return True + + res = [] + if not reference: + return res + + lref = reference.lower() + reference_patterns = (lref, 'tid_' + lref, 'tid_mag_' + lref) + res_append = res.append + for payment in all_payments: + if (compare_key(payment, 'ref', reference_patterns) or + compare_key(payment, 'name', reference_patterns)): + res_append(payment) + # remove payment from all_payments? + +# if res: +# print '----------------------------------' +# print 'ref: ' + reference +# for l in res: +# print (l.get('ref','') or '') + ' ' + (l.get('name','') or '') + return res + + def _search_payments(self, cr, uid, all_payments, + references, context=None): + payments = [] + for field_reference in references: + ref_key, reference = field_reference + payments = self._search_payment_ref( + cr, uid, all_payments, ref_key, reference, context=context) + # if match is found for one reference (transaction_id or origin) + # we have found our payments, don't need to search for the order + # reference + if payments: + break + return payments + + def reconcile(self, cr, uid, form_id, context=None): + context = context or {} + move_line_obj = self.pool.get('account.move.line') + period_obj = self.pool.get('account.period') + + if isinstance(form_id, list): + form_id = form_id[0] + + form = self.browse(cr, uid, form_id) + + allow_write_off = form.allow_write_off + + if not form.account_ids : + raise osv.except_osv(_('UserError'), + _('You must select accounts to reconcile')) + + # returns a list with a dict per line : + # [{'account_id': 5,'reference': 'A', 'move_id': 1, 'move_line_id': 1}, + # {'account_id': 5,'reference': 'A', 'move_id': 1, 'move_line_id': 2}, + # {'account_id': 6,'reference': 'B', 'move_id': 3, 'move_line_id': 3}], + moves = self._query_moves(cr, uid, form, context=context) + if not moves: + return False + # returns a tree : + # { 5: {1: {1: {'reference': 'A', 'move_id': 1, 'move_line_id': 1}}, + # {2: {'reference': 'A', 'move_id': 1, 'move_line_id': 2}}}}, + # 6: {3: {3: {'reference': 'B', 'move_id': 3, 'move_line_id': 3}}}}} + moves_tree = self._groupby_keys(['account_id', + 'move_id', + 'move_line_id'], + moves) + + reconciled = 0 + details = "" + for account_id, account_tree in moves_tree.iteritems(): + # [0] because one move id per invoice + account_move_ids = [move_tree.keys() for + move_tree in account_tree.values()] + + account_payments = self._query_payments(cr, uid, + account_id, + account_move_ids[0], + context=context) + + for move_id, move_tree in account_tree.iteritems(): + + # in any case one invoice = one move + # move_id, move_tree = invoice_tree.items()[0] + + move_line_ids = [] + move_lines = [] + move_lines_ids_append = move_line_ids.append + move_lines_append = move_lines.append + for move_line_id, vals in move_tree.iteritems(): + move_lines_ids_append(move_line_id) + move_lines_append(vals) + + # take the first one because the reference + # is the same everywhere for an invoice + transaction_id = move_lines[0]['transaction_id'] + origin = move_lines[0]['origin'] + partner_id = move_lines[0]['partner_id'] + + references = (('transaction_id', transaction_id), + ('origin', origin)) + + partner_payments = [p for p in account_payments if \ + p['partner_id'] == partner_id] + payments = self._search_payments( + cr, uid, partner_payments, references, context=context) + + if not payments: + continue + + payment_ids = [p['id'] for p in payments] + # take the period of the payment last move line + # it will be used as the reconciliation date + # and for the write off date + period_ids = [ml['period_id'] for ml in payments] + periods = period_obj.browse( + cr, uid, period_ids, context=context) + last_period = max(periods, key=attrgetter('date_stop')) + + reconcile_ids = move_line_ids + payment_ids + do_write_off = (allow_write_off and + self._below_write_off_limit( + cr, uid, move_lines + payments, + form.writeoff_amount_limit, + context=context)) + # date of reconciliation + rec_ctx = dict(context, date_p=last_period.date_stop) + try: + if do_write_off: + r_id = move_line_obj.reconcile(cr, + uid, + reconcile_ids, + 'auto', + form.writeoff_acc_id.id, + # period of the write-off + last_period.id, + form.journal_id.id, + context=rec_ctx) + logger.info("Auto statement reconcile: Reconciled with write-off move id %s" % (move_id,)) + else: + r_id = move_line_obj.reconcile_partial(cr, + uid, + reconcile_ids, + 'manual', + context=rec_ctx) + logger.info("Auto statement reconcile: Partial Reconciled move id %s" % (move_id,)) + except Exception, exc: + logger.error("Auto statement reconcile: Can't reconcile move id %s because: %s" % (move_id, exc,)) + reconciled += 1 + cr.commit() + return self.return_stats(cr, uid, reconciled, context) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/account_advanced_reconcile/wizard/statement_auto_reconcile_view.xml b/account_advanced_reconcile/wizard/statement_auto_reconcile_view.xml new file mode 100644 index 00000000..12c9855e --- /dev/null +++ b/account_advanced_reconcile/wizard/statement_auto_reconcile_view.xml @@ -0,0 +1,72 @@ + + + + + + Account Automatic Reconcile + account.statement.import.automatic.reconcile + form + +
+ +