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,3 @@
from . import eurooffice_odoo_templates
from . import eurooffice_odoo_demo_templates
from . import res_config_settings

View file

@ -0,0 +1,105 @@
import base64
import json
import logging
import os
from odoo import api, fields, models
from odoo.modules import get_module_path
_logger = logging.getLogger(__name__)
class EuroOfficeDemoTemplate(models.Model):
_name = "eurooffice.odoo.demo.templates"
_description = "Euro-Office Demo Templates"
selected_templates = fields.Text(string="Selected Templates")
def _get_template_structure(self):
templates_dir = self._get_templates_dir()
structure = {}
for root, _dirs, files in os.walk(templates_dir):
if files:
model = os.path.basename(root)
model_exists = bool(self.env["ir.model"].search([("model", "=", model)], limit=1))
if not model_exists:
continue
name = self._get_model_name(model)
rel_path = os.path.relpath(root, templates_dir)
structure[model] = {
"model": model,
"name": name,
"files": [
{
"name": f,
"path": os.path.join(rel_path, f) if rel_path != "." else f,
}
for f in files
],
}
return structure
def _get_model_name(self, model_name):
model = self.env["ir.model"].search([("model", "=", model_name)], limit=1)
return model.name if model else model_name
def _get_templates_dir(self):
module_path = get_module_path(self._module)
return os.path.join(module_path, "data", "templates")
@api.model
def get_template_data(self):
structure = self._get_template_structure()
selected = json.loads(self.selected_templates or "[]")
return {"structure": structure, "selected": selected}
def action_save(self):
selected_templates = json.loads(self.selected_templates or "[]")
if len(selected_templates) == 0:
return
templates_dir = self._get_templates_dir()
template_model = self.env["eurooffice.odoo.templates"]
for template_path in selected_templates:
model_name, filename = template_path.split("/")
full_path = os.path.join(templates_dir, template_path)
try:
with open(full_path, "rb") as f:
template_data = base64.b64encode(f.read())
model = self.env["ir.model"].search([("model", "=", model_name)], limit=1)
if not model:
continue
template_model.create(
{
"name": os.path.splitext(filename)[0],
"template_model_id": model.id,
"file": template_data,
"mimetype": "application/pdf",
}
)
except Exception as e:
_logger.error("Failed to process template %s: %s", template_path, str(e))
continue
return {
"type": "ir.actions.client",
"tag": "soft_reload",
}
def get_template_content(self, template_path):
templates_dir = self._get_templates_dir()
file_path = os.path.join(templates_dir, template_path)
with open(file_path, "rb") as f:
return f.read()

View file

@ -0,0 +1,344 @@
import base64
import json
import logging
import os
import time
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError
from odoo.modules import get_module_path
from odoo.addons.eurooffice_odoo.controllers.controllers import eurooffice_request
from odoo.addons.eurooffice_odoo.utils import config_utils, file_utils, jwt_utils, url_utils
from odoo.addons.eurooffice_odoo_templates.utils import pdf_utils
logger = logging.getLogger(__name__)
class EuroOfficeTemplate(models.Model):
_name = "eurooffice.odoo.templates"
_description = "Euro-Office Templates"
name = fields.Char(required=True, string="Template Name")
template_model_id = fields.Many2one("ir.model", string="Select Model")
template_model_name = fields.Char(string="Model Description", compute="_compute_template_model_fields", store=True)
template_model_related_name = fields.Char("Model Description", related="template_model_id.name")
template_model_model = fields.Char(string=" ", compute="_compute_template_model_fields", store=True)
file = fields.Binary(string="Upload an existing template")
hide_file_field = fields.Boolean(string="Hide File Field", default=False)
attachment_id = fields.Many2one("ir.attachment", readonly=True)
mimetype = fields.Char(default="application/pdf")
@api.onchange("name")
def _onchange_name(self):
if self.attachment_id:
self.attachment_id.name = self.name + ".pdf"
self.attachment_id.display_name = self.name
@api.depends("template_model_id")
def _compute_template_model_fields(self):
for record in self:
if record.template_model_id:
record.template_model_name = record.template_model_id.name
record.template_model_model = record.template_model_id.model
else:
record.template_model_name = False
record.template_model_model = False
@api.onchange("file")
def _onchange_file(self):
if self.file and self.create_date: # if file exist
decode_file = base64.b64decode(self.file)
is_pdf_form = pdf_utils.is_pdf_form(decode_file)
old_datas = self.attachment_id.datas
self.attachment_id.write({"datas": self.file})
self.file = False
if not is_pdf_form:
self.env.cr.commit()
converted_result = self._convert_to_form(self.attachment_id)
if converted_result.get("error"):
self.attachment_id.write({"datas": old_datas})
self.env.cr.commit()
raise UserError(converted_result.get("message"))
if converted_result.get("fileUrl"):
try:
response = eurooffice_request(
url=converted_result["fileUrl"],
method="get",
)
new_datas = base64.b64encode(response.content)
self.attachment_id.write({"datas": new_datas})
self.env.cr.commit()
except Exception as e:
logger.error("Failed to download and update PDF form: %s", str(e))
self.attachment_id.write({"datas": old_datas})
self.env.cr.commit()
raise UserError(_("Failed to download converted PDF form")) from e
@api.model
def _create_demo_data(self):
module_path = get_module_path(self._module)
templates_dir = os.path.join(module_path, "data", "templates")
if not os.path.exists(templates_dir):
return
model_folders = [name for name in os.listdir(templates_dir) if os.path.isdir(os.path.join(templates_dir, name))]
installed_models = self.env["ir.model"].search([])
installed_models_list = [(model.model, model.name) for model in installed_models]
for model_name in model_folders:
if any(model_name == model[0] for model in installed_models_list):
templates_path = os.path.join(templates_dir, model_name)
templates_name = [
name
for name in os.listdir(templates_path)
if os.path.isfile(os.path.join(templates_path, name)) and name.lower().endswith(".pdf")
]
for template_name in templates_name:
template_path = os.path.join(templates_path, template_name)
template = open(template_path, "rb")
try:
template_data = template.read()
template_data = base64.encodebytes(template_data)
model = self.env["ir.model"].search([("model", "=", model_name)], limit=1)
name = template_name.rstrip(".pdf")
self.create(
{
"name": name,
"template_model_id": model.id,
"file": template_data,
}
)
finally:
template.close()
return
@api.model
def create(self, vals):
url = self._context.get("url", None)
if isinstance(url, str) and url.startswith(("http://", "https://")) and url.endswith(".pdf"):
try:
response = eurooffice_request(
url=url,
method="get",
)
file_content = response.content
vals["file"] = base64.b64encode(file_content)
except Exception as e:
raise UserError(_("Failed to download form")) from e
is_pdf_form = None
if "file" in vals and vals["file"]:
try:
decode_file = base64.b64decode(vals["file"])
is_pdf_form = pdf_utils.is_pdf_form(decode_file)
except Exception as e:
raise UserError(_("Invalid file format.")) from e
else:
vals["file"] = base64.encodebytes(file_utils.get_default_file_template(self.env.user.lang, "pdf"))
is_pdf_form = True
model = self.env["ir.model"].search([("id", "=", vals["template_model_id"])], limit=1)
vals["template_model_name"] = model.name
vals["template_model_model"] = model.model
vals["mimetype"] = file_utils.get_mime_by_ext("pdf")
datas = vals.pop("file")
vals.pop("hide_file_field", None)
vals.pop("datas", None)
record = super().create(
{
"name": vals.get("name", "New Template"),
"template_model_id": vals.get("template_model_id"),
"mimetype": vals.get("mimetype", "application/pdf"),
"template_model_name": vals.get("template_model_name", ""),
"template_model_model": vals.get("template_model_model", ""),
}
)
attachment = self.env["ir.attachment"].create(
{
"name": vals.get("name", record.name) + ".pdf",
"display_name": vals.get("name", record.name),
"mimetype": vals.get("mimetype"),
"datas": datas,
"res_model": self._name,
"res_id": record.id,
}
)
record.attachment_id = attachment.id
if not is_pdf_form:
self.env.cr.commit()
converted_result = self._convert_to_form(attachment)
if converted_result.get("error"):
attachment.unlink()
record.unlink()
super().unlink()
self.env.cr.commit()
raise UserError(converted_result.get("message"))
if converted_result.get("fileUrl"):
try:
response = eurooffice_request(
url=converted_result["fileUrl"],
method="get",
)
new_datas = base64.b64encode(response.content)
attachment.write({"datas": new_datas, "mimetype": vals.get("mimetype")})
self.env.cr.commit()
except Exception as e:
logger.error("Failed to download and update PDF form: %s", str(e))
attachment.unlink()
record.unlink()
super().unlink()
self.env.cr.commit()
raise UserError(_("Failed to download converted PDF form")) from e
return record
@api.model
def _convert_to_form(self, attachment):
jwt_header = config_utils.get_jwt_header(self.env)
jwt_secret = config_utils.get_jwt_secret(self.env)
docserver_url = config_utils.get_doc_server_public_url(self.env)
docserver_url = url_utils.replace_public_url_to_internal(self.env, docserver_url)
odoo_url = config_utils.get_base_or_odoo_url(self.env)
internal_jwt_secret = config_utils.get_internal_jwt_secret(self.env)
oo_security_token = jwt_utils.encode_payload(self.env, {"id": self.env.user.id}, internal_jwt_secret)
oo_security_token = (
oo_security_token.decode("utf-8") if isinstance(oo_security_token, bytes) else oo_security_token
)
conversion_url = os.path.join(docserver_url, "ConvertService.ashx")
payload = {
"url": f"{odoo_url}eurooffice/template/download/{attachment.id}?oo_security_token={oo_security_token}",
"key": int(time.time()),
"filetype": "pdf",
"outputtype": "pdf",
"pdf": {
"form": True,
},
}
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if bool(jwt_secret):
payload = {"payload": payload}
token = jwt_utils.encode_payload(self.env, payload, jwt_secret)
headers[jwt_header] = "Bearer " + token
payload["token"] = token
try:
response = eurooffice_request(
url=conversion_url,
method="get",
opts={
"data": json.dumps(payload),
"headers": headers,
},
)
if response.status_code == 200:
response_json = response.json()
if "error" in response_json:
return {
"error": response_json.get("error"),
"message": self._get_conversion_error_message(response_json.get("error")),
}
else:
return response_json
else:
return {
"error": response.status_code,
"message": f"Document conversion service returned status {response.status_code}",
}
except Exception:
return {
"error": 1,
"message": "Document conversion service cannot be reached",
}
def _get_conversion_error_message(self, error_code):
error_dictionary = {
-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 error_dictionary[error_code]
except Exception:
return "Undefined error code"
@api.model
def get_fields_for_model(self, model, prefix="", parent_name="", exclude=None):
try:
m = self.env[model]
fields = m.fields_get()
except Exception:
return []
fields = sorted(fields.items(), key=lambda field: tools.ustr(field[1].get("string", "").lower()))
records = []
for field_name, field in fields:
if exclude and field_name in exclude:
continue
if field.get("type") in ("properties", "properties_definition", "html", "json"):
continue
if not field.get("exportable", True):
continue
ident = prefix + ("/" if prefix else "") + field_name
val = ident
name = parent_name + (parent_name and "/" or "") + field["string"]
record = {
"id": ident,
"string": name,
"value": val,
"children": False,
"field_type": field.get("type"),
"required": field.get("required"),
"relation_field": field.get("relation_field"),
}
records.append(record)
if len(ident.split("/")) < 4 and "relation" in field:
ref = field.pop("relation")
record["value"] += "/id"
record["params"] = {"model": ref, "prefix": ident, "name": name}
record["children"] = True
return records
@api.model
def update_relationship(self, template_model_id, model):
"""
If the module was uninstalled and reinstalled, its model id may have changed.
Update the model id in the template record
"""
if not template_model_id or not model:
return
model_id = self.sudo().env["ir.model"].search([("model", "=", model)]).id
if not model_id:
return
record = self.sudo().env["eurooffice.odoo.templates"].browse(template_model_id)
if not record:
return
if record.template_model_id != model_id:
record.template_model_id = model_id
return

View file

@ -0,0 +1,21 @@
from odoo import fields, models
from odoo.addons.eurooffice_odoo_templates.utils import config_utils
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
editable_form_fields = fields.Boolean("Disable form fields after printing PDF form")
def set_values(self):
res = super().set_values()
previous_disable_form_fields = config_utils.get_editable_form_fields(self.env)
if previous_disable_form_fields != self.editable_form_fields:
config_utils.set_editable_form_fields(self.env, self.editable_form_fields)
return res
def get_values(self):
res = super().get_values()
editable_form_fields = config_utils.get_editable_form_fields(self.env)
res.update(editable_form_fields=editable_form_fields)
return res