[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:
@
2012-06-12 22:41:47 +02:00
committed by mpanarin
parent c769d391c5
commit 78f7fdf929
9 changed files with 446 additions and 510 deletions

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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()))

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View 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

View 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:

View File

@@ -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>