mirror of
https://github.com/OCA/account-reconcile.git
synced 2025-01-20 12:27:39 +02:00
[REF] refactoring of account_advanced_reconcile
using account_easy_reconcile (lp:c2c-financial-addons/6.1 rev 24.2.1)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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()))
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
<record id="view_easy_reconcile_form" model="ir.ui.view">
|
||||
<field name="name">account.easy.reconcile.form</field>
|
||||
<field name="model">account.easy.reconcile</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="account_easy_reconcile.account_easy_reconcile_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<page name="information" position="inside">
|
||||
<group colspan="2" col="2">
|
||||
<separator colspan="4" string="Advanced. Partner and Ref"/>
|
||||
<label string="Match multiple debit vs multiple credit entries. Allow partial reconcilation.
|
||||
The lines should have the partner, the credit entry ref. is matched vs the debit entry ref. or name." colspan="4"/>
|
||||
</group>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
20
account_advanced_reconcile/wizard/__init__.py
Normal file
20
account_advanced_reconcile/wizard/__init__.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import statement_auto_reconcile
|
||||
338
account_advanced_reconcile/wizard/statement_auto_reconcile.py
Normal file
338
account_advanced_reconcile/wizard/statement_auto_reconcile.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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:
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="account_automatic_reconcile_view" model="ir.ui.view">
|
||||
<field name="name">Account Automatic Reconcile</field>
|
||||
<field name="model">account.statement.import.automatic.reconcile</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reconciliation">
|
||||
<separator string="Reconciliation" colspan="4"/>
|
||||
<label colspan="4" nolabel="1" string="For an invoice to be considered as paid, the invoice entries must be reconciled with counterparts, usually bank payments. It could also be reconciled with an intermediate account of a payment office (like PayPal, Amazone, ...).
|
||||
With this automatic reconciliation functionality, OpenERP makes its own search for entries to reconcile in a series of accounts. It finds entries for each transaction where the id or origin correspond."/>
|
||||
<newline/>
|
||||
<group>
|
||||
<field name="account_ids" colspan="4" domain="[('reconcile','=',True)]"/>
|
||||
<field name="partner_ids" colspan="4"/>
|
||||
<field name="invoice_ids" colspan="4" domain="[('type', '=', 'out_invoice'), ('state', '=', 'open')]"/>
|
||||
<field name="allow_write_off"/>
|
||||
</group>
|
||||
<newline/>
|
||||
<group attrs="{'readonly':[('allow_write_off', '!=', True)]}">
|
||||
<separator string="Write-Off Move" colspan="4"/>
|
||||
<field name="writeoff_acc_id" attrs="{ 'required':[('allow_write_off', '=', True)]}"/>
|
||||
<field name="writeoff_amount_limit" attrs="{ 'required':[('allow_write_off', '=', True)]}"/>
|
||||
<field name="journal_id" attrs="{ 'required':[('allow_write_off', '=', True)]}"/>
|
||||
</group>
|
||||
<separator string ="" colspan="4"/>
|
||||
<group colspan="2" col="4">
|
||||
<button special="cancel" string="Cancel" icon="gtk-cancel"/>
|
||||
<button name="reconcile" string="Reconcile" type="object" icon="terp-stock_effects-object-colorize"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_automatic_reconcile" model="ir.actions.act_window">
|
||||
<field name="name">Account Automatic Reconcile</field>
|
||||
<field name="res_model">account.statement.import.automatic.reconcile</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="account_automatic_reconcile_view"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
icon="STOCK_EXECUTE"
|
||||
name="Automatic Statement Reconciliation"
|
||||
action="action_account_automatic_reconcile"
|
||||
id="menu_automatic_reconcile"
|
||||
parent="account.menu_finance_periodical_processing"
|
||||
/>
|
||||
|
||||
<record id="stat_account_automatic_reconcile_view1" model="ir.ui.view">
|
||||
<field name="name">Automatic reconcile unreconcile</field>
|
||||
<field name="model">account.statement.import.automatic.reconcile</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Statement Reconciliation result">
|
||||
<field name="reconciled"/>
|
||||
<newline/>
|
||||
<group colspan="4" col="6">
|
||||
<separator colspan="6"/>
|
||||
<button special="cancel" string="Ok" icon="terp-dialog-close" default_focus="1"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
Reference in New Issue
Block a user