Skip to content
Snippets Groups Projects

Add dynamic expression button for substitution line and new converter features

Merged oury.balde requested to merge topic/17.0/fsb into branch/17.0
6 files
+ 427
276
Compare changes
  • Side-by-side
  • Inline
Files
6
+ 399
268
@@ -26,10 +26,10 @@
from odoo.tools.cache import ormcache
from ..redner import REDNER_API_PATH, Redner
from ..utils.mimetype import b64_to_extension
from ..utils.mimetype import get_file_extension
logger = logging.getLogger(__name__)
_redner = None
LANGUAGE_MJML_MUSTACHE = "text/mjml|mustache"
@@ -30,9 +30,10 @@
logger = logging.getLogger(__name__)
_redner = None
LANGUAGE_MJML_MUSTACHE = "text/mjml|mustache"
LANGUAGE_OPENDOCUMENT_MUSTACHE = "application/vnd.oasis.opendocument.text|od+mustache"
DEFAULT_LANGUAGE = LANGUAGE_MJML_MUSTACHE
COMPUTED_FIELDS = [
"body",
@@ -87,7 +88,11 @@
string="Name",
default="New",
required=True,
help="This is a name of template mjml redner",
help=(
"The name of the template. Once the template is created, "
"updating the name is not allowed. To change the name, "
"delete the template and create a new one."
),
)
description = fields.Char(
@@ -138,7 +143,7 @@
("text/html|mustache", "HTML + mustache"),
(LANGUAGE_MJML_MUSTACHE, "MJML + mustache"),
(
"application/vnd.oasis.opendocument.text|od+mustache",
LANGUAGE_OPENDOCUMENT_MUSTACHE,
"OpenDocument + mustache",
),
],
@@ -163,7 +168,8 @@
string="Libreoffice Template",
readonly=False,
compute="_compute_template_data",
inverse="_inverse_template_data",
)
template_data_filename = fields.Char(
string="Libreoffice Template Filename",
@@ -166,7 +172,7 @@
)
template_data_filename = fields.Char(
string="Libreoffice Template Filename",
readonly=True,
compute="_compute_template_data_filename",
)
@@ -171,7 +177,6 @@
)
def import_from_redner(self):
tl_wizard = self.env["template.list.wizard"]
templates = self.list_external_templates()
tl_wizard.populate(templates)
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@@ -177,13 +182,9 @@
return {
"type": "ir.actions.act_window",
"res_model": "template.list.wizard",
"view_mode": "tree",
"view_id": self.env.ref("redner.view_template_list_wizard_tree").id,
"target": "new",
#'context': self.env.context,
}
@api.depends("body", "template_data")
def _compute_keywords(self):
for record in self:
record.detected_keywords = "\n".join(record.template_varlist_fetch())
@api.depends("body", "template_data")
def _compute_preview(self):
for record in self:
@@ -186,7 +187,6 @@
@api.depends("body", "template_data")
def _compute_preview(self):
for record in self:
record.preview = False
if record.redner_id:
if record.body or record.template_data:
response = self.get_preview(record.redner_id)
@@ -192,5 +192,146 @@
response = self.get_preview(record.redner_id)
if response:
b64 = base64.b64encode(response.content)
record.preview = b64
# Check if response is valid and has content
if response and hasattr(response, "content"):
record.preview = base64.b64encode(response.content)
else:
record.preview = False # If no valid response, set preview to False
else:
record.preview = False
def _compute_template(self):
"""
Computes the template values for the records and applies cached or fetched data.
"""
for record in self:
if not record.id or not record.redner_id:
continue
# Fetch the cached template
cached_template = self._get_cached_template(record.id)
if not any([getattr(record, f) for f in COMPUTED_FIELDS]):
# If all computed fields are undefined, populate them
# from the cached template.
for f in COMPUTED_FIELDS + EDITABLE_FIELDS:
if f in cached_template:
setattr(record, f, cached_template[f])
else:
# If at least one field is defined, populate only undefined fields
for f in COMPUTED_FIELDS:
if not getattr(record, f):
setattr(record, f, cached_template.get(f, None))
def _inverse_template_data(self):
"""
Inverse function for `template_data`. Called when `template_data` is"
manually set.
"""
for record in self:
if not record.template_data or not record.id or not record.language:
continue
try:
# Update the external system with the new data
self._set_cached_template(record.id, record.template_data)
except Exception as e:
logger.error("Failed to update template data in Redner: %s", e)
raise ValidationError(
_(
"Unable to update the template data. Please check the logs "
"for more details."
)
) from e
def _compute_template_data(self):
for record in self:
# Skip records that do not have a redner_id or are missing essential data
if not record.id or not record.redner_id:
continue
if (
not record.template_data
and record.language == LANGUAGE_OPENDOCUMENT_MUSTACHE
):
cached_template = self._get_cached_template(record.id)
template_data = (
cached_template.get("template_data") if cached_template else None
)
if template_data:
try:
# Perform base64 encoding and store the result
record.template_data = base64.b64encode(template_data).decode(
"utf-8"
)
except Exception as e:
logger.error(
"Failed to encode redner template data for record %s: %s",
record.id,
e,
)
continue # Proceed with next record if encoding fails
@api.depends("template_data")
def _compute_template_data_filename(self):
"""Compute the template filename based on the template data"""
for record in self:
if not record.id or not record.redner_id or not record.template_data:
record.template_data_filename = (
f"{record.name}.odt" if record.name else "template.odt"
)
else:
try:
# Attempt to extract the file extension from the base64 data
ext = get_file_extension(record.template_data)
record.template_data_filename = f"{record.name}{ext}"
except Exception as e:
logger.error("Error while computing template filename: %s", e)
record.template_data_filename = False
@property
def redner(self):
"""
Returns a Redner instance.
Recomputes the instance if any system parameter changes.
Uses a global variable to cache the instance across sessions.
"""
global _redner, _redner_params
# Fetch current system parameters
config_model = self.env["ir.config_parameter"].sudo()
current_params = {
"api_key": config_model.get_param("redner.api_key"),
"server_url": config_model.get_param("redner.server_url"),
"account": config_model.get_param("redner.account"),
"timeout": int(config_model.get_param("redner.timeout", default="20")),
}
# Check if parameters have changed or if _redner is None
if _redner is None or _redner_params != current_params:
# Recompute the Redner instance
_redner = Redner(
current_params["api_key"],
current_params["server_url"],
current_params["account"],
current_params["timeout"],
)
_redner_params = current_params # Update the stored parameters
return _redner
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
"""Overwrite create to create redner template"""
for vals in vals_list:
# If "name" is missing or equals "New", set the source and
# proceed with creation.
if not vals.get("name", False) or vals["name"] == "New":
vals["source"] = "redner"
continue # Continue processing the next record
@@ -196,3 +337,99 @@
def get_preview(self, redner_id):
# Prepare template params according to the selected language.
# Use template data field if the selected language is "od";
# otherwise the body field is used.
produces, language = vals.get("language", DEFAULT_LANGUAGE).split("|")
body, body_format = (
(vals.get("template_data", ""), "base64")
if language == "od+mustache"
else (vals.get("body"), "text")
)
locale = self.env["res.lang"].browse(vals.get("locale_id")).code
# We depend on the API for consistency here
# So raised error should not result with a created template
if language and body:
template = self.redner.templates.account_template_add(
language=language,
body=body,
name=vals.get("name"),
description=vals.get("description"),
produces=produces,
body_format=body_format,
version=fields.Datetime.to_string(fields.Datetime.now()),
locale=locale if locale else "fr_FR",
)
vals["redner_id"] = template["name"]
else:
# If the language and body are not defined, we return early
# to prevent saving an incomplete template and sending it
# to the Redner server.
return self
return super().create(vals_list)
def write(self, vals):
"""Overwrite write to update redner template"""
# Determine if we should update redner or not
should = self._should_update_redner(vals)
# Perform the write operation
ret = super().write(vals)
# Update Redner templates if applicable
if should:
for record in self:
if (
not self.env.context.get("importing") # Skip during imports
and record.allow_modification_from_odoo
):
record._sync_with_redner()
return ret
def unlink(self):
"""Overwrite unlink to delete redner template"""
# We do NOT depend on the API for consistency here
# So raised error should not result block template deletion
for record in self:
if record.redner_id and record.allow_modification_from_odoo:
try:
self.redner.templates.account_template_delete(record.redner_id)
except Exception as e:
logger.warning(
"Failed to delete Redner template with ID %s. Reason: %s",
record.redner_id,
e,
)
self.env.registry.clear_cache()
return super().unlink()
def copy(self, default=None):
self.ensure_one()
default = dict(default or {}, name=_("%s (copy)") % self.name)
return super().copy(default)
# ------------------------------------------------------------
# ACTIONS / BUSINESS
# ------------------------------------------------------------
def _should_update_redner(self, vals):
"""
Determine if Redner should be updated based on the modified fields.
"""
for field in COMPUTED_FIELDS + ["template_data"]:
if field in vals:
current_value = getattr(self, field)
if vals[field] != current_value and vals[field]:
return True
return False
def _sync_with_redner(self):
"""
Sync the current record's template with Redner.
"""
self.ensure_one()
try:
@@ -198,4 +435,31 @@
try:
preview = self.redner.templates.account_template_preview(redner_id)
return preview
# Check if 'language' is a valid string before splitting
if isinstance(self.language, str) and "|" in self.language:
produces, language = self.language.split("|")
else:
logger.warning(
"Invalid language format for record %s: %s",
self.id,
self.language,
)
body, body_format = (
(self.template_data.decode(), "base64")
if language == "od+mustache"
else (self.body, "text")
)
# Use the existing `redner_id`
redner_id = self.redner_id
self._update_redner_template(
template_id=redner_id,
language=language,
body=body,
name=self.name,
description=self.description,
produces=produces,
body_format=body_format,
version=fields.Datetime.to_string(self.write_date),
locale=self.locale_id.code,
)
except Exception as e:
@@ -201,6 +465,38 @@
except Exception as e:
logger.error("Failed to get preview of redner template :%s", e)
return
logger.error("Failed to sync with Redner template: %s", e)
raise ValidationError(_("Failed to update Redner template, %s") % e) from e
def _update_redner_template(self, **kwargs):
"""
Perform the Redner `account_template_update` API call.
:param kwargs: Payload for the `account_template_update` API.
"""
try:
self.redner.templates.account_template_update(**kwargs)
self.env.registry.clear_cache()
except Exception as e:
logger.error("Redner API update failed: %s", e)
raise ValueError("Unable to update the Redner template.") from e
@ormcache("redner_id")
def get_preview(self, redner_id):
"""
Retrieve the preview of a Redner template by its ID.
Returns None if the redner_id is not provided or if the preview
cannot be retrieved.
"""
if not redner_id:
return None
result = None
try:
result = self.redner.templates.account_template_preview(redner_id)
except Exception as e:
logger.error(
"Failed to get preview of Redner template with ID %s: %s", redner_id, e
)
return result
def _to_odoo_template(self, template):
"""
@@ -249,5 +545,6 @@
logger.error("Failed to read Redner template: %s", e)
return {}
def _compute_template(self):
@ormcache("record_id", "new_template_data")
def _set_cached_template(self, record_id, new_template_data):
"""
@@ -253,3 +550,3 @@
"""
Computes the template values for the records and applies cached or fetched data.
Sets and caches the template in Redner for a given record.
"""
@@ -255,5 +552,5 @@
"""
for record in self:
if not record.id or not record.redner_id:
continue
record = self.browse(record_id)
if not record.redner_id:
raise ValueError("The record must have a valid Redner ID.")
@@ -259,4 +556,9 @@
# Fetch the cached template
cached_template = self._get_cached_template(record.id)
try:
produces, language = record.language.split("|")
body, body_format = (
(new_template_data.decode(), "base64")
if language == "od+mustache"
else (record.body, "text")
)
@@ -262,13 +564,21 @@
if not any([getattr(record, f) for f in COMPUTED_FIELDS]):
# If all computed fields are undefined, populate them
# from the cached template.
for f in COMPUTED_FIELDS + EDITABLE_FIELDS:
if f in cached_template:
setattr(record, f, cached_template[f])
else:
# If at least one field is defined, populate only undefined fields
for f in COMPUTED_FIELDS:
if not getattr(record, f):
setattr(record, f, cached_template.get(f, None))
# Send the updated template to the external system
self.redner.templates.account_template_update(
template_id=record.redner_id,
language=language,
body=body,
name=record.name,
description=record.description,
produces=produces,
body_format=body_format,
version=fields.Datetime.to_string(record.write_date),
locale=record.locale_id.code,
)
self.env.registry.clear_cache()
return True
except Exception as e:
logger.error("Failed to set Redner template: %s", e)
raise ValueError("Unable to update the Redner template.") from e
@@ -274,26 +584,49 @@
@api.depends("template_data")
def _compute_template_data(self):
for record in self:
if not record.id or not record.redner_id:
continue
if not record.template_data:
try:
cached_template = self._get_cached_template(record.id)
if "template_data" in cached_template:
new_val = cached_template["template_data"]
encoded = base64.b64encode(new_val).decode("utf-8")
ext = ".odt" # default extension
try:
ext = b64_to_extension(encoded)
except Exception as e:
logger.error("Failed to read extension from file:%s", e)
return
record.template_data = encoded
record.template_data_filename = "template" + ext
except Exception as e:
logger.error("Failed to read redner template :%s", e)
return
def import_from_redner(self):
tl_wizard = self.env["template.list.wizard"]
templates = self.list_external_templates()
tl_wizard.populate(templates)
return {
"type": "ir.actions.act_window",
"res_model": "template.list.wizard",
"view_mode": "list",
"view_id": self.env.ref("redner.view_template_list_wizard_tree").id,
"target": "new",
#'context': self.env.context,
}
@api.model
def get_keywords(self):
"""Return template redner keywords"""
self.ensure_one()
varlist = self.template_varlist_fetch()
for name in varlist:
while "." in name:
name = name[: name.rfind(".")]
if name not in varlist:
varlist.append(name)
varlist.sort()
return varlist
@api.model
@ormcache("self.redner_id")
def template_varlist_fetch(self):
"""Retrieve the list of variables present in the template."""
self.ensure_one()
try:
if not self.redner_id:
return []
return self.redner.templates.account_template_varlist(self.redner_id)
except Exception as e:
logger.warning("Failed to fetch account template varlist: %s", e)
return []
def list_external_templates(self):
try:
@@ -312,208 +645,6 @@
new_templates.append(self._to_odoo_template(template))
return new_templates
@property
def redner(self):
"""
Returns a Redner instance.
Recomputes the instance if any system parameter changes.
Uses a global variable to cache the instance across sessions.
"""
global _redner, _redner_params
# Fetch current system parameters
config_model = self.env["ir.config_parameter"].sudo()
current_params = {
"api_key": config_model.get_param("redner.api_key"),
"server_url": config_model.get_param("redner.server_url"),
"account": config_model.get_param("redner.account"),
"timeout": int(config_model.get_param("redner.timeout", default="20")),
}
# Check if parameters have changed or if _redner is None
if _redner is None or _redner_params != current_params:
# Recompute the Redner instance
_redner = Redner(
current_params["api_key"],
current_params["server_url"],
current_params["account"],
current_params["timeout"],
)
_redner_params = current_params # Update the stored parameters
return _redner
@api.model_create_multi
def create(self, vals_list):
"""Overwrite create to create redner template"""
for vals in vals_list:
if not vals.get("name", False) or vals["name"] == "New":
# source
vals["source"] = "redner"
return super().create(vals_list)
# Prepare template params according to the selected language.
# Use template data field if the selected language is "od";
# otherwise the body field is used.
produces, language = vals.get("language", DEFAULT_LANGUAGE).split("|")
body, body_format = (
(vals.get("template_data", ""), "base64")
if language == "od+mustache"
else (vals.get("body"), "text")
)
locale = self.env["res.lang"].browse(vals.get("locale_id")).code
# We depend on the API for consistency here
# So raised error should not result with a created template
template = self.redner.templates.account_template_add(
language=language,
body=body,
name=vals.get("name"),
description=vals.get("description"),
produces=produces,
body_format=body_format,
version=fields.Datetime.to_string(fields.Datetime.now()),
locale=locale if locale else "fr_FR",
)
vals["redner_id"] = template["name"]
return super().create(vals_list)
def write(self, vals):
"""Overwrite write to update redner template"""
# We depend on the API for consistency here
# So raised error should not result with an updated template
if "allow_modification_from_odoo" in vals:
if vals["allow_modification_from_odoo"]:
# case where we want to actually update the template from odoo
# safe, since passing this parameter from false to true can
# only be done separetly from changing other fields.
# we write the change and move on
return super().write(vals)
else:
# this case is more tricky
if len(vals) == 1:
# ie the only field to be changed is the boolean
# we write and move on. no need for updates
return super().write(vals)
# at the same time, we updated some fields and
# we decided that odoo could not longer update the fields.
# so we decide that this is the last update we send to
# redner
# compute if we should update redner or not
should_update_redner = False
for f in EDITABLE_FIELDS + COMPUTED_FIELDS + ["template_data"]:
# if we made a change in the record, update redner
if f in vals:
attr = getattr(self, f)
if vals[f] != attr:
should_update_redner = True
if "name" in vals:
self.ensure_one()
redner_id = self.redner_id
vals["redner_id"] = vals["name"]
ret = super().write(vals)
for record in self:
if (
should_update_redner
and not self.env.context.get("importing")
and record.allow_modification_from_odoo
):
try:
produces, language = record.language.split("|")
body, body_format = (
(record.template_data.decode(), "base64")
if language == "od+mustache"
else (record.body, "text")
)
if "name" not in vals:
redner_id = record.redner_id
record.redner.templates.account_template_update(
template_id=redner_id,
language=language,
body=body,
name=vals.get("name", ""),
description=record.description,
produces=produces,
body_format=body_format,
version=fields.Datetime.to_string(record.write_date),
locale=record.locale_id.code,
)
except Exception as e:
logger.error("Failed to update redner template :%s", e)
raise ValidationError(
_("Failed to update render template, %s") % e
) from e
return ret
def unlink(self):
"""Overwrite unlink to delete redner template"""
# We do NOT depend on the API for consistency here
# So raised error should not result block template deletion
for record in self:
if record.redner_id and record.allow_modification_from_odoo:
try:
self.redner.templates.account_template_delete(record.redner_id)
except Exception as e:
# Log the exception if needed (for debugging purposes)
logger.warning(
"Failed to delete redner template for record %s: %s",
record.id,
{str(e)},
)
return super().unlink()
def copy(self, default=None):
self.ensure_one()
default = dict(default or {}, name=_("%s (copy)") % self.name)
return super().copy(default)
@api.depends("body", "template_data")
def _compute_keywords(self):
for record in self:
record.detected_keywords = "\n".join(record.template_varlist_fetch())
@api.model
def get_keywords(self):
"""Return template redner keywords"""
varlist = self.template_varlist_fetch()
for name in varlist:
while "." in name:
name = name[: name.rfind(".")]
if name not in varlist and name != "":
varlist.append(name)
varlist.sort()
return varlist
@api.model
def template_varlist_fetch(self):
"""Retrieve the list of variables present in the template."""
try:
if not self.redner_id:
return []
return self.redner.templates.account_template_varlist(self.redner_id)
except Exception as e:
logger.warning("Failed to fetch account template varlist: %s", e)
return []
def redner_url(self):
if self.redner_id is None:
return ""
Loading