[IMP] account_easy_reconcile: extracted classes in modules

(lp:account-extra-addons rev 26.1.10)
This commit is contained in:
@
2012-06-13 16:35:52 +02:00
committed by mpanarin
parent 9fe7de21bd
commit 1310d2f7ba
6 changed files with 617 additions and 466 deletions

View File

@@ -1,21 +1,24 @@
# -*- coding: utf-8 -*-
#########################################################################
# #
# Copyright (C) 2010 Sébastien Beau #
# #
#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/>. #
#########################################################################
##############################################################################
#
# Copyright 2012 Camptocamp SA (Guewen Baconnier)
# Copyright (C) 2010 Sébastien Beau
#
# 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 easy_reconcile
import base_reconciliation
import simple_reconciliation

View File

@@ -1,35 +1,56 @@
# -*- coding: utf-8 -*-
#########################################################################
# #
# Copyright (C) 2010 Sébastien Beau #
# #
#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/>. #
#########################################################################
##############################################################################
#
# Copyright 2012 Camptocamp SA (Guewen Baconnier)
# Copyright (C) 2010 Sébastien Beau
#
# 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/>.
#
##############################################################################
{
"name" : "Easy Reconcile",
"version" : "1.0",
"depends" : ["account", "base_scheduler_creator"
],
"author" : "Sébastien Beau",
"description": """A new view to reconcile easily your account
"author" : "Akretion,Camptocamp",
"description": """
This is a shared work between Akretion and Camptocamp in order to provide:
- 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
2 simple reconciliation methods are integrated in this module, the simple
reconciliations works on 2 lines (1 debit / 1 credit) and do not allows
partial reconcilation, they also match on 1 key, partner or entry name.
You may be interested to install also the account_advanced_reconciliation
module available at: https://code.launchpad.net/c2c-financial-addons
This latter add more complex reconciliations, allows multiple lines and partial.
""",
"website" : "http://www.akretion.com/",
"category" : "Customer Modules",
"category" : "Finance",
"init_xml" : [],
"demo_xml" : [],
"update_xml" : ["easy_reconcile.xml"],
"active": False,
'license': 'AGPL-3',
"auto_install": False,
"installable": True,
}

View File

@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright 2012 Camptocamp SA (Guewen Baconnier)
# Copyright (C) 2010 Sébastien Beau
#
# 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 AbstractModel
from openerp.osv import fields
from operator import itemgetter, attrgetter
class easy_reconcile_base(AbstractModel):
"""Abstract Model for reconciliation methods"""
_name = 'easy.reconcile.base'
_inherit = 'easy.reconcile.options'
_auto = True # restore property set to False by AbstractModel
_columns = {
'account_id': fields.many2one('account.account', 'Account', required=True),
'partner_ids': fields.many2many('res.partner',
string="Restrict on partners"),
# other columns are inherited from easy.reconcile.options
}
def automatic_reconcile(self, cr, uid, ids, context=None):
"""
:return: list of reconciled ids, list of partially reconciled entries
"""
if isinstance(ids, (int, long)):
ids = [ids]
assert len(ids) == 1, "Has to be called on one id"
rec = self.browse(cr, uid, ids[0], context=context)
return self._action_rec(cr, uid, rec, context=context)
def _action_rec(self, cr, uid, rec, context=None):
"""Must be inherited to implement the reconciliation
:return: list of reconciled ids
"""
raise NotImplementedError
def _base_columns(self, rec):
"""Mandatory columns for move lines queries
An extra column aliased as `key` should be defined
in each query."""
aml_cols = (
'id',
'debit',
'credit',
'date',
'period_id',
'ref',
'name',
'partner_id',
'account_id',
'move_id')
return ["account_move_line.%s" % col for col in aml_cols]
def _select(self, rec, *args, **kwargs):
return "SELECT %s" % ', '.join(self._base_columns(rec))
def _from(self, rec, *args, **kwargs):
return "FROM account_move_line"
def _where(self, rec, *args, **kwargs):
where = ("WHERE account_move_line.account_id = %s "
"AND account_move_line.reconcile_id IS NULL ")
# it would be great to use dict for params
# but as we use _where_calc in _get_filter
# which returns a list, we have to
# accomodate with that
params = [rec.account_id.id]
if rec.partner_ids:
where += " AND account_move_line.partner_id IN %s"
params.append(tuple([l.id for l in rec.partner_ids]))
return where, params
def _get_filter(self, cr, uid, rec, context):
ml_obj = self.pool.get('account.move.line')
where = ''
params = []
if rec.filter:
dummy, where, params = ml_obj._where_calc(
cr, uid, rec.filter, context=context).get_sql()
if where:
where = " AND %s" % where
return where, params
def _below_writeoff_limit(self, cr, uid, rec, lines,
writeoff_limit, context=None):
precision = self.pool.get('decimal.precision').precision_get(
cr, uid, 'Account')
keys = ('debit', 'credit')
sums = reduce(
lambda line, memo:
dict((key, value + memo[key])
for key, value
in line.iteritems()
if key in keys), lines)
debit, credit = sums['debit'], sums['credit']
writeoff_amount = round(debit - credit, precision)
return bool(writeoff_limit >= abs(writeoff_amount)), debit, credit
def _get_rec_date(self, cr, uid, rec, lines, based_on='end_period_last_credit', context=None):
period_obj = self.pool.get('account.period')
def last_period(mlines):
period_ids = [ml['period_id'] for ml in mlines]
periods = period_obj.browse(
cr, uid, period_ids, context=context)
return max(periods, key=attrgetter('date_stop'))
def last_date(mlines):
return max(mlines, key=itemgetter('date'))
def credit(mlines):
return [l for l in mlines if l['credit'] > 0]
def debit(mlines):
return [l for l in mlines if l['debit'] > 0]
if based_on == 'end_period_last_credit':
return last_period(credit(lines)).date_stop
if based_on == 'end_period':
return last_period(lines).date_stop
elif based_on == 'newest':
return last_date(lines)['date']
elif based_on == 'newest_credit':
return last_date(credit(lines))['date']
elif based_on == 'newest_debit':
return last_date(debit(lines))['date']
# reconcilation date will be today
# when date is None
return None
def _reconcile_lines(self, cr, uid, rec, lines, allow_partial=False, context=None):
""" Try to reconcile given lines
:param list lines: list of dict of move lines, they must at least
contain values for : id, debit, credit
:param boolean allow_partial: if True, partial reconciliation will be
created, otherwise only Full reconciliation will be created
:return: tuple of boolean values, first item is wether the the entries
have been reconciled or not, the second is wether the reconciliation
is full (True) or partial (False)
"""
if context is None:
context = {}
ml_obj = self.pool.get('account.move.line')
writeoff = rec.write_off
keys = ('debit', 'credit')
line_ids = [l['id'] for l in lines]
below_writeoff, sum_debit, sum_credit = self._below_writeoff_limit(
cr, uid, rec, lines, writeoff, context=context)
date = self._get_rec_date(
cr, uid, rec, lines, rec.date_base_on, context=context)
rec_ctx = dict(context, date_p=date)
if below_writeoff:
if sum_credit < sum_debit:
writeoff_account_id = rec.account_profit_id.id
else:
writeoff_account_id = rec.account_lost_id.id
period_id = self.pool.get('account.period').find(
cr, uid, dt=date, context=context)[0]
ml_obj.reconcile(
cr, uid,
line_ids,
type='auto',
writeoff_acc_id=writeoff_account_id,
writeoff_period_id=period_id,
writeoff_journal_id=rec.journal_id.id,
context=rec_ctx)
return True, True
elif allow_partial:
ml_obj.reconcile_partial(
cr, uid,
line_ids,
type='manual',
context=rec_ctx)
return True, False
return False, False

View File

@@ -1,58 +1,80 @@
# -*- coding: utf-8 -*-
#########################################################################
# #
# Copyright (C) 2010 Sébastien Beau #
# Copyright (C) 2012 Camptocamp SA (authored by Guewen Baconnier) #
# #
#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/>. #
#########################################################################
##############################################################################
#
# Copyright 2012 Camptocamp SA (Guewen Baconnier)
# Copyright (C) 2010 Sébastien Beau
#
# 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 time
import string
from operator import itemgetter, attrgetter
from openerp.osv.orm import Model, TransientModel, AbstractModel
from openerp.osv.orm import Model, AbstractModel
from openerp.osv import fields
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
class easy_reconcile_options(AbstractModel):
"""Options of a reconciliation profile, columns
shared by the configuration of methods and by the
reconciliation wizards. This allows decoupling
of the methods with the wizards and allows to
launch the wizards alone
"""
_name = 'easy.reconcile.options'
def _get_rec_base_date(self, cr, uid, context=None):
return [('end_period_last_credit', 'End of period of most recent credit'),
('newest', 'Most recent move line'),
('actual', 'Today'),
('end_period', 'End of period of most recent move line'),
('newest_credit', 'Date of most recent credit'),
('newest_debit', 'Date of most recent debit')]
_columns = {
'write_off': fields.float('Write off allowed'),
'account_lost_id': fields.many2one('account.account', 'Account Lost'),
'account_profit_id': fields.many2one('account.account', 'Account Profit'),
'journal_id': fields.many2one('account.journal', 'Journal'),
'date_base_on': fields.selection(_get_rec_base_date,
required=True,
string='Date of reconcilation'),
'filter': fields.char('Filter', size=128),
}
_defaults = {
'write_off': 0.,
'date_base_on': 'end_period_last_credit',
}
class account_easy_reconcile_method(Model):
_name = 'account.easy.reconcile.method'
_description = 'reconcile method for account_easy_reconcile'
def onchange_name(self, cr, uid, id, name, write_off, context=None):
if name in ['easy.reconcile.simple.name',
'easy.reconcile.simple.partner']:
if write_off>0:
return {'value': {'require_write_off': True, 'require_account_id': True, 'require_journal_id': True}}
return {'value': {'require_write_off': True}}
return {}
_inherit = 'easy.reconcile.options'
_auto = True # restore property set to False by AbstractModel
def onchange_write_off(self, cr, uid, id, name, write_off, context=None):
if name in ['easy.reconcile.simple.name',
'easy.reconcile.simple.partner']:
if write_off>0:
return {'value': {'require_account_id': True, 'require_journal_id': True}}
else:
return {'value': {'require_account_id': False, 'require_journal_id': False}}
return {}
_order = 'sequence'
def _get_all_rec_method(self, cr, uid, context=None):
return [
('easy.reconcile.simple.name', 'Simple method based on amount and name'),
('easy.reconcile.simple.partner', 'Simple method based on amount and partner'),
('easy.reconcile.simple.name', 'Simple. Amount and Name'),
('easy.reconcile.simple.partner', 'Simple. Amount and Partner'),
]
def _get_rec_method(self, cr, uid, context=None):
@@ -60,32 +82,16 @@ class account_easy_reconcile_method(Model):
_columns = {
'name': fields.selection(_get_rec_method, 'Type', size=128, required=True),
'sequence': fields.integer('Sequence', required=True, help="The sequence field is used to order the reconcile method"),
'write_off': fields.float('Write off Value'),
'account_lost_id': fields.many2one('account.account', 'Account Lost'),
'account_profit_id': fields.many2one('account.account', 'Account Profit'),
'journal_id': fields.many2one('account.journal', 'Journal'),
'require_write_off': fields.boolean('Require Write-off'),
'require_account_id': fields.boolean('Require Account'),
'require_journal_id': fields.boolean('Require Journal'),
'date_base_on': fields.selection(
[('newest', 'Most recent move line'),
('actual', 'Today'),
('end_period_last_credit', 'End of period of most recent credit'),
('end_period', 'End of period of most recent move line'),
('newest_credit', 'Date of most recent credit'),
('newest_debit', 'Date of most recent debit')],
string='Date of reconcilation'),
'filter': fields.char('Filter', size=128),
'task_id': fields.many2one('account.easy.reconcile', 'Task', required=True, ondelete='cascade'),
'sequence': fields.integer('Sequence', required=True,
help="The sequence field is used to order the reconcile method"),
'task_id': fields.many2one('account.easy.reconcile', 'Task',
required=True, ondelete='cascade'),
}
_defaults = {
'write_off': lambda *a: 0,
'sequence': 1,
}
_order = 'sequence'
def init(self, cr):
""" Migration stuff, name is not anymore methods names
but models name"""
@@ -100,16 +106,34 @@ class account_easy_reconcile_method(Model):
WHERE name = 'action_rec_auto_name'
""")
class account_easy_reconcile(Model):
_name = 'account.easy.reconcile'
_description = 'account easy reconcile'
def _get_unrec_number(self, cr, uid, ids, name, arg, context=None):
def _get_total_unrec(self, cr, uid, ids, name, arg, context=None):
obj_move_line = self.pool.get('account.move.line')
res={}
for task in self.read(cr, uid, ids, ['account'], context=context):
res[task['id']] = len(obj_move_line.search(cr, uid, [('account_id', '=', task['account'][0]), ('reconcile_id', '=', False)], context=context))
res = {}
for task in self.browse(cr, uid, ids, context=context):
res[task.id] = len(obj_move_line.search(
cr, uid,
[('account_id', '=', task.account.id),
('reconcile_id', '=', False),
('reconcile_partial_id', '=', False)],
context=context))
return res
def _get_partial_rec(self, cr, uid, ids, name, arg, context=None):
obj_move_line = self.pool.get('account.move.line')
res = {}
for task in self.browse(cr, uid, ids, context=context):
res[task.id] = len(obj_move_line.search(
cr, uid,
[('account_id', '=', task.account.id),
('reconcile_id', '=', False),
('reconcile_partial_id', '!=', False)],
context=context))
return res
_columns = {
@@ -118,285 +142,65 @@ class account_easy_reconcile(Model):
'reconcile_method': fields.one2many('account.easy.reconcile.method', 'task_id', 'Method'),
'scheduler': fields.many2one('ir.cron', 'scheduler', readonly=True),
'rec_log': fields.text('log', readonly=True),
'unreconcile_entry_number': fields.function(_get_unrec_number, method=True, type='integer', string='Unreconcile Entries'),
'unreconciled_count': fields.function(_get_total_unrec,
type='integer', string='Fully Unreconciled Entries'),
'reconciled_partial_count': fields.function(_get_partial_rec,
type='integer', string='Partially Reconciled Entries'),
}
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
default = dict(default, rec_log=False, scheduler=False)
return super(account_easy_reconcile, self).copy_data(
cr, uid, id, default=default, context=context)
def _prepare_run_transient(self, cr, uid, rec_method, context=None):
return {'account_id': rec_method.task_id.account.id,
'write_off': rec_method.write_off,
'account_lost_id': rec_method.account_lost_id and \
rec_method.account_lost_id.id,
'account_profit_id': rec_method.account_profit_id and \
rec_method.account_profit_id.id,
'journal_id': rec_method.journal_id and rec_method.journal_id.id,
'date_base_on': rec_method.date_base_on,
'filter': rec_method.filter}
def run_reconcile(self, cr, uid, ids, context=None):
if context is None:
context = {}
for rec_id in ids:
rec = self.browse(cr, uid, rec_id, context=context)
total_rec = 0
details = ''
total_partial_rec = 0
details = []
count = 0
for method in rec.reconcile_method:
count += 1
ctx = dict(
context,
date_base_on=method.date_base_on,
filter=eval(method.filter or '[]'),
write_off=(method.write_off > 0 and method.write_off) or 0,
account_lost_id=method.account_lost_id.id,
account_profit_id=method.account_profit_id.id,
journal_id=method.journal_id.id)
rec_model = self.pool.get(method.name)
auto_rec_id = rec_model.create(
cr, uid, {'easy_reconcile_id': rec_id}, context=ctx)
res = rec_model.automatic_reconcile(cr, uid, auto_rec_id, context=ctx)
cr, uid,
self._prepare_run_transient(cr, uid, method, context=context),
context=context)
rec_ids, partial_ids = rec_model.automatic_reconcile(
cr, uid, auto_rec_id, context=context)
details.append(_('method %d : full: %d lines, partial: %d lines') % \
(count, len(rec_ids), len(partial_ids)))
total_rec += len(rec_ids)
total_partial_rec += len(partial_ids)
details += _(' method %d : %d lines |') % (count, res)
log = self.read(cr, uid, rec_id, ['rec_log'], context=context)['rec_log']
log_lines = log and log.splitlines() or []
log_lines[0:0] = [_('%s : %d lines have been reconciled (%s)') %
(time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), total_rec, details[0:-2])]
log_lines[0:0] = [_("%s : %d lines have been fully reconciled" \
" and %d lines have been partially reconciled (%s)") % \
(time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), total_rec,
total_partial_rec, ' | '.join(details))]
log = "\n".join(log_lines)
self.write(cr, uid, rec_id, {'rec_log': log}, context=context)
return True
class easy_reconcile_base(AbstractModel):
"""Abstract Model for reconciliation methods"""
_name = 'easy.reconcile.base'
_columns = {
'easy_reconcile_id': fields.many2one('account.easy.reconcile', string='Easy Reconcile')
}
def automatic_reconcile(self, cr, uid, ids, context=None):
"""Must be inherited to implement the reconciliation"""
raise NotImplementedError
def _base_columns(self, rec):
"""Mandatory columns for move lines queries
An extra column aliased as `key` should be defined
in each query."""
aml_cols = (
'id',
'debit',
'credit',
'date',
'period_id',
'ref',
'name',
'partner_id',
'account_id',
'move_id')
return ["account_move_line.%s" % col for col in aml_cols]
def _select(self, rec, *args, **kwargs):
return "SELECT %s" % ', '.join(self._base_columns(rec))
def _from(self, rec, *args, **kwargs):
return "FROM account_move_line"
def _where(self, rec, *args, **kwargs):
where = ("WHERE account_move_line.account_id = %s "
"AND account_move_line.reconcile_id IS NULL ")
# it would be great to use dict for params
# but as we use _where_calc in _get_filter
# which returns a list, we have to
# accomodate with that
params = [rec.easy_reconcile_id.account.id]
return where, params
def _get_filter(self, cr, uid, rec, context):
ml_obj = self.pool.get('account.move.line')
where = ''
params = []
if context.get('filter'):
dummy, where, params = ml_obj._where_calc(
cr, uid, context['filter'], context=context).get_sql()
if where:
where = " AND %s" % where
return where, params
def _below_writeoff_limit(self, cr, uid, lines,
writeoff_limit, context=None):
precision = self.pool.get('decimal.precision').precision_get(
cr, uid, 'Account')
keys = ('debit', 'credit')
sums = reduce(
lambda line, memo:
dict((key, value + memo[key])
for key, value
in line.iteritems()
if key in keys), lines)
debit, credit = sums['debit'], sums['credit']
writeoff_amount = round(debit - credit, precision)
return bool(writeoff_limit >= abs(writeoff_amount)), debit, credit
def _get_rec_date(self, cr, uid, lines, based_on='end_period_last_credit', context=None):
period_obj = self.pool.get('account.period')
def last_period(mlines):
period_ids = [ml['period_id'] for ml in mlines]
periods = period_obj.browse(
cr, uid, period_ids, context=context)
return max(periods, key=attrgetter('date_stop'))
def last_date(mlines):
return max(mlines, key=itemgetter('date'))
def credit(mlines):
return [l for l in mlines if l['credit'] > 0]
def debit(mlines):
return [l for l in mlines if l['debit'] > 0]
if based_on == 'end_period_last_credit':
return last_period(credit(lines)).date_stop
if based_on == 'end_period':
return last_period(lines).date_stop
elif based_on == 'newest':
return last_date(lines)['date']
elif based_on == 'newest_credit':
return last_date(credit(lines))['date']
elif based_on == 'newest_debit':
return last_date(debit(lines))['date']
# reconcilation date will be today
# when date is None
return None
def _reconcile_lines(self, cr, uid, lines, allow_partial=False, context=None):
if context is None:
context = {}
ml_obj = self.pool.get('account.move.line')
writeoff = context.get('write_off', 0.)
keys = ('debit', 'credit')
line_ids = [l['id'] for l in lines]
below_writeoff, sum_debit, sum_credit = self._below_writeoff_limit(
cr, uid, lines, writeoff, context=context)
date = self._get_rec_date(
cr, uid, lines, context.get('date_base_on'), context=context)
rec_ctx = dict(context, date_p=date)
if below_writeoff:
if sum_credit < sum_debit:
writeoff_account_id = context.get('account_profit_id', False)
else:
writeoff_account_id = context.get('account_lost_id', False)
period_id = self.pool.get('account.period').find(
cr, uid, dt=date, context=context)[0]
ml_obj.reconcile(
cr, uid,
line_ids,
type='auto',
writeoff_acc_id=writeoff_account_id,
writeoff_period_id=period_id,
writeoff_journal_id=context.get('journal_id'),
context=rec_ctx)
return True
elif allow_partial:
ml_obj.reconcile_partial(
cr, uid,
line_ids,
type='manual',
context=rec_ctx)
return True
return False
class easy_reconcile_simple(AbstractModel):
_name = 'easy.reconcile.simple'
_inherit = 'easy.reconcile.base'
# has to be subclassed
# field name used as key for matching the move lines
_key_field = None
def rec_auto_lines_simple(self, cr, uid, lines, context=None):
if context is None:
context = {}
if self._key_field is None:
raise ValueError("_key_field has to be defined")
count = 0
res = 0
while (count < len(lines)):
for i in range(count+1, len(lines)):
writeoff_account_id = False
if lines[count][self._key_field] != lines[i][self._key_field]:
break
check = False
if lines[count]['credit'] > 0 and lines[i]['debit'] > 0:
credit_line = lines[count]
debit_line = lines[i]
check = True
elif lines[i]['credit'] > 0 and lines[count]['debit'] > 0:
credit_line = lines[i]
debit_line = lines[count]
check = True
if not check:
continue
if self._reconcile_lines(cr, uid, [credit_line, debit_line],
allow_partial=False, context=context):
res += 2
del lines[i]
break
count += 1
return res
def _simple_order(self, rec, *args, **kwargs):
return "ORDER BY account_move_line.%s" % self._key_field
def _action_rec_simple(self, cr, uid, rec, context=None):
"""Match only 2 move lines, do not allow partial reconcile"""
select = self._select(rec)
select += ", account_move_line.%s " % self._key_field
where, params = self._where(rec)
where += " AND account_move_line.%s IS NOT NULL " % self._key_field
where2, params2 = self._get_filter(cr, uid, rec, context=context)
query = ' '.join((
select,
self._from(rec),
where, where2,
self._simple_order(rec)))
cr.execute(query, params + params2)
lines = cr.dictfetchall()
return self.rec_auto_lines_simple(cr, uid, lines, context)
def automatic_reconcile(self, cr, uid, ids, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
assert len(ids) == 1, "Has to be called on one id"
rec = self.browse(cr, uid, ids[0], context=context)
return self._action_rec_simple(cr, uid, rec, context=context)
class easy_reconcile_simple_name(TransientModel):
_name = 'easy.reconcile.simple.name'
_inherit = 'easy.reconcile.simple'
_auto = True # False when inherited from AbstractModel
# has to be subclassed
# field name used as key for matching the move lines
_key_field = 'name'
class easy_reconcile_simple_partner(TransientModel):
_name = 'easy.reconcile.simple.partner'
_inherit = 'easy.reconcile.simple'
_auto = True # False when inherited from AbstractModel
# has to be subclassed
# field name used as key for matching the move lines
_key_field = 'partner'

View File

@@ -1,121 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<data>
<!-- account.easy.reconcile view -->
<record id="account_easy_reconcile_form" model="ir.ui.view">
<field name="name">account.easy.reconcile.form</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="easy reconcile">
<separator colspan="4" string="Task Information" />
<field name="name" select="1"/>
<field name="account"/>
<field name="unreconcile_entry_number"/>
<field name="scheduler"/>
<separator colspan="4" string="Reconcile Method" />
<!-- account.easy.reconcile view -->
<record id="account_easy_reconcile_form" model="ir.ui.view">
<field name="name">account.easy.reconcile.form</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Automatic Easy Reconcile">
<separator colspan="4" string="Task Information" />
<field name="name" select="1"/>
<field name="account"/>
<field name="unreconciled_count"/>
<field name="reconciled_partial_count"/>
<field name="scheduler"/>
<separator colspan="4" string="Reconcile Method" />
<notebook colspan="4">
<page name="methods" string="Configuration">
<field name="reconcile_method" colspan = "4" nolabel="1"/>
<button icon="gtk-ok" name="run_reconcile" colspan = "4" string="Start Auto Reconcilation" type="object"/>
<separator colspan="4" string="Log" />
<field name="rec_log" colspan = "4" nolabel="1"/>
</form>
</field>
</record>
<record id="account_easy_reconcile_tree" model="ir.ui.view">
<field name="name">account.easy.reconcile.tree</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="easy reconcile">
<field name="name"/>
<field name="account"/>
<field name="reconcile_method"/>
<field name="scheduler"/>
<field name="unreconcile_entry_number"/>
</tree>
</field>
</record>
<record id="action_account_easy_reconcile" model="ir.actions.act_window">
<field name="name">account easy reconcile</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.easy.reconcile</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'wizard_object' : 'account.easy.reconcile', 'function' : 'action_rec_auto', 'object_link' : 'account.easy.reconcile' }</field>
</record>
</page>
<page name="information" string="Information">
<separator colspan="4" string="Simple. Amount and Name"/>
<label string="Match one debit line vs one credit line. Do not allow partial reconcilation.
The lines should have the same amount (with the write-off) and the same name to be reconciled." colspan="4"/>
<separator colspan="4" string="Simple. Amount and Name"/>
<label string="Match one debit line vs one credit line. Do not allow partial reconcilation.
The lines should have the same amount (with the write-off) and the same partner to be reconciled." colspan="4"/>
</page>
</notebook>
<button icon="gtk-ok" name="run_reconcile" colspan = "4" string="Start Auto Reconcilation" type="object"/>
<separator colspan="4" string="Log" />
<field name="rec_log" colspan = "4" nolabel="1"/>
</form>
</field>
</record>
<record id="account_easy_reconcile_tree" model="ir.ui.view">
<field name="name">account.easy.reconcile.tree</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Automatic Easy Reconcile">
<field name="name"/>
<field name="account"/>
<field name="scheduler"/>
<field name="unreconciled_count"/>
<field name="reconciled_partial_count"/>
</tree>
</field>
</record>
<record id="action_account_easy_reconcile" model="ir.actions.act_window">
<field name="name">Easy Automatic Reconcile</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.easy.reconcile</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'wizard_object' : 'account.easy.reconcile', 'function' : 'action_rec_auto', 'object_link' : 'account.easy.reconcile' }</field>
</record>
<!-- account.easy.reconcile.method view -->
<record id="account_easy_reconcile_method_form" model="ir.ui.view">
<field name="name">account.easy.reconcile.method.form</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile.method</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="easy reconcile method">
<field name="sequence"/>
<field name="name" on_change="onchange_name(name, write_off)"/>
<field name="write_off" attrs="{'required':[('require_write_off','=',True)]}"/>
<field name="account_lost_id" attrs="{'required':[('require_account_id','=',True)]}"/>
<field name="account_profit_id" attrs="{'required':[('require_account_id','=',True)]}"/>
<field name="journal_id" attrs="{'required':[('require_journal_id','=',True)]}"/>
<field name="date_base_on"/>
<field name="filter"/>
<field name="require_write_off" invisible="1"/>
<field name="require_account_id" invisible="1"/>
<field name="require_journal_id" invisible="1"/>
</form>
</field>
</record>
<record id="account_easy_reconcile_method_tree" model="ir.ui.view">
<field name="name">account.easy.reconcile.method.tree</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile.method</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree editable="top" string="easy reconcile method">
<field name="sequence"/>
<field name="name" on_change="onchange_name(name, write_off)"/>
<field name="write_off" attrs="{'required':[('require_write_off','=',True)]}" on_change="onchange_write_off(name, write_off)"/>
<field name="account_lost_id" attrs="{'required':[('require_account_id','=',True)]}"/>
<field name="account_profit_id" attrs="{'required':[('require_account_id','=',True)]}"/>
<field name="journal_id" attrs="{'required':[('require_journal_id','=',True)]}"/>
<field name="date_base_on"/>
<field name="filter"/>
<field name="require_write_off" invisible="1"/>
<field name="require_account_id" invisible="1"/>
<field name="require_journal_id" invisible="1"/>
<record id="account_easy_reconcile_method_form" model="ir.ui.view">
<field name="name">account.easy.reconcile.method.form</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile.method</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Automatic Easy Reconcile Method">
<field name="sequence"/>
<field name="name"/>
<field name="write_off"/>
<field name="account_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="account_profit_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="journal_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="date_base_on"/>
<field name="filter" groups="base.group_extended"/>
</form>
</field>
</record>
</tree>
</field>
</record>
<record id="account_easy_reconcile_method_tree" model="ir.ui.view">
<field name="name">account.easy.reconcile.method.tree</field>
<field name="priority">20</field>
<field name="model">account.easy.reconcile.method</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree editable="top" string="Automatic Easy Reconcile Method">
<field name="sequence"/>
<field name="name"/>
<field name="write_off"/>
<field name="account_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="account_profit_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="journal_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="date_base_on"/>
<field name="filter"/>
</tree>
</field>
</record>
<!-- menu item -->
<menuitem action="action_account_easy_reconcile" id="menu_easy_reconcile" parent="account.periodical_processing_reconciliation"/>
<menuitem action="action_account_easy_reconcile" id="menu_easy_reconcile" parent="account.periodical_processing_reconciliation"/>
<!-- button on the left -->
<record id="ir_action_create_scheduler_in_easy_reconcile" model="ir.values">
<field name="key2">client_action_multi</field>
<field name="model">account.easy.reconcile</field>
<field name="name">Create a Scheduler</field>
<field eval="'ir.actions.act_window,%d'%ref('base_scheduler_creator.action_scheduler_creator_wizard')" name="value"/>
<field eval="True" name="object"/>
</record>
<record id="ir_action_create_scheduler_in_easy_reconcile" model="ir.values">
<field name="key2">client_action_multi</field>
<field name="model">account.easy.reconcile</field>
<field name="name">Create a Scheduler</field>
<field eval="'ir.actions.act_window,%d'%ref('base_scheduler_creator.action_scheduler_creator_wizard')" name="value"/>
<field eval="True" name="object"/>
</record>
</data>
</data>
</openerp>

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright 2012 Camptocamp SA (Guewen Baconnier)
# Copyright (C) 2010 Sébastien Beau
#
# 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 AbstractModel, TransientModel
class easy_reconcile_simple(AbstractModel):
_name = 'easy.reconcile.simple'
_inherit = 'easy.reconcile.base'
# has to be subclassed
# field name used as key for matching the move lines
_key_field = None
def rec_auto_lines_simple(self, cr, uid, rec, lines, context=None):
if context is None:
context = {}
if self._key_field is None:
raise ValueError("_key_field has to be defined")
count = 0
res = []
while (count < len(lines)):
for i in range(count+1, len(lines)):
writeoff_account_id = False
if lines[count][self._key_field] != lines[i][self._key_field]:
break
check = False
if lines[count]['credit'] > 0 and lines[i]['debit'] > 0:
credit_line = lines[count]
debit_line = lines[i]
check = True
elif lines[i]['credit'] > 0 and lines[count]['debit'] > 0:
credit_line = lines[i]
debit_line = lines[count]
check = True
if not check:
continue
reconciled, dummy = self._reconcile_lines(
cr, uid, rec, [credit_line, debit_line],
allow_partial=False, context=context)
if reconciled:
res += [credit_line['id'], debit_line['id']]
del lines[i]
break
count += 1
return res, [] # empty list for partial, only full rec in "simple" rec
def _simple_order(self, rec, *args, **kwargs):
return "ORDER BY account_move_line.%s" % self._key_field
def _action_rec(self, cr, uid, rec, context=None):
"""Match only 2 move lines, do not allow partial reconcile"""
select = self._select(rec)
select += ", account_move_line.%s " % self._key_field
where, params = self._where(rec)
where += " AND account_move_line.%s IS NOT NULL " % self._key_field
where2, params2 = self._get_filter(cr, uid, rec, context=context)
query = ' '.join((
select,
self._from(rec),
where, where2,
self._simple_order(rec)))
cr.execute(query, params + params2)
lines = cr.dictfetchall()
return self.rec_auto_lines_simple(cr, uid, rec, lines, context)
class easy_reconcile_simple_name(TransientModel):
_name = 'easy.reconcile.simple.name'
_inherit = 'easy.reconcile.simple'
_auto = True # False when inherited from AbstractModel
# has to be subclassed
# field name used as key for matching the move lines
_key_field = 'name'
class easy_reconcile_simple_partner(TransientModel):
_name = 'easy.reconcile.simple.partner'
_inherit = 'easy.reconcile.simple'
_auto = True # False when inherited from AbstractModel
# has to be subclassed
# field name used as key for matching the move lines
_key_field = 'partner_id'