diff --git a/account_easy_reconcile/README.rst b/account_easy_reconcile/README.rst new file mode 100644 index 00000000..39e6b2c9 --- /dev/null +++ b/account_easy_reconcile/README.rst @@ -0,0 +1,63 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +Easy Reconcile +============== + +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 reconciliation runs with an history + which keep track of the reconciled Journal items + +2 simple reconciliation methods are integrated +in this module, the simple reconciliations works +on 2 lines (1 debit / 1 credit) and do not allow +partial reconcilation, they also match on 1 key, +partner or Journal item name. + +You may be interested to install also the +``account_advanced_reconciliation`` module. +This latter add more complex reconciliations, +allows multiple lines and partial. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + + +Credits +======= + +Contributors +------------ + +* Damien Crier +* Frédéric Clémenti + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. + diff --git a/account_easy_reconcile/__init__.py b/account_easy_reconcile/__init__.py new file mode 100755 index 00000000..403b65d3 --- /dev/null +++ b/account_easy_reconcile/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright 2012 Camptocamp SA (Guewen Baconnier) +# Copyright (C) 2010 Sébastien Beau +# Copyright 2015 Camptocamp SA (Damien Crier) +# +# 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 . import easy_reconcile +from . import base_reconciliation +from . import simple_reconciliation +from . import easy_reconcile_history +from . import res_config diff --git a/account_easy_reconcile/__openerp__.py b/account_easy_reconcile/__openerp__.py new file mode 100755 index 00000000..df579062 --- /dev/null +++ b/account_easy_reconcile/__openerp__.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright 2012 Camptocamp SA (Guewen Baconnier) +# Copyright (C) 2010 Sébastien Beau +# Copyright 2015 Camptocamp SA (Damien Crier) +# +# 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.3.1", + "depends": ["account"], + "author": "Akretion,Camptocamp,Odoo Community Association (OCA)", + "description": """ +Easy Reconcile +============== + +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 reconciliation runs with an history + which keep track of the reconciled Journal items + +2 simple reconciliation methods are integrated +in this module, the simple reconciliations works +on 2 lines (1 debit / 1 credit) and do not allow +partial reconcilation, they also match on 1 key, +partner or Journal item name. + +You may be interested to install also the +``account_advanced_reconciliation`` module. +This latter add more complex reconciliations, +allows multiple lines and partial. + +""", + "website": "http://www.akretion.com/", + "category": "Finance", + "data": ["easy_reconcile.xml", + "easy_reconcile_history_view.xml", + "security/ir_rule.xml", + "security/ir.model.access.csv", + "res_config_view.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..787f0f76 --- /dev/null +++ b/account_easy_reconcile/base_reconciliation.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright 2012-2013 Camptocamp SA (Guewen Baconnier) +# Copyright (C) 2010 Sébastien Beau +# Copyright 2015 Camptocamp SA (Damien Crier) +# +# 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 import models, api, fields +from operator import itemgetter, attrgetter + + +class EasyReconcileBase(models.AbstractModel): + + """Abstract Model for reconciliation methods""" + + _name = 'easy.reconcile.base' + + _inherit = 'easy.reconcile.options' + + account_id = fields.Many2one( + 'account.account', + string='Account', + required=True + ) + partner_ids = fields.Many2many( + comodel_name='res.partner', + string='Restrict on partners', + ) + # other fields are inherited from easy.reconcile.options + + @api.multi + def automatic_reconcile(self): + """ Reconciliation method called from the view. + + :return: list of reconciled ids, list of partially reconciled items + """ + self.ensure_one() + return self._action_rec() + + @api.multi + def _action_rec(self): + """ Must be inherited to implement the reconciliation + + :return: list of reconciled ids + """ + raise NotImplementedError + + def _base_columns(self): + """ 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', + 'reconcile_partial_id', + 'move_id') + return ["account_move_line.%s" % col for col in aml_cols] + + def _select(self, *args, **kwargs): + return "SELECT %s" % ', '.join(self._base_columns()) + + def _from(self, *args, **kwargs): + return ("FROM account_move_line " + "LEFT OUTER JOIN account_move_reconcile ON " + "(account_move_line.reconcile_partial_id " + "= account_move_reconcile.id)" + ) + + def _where(self, *args, **kwargs): + where = ("WHERE account_move_line.account_id = %s " + "AND COALESCE(account_move_reconcile.type,'') <> 'manual' " + "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 = [self.account_id.id] + if self.partner_ids: + where += " AND account_move_line.partner_id IN %s" + params.append(tuple([l.id for l in self.partner_ids])) + return where, params + + def _get_filter(self): + ml_obj = self.pool.get('account.move.line') + where = '' + params = [] + if self.filter: + dummy, where, params = ml_obj._where_calc( + eval(self.filter)).get_sql() + if where: + where = " AND %s" % where + return where, params + + @api.multi + def _below_writeoff_limit(self, lines, writeoff_limit): + self.ensure_one() + precision = self.pool.get('decimal.precision').precision_get('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 + + @api.multi + def _get_rec_date(self, lines, based_on='end_period_last_credit'): + self.ensure_one() + + def last_period(mlines): + period_ids = [ml['period_id'] for ml in mlines] + return max(period_ids, 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 + + @api.multi + def _reconcile_lines(self, lines, allow_partial=False): + """ 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 items + have been reconciled or not, + the second is wether the reconciliation is full (True) + or partial (False) + """ + self.ensure_one() + ml_obj = self.env['account.move.line'] + writeoff = self.write_off + line_ids = [l['id'] for l in lines] + below_writeoff, sum_debit, sum_credit = self._below_writeoff_limit( + lines, writeoff + ) + date = self._get_rec_date(lines, self.date_base_on) + rec_ctx = dict(self.env.context or {}, date_p=date) + if below_writeoff: + if sum_credit > sum_debit: + writeoff_account_id = self.account_profit_id.id + else: + writeoff_account_id = self.account_lost_id.id + period_id = self.env['account.period'].find(dt=date)[0] + if self.analytic_account_id: + rec_ctx['analytic_id'] = self.analytic_account_id.id + ml_obj.with_context(rec_ctx).reconcile( + line_ids, + type='auto', + writeoff_acc_id=writeoff_account_id, + writeoff_period_id=period_id, + writeoff_journal_id=self.journal_id.id + ) + return True, True + elif allow_partial: + # Check if the group of move lines was already partially + # reconciled and if all the lines were the same, in such + # case, just skip the group and consider it as partially + # reconciled (no change). + if lines: + existing_partial_id = lines[0]['reconcile_partial_id'] + if existing_partial_id: + partial_line_ids = set(ml_obj.search( + [('reconcile_partial_id', '=', existing_partial_id)], + )) + if set(line_ids) == partial_line_ids: + return True, False + + # We need to give a writeoff_acc_id + # in case we have a multi currency lines + # to reconcile. + # If amount in currency is equal between + # lines to reconcile + # it will do a full reconcile instead of a partial reconcile + # and make a write-off for exchange + if sum_credit > sum_debit: + writeoff_account_id = self.income_exchange_account_id.id + else: + writeoff_account_id = self.expense_exchange_account_id.id + period_id = self.env['account.period'].find(dt=date)[0] + if self.analytic_account_id: + rec_ctx['analytic_id'] = self.analytic_account_id.id + ml_obj.with_context(rec_ctx).reconcile_partial( + line_ids, + type='manual', + writeoff_acc_id=writeoff_account_id, + writeoff_period_id=period_id, + writeoff_journal_id=self.journal_id.id, + ) + 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..376a056e --- /dev/null +++ b/account_easy_reconcile/easy_reconcile.py @@ -0,0 +1,424 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright 2012-2013 Camptocamp SA (Guewen Baconnier) +# Copyright (C) 2010 Sébastien Beau +# Copyright 2015 Camptocamp SA (Damien Crier) +# +# 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 datetime import datetime +from openerp import models, api, fields, _ +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.exceptions import except_orm +from openerp import sql_db + +# from openerp import pooler + +import logging +_logger = logging.getLogger(__name__) + + +class EasyReconcileOptions(models.AbstractModel): + """Options of a reconciliation profile + + Columns shared by the configuration of methods + and by the reconciliation wizards. + This allows decoupling of the methods and the + wizards and allows to launch the wizards alone + """ + + _name = 'easy.reconcile.options' + + @api.model + def _get_rec_base_date(self): + 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') + ] + + write_off = fields.Float('Write off allowed', default=0.) + account_lost_id = fields.Many2one('account.account', + string="Account Lost") + account_profit_id = fields.Many2one('account.account', + string="Account Profit") + journal_id = fields.Many2one('account.journal', + string="Journal") + date_base_on = fields.Selection('_get_rec_base_date', + required=True, + string='Date of reconciliation', + default='end_period_last_credit') + filter = fields.Char(string='Filter', size=128) + analytic_account_id = fields.Many2one('account.analytic.account', + string='Analytic_account', + help="Analytic account" + "for the write-off") + income_exchange_account_id = fields.Many2one('account.account', + string='Gain Exchange' + 'Rate Account') + expense_exchange_account_id = fields.Many2one('account.account', + string='Loss Exchange' + 'Rate Account') + + +class AccountEasyReconcileMethod(models.Model): + _name = 'account.easy.reconcile.method' + _description = 'reconcile method for account_easy_reconcile' + _inherit = 'easy.reconcile.options' + _order = 'sequence' + + @api.model + def _get_all_rec_method(self): + 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'), + ] + + @api.model + def _get_rec_method(self): + return self._get_all_rec_method() + + name = fields.Selection('_get_rec_method', string='Type', required=True) + sequence = fields.Integer(string='Sequence', + default=1, + required=True, + help="The sequence field is used to order " + "the reconcile method" + ) + task_id = fields.Many2one('account.easy.reconcile', + string='Task', + required=True, + ondelete='cascade' + ) + company_id = fields.Many2one('res.company', + string='Company', + related="task_id.company_id", + store=True, + readonly=True + ) + +# def init(self, cr): +# """ Migration stuff +# +# Name is not anymore methods names but the name +# of the model which does the reconciliation +# """ +# 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 AccountEasyReconcile(models.Model): + + _name = 'account.easy.reconcile' + _inherit = ['mail.thread'] + _description = 'account easy reconcile' + + @api.one + @api.depends('account') + def _get_total_unrec(self): + obj_move_line = self.env['account.move.line'] + self.unreconciled_count = len(obj_move_line.search( + [('account_id', '=', self.account.id), + ('reconcile_id', '=', False), + ('reconcile_partial_id', '=', False)], + )) + + @api.one + @api.depends('account') + def _get_partial_rec(self): + obj_move_line = self.env['account.move.line'] + self.reconciled_partial_count = len(obj_move_line.search( + [('account_id', '=', self.account.id), + ('reconcile_id', '=', False), + ('reconcile_partial_id', '!=', False)], + )) + + @api.one + @api.depends('history_ids') + def _last_history(self): + # do a search() for retrieving the latest history line, + # as a read() will badly split the list of ids with 'date desc' + # and return the wrong result. + history_obj = self.env['easy.reconcile.history'] + last_history_rs = history_obj.search( + [('easy_reconcile_id', '=', self.id)], + limit=1, order='date desc' + ) + self.last_history = last_history_rs[0] if last_history_rs else False + + name = fields.Char(string='Name', size=32, required=True) + account = fields.Many2one('account.account', + string='Account', + required=True, + ) + reconcile_method = fields.One2many('account.easy.reconcile.method', + 'task_id', + string='Method' + ) + unreconciled_count = fields.Integer(string='Unreconciled Items', + compute='_get_total_unrec' + ) + reconciled_partial_count = fields.Integer( + string='Partially Reconciled Items', + compute='_get_partial_rec' + ) + history_ids = fields.One2many('easy.reconcile.history', + 'easy_reconcile_id', + string='History', + readonly=True + ) + last_history = fields.Many2one('easy.reconcile.history', + string='readonly=True', + compute='_last_history', + readonly=True + ) + company_id = fields.Many2one('res.company', string='Company') + + @api.model + def _prepare_run_transient(self, rec_method): + 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), + 'analytic_account_id': (rec_method.analytic_account_id and + rec_method.analytic_account_id.id), + 'income_exchange_account_id': + (rec_method.income_exchange_account_id and + rec_method.income_exchange_account_id.id), + 'expense_exchange_account_id': + (rec_method.income_exchange_account_id and + rec_method.income_exchange_account_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} + + @api.multi + def run_reconcile(self): + def find_reconcile_ids(fieldname, move_line_ids): + if not move_line_ids: + return [] + sql = ("SELECT DISTINCT " + fieldname + + " FROM account_move_line " + " WHERE id in %s " + " AND " + fieldname + " IS NOT NULL") + self.env.cr.execute(sql, (tuple(move_line_ids),)) + res = self.env.cr.fetchall() + return [row[0] for row in res] + + # we use a new cursor to be able to commit the reconciliation + # often. We have to create it here and not later to avoid problems + # where the new cursor sees the lines as reconciles but the old one + # does not. + + for rec in self: + ctx = self.env.context.copy() + ctx['commit_every'] = ( + rec.account.company_id.reconciliation_commit_every + ) + if ctx['commit_every']: + new_cr = sql_db.db_connect(self.env.cr.dbname).cursor() + else: + new_cr = self.env.cr + + uid, context = self.env.uid, self.env.context + with api.Environment.manage(): + self.env = api.Environment(new_cr, uid, context) + + try: + all_ml_rec_ids = [] + all_ml_partial_ids = [] + + for method in rec.reconcile_method: + rec_model = self.env[method.name] + auto_rec_id = rec_model.create( + self._prepare_run_transient(method) + ) + + ml_rec_ids, ml_partial_ids = ( + auto_rec_id.automatic_reconcile() + ) + + all_ml_rec_ids += ml_rec_ids + all_ml_partial_ids += ml_partial_ids + + reconcile_ids = find_reconcile_ids( + 'reconcile_id', + all_ml_rec_ids + ) + partial_ids = find_reconcile_ids( + 'reconcile_partial_id', + all_ml_partial_ids + ) + + self.env['easy.reconcile.history'].create( + { + 'easy_reconcile_id': rec.id, + 'date': fields.datetime.now(), + 'reconcile_ids': [ + (4, rid) for rid in reconcile_ids + ], + 'reconcile_partial_ids': [ + (4, rid) for rid in partial_ids + ], + }) + except Exception as e: + # In case of error, we log it in the mail thread, log the + # stack trace and create an empty history line; otherwise, + # the cron will just loop on this reconcile task. + _logger.exception( + "The reconcile task %s had an exception: %s", + rec.name, e.message + ) + message = "There was an error during reconciliation : %s" \ + % e.message + rec.message_post(body=message) + self.env['easy.reconcile.history'].create( + { + 'easy_reconcile_id': rec.id, + 'date': fields.datetime.now(), + 'reconcile_ids': [], + 'reconcile_partial_ids': [], + } + ) + finally: + if ctx['commit_every']: + new_cr.commit() + new_cr.close() + +# self.env.cr.close() + + return True + + @api.model + def _no_history(self, rec): + """ Raise an `orm.except_orm` error, supposed to + be called when there is no history on the reconciliation + task. + """ + raise except_orm( + _('Error'), + _('There is no history of reconciled ' + 'items on the task: %s.') % rec.name) + + @api.model + def _open_move_line_list(self, move_line_ids, name): + return { + 'name': name, + 'view_mode': 'tree,form', + 'view_id': False, + 'view_type': 'form', + 'res_model': 'account.move.line', + 'type': 'ir.actions.act_window', + 'nodestroy': True, + 'target': 'current', + 'domain': unicode([('id', 'in', move_line_ids)]), + } + + @api.multi + def open_unreconcile(self): + """ Open the view of move line with the unreconciled move lines""" + self.ensure_one() + obj_move_line = self.env['account.move.line'] + line_ids = obj_move_line.search( + [('account_id', '=', self.account.id), + ('reconcile_id', '=', False), + ('reconcile_partial_id', '=', False)]) + name = _('Unreconciled items') + return self._open_move_line_list(line_ids and line_ids.ids or [], name) + + @api.multi + def open_partial_reconcile(self): + """ Open the view of move line with the unreconciled move lines""" + self.ensure_one() + obj_move_line = self.env['account.move.line'] + line_ids = obj_move_line.search( + [('account_id', '=', self.account.id), + ('reconcile_id', '=', False), + ('reconcile_partial_id', '!=', False)]) + name = _('Partial reconciled items') + return self._open_move_line_list(line_ids and line_ids.ids or [], name) + + @api.model + def last_history_reconcile(self, rec_id): + """ Get the last history record for this reconciliation profile + and return the action which opens move lines reconciled + """ + if isinstance(rec_id, (tuple, list)): + assert len(rec_id) == 1, \ + "Only 1 id expected" + rec_id = rec_id[0] + rec = self.browse(rec_id) + if not rec.last_history: + self._no_history(rec) + return rec.last_history.open_reconcile() + + @api.model + def last_history_partial(self, rec_id): + """ Get the last history record for this reconciliation profile + and return the action which opens move lines reconciled + """ + if isinstance(rec_id, (tuple, list)): + assert len(rec_id) == 1, \ + "Only 1 id expected" + rec_id = rec_id[0] + rec = self.browse(rec_id) + if not rec.last_history: + self._no_history(rec) + return rec.last_history.open_partial() + + @api.model + def run_scheduler(self, run_all=None): + """ Launch the reconcile with the oldest run + This function is mostly here to be used with cron task + + :param run_all: if set it will ingore lookup and launch + all reconciliation + :returns: True in case of success or raises an exception + + """ + def _get_date(reconcile): + if reconcile.last_history.date: + return datetime.strptime(reconcile.last_history.date, + DEFAULT_SERVER_DATETIME_FORMAT) + else: + return datetime.min + + reconciles = self.search([]) + assert reconciles.ids, "No easy reconcile available" + if run_all: + reconciles.run_reconcile() + return True + reconciles.sorted(key=_get_date) + older = reconciles[0] + older.run_reconcile() + return True diff --git a/account_easy_reconcile/easy_reconcile.xml b/account_easy_reconcile/easy_reconcile.xml new file mode 100644 index 00000000..076bd3b3 --- /dev/null +++ b/account_easy_reconcile/easy_reconcile.xml @@ -0,0 +1,190 @@ + + + + + + + account.easy.reconcile.form + 20 + account.easy.reconcile + +
+
+
+ + + + + + + + + + + +