# Copyright 2016 Ildar Nasyrov # Copyright 2016-2018 Ivan Yelizariev # Copyright 2016 intero-chz # Copyright 2016 manawi # Copyright 2018 Kolushov Alexandr # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import api, exceptions, fields, models from odoo.tools import email_split from odoo.tools.translate import _ class Wizard(models.TransientModel): _name = "mail_move_message.wizard" _description = "Mail move message wizard" @api.model def _model_selection(self): selection = [] config_parameters = self.env["ir.config_parameter"] model_names = config_parameters.sudo().get_param("mail_relocation_models") model_names = model_names.split(",") if model_names else [] if "default_message_id" in self.env.context: message = self.env["mail.message"].browse( self.env.context["default_message_id"] ) if message.model and message.model not in model_names: model_names.append(message.model) if message.moved_from_model and message.moved_from_model not in model_names: model_names.append(message.moved_from_model) if model_names: selection = [ (m.model, m.display_name) for m in self.env["ir.model"].search([("model", "in", model_names)]) ] return selection @api.model def default_get(self, fields_list): res = super(Wizard, self).default_get(fields_list) available_models = self._model_selection() if len(available_models): record = self.env[available_models[0][0]].search([], limit=1) res["model_record"] = ( len(record) and (available_models[0][0] + "," + str(record.id)) or False ) if "message_id" in res: message = self.env["mail.message"].browse(res["message_id"]) email_from = message.email_from parts = email_split(email_from.replace(" ", ",")) if parts: email = parts[0] name = ( email_from.find(email) != -1 and email_from[: email_from.index(email)] .replace('"', "") .replace("<", "") .strip() or email_from ) else: name, email = email_from res["message_name_from"] = name res["message_email_from"] = email res["partner_id"] = message.author_id.id if message.author_id and self.env.uid not in [ u.id for u in message.author_id.user_ids ]: res["filter_by_partner"] = True if message.author_id and res.get("model"): res_id = self.env[res["model"]].search([], order="id desc", limit=1) if res_id: res["res_id"] = res_id[0].id config_parameters = self.env["ir.config_parameter"] res["move_followers"] = config_parameters.sudo().get_param( "mail_relocation_move_followers" ) res["uid"] = self.env.uid return res message_id = fields.Many2one("mail.message", string="Message") message_body = fields.Html( related="message_id.body", string="Message to move", readonly=True ) message_from = fields.Char( related="message_id.email_from", string="From", readonly=True ) message_subject = fields.Char( related="message_id.subject", string="Subject", readonly=True ) message_moved_by_message_id = fields.Many2one( "mail.message", related="message_id.moved_by_message_id", string="Moved with", readonly=True, ) message_moved_by_user_id = fields.Many2one( "res.users", related="message_id.moved_by_user_id", string="Moved by", readonly=True, ) message_is_moved = fields.Boolean( string="Is Moved", related="message_id.is_moved", readonly=True ) parent_id = fields.Many2one("mail.message", string="Search by name",) model_record = fields.Reference(selection="_model_selection", string="Record") model = fields.Char(compute="_compute_model_res_id", string="Model") res_id = fields.Integer(compute="_compute_model_res_id", string="Record ID") can_move = fields.Boolean("Can move", compute="_compute_get_can_move") move_back = fields.Boolean( "MOVE TO ORIGIN", help="Move message and submessages to original place" ) partner_id = fields.Many2one("res.partner", string="Author") filter_by_partner = fields.Boolean("Filter Records by partner") message_email_from = fields.Char() message_name_from = fields.Char() # FIXME message_to_read should be True even if current message or any his childs are unread message_to_read = fields.Boolean( compute="_compute_is_read", string="Unread message", help="Service field shows that this message were unread when moved", ) uid = fields.Integer() move_followers = fields.Boolean( "Move Followers", help="Add followers of current record to a new record.\n" "You must use this option, if new record has restricted access.\n" "You can change default value for this option at Settings/System Parameters", ) @api.depends("model_record") def _compute_model_res_id(self): for rec in self: rec.model = rec.model_record and rec.model_record._name or False rec.res_id = rec.model_record and rec.model_record.id or False @api.depends("message_id") def _compute_get_can_move(self): for r in self: r.get_can_move_one() def _compute_is_read(self): messages = ( self.env["mail.message"] .sudo() .browse(self.message_id.all_child_ids.ids + [self.message_id.id]) ) self.message_to_read = True in [m.needaction for m in messages] def get_can_move_one(self): self.ensure_one() # message was not moved before OR message is a top message of previous move self.can_move = ( not self.message_id.moved_by_message_id or self.message_id.moved_by_message_id.id == self.message_id.id ) @api.onchange("move_back") def on_change_move_back(self): if not self.move_back: return self.parent_id = self.message_id.moved_from_parent_id message = self.message_id if message.is_moved: self.model_record = self.env[message.moved_from_model].browse( message.moved_from_res_id ) @api.onchange("parent_id", "model_record") def update_move_back(self): model = self.message_id.moved_from_model self.move_back = ( self.parent_id == self.message_id.moved_from_parent_id and self.res_id == self.message_id.moved_from_res_id and (self.model == model or (not self.model and not model)) ) @api.onchange("parent_id") def on_change_parent_id(self): if self.parent_id and self.parent_id.model: self.model = self.parent_id.model self.res_id = self.parent_id.res_id else: self.model = None self.res_id = None @api.onchange("model", "filter_by_partner", "partner_id") def on_change_partner(self): domain = {"res_id": [("id", "!=", self.message_id.res_id)]} if self.model and self.filter_by_partner and self.partner_id: fields = self.env[self.model].fields_get(False) contact_field = False for n, f in fields.items(): if f["type"] == "many2one" and f["relation"] == "res.partner": contact_field = n break if contact_field: domain["res_id"].append((contact_field, "=", self.partner_id.id)) if self.model: res_id = self.env[self.model].search( domain["res_id"], order="id desc", limit=1 ) self.res_id = res_id and res_id[0].id else: self.res_id = None return {"domain": domain} def check_access(self): for r in self: r.check_access_one() def check_access_one(self): self.ensure_one() operation = "write" if not (self.model and self.res_id): return True model_obj = self.env[self.model] mids = model_obj.browse(self.res_id).exists() if hasattr(model_obj, "check_mail_message_access"): model_obj.check_mail_message_access(mids.ids, operation) else: self.env["mail.thread"].check_mail_message_access( mids.ids, operation, model_name=self.model ) def open_moved_by_message_id(self): message_id = None for r in self: message_id = r.message_moved_by_message_id.id return { "type": "ir.actions.act_window", "res_model": "mail_move_message.wizard", "view_mode": "form", "view_type": "form", "views": [[False, "form"]], "target": "new", "context": {"default_message_id": message_id}, } def move(self): for r in self: if not r.model: raise exceptions.except_orm( _("Record field is empty!"), _("Select a record for relocation first"), ) for r in self: r.check_access() if not r.parent_id or not ( r.parent_id.model == r.model and r.parent_id.res_id == r.res_id ): # link with the first message of record parent = self.env["mail.message"].search( [("model", "=", r.model), ("res_id", "=", r.res_id)], order="id", limit=1, ) r.parent_id = parent.id or None r.message_id.move( r.parent_id.id, r.res_id, r.model, r.move_back, r.move_followers, r.message_to_read, r.partner_id, ) if r.model in ["mail.message", "mail.channel", False]: return { "name": "Chess game page", "type": "ir.actions.act_url", "url": "/web", "target": "self", } return { "name": _("Record"), "view_type": "form", "view_mode": "form", "res_model": r.model, "res_id": r.res_id, "views": [(False, "form")], "type": "ir.actions.act_window", } def delete(self): for r in self: r.delete_one() def delete_one(self): self.ensure_one() msg_id = self.message_id.id # Send notification notification = {"id": msg_id} self.env["bus.bus"].sendone( (self._cr.dbname, "mail_move_message.delete_message"), notification ) self.message_id.unlink() return {} def read_close(self): for r in self: r.read_close_one() def read_close_one(self): self.ensure_one() self.message_id.set_message_done() self.message_id.child_ids.set_message_done() return {"type": "ir.actions.act_window_close"} class MailMessage(models.Model): _inherit = "mail.message" is_moved = fields.Boolean("Is moved") moved_from_res_id = fields.Integer("Related Document ID (Original)") moved_from_model = fields.Char("Related Document Model (Original)") moved_from_parent_id = fields.Many2one( "mail.message", "Parent Message (Original)", ondelete="set null" ) moved_by_message_id = fields.Many2one( "mail.message", "Moved by message", ondelete="set null", help="Top message, that initate moving this message", ) moved_by_user_id = fields.Many2one( "res.users", "Moved by user", ondelete="set null" ) all_child_ids = fields.One2many( "mail.message", string="All childs", compute="_compute_get_all_childs", help="all childs, including subchilds", ) moved_as_unread = fields.Boolean("Was Unread", default=False) def _compute_get_all_childs(self, include_myself=True): for r in self: r._get_all_childs_one(include_myself=include_myself) def _get_all_childs_one(self, include_myself=True): self.ensure_one() ids = [] if include_myself: ids.append(self.id) while True: new_ids = self.search([("parent_id", "in", ids), ("id", "not in", ids)]).ids if new_ids: ids = ids + new_ids continue break moved_childs = self.search([("moved_by_message_id", "=", self.id)]).ids self.all_child_ids = ids + moved_childs def move_followers(self, model, ids): fol_obj = self.env["mail.followers"] for message in self: followers = fol_obj.sudo().search( [("res_model", "=", message.model), ("res_id", "=", message.res_id)] ) for f in followers: self.env[model].browse(ids).message_subscribe( [f.partner_id.id], [s.id for s in f.subtype_ids] ) def move( self, parent_id, res_id, model, move_back, move_followers=False, message_to_read=False, author=False, ): for r in self: r.move_one( parent_id, res_id, model, move_back, move_followers=move_followers, message_to_read=message_to_read, author=author, ) def move_one( self, parent_id, res_id, model, move_back, move_followers=False, message_to_read=False, author=False, ): self.ensure_one() if parent_id == self.id: # if for any reason method is called to move message with parent # equal to oneself, we need stop to prevent infinitive loop in # building message tree return if not self.author_id: self.write( {"author_id": author.id,} ) vals = {} if move_back: # clear variables if we move everything back vals["is_moved"] = False vals["moved_by_user_id"] = None vals["moved_by_message_id"] = None vals["moved_from_res_id"] = None vals["moved_from_model"] = None vals["moved_from_parent_id"] = None vals["moved_as_unread"] = None else: vals["parent_id"] = parent_id vals["res_id"] = res_id vals["model"] = model vals["is_moved"] = True vals["moved_by_user_id"] = self.env.user.id vals["moved_by_message_id"] = self.id vals["moved_as_unread"] = message_to_read # Update record_name in message vals["record_name"] = self._get_record_name(vals) # unread message remains unread after moving back to origin if self.moved_as_unread and move_back: notification = { "mail_message_id": self.id, "res_partner_id": self.env.user.partner_id.id, "is_read": False, } self.write( {"notification_ids": [(0, 0, notification)],} ) for r in self.all_child_ids: r_vals = vals.copy() if not r.is_moved: # moved_from_* variables contain not last, but original # reference r_vals["moved_from_parent_id"] = r.parent_id.id or r.env.context.get( "uid" ) r_vals["moved_from_res_id"] = r.res_id or r.id r_vals["moved_from_model"] = r.model or r._name elif move_back: r_vals["parent_id"] = r.moved_from_parent_id.id r_vals["res_id"] = r.moved_from_res_id r_vals["model"] = ( r.moved_from_model and r.moved_from_model not in ["mail.message", "mail.channel", False] ) and r.moved_from_model r_vals["record_name"] = ( r_vals["model"] and self.env[r.moved_from_model].browse(r.moved_from_res_id).name ) if move_followers: r.sudo().move_followers(r_vals.get("model"), r_vals.get("res_id")) r.sudo().write(r_vals) r.attachment_ids.sudo().write( {"res_id": r_vals.get("res_id"), "res_model": r_vals.get("model")} ) # Send notification notification = { "id": self.id, "res_id": vals.get("res_id"), "model": vals.get("model"), "is_moved": vals["is_moved"], "record_name": "record_name" in vals and vals["record_name"], } self.env["bus.bus"].sendone( (self._cr.dbname, "mail_move_message"), notification ) def name_get(self): context = self.env.context if not (context or {}).get("extended_name"): return super(MailMessage, self).name_get() reads = self.read(["record_name", "model", "res_id"]) res = [] for record in reads: name = record["record_name"] or "" extended_name = " [{}] ID {}".format( record.get("model", "UNDEF"), record.get("res_id", "UNDEF"), ) res.append((record["id"], name + extended_name)) return res def message_format(self): message_values = super(MailMessage, self).message_format() message_index = {message["id"]: message for message in message_values} for item in self: msg = message_index.get(item.id) if msg: msg["is_moved"] = item.is_moved return message_values class MailMoveMessageConfiguration(models.TransientModel): _inherit = "res.config.settings" model_ids = fields.Many2many(comodel_name="ir.model", string="Models") move_followers = fields.Boolean("Move Followers") @api.model def get_values(self): res = super(MailMoveMessageConfiguration, self).get_values() config_parameters = self.env["ir.config_parameter"].sudo() model_names = config_parameters.sudo().get_param("mail_relocation_models") model_names = model_names.split(",") model_ids = self.env["ir.model"].sudo().search([("model", "in", model_names)]) res.update( model_ids=[m.id for m in model_ids], move_followers=config_parameters.sudo().get_param( "mail_relocation_move_followers" ), ) return res def set_values(self): super(MailMoveMessageConfiguration, self).set_values() config_parameters = self.env["ir.config_parameter"].sudo() for record in self: model_names = ",".join([x.model for x in record.model_ids]) config_parameters.set_param("mail_relocation_models", model_names or "") config_parameters.set_param( "mail_relocation_move_followers", record.move_followers or "" ) class ResPartner(models.Model): _inherit = "res.partner" @api.model def create(self, vals): res = super(ResPartner, self).create(vals) if "update_message_author" in self.env.context and "email" in vals: mail_message_obj = self.env["mail.message"] # Escape special SQL characters in email_address to avoid invalid matches email_address = ( vals["email"] .replace("\\", "\\\\") .replace("%", "\\%") .replace("_", "\\_") ) email_brackets = "<%s>" % email_address messages = mail_message_obj.search( [ "|", ("email_from", "=ilike", email_address), ("email_from", "ilike", email_brackets), ("author_id", "=", False), ] ) if messages: messages.sudo().write({"author_id": res.id}) return res @api.model def default_get(self, default_fields): contextual_self = self if ( "mail_move_message" in self.env.context and self.env.context["mail_move_message"] ): contextual_self = self.with_context( default_name=self.env.context["message_name_from"] or "", default_email=self.env.context["message_email_from"] or "", ) return super(ResPartner, contextual_self).default_get(default_fields)