odoo18/addons_extensions/universal_attachment_preview/controllers/main.py

317 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<int:attachment_id>", 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)),
# }