diff --git a/account_advanced_reconcile/__openerp__.py b/account_advanced_reconcile/__openerp__.py index 344da635..4212c3bc 100644 --- a/account_advanced_reconcile/__openerp__.py +++ b/account_advanced_reconcile/__openerp__.py @@ -25,7 +25,7 @@ 'maintainer': 'Camptocamp', 'category': 'Finance', 'complexity': 'normal', - 'depends': ['account_easy_reconcile', # this comes from lp:account-extra-addons + 'depends': ['account_easy_reconcile', ], 'description': """ Advanced reconciliation methods for the module account_easy_reconcile. diff --git a/account_easy_reconcile/__init__.py b/account_easy_reconcile/__init__.py new file mode 100755 index 00000000..19e90a30 --- /dev/null +++ b/account_easy_reconcile/__init__.py @@ -0,0 +1,24 @@ +# -*- 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 . +# +############################################################################## + +import easy_reconcile +import base_reconciliation +import simple_reconciliation diff --git a/account_easy_reconcile/__openerp__.py b/account_easy_reconcile/__openerp__.py new file mode 100755 index 00000000..3d341325 --- /dev/null +++ b/account_easy_reconcile/__openerp__.py @@ -0,0 +1,56 @@ +# -*- 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 . +# +############################################################################## + +{ + "name" : "Easy Reconcile", + "version" : "1.1", + "depends" : ["account", "base_scheduler_creator" + ], + "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" : "Finance", + "init_xml" : [], + "demo_xml" : [], + "update_xml" : ["easy_reconcile.xml"], + '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..c1f4041c --- /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, eval(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 new file mode 100644 index 00000000..6a2899cd --- /dev/null +++ b/account_easy_reconcile/easy_reconcile.py @@ -0,0 +1,206 @@ +# -*- 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 . +# +############################################################################## + +import time +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' + + _inherit = 'easy.reconcile.options' + _auto = True # restore property set to False by AbstractModel + + _order = 'sequence' + + def _get_all_rec_method(self, cr, uid, context=None): + return [ + ('easy.reconcile.simple.name', 'Simple. Amount and Name'), + ('easy.reconcile.simple.partner', 'Simple. Amount and Partner'), + ('easy.reconcile.simple.reference', 'Simple. Amount and Reference'), + ] + + def _get_rec_method(self, cr, uid, context=None): + return self._get_all_rec_method(cr, uid, context=None) + + _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"), + 'task_id': fields.many2one('account.easy.reconcile', 'Task', + required=True, ondelete='cascade'), + } + + _defaults = { + 'sequence': 1, + } + + def init(self, cr): + """ Migration stuff, name is not anymore methods names + but models name""" + cr.execute(""" + UPDATE account_easy_reconcile_method + SET name = 'easy.reconcile.simple.partner' + WHERE name = 'action_rec_auto_partner' + """) + cr.execute(""" + UPDATE account_easy_reconcile_method + SET name = 'easy.reconcile.simple.name' + WHERE name = 'action_rec_auto_name' + """) + + +class account_easy_reconcile(Model): + + _name = 'account.easy.reconcile' + _description = 'account easy reconcile' + + 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.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 = { + 'name': fields.char('Name', size=64, required=True), + 'account': fields.many2one('account.account', 'Account', required=True), + '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), + '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 + total_partial_rec = 0 + details = [] + count = 0 + for method in rec.reconcile_method: + count += 1 + + rec_model = self.pool.get(method.name) + auto_rec_id = rec_model.create( + 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) + + 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 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 + diff --git a/account_easy_reconcile/easy_reconcile.xml b/account_easy_reconcile/easy_reconcile.xml new file mode 100644 index 00000000..1bd84c66 --- /dev/null +++ b/account_easy_reconcile/easy_reconcile.xml @@ -0,0 +1,124 @@ + + + + + + + account.easy.reconcile.form + 20 + account.easy.reconcile + form + +
+ + + + + + + + + + + + + + + +