# Copyright 2012-2016 Camptocamp SA # Copyright 2010 Sébastien Beau # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging from datetime import datetime import psycopg2 from psycopg2.extensions import AsIs from odoo import _, api, exceptions, fields, models, sql_db from odoo.exceptions import Warning as UserError _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.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") 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."), ("mass.reconcile.advanced.name", "Advanced. Partner and Name."), ] 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.depends("account") def _compute_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), ("parent_state", "=", "posted"), ] ) @api.depends("history_ids") def _compute_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="_compute_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="_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), "journal_id": (rec_method.journal_id.id), "date_base_on": rec_method.date_base_on, "_filter": rec_method._filter, } def _run_reconcile_method(self, reconcile_method): rec_model = self.env[reconcile_method.name] auto_rec_id = rec_model.create(self._prepare_run_transient(reconcile_method)) return auto_rec_id.automatic_reconcile() def run_reconcile(self): def find_reconcile_ids(fieldname, move_line_ids): if not move_line_ids: return [] self.flush() sql = """ SELECT DISTINCT %s FROM account_move_line WHERE %s IS NOT NULL AND id in %s """ params = [AsIs(fieldname), AsIs(fieldname), tuple(move_line_ids)] self.env.cr.execute(sql, params) 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: # SELECT FOR UPDATE the mass reconcile row ; this is done in order # to avoid 2 processes on the same mass reconcile method. try: self.env.cr.execute( "SELECT id FROM account_mass_reconcile" " WHERE id = %s" " FOR UPDATE NOWAIT", (rec.id,), ) except psycopg2.OperationalError: raise exceptions.UserError( _( "A mass reconcile is already ongoing for this account, " "please try again later." ) ) 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() new_env = api.Environment(new_cr, self.env.uid, ctx) else: new_cr = self.env.cr new_env = self.env try: all_ml_rec_ids = [] for method in rec.reconcile_method: ml_rec_ids = self.with_env(new_env)._run_reconcile_method(method) 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)], } 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), ("parent_state", "=", "posted"), ] ) 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