init: Euro-Office Odoo 16.0 modules

Based on onlyoffice_odoo by Ascensio System SIA (ONLYOFFICE, LGPL-3).
Rebranded and adapted for Euro-Office by bring.out d.o.o.

Modules:
- eurooffice_odoo: base integration
- eurooffice_odoo_templates: document templates
- eurooffice_odoo_oca_dms: OCA DMS integration (replaces Enterprise documents)

All references renamed: onlyoffice -> eurooffice, ONLYOFFICE -> Euro-Office.
Original copyright notices preserved.
This commit is contained in:
Ernad Husremovic 2026-03-31 17:24:17 +02:00
commit b59a9dc6bb
347 changed files with 16699 additions and 0 deletions

View file

@ -0,0 +1,15 @@
#
# (c) Copyright Ascensio System SIA 2024
#
DOC_SERVER_PUBLIC_URL = "eurooffice_connector.doc_server_public_url"
DOC_SERVER_ODOO_URL = "eurooffice_connector.doc_server_odoo_url"
DOC_SERVER_INNER_URL = "eurooffice_connector.doc_server_inner_url"
DOC_SERVER_JWT_SECRET = "eurooffice_connector.doc_server_jwt_secret"
DOC_SERVER_JWT_HEADER = "eurooffice_connector.doc_server_jwt_header"
DOC_SERVER_DEMO = "eurooffice_connector.doc_server_demo"
DOC_SERVER_DEMO_DATE = "eurooffice_connector.doc_server_demo_date"
DOC_SERVER_DISABLE_CERTIFICATE = "eurooffice_connector.doc_server_disable_certificate"
SAME_TAB = "eurooffice_connector.same_tab"
INTERNAL_JWT_SECRET = "eurooffice_connector.internal_jwt_secret"

View file

@ -0,0 +1,130 @@
#
# (c) Copyright Ascensio System SIA 2024
#
import uuid
from datetime import date
from odoo.addons.eurooffice_odoo.utils import config_constants
def get_base_or_odoo_url(env):
url = env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_ODOO_URL)
return fix_url(url or env["ir.config_parameter"].sudo().get_param("web.base.url"))
def get_doc_server_public_url(env):
url = env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_PUBLIC_URL)
if not url:
url = "http://documentserver/"
return fix_url(url)
def get_doc_server_inner_url(env):
url = env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_INNER_URL)
return fix_url(url or get_doc_server_public_url(env))
def get_jwt_header(env):
header = env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_JWT_HEADER)
if not header:
header = "Authorization"
return header
def get_jwt_secret(env):
return env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_JWT_SECRET)
def get_internal_jwt_secret(env):
secret = env["ir.config_parameter"].sudo().get_param(config_constants.INTERNAL_JWT_SECRET)
if not secret:
secret = uuid.uuid4().hex
env["ir.config_parameter"].sudo().set_param(config_constants.INTERNAL_JWT_SECRET, secret)
env.cr.commit()
return secret
def get_demo(env):
return env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_DEMO)
def get_demo_date(env):
return env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_DEMO_DATE)
def get_same_tab(env):
return env["ir.config_parameter"].sudo().get_param(config_constants.SAME_TAB)
def get_certificate_verify_disabled(env):
return env["ir.config_parameter"].sudo().get_param(config_constants.DOC_SERVER_DISABLE_CERTIFICATE)
def set_doc_server_public_url(env, url):
if not url:
url = "http://documentserver/"
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_PUBLIC_URL, fix_url(url))
def set_doc_server_odoo_url(env, url):
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_ODOO_URL, fix_url(url))
def set_doc_server_inner_url(env, url):
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_INNER_URL, fix_url(url))
def set_jwt_header(env, header):
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_JWT_HEADER, header)
def set_jwt_secret(env, secret):
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_JWT_SECRET, secret)
def set_demo(env, param):
demo = get_demo(env)
demo_date = get_demo_date(env)
if not demo_date:
set_demo_date(env)
if param:
set_doc_server_public_url(env, "https://onlinedocs.docs.eurooffice.com/")
set_doc_server_odoo_url(env, "")
set_doc_server_inner_url(env, "")
set_jwt_header(env, "AuthorizationJWT")
set_jwt_secret(env, "sn2puSUF7muF5Jas")
elif demo and not param:
set_doc_server_public_url(env, "http://documentserver/")
set_doc_server_odoo_url(env, "")
set_doc_server_inner_url(env, "")
set_jwt_header(env, "Authorization")
set_jwt_secret(env, "")
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_DEMO, param)
def set_demo_date(env):
demo_date = date.today()
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_DEMO_DATE, demo_date)
def set_same_tab(env, param):
env["ir.config_parameter"].sudo().set_param(config_constants.SAME_TAB, param)
def set_certificate_verify_disabled(env, param):
env["ir.config_parameter"].sudo().set_param(config_constants.DOC_SERVER_DISABLE_CERTIFICATE, param)
def fix_url(url):
if url:
return fix_end_slash(fix_proto(url))
def fix_proto(url):
return url if url.startswith("http") else ("http://" + url)
def fix_end_slash(url):
return url if url.endswith("/") else (url + "/")

View file

@ -0,0 +1,138 @@
#
# (c) Copyright Ascensio System SIA 2024
#
import os
from odoo.addons.eurooffice_odoo.utils import format_utils
def get_file_title_without_ext(name):
ind = name.rfind(".")
return name[:ind]
def get_file_name_without_ext(name):
ind = name.rfind(".")
return name[:ind]
def get_file_ext(name):
return name[name.rfind(".") + 1 :].lower()
def get_file_type(context):
for supported_format in format_utils.get_supported_formats():
if supported_format.name == get_file_ext(context):
return supported_format.type
return None
def can_view(context):
for supported_format in format_utils.get_supported_formats():
if supported_format.name == get_file_ext(context):
if "view" in supported_format.actions:
return True
return False
def can_edit(context):
for supported_format in format_utils.get_supported_formats():
if supported_format.name == get_file_ext(context):
if "edit" in supported_format.actions:
return True
return False
def can_fill_form(context):
for supported_format in format_utils.get_supported_formats():
if supported_format.name == get_file_ext(context):
if "fill" in supported_format.actions:
return True
return False
def get_mime_by_ext(ext):
if ext == "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
if ext == "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
if ext == "pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
if ext == "pdf":
return "application/pdf"
return None
def get_default_file_template(lang, ext):
locale_path = {
"az": "az-Latn-AZ",
"bg": "bg-BG",
"cs": "cs-CZ",
"de": "de-DE",
"default": "default",
"el": "el-GR",
"en-gb": "en-GB",
"en": "en-US",
"es": "es-ES",
"eu": "eu-ES",
"fi": "fi-FI",
"fr": "fr-FR",
"gl": "gl-ES",
"he": "he-IL",
"it": "it-IT",
"ja": "ja-JP",
"ko": "ko-KR",
"lv": "lv-LV",
"nb": "nb-NO",
"nl": "nl-NL",
"pl": "pl-PL",
"pt-br": "pt-BR",
"pt": "pt-PT",
"ru": "ru-RU",
"sk": "sk-SK",
"sv": "sv-SE",
"tr": "tr-TR",
"uk": "uk-UA",
"vi": "vi-VN",
"zh-CN": "zh-CN",
"zh-TW": "zh-TW",
"ca": "ca-ES",
"da": "da-DK",
"hu": "hu-HU",
"id": "id-ID",
"ro": "ro-RO",
}
lang = lang.replace("_", "-")
locale = locale_path.get(lang)
if locale is None:
lang = lang.split("-")[0]
locale = locale_path.get(lang)
if locale is None:
locale = locale_path.get("default")
file = open(
os.path.join(
os.path.abspath(os.path.dirname(__file__)),
"..",
"static",
"assets",
"document_templates",
locale,
"new." + ext,
),
"rb",
)
try:
file_data = file.read()
return file_data
finally:
file.close()

View file

@ -0,0 +1,42 @@
#
# (c) Copyright Ascensio System SIA 2024
#
import json
import os
class Format:
def __init__(self, name, type, actions=None, convert=None, mime=None):
if actions is None:
actions = []
if convert is None:
convert = []
if mime is None:
mime = []
self.name = name
self.type = type
self.actions = actions
self.convert = convert
self.mime = mime
def get_supported_formats():
file_path = os.path.join(
os.path.dirname(__file__), "..", "static", "assets", "document_formats", "eurooffice-docs-formats.json"
)
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
formats = []
for item in data:
n = item["name"]
t = item["type"]
a = item.get("actions", [])
c = item.get("convert", [])
m = item.get("mime", [])
formats.append(Format(n, t, a, c, m))
return formats

View file

@ -0,0 +1,29 @@
#
# (c) Copyright Ascensio System SIA 2024
#
import datetime
import jwt
from odoo.addons.eurooffice_odoo.utils import config_utils
def is_jwt_enabled(env):
return bool(config_utils.get_jwt_secret(env))
def encode_payload(env, payload, secret=None):
if secret is None:
secret = config_utils.get_jwt_secret(env)
now = datetime.datetime.utcnow()
exp = now + datetime.timedelta(hours=24)
payload["iat"] = int(now.timestamp())
payload["exp"] = int(exp.timestamp())
return jwt.encode(payload, secret, algorithm="HS256")
def decode_token(env, token, secret=None):
if secret is None:
secret = config_utils.get_jwt_secret(env)
return jwt.decode(token, secret, algorithms=["HS256"])

View file

@ -0,0 +1,14 @@
import logging
from odoo.addons.eurooffice_odoo.utils import config_utils
_logger = logging.getLogger(__name__)
def replace_public_url_to_internal(env, url):
public_url = config_utils.get_doc_server_public_url(env)
inner_url = config_utils.get_doc_server_inner_url(env)
if inner_url and inner_url != public_url:
_logger.info("Replace public url %s to internal url %s", public_url, inner_url)
url = url.replace(public_url, inner_url)
return url

View file

@ -0,0 +1,183 @@
import json
import os
import re
import time
from urllib.request import urlopen
import requests
from odoo.exceptions import ValidationError
from odoo.addons.eurooffice_odoo.utils import jwt_utils
def valid_url(url):
if not url:
return True
# pylint: disable=anomalous-backslash-in-string
pattern = "^(https?:\/\/)?[\w-]{1,32}(\.[\w-]{1,32})*[\/\w-]*(:[\d]{1,5}\/?)?$"
# pylint: enable=anomalous-backslash-in-string
if re.findall(pattern, url):
return True
return False
def settings_validation(self):
base_url = self.doc_server_odoo_url
public_url = self.doc_server_public_url
inner_url = self.doc_server_inner_url
jwt_secret = self.doc_server_jwt_secret
jwt_header = self.doc_server_jwt_header
disable_certificate = self.doc_server_disable_certificate
demo = self.doc_server_demo
url = public_url
if inner_url and inner_url != public_url:
url = inner_url
check_mixed_content(base_url, url, demo)
check_doc_serv_url(url, demo, disable_certificate)
check_doc_serv_command_service(self.env, url, jwt_secret, jwt_header, disable_certificate, demo)
check_doc_serv_convert_service(self.env, url, base_url, jwt_secret, jwt_header, disable_certificate, demo)
def check_mixed_content(base_url, url, demo):
if base_url.startswith("https") and not url.startswith("https"):
get_message_error("Mixed Active Content is not allowed. HTTPS address for Document Server is required.", demo)
def check_doc_serv_url(url, demo, disable_certificate):
try:
url = os.path.join(url, "healthcheck")
context = None
if disable_certificate and url.startswith("https://"):
import ssl
context = ssl._create_unverified_context()
response = urlopen(url, timeout=30, context=context)
healthcheck = response.read()
if not healthcheck:
get_message_error(os.path.join(url, "healthcheck") + " returned false.", demo)
except ValidationError as e:
get_message_error(str(e), demo)
except Exception:
get_message_error("Euro-Office cannot be reached", demo)
def check_doc_serv_command_service(env, url, jwt_secret, jwt_header, disable_certificate, demo):
try:
headers = {"Content-Type": "application/json"}
body_json = {"c": "version"}
if jwt_secret is not None and jwt_secret is not False and jwt_secret != "":
payload = {"payload": body_json}
header_token = jwt_utils.encode_payload(env, payload, jwt_secret)
headers[jwt_header] = "Bearer " + header_token
token = jwt_utils.encode_payload(env, body_json, jwt_secret)
body_json["token"] = token
response = requests.post(
os.path.join(url, "coauthoring/CommandService.ashx"),
verify=not disable_certificate,
timeout=60,
data=json.dumps(body_json),
headers=headers,
)
if response.json()["error"] == 6:
get_message_error("Authorization error", demo)
if response.json()["error"] != 0:
get_message_error(
os.path.join(url, "coauthoring/CommandService.ashx")
+ " returned error: "
+ str(response.json()["error"]),
demo,
)
except ValidationError as e:
get_message_error(str(e), demo)
except Exception:
get_message_error("Error when trying to check CommandService", demo)
def check_doc_serv_convert_service(env, url, base_url, jwt_secret, jwt_header, disable_certificate, demo):
file_url = os.path.join(base_url, "eurooffice/file/content/test.txt")
result = convert(env, file_url, url, jwt_secret, jwt_header, disable_certificate)
if isinstance(result, str):
return get_message_error(result, demo)
def convert(env, file_url, url, jwt_secret, jwt_header, disable_certificate):
body_json = {
"key": int(time.time()),
"url": file_url,
"filetype": "txt",
"outputtype": "txt",
}
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if bool(jwt_secret):
payload = {"payload": body_json}
header_token = jwt_utils.encode_payload(env, payload, jwt_secret)
headers[jwt_header] = "Bearer " + header_token
token = jwt_utils.encode_payload(env, body_json, jwt_secret)
body_json["token"] = token
try:
response = requests.post(
os.path.join(url, "ConvertService.ashx"),
verify=not disable_certificate,
timeout=60,
data=json.dumps(body_json),
headers=headers,
)
if response.status_code == 200:
response_json = response.json()
if "error" in response_json:
return get_conversion_error_message(response_json.get("error"))
else:
return f"Document conversion service returned status {response.status_code}"
except Exception:
return "Document conversion service cannot be reached"
def get_message_error(message, demo):
if demo:
raise ValidationError(f"Error connecting to demo server: {message}")
else:
raise ValidationError(message)
def get_conversion_error_message(errorCode):
errorDictionary = {
-1: "Unknown error",
-2: "Conversion timeout error",
-3: "Conversion error",
-4: "Error while downloading the document file to be converted",
-5: "Incorrect password",
-6: "Error while accessing the conversion result database",
-7: "Input error",
-8: "Invalid token",
}
try:
return errorDictionary[errorCode]
except Exception:
return "Undefined error code"