From 1310d2f7ba0ac249d1fb6c60ab1b8d524c442936 Mon Sep 17 00:00:00 2001
From: "@" <@>
Date: Wed, 13 Jun 2012 16:35:52 +0200
Subject: [PATCH] [IMP] account_easy_reconcile: extracted classes in modules
(lp:account-extra-addons rev 26.1.10)
---
account_easy_reconcile/__init__.py | 39 +-
account_easy_reconcile/__openerp__.py | 63 ++-
account_easy_reconcile/base_reconciliation.py | 207 ++++++++
account_easy_reconcile/easy_reconcile.py | 450 +++++-------------
account_easy_reconcile/easy_reconcile.xml | 211 ++++----
.../simple_reconciliation.py | 113 +++++
6 files changed, 617 insertions(+), 466 deletions(-)
create mode 100644 account_easy_reconcile/base_reconciliation.py
create mode 100644 account_easy_reconcile/simple_reconciliation.py
diff --git a/account_easy_reconcile/__init__.py b/account_easy_reconcile/__init__.py
index 8b179243..19e90a30 100755
--- a/account_easy_reconcile/__init__.py
+++ b/account_easy_reconcile/__init__.py
@@ -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 . #
-#########################################################################
+##############################################################################
+#
+# 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 .
+#
+##############################################################################
import easy_reconcile
-
+import base_reconciliation
+import simple_reconciliation
diff --git a/account_easy_reconcile/__openerp__.py b/account_easy_reconcile/__openerp__.py
index d3143a5c..a6dae87a 100755
--- a/account_easy_reconcile/__openerp__.py
+++ b/account_easy_reconcile/__openerp__.py
@@ -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 . #
-#########################################################################
+##############################################################################
+#
+# 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 .
+#
+##############################################################################
+
{
"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,
}
diff --git a/account_easy_reconcile/base_reconciliation.py b/account_easy_reconcile/base_reconciliation.py
new file mode 100644
index 00000000..b688e339
--- /dev/null
+++ b/account_easy_reconcile/base_reconciliation.py
@@ -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 .
+#
+##############################################################################
+
+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
+
diff --git a/account_easy_reconcile/easy_reconcile.py b/account_easy_reconcile/easy_reconcile.py
index eae444c8..0c7b2372 100644
--- a/account_easy_reconcile/easy_reconcile.py
+++ b/account_easy_reconcile/easy_reconcile.py
@@ -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 . #
-#########################################################################
+##############################################################################
+#
+# 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 .
+#
+##############################################################################
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'
-
diff --git a/account_easy_reconcile/easy_reconcile.xml b/account_easy_reconcile/easy_reconcile.xml
index 58738641..1bd84c66 100644
--- a/account_easy_reconcile/easy_reconcile.xml
+++ b/account_easy_reconcile/easy_reconcile.xml
@@ -1,121 +1,124 @@
-
-
+
-
+
-
-
-
- account.easy.reconcile.form
- 20
- account.easy.reconcile
- form
-
-
+
+
+
+
+ account.easy.reconcile.tree
+ 20
+ account.easy.reconcile
+ tree
+
+
+
+
+
+
+
+
+
+
+
+
+ Easy Automatic Reconcile
+ ir.actions.act_window
+ account.easy.reconcile
+ form
+ tree,form
+ {'wizard_object' : 'account.easy.reconcile', 'function' : 'action_rec_auto', 'object_link' : 'account.easy.reconcile' }
+
-
- account.easy.reconcile.method.form
- 20
- account.easy.reconcile.method
- form
-
-
-
-
-
-
- account.easy.reconcile.method.tree
- 20
- account.easy.reconcile.method
- tree
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ account.easy.reconcile.method.form
+ 20
+ account.easy.reconcile.method
+ form
+
+
+
+
-
-
-
+
+ account.easy.reconcile.method.tree
+ 20
+ account.easy.reconcile.method
+ tree
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
- client_action_multi
- account.easy.reconcile
- Create a Scheduler
-
-
-
+
+ client_action_multi
+ account.easy.reconcile
+ Create a Scheduler
+
+
+
-
-
-
-
+
diff --git a/account_easy_reconcile/simple_reconciliation.py b/account_easy_reconcile/simple_reconciliation.py
new file mode 100644
index 00000000..474eff84
--- /dev/null
+++ b/account_easy_reconcile/simple_reconciliation.py
@@ -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 .
+#
+##############################################################################
+
+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'
+