From 78f7fdf92971a89697bcfc08e1e8f1674fd694c0 Mon Sep 17 00:00:00 2001
From: "@" <@>
Date: Tue, 12 Jun 2012 22:41:47 +0200
Subject: [PATCH] [REF] refactoring of account_advanced_reconcile
using account_easy_reconcile (lp:c2c-financial-addons/6.1 rev 24.2.1)
---
account_advanced_reconcile/__init__.py | 8 +-
account_advanced_reconcile/__openerp__.py | 67 +---
.../advanced_reconciliation.py | 120 -------
.../base_advanced_reconciliation.py | 274 --------------
account_advanced_reconcile/easy_reconcile.py | 37 --
.../easy_reconcile_view.xml | 20 --
account_advanced_reconcile/wizard/__init__.py | 20 ++
.../wizard/statement_auto_reconcile.py | 338 ++++++++++++++++++
.../wizard/statement_auto_reconcile_view.xml | 72 ++++
9 files changed, 446 insertions(+), 510 deletions(-)
delete mode 100644 account_advanced_reconcile/advanced_reconciliation.py
delete mode 100644 account_advanced_reconcile/base_advanced_reconciliation.py
delete mode 100644 account_advanced_reconcile/easy_reconcile.py
delete mode 100644 account_advanced_reconcile/easy_reconcile_view.xml
create mode 100644 account_advanced_reconcile/wizard/__init__.py
create mode 100644 account_advanced_reconcile/wizard/statement_auto_reconcile.py
create mode 100644 account_advanced_reconcile/wizard/statement_auto_reconcile_view.xml
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_advanced_reconcile/wizard/__init__.py b/account_advanced_reconcile/wizard/__init__.py
new file mode 100644
index 00000000..f72fd976
--- /dev/null
+++ b/account_advanced_reconcile/wizard/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# 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 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+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
+
+
+
+
+
+
+ Account Automatic Reconcile
+ account.statement.import.automatic.reconcile
+ ir.actions.act_window
+ form
+ tree,form
+
+ new
+
+
+
+
+
+ Automatic reconcile unreconcile
+ account.statement.import.automatic.reconcile
+ form
+
+
+
+
+
+
+