account-reconcile/account_mass_reconcile/models/mass_reconcile.py

336 lines
11 KiB
Python

# Copyright 2012-2016 Camptocamp SA
# Copyright 2010 Sébastien Beau
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
import logging
from odoo import models, api, fields, _
from odoo.exceptions import Warning as UserError
from odoo import sql_db
_logger = logging.getLogger(__name__)
class MassReconcileOptions(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 = 'mass.reconcile.options'
_description = 'Options of a reconciliation profile'
@api.model
def _get_rec_base_date(self):
return [
('newest', 'Most recent move line'),
('actual', 'Today'),
]
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='newest',
)
_filter = fields.Char(
string='Filter', oldname='filter',
)
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 AccountMassReconcileMethod(models.Model):
_name = 'account.mass.reconcile.method'
_description = 'reconcile method for account_mass_reconcile'
_inherit = 'mass.reconcile.options'
_order = 'sequence'
@staticmethod
def _get_reconcilation_methods():
return [
('mass.reconcile.simple.name', 'Simple. Amount and Name'),
('mass.reconcile.simple.partner', 'Simple. Amount and Partner'),
('mass.reconcile.simple.reference',
'Simple. Amount and Reference'),
('mass.reconcile.advanced.ref',
'Advanced. Partner and Ref.'),
]
def _selection_name(self):
return self._get_reconcilation_methods()
name = fields.Selection(
'_selection_name',
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.mass.reconcile',
string='Task',
required=True,
ondelete='cascade',
)
company_id = fields.Many2one(
'res.company',
string='Company',
related="task_id.company_id",
store=True,
readonly=True,
)
class AccountMassReconcile(models.Model):
_name = 'account.mass.reconcile'
_inherit = ['mail.thread']
_description = 'account mass reconcile'
@api.multi
@api.depends('account')
def _get_total_unrec(self):
obj_move_line = self.env['account.move.line']
for rec in self:
rec.unreconciled_count = obj_move_line.search_count(
[('account_id', '=', rec.account.id),
('reconciled', '=', False)])
@api.multi
@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['mass.reconcile.history']
for rec in self:
last_history_rs = history_obj.search(
[('mass_reconcile_id', '=', rec.id)],
limit=1, order='date desc'
)
rec.last_history = last_history_rs or False
name = fields.Char(
string='Name',
required=True,
)
account = fields.Many2one(
'account.account',
string='Account',
required=True,
)
reconcile_method = fields.One2many(
'account.mass.reconcile.method',
'task_id',
string='Method',
)
unreconciled_count = fields.Integer(
string='Unreconciled Items',
compute='_get_total_unrec',
)
history_ids = fields.One2many(
'mass.reconcile.history',
'mass_reconcile_id',
string='History',
readonly=True,
)
last_history = fields.Many2one(
'mass.reconcile.history',
string='Last history', readonly=True,
compute='_last_history',
)
company_id = fields.Many2one(
'res.company',
string='Company',
)
@staticmethod
def _prepare_run_transient(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.id),
'account_profit_id': (rec_method.account_profit_id.id),
'income_exchange_account_id':
(rec_method.income_exchange_account_id.id),
'expense_exchange_account_id':
(rec_method.income_exchange_account_id.id),
'journal_id': (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
try:
all_ml_rec_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 = auto_rec_id.automatic_reconcile()
all_ml_rec_ids += ml_rec_ids
reconcile_ids = find_reconcile_ids(
'full_reconcile_id',
all_ml_rec_ids
)
self.env['mass.reconcile.history'].create(
{
'mass_reconcile_id': rec.id,
'date': fields.Datetime.now(),
'reconcile_ids': [
(4, rid) for rid in reconcile_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, str(e)
)
message = _("There was an error during reconciliation : %s") \
% str(e)
rec.message_post(body=message)
self.env['mass.reconcile.history'].create(
{
'mass_reconcile_id': rec.id,
'date': fields.Datetime.now(),
'reconcile_ids': [],
}
)
finally:
if ctx['commit_every']:
new_cr.commit()
new_cr.close()
return True
def _no_history(self):
""" Raise an `orm.except_orm` error, supposed to
be called when there is no history on the reconciliation
task.
"""
raise UserError(
_('There is no history of reconciled '
'items on the task: %s.') % self.name
)
@staticmethod
def _open_move_line_list(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': [('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']
lines = obj_move_line.search(
[('account_id', '=', self.account.id),
('reconciled', '=', False)])
name = _('Unreconciled items')
return self._open_move_line_list(lines.ids or [], name)
def last_history_reconcile(self):
""" Get the last history record for this reconciliation profile
and return the action which opens move lines reconciled
"""
if not self.last_history:
self._no_history()
return self.last_history.open_reconcile()
@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 fields.Datetime.to_datetime(reconcile.last_history.date)
else:
return datetime.min
reconciles = self.search([])
assert reconciles.ids, "No mass reconcile available"
if run_all:
reconciles.run_reconcile()
return True
reconciles.sorted(key=_get_date)
older = reconciles[0]
older.run_reconcile()
return True