# Part of Odoo. See LICENSE file for full copyright and licensing details. import contextlib from odoo import _, models, SUPERUSER_ID from odoo.exceptions import AccessError, MissingError, UserError from odoo.tools import consteq from odoo.addons.mail.tools.discuss import Store class IrAttachment(models.Model): _inherit = 'ir.attachment' def _check_attachments_access(self, attachment_tokens): """This method relies on access rules/rights and therefore it should not be called from a sudo env.""" self = self.sudo(False) attachment_tokens = attachment_tokens or ([None] * len(self)) if len(attachment_tokens) != len(self): raise UserError(_("An access token must be provided for each attachment.")) for attachment, access_token in zip(self, attachment_tokens): try: attachment_sudo = attachment.with_user(SUPERUSER_ID).exists() if not attachment_sudo: raise MissingError(_("The attachment %s does not exist.", attachment.id)) try: attachment.check('write') except AccessError: if not access_token or not attachment_sudo.access_token or not consteq(attachment_sudo.access_token, access_token): message_sudo = self.env['mail.message'].sudo().search([('attachment_ids', 'in', attachment_sudo.ids)], limit=1) if not message_sudo or not message_sudo.is_current_user_or_guest_author: raise except (AccessError, MissingError): raise UserError(_("The attachment %s does not exist or you do not have the rights to access it.", attachment.id)) def _post_add_create(self, **kwargs): """ Overrides behaviour when the attachment is created through the controller """ super()._post_add_create(**kwargs) self.register_as_main_attachment(force=False) def register_as_main_attachment(self, force=True): """ Registers this attachment as the main one of the model it is attached to. :param bool force: if set, the method always updates the existing main attachment otherwise it only sets the main attachment if there is none. """ todo = self.filtered(lambda a: a.res_model and a.res_id) if not todo: return for model, attachments in todo.grouped("res_model").items(): related_records = self.env[model].browse(attachments.mapped("res_id")) if not hasattr(related_records, '_message_set_main_attachment_id'): return # this action is generic; if user cannot update record do not crash # just skip update for related_record, attachment in zip(related_records, attachments): with contextlib.suppress(AccessError): related_record._message_set_main_attachment_id(attachment, force=force) def _delete_and_notify(self, message=None): if message: # sudo: mail.message - safe write just updating the date, because guests don't have the rights message.sudo().write({}) # to make sure write_date on the message is updated for attachment in self: attachment._bus_send( "ir.attachment/delete", { "id": attachment.id, "message": ( {"id": message.id, "write_date": message.write_date} if message else None ), }, ) self.unlink() def _to_store(self, store: Store, /, *, fields=None, extra_fields=None): if fields is None: fields = [ "checksum", "create_date", "filename", "mimetype", "name", "res_name", "size", "thread", "type", "url", ] if extra_fields: fields.extend(extra_fields) for attachment in self: data = attachment._read_format( [field for field in fields if field not in ["filename", "size", "thread"]], load=False, )[0] if "filename" in fields: data["filename"] = attachment.name if "size" in fields: data["size"] = attachment.file_size if "thread" in fields: data["thread"] = ( Store.one( self.env[attachment.res_model].browse(attachment.res_id), as_thread=True, only_id=True, ) if attachment.res_model != "mail.compose.message" and attachment.res_id else False ) store.add(attachment, data)