mirror of
https://github.com/OCA/account-reconcile.git
synced 2025-01-20 12:27:39 +02:00
421 lines
17 KiB
Python
421 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# Copyright 2012-2013 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 datetime import datetime
|
|
from openerp.osv import fields, orm
|
|
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
|
from openerp.tools.translate import _
|
|
from openerp import pooler
|
|
|
|
import logging
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EasyReconcileOptions(orm.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'
|
|
|
|
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 reconciliation'),
|
|
'filter': fields.char('Filter', size=128),
|
|
'analytic_account_id': fields.many2one(
|
|
'account.analytic.account', 'Analytic Account',
|
|
help="Analytic account for the write-off"),
|
|
'income_exchange_account_id': fields.many2one(
|
|
'account.account', 'Gain Exchange Rate Account'),
|
|
'expense_exchange_account_id': fields.many2one(
|
|
'account.account', 'Loss Exchange Rate Account'),
|
|
|
|
}
|
|
|
|
_defaults = {
|
|
'write_off': 0.,
|
|
'date_base_on': 'end_period_last_credit',
|
|
}
|
|
|
|
|
|
class AccountEasyReconcileMethod(orm.Model):
|
|
_name = 'account.easy.reconcile.method'
|
|
_description = 'reconcile method for account_easy_reconcile'
|
|
_inherit = 'easy.reconcile.options'
|
|
_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', 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',
|
|
string='Task',
|
|
required=True,
|
|
ondelete='cascade'),
|
|
'company_id': fields.related('task_id', 'company_id',
|
|
relation='res.company',
|
|
type='many2one',
|
|
string='Company',
|
|
store=True,
|
|
readonly=True),
|
|
}
|
|
|
|
_defaults = {
|
|
'sequence': 1,
|
|
}
|
|
|
|
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(orm.Model):
|
|
|
|
_name = 'account.easy.reconcile'
|
|
_inherit = ['mail.thread']
|
|
_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
|
|
|
|
def _last_history(self, cr, uid, ids, name, args, context=None):
|
|
result = {}
|
|
# 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.pool['easy.reconcile.history']
|
|
for reconcile_id in ids:
|
|
last_history = history_obj.search(
|
|
cr, uid, [('easy_reconcile_id', '=', reconcile_id)],
|
|
limit=1, order='date desc', context=context
|
|
)
|
|
result[reconcile_id] = last_history[0] if last_history else False
|
|
return result
|
|
|
|
_columns = {
|
|
'name': fields.char('Name', required=True),
|
|
'account': fields.many2one(
|
|
'account.account', 'Account', required=True),
|
|
'reconcile_method': fields.one2many(
|
|
'account.easy.reconcile.method', 'task_id', 'Method'),
|
|
'unreconciled_count': fields.function(
|
|
_get_total_unrec, type='integer', string='Unreconciled Items'),
|
|
'reconciled_partial_count': fields.function(
|
|
_get_partial_rec,
|
|
type='integer',
|
|
string='Partially Reconciled Items'),
|
|
'history_ids': fields.one2many(
|
|
'easy.reconcile.history',
|
|
'easy_reconcile_id',
|
|
string='History',
|
|
readonly=True),
|
|
'last_history':
|
|
fields.function(
|
|
_last_history,
|
|
string='Last History',
|
|
type='many2one',
|
|
relation='easy.reconcile.history',
|
|
readonly=True),
|
|
'company_id': fields.many2one('res.company', 'Company'),
|
|
}
|
|
|
|
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),
|
|
'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}
|
|
|
|
def run_reconcile(self, cr, uid, ids, context=None):
|
|
def find_reconcile_ids(cr, 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")
|
|
cr.execute(sql, (tuple(move_line_ids),))
|
|
res = 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.
|
|
if context is None:
|
|
context = {}
|
|
|
|
for rec in self.browse(cr, uid, ids, context=context):
|
|
ctx = context.copy()
|
|
ctx['commit_every'] = (
|
|
rec.account.company_id.reconciliation_commit_every
|
|
)
|
|
if ctx['commit_every']:
|
|
new_cr = pooler.get_db(cr.dbname).cursor()
|
|
else:
|
|
new_cr = cr
|
|
try:
|
|
all_ml_rec_ids = []
|
|
all_ml_partial_ids = []
|
|
|
|
for method in rec.reconcile_method:
|
|
rec_model = self.pool.get(method.name)
|
|
auto_rec_id = rec_model.create(
|
|
new_cr, uid,
|
|
self._prepare_run_transient(
|
|
new_cr, uid, method, context=context),
|
|
context=context)
|
|
|
|
ml_rec_ids, ml_partial_ids = rec_model.automatic_reconcile(
|
|
new_cr, uid, auto_rec_id, context=ctx)
|
|
|
|
all_ml_rec_ids += ml_rec_ids
|
|
all_ml_partial_ids += ml_partial_ids
|
|
|
|
reconcile_ids = find_reconcile_ids(
|
|
new_cr, 'reconcile_id', all_ml_rec_ids)
|
|
partial_ids = find_reconcile_ids(
|
|
new_cr, 'reconcile_partial_id', all_ml_partial_ids)
|
|
|
|
self.pool.get('easy.reconcile.history').create(new_cr, uid, {
|
|
'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],
|
|
}, context=context)
|
|
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, ", ".join([str(error) for error in e.args]))
|
|
message = "There was an error during reconciliation : %s" \
|
|
% ", ".join([str(error) for error in e.args])
|
|
self.message_post(cr, uid, rec.id,
|
|
body=message, context=context)
|
|
self.pool.get('easy.reconcile.history').create(new_cr, uid, {
|
|
'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()
|
|
return True
|
|
|
|
def _no_history(self, cr, uid, rec, context=None):
|
|
""" Raise an `orm.except_orm` error, supposed to
|
|
be called when there is no history on the reconciliation
|
|
task.
|
|
"""
|
|
raise orm.except_orm(
|
|
_('Error'),
|
|
_('There is no history of reconciled '
|
|
'items on the task: %s.') % rec.name)
|
|
|
|
def _open_move_line_list(sefl, cr, uid, move_line_ids, name, context=None):
|
|
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)]),
|
|
}
|
|
|
|
def open_unreconcile(self, cr, uid, ids, context=None):
|
|
""" Open the view of move line with the unreconciled move lines"""
|
|
assert len(ids) == 1, \
|
|
"You can only open entries from one profile at a time"
|
|
obj_move_line = self.pool.get('account.move.line')
|
|
for task in self.browse(cr, uid, ids, context=context):
|
|
line_ids = obj_move_line.search(
|
|
cr, uid,
|
|
[('account_id', '=', task.account.id),
|
|
('reconcile_id', '=', False),
|
|
('reconcile_partial_id', '=', False)],
|
|
context=context)
|
|
|
|
name = _('Unreconciled items')
|
|
return self._open_move_line_list(cr, uid, line_ids, name,
|
|
context=context)
|
|
|
|
def open_partial_reconcile(self, cr, uid, ids, context=None):
|
|
""" Open the view of move line with the unreconciled move lines"""
|
|
assert len(ids) == 1, \
|
|
"You can only open entries from one profile at a time"
|
|
obj_move_line = self.pool.get('account.move.line')
|
|
for task in self.browse(cr, uid, ids, context=context):
|
|
line_ids = obj_move_line.search(
|
|
cr, uid,
|
|
[('account_id', '=', task.account.id),
|
|
('reconcile_id', '=', False),
|
|
('reconcile_partial_id', '!=', False)],
|
|
context=context)
|
|
name = _('Partial reconciled items')
|
|
return self._open_move_line_list(cr, uid, line_ids, name,
|
|
context=context)
|
|
|
|
def last_history_reconcile(self, cr, uid, rec_id, context=None):
|
|
""" 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(cr, uid, rec_id, context=context)
|
|
if not rec.last_history:
|
|
self._no_history(cr, uid, rec, context=context)
|
|
return rec.last_history.open_reconcile()
|
|
|
|
def last_history_partial(self, cr, uid, rec_id, context=None):
|
|
""" 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(cr, uid, rec_id, context=context)
|
|
if not rec.last_history:
|
|
self._no_history(cr, uid, rec, context=context)
|
|
return rec.last_history.open_partial()
|
|
|
|
def run_scheduler(self, cr, uid, run_all=None, context=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
|
|
|
|
ids = self.search(cr, uid, [], context=context)
|
|
assert ids, "No easy reconcile available"
|
|
if run_all:
|
|
self.run_reconcile(cr, uid, ids, context=context)
|
|
return True
|
|
reconciles = self.browse(cr, uid, ids, context=context)
|
|
reconciles.sort(key=_get_date)
|
|
older = reconciles[0]
|
|
self.run_reconcile(cr, uid, [older.id], context=context)
|
|
return True
|