# file: my_module/controllers/main.py import base64 import tempfile import subprocess import os import re import markupsafe import json from odoo import http from odoo.http import request import logging from odoo.addons.onlyoffice_odoo.controllers.controllers import Onlyoffice_Connector from odoo.addons.onlyoffice_odoo.utils import config_utils, file_utils, jwt_utils, url_utils _logger = logging.getLogger(__name__) _mobile_regex = r"android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino" # noqa: E501 import mimetypes # --------------------------------------------------------- # Reliable MIME → EXT mapping for Office, PDF, others # --------------------------------------------------------- OFFICE_MAPPING = { "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template": ".dotx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template": ".xltx", "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow": ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.template": ".potx", "application/pdf": ".pdf", } def _get_extension_from_mimetype(mimetype): if not mimetype: return None # 1️⃣ Exact match for Office formats if mimetype in OFFICE_MAPPING: return OFFICE_MAPPING[mimetype] # 2️⃣ Try Python's builtin guess ext = mimetypes.guess_extension(mimetype) if ext: return ext # 3️⃣ Fallback return None class AttachmentPreview(http.Controller): @http.route("/lookup_or_create/attachment", type="json", auth="user") def lookup_or_create_attachment(self, model, res_id, field): record = request.env[model].sudo().browse(int(res_id)) if not record.exists(): return {"error": "Record not found"} # Check existing attachment attach = request.env["ir.attachment"].sudo().search([ ("res_model", "=", model), ("res_id", "=", int(res_id)), ("res_field", "=", field) ], limit=1) # Create attachment if missing if not attach: binary_data = getattr(record, field) if not binary_data: return {"error": "Binary field empty"} filename = record._fields[field].string or "file.bin" attach = request.env["ir.attachment"].sudo().create({ "name": filename, "datas": binary_data, "res_model": model, "res_id": int(res_id), "res_field": field, "type": "binary", }) return { "attachment_id": attach.id, "name": attach.name, "mimetype": attach.mimetype, "url": f"/web/content/{attach.id}?download=false", } def convert_office_to_pdf(self,data, ext): """Convert office files to PDF using LibreOffice.""" with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as f: f.write(data) input_path = f.name output_path = input_path.replace(f".{ext}", ".pdf") subprocess.run([ "libreoffice", "--headless", "--convert-to", "pdf", "--outdir", os.path.dirname(input_path), input_path ], check=True) with open(output_path, "rb") as f: pdf_data = f.read() return pdf_data @http.route("/universal_preview/get_file",type="json",auth="user",methods=["POST"],) def get_file(self, model, res_id, field): """ Input JSON: { "model": "res.partner", "res_id": 12, "field": "x_document" } """ # model = model # res_id = res_id # field = field record = request.env[model].sudo().browse(res_id) if not record.exists(): return {"error": "Record not found"} attachment = request.env["ir.attachment"].sudo().search([ ("res_model", "=", model), ("res_id", "=", res_id), ("res_field", "=", field) ], limit=1) if not attachment: return {"error": "Attachment not found"} mimetype = attachment.mimetype file_data = base64.b64decode(attachment.datas) filename = attachment.name.lower() # office → pdf conversion office_map = { "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", "application/msword": "doc", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx", "application/vnd.ms-excel": "xls", "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx", "application/vnd.ms-powerpoint": "ppt", } ext = office_map.get(mimetype) if ext: try: pdf_data = self.convert_office_to_pdf(file_data, ext) return { "filename": filename.replace(f".{ext}", ".pdf"), "mimetype": "application/pdf", "data": base64.b64encode(pdf_data).decode() } except Exception as e: return {"error": f"Conversion failed: {str(e)}"} # return original file return { "filename": filename, "mimetype": mimetype, "data": attachment.datas, } class OnlyofficeConnector(Onlyoffice_Connector): @http.route("/onlyoffice/editor/", auth="public", type="http", website=True) def render_editor(self, attachment_id, access_token=None, readonly=False): _logger.info("GET /onlyoffice/editor/%s", attachment_id) attachment = self.get_attachment(attachment_id) if not attachment: _logger.warning("GET /onlyoffice/editor/%s - attachment not found", attachment_id) return request.not_found() attachment.validate_access(access_token) if attachment.res_model == "documents.document": document = request.env["documents.document"].browse(int(attachment.res_id)) self._check_document_access(document) data = attachment.read(["id", "checksum", "public", "name", "access_token"])[0] filename = data["name"] can_read = attachment.has_access("read") and file_utils.can_view(filename) if readonly: can_read = attachment.has_access("read") can_write = not readonly and (attachment.has_access("write") and file_utils.can_edit(filename)) if not can_read: _logger.warning("GET /onlyoffice/editor/%s - no read access", attachment_id) raise Exception("cant read") _logger.info("GET /onlyoffice/editor/%s - success", attachment_id) return request.render( "onlyoffice_odoo.onlyoffice_editor", self.prepare_editor_values(attachment, access_token, can_write) ) # # def prepare_editor_values(self, attachment, access_token, can_write): # import pdb # pdb.set_trace() # _logger.info("prepare_editor_values - attachment: %s", attachment.id) # data = attachment.read(["id", "checksum", "public", "name", "access_token", "mimetype"])[0] # # filename = data.get("name") or "" # mimetype = attachment.mimetype or "" # # # Extract current extension (if exists) # current_ext = "" # if "." in filename: # current_ext = "." + filename.split(".")[-1].lower() # # # Compute correct extension from mimetype # correct_ext = _get_extension_from_mimetype(mimetype) # # # ----------------------------------------------------- # # FIX #1: filename has NO extension → add correct ext # # FIX #2: filename has WRONG extension → replace it # # ----------------------------------------------------- # if not correct_ext: # # fallback for unknown mimetypes # correct_ext = ".bin" # # if not filename or "." not in filename: # # No extension → set new name # new_name = f"{attachment.res_field}_{attachment.id}{correct_ext}" # attachment.write({"name": new_name}) # data["name"] = new_name # # else: # # Has an extension → check if it matches # if current_ext != correct_ext: # base = filename.rsplit(".", 1)[0] # new_name = f"{base}{correct_ext}" # attachment.write({"name": new_name}) # data["name"] = new_name # else: # # extension is correct # data["name"] = filename # # # Continue your original logic without changes # key = str(data["id"]) + str(data["checksum"]) # docserver_url = config_utils.get_doc_server_public_url(request.env) # odoo_url = config_utils.get_base_or_odoo_url(request.env) # # filename = self.filter_xss(data["name"]) # # security_token = jwt_utils.encode_payload( # request.env, {"id": request.env.user.id}, config_utils.get_internal_jwt_secret(request.env) # ) # security_token = security_token.decode("utf-8") if isinstance(security_token, bytes) else security_token # access_token = access_token.decode("utf-8") if isinstance(access_token, bytes) else access_token # # path_part = ( # str(data["id"]) # + "?oo_security_token=" # + security_token # + ("&access_token=" + access_token if access_token else "") # + "&shardkey=" # + key # ) # # document_type = file_utils.get_file_type(filename) # is_mobile = bool(re.search(_mobile_regex, request.httprequest.headers.get("User-Agent"), re.IGNORECASE)) # # root_config = { # "width": "100%", # "height": "100%", # "type": "mobile" if is_mobile else "desktop", # "documentType": document_type, # "document": { # "title": filename, # "url": odoo_url + "onlyoffice/file/content/" + path_part, # "fileType": file_utils.get_file_ext(filename), # "key": key, # "permissions": {}, # }, # "editorConfig": { # "lang": request.env.user.lang, # "user": {"id": str(request.env.user.id), "name": request.env.user.name}, # "customization": {}, # }, # } # # if can_write: # root_config["editorConfig"]["callbackUrl"] = odoo_url + "onlyoffice/editor/callback/" + path_part # # if attachment.res_model != "documents.document": # root_config["editorConfig"]["mode"] = "edit" if can_write else "view" # root_config["document"]["permissions"]["edit"] = can_write # else: # root_config = self.get_documents_permissions(attachment, can_write, root_config) # # if jwt_utils.is_jwt_enabled(request.env): # root_config["token"] = jwt_utils.encode_payload(request.env, root_config) # # _logger.info("prepare_editor_values - success: %s", attachment.id) # return { # "docTitle": filename, # "docIcon": f"/onlyoffice_odoo/static/description/editor_icons/{document_type}.ico", # "docApiJS": docserver_url + "web-apps/apps/api/documents/api.js", # "editorConfig": markupsafe.Markup(json.dumps(root_config)), # }