[ADD] report_carbone, jsonifier, export_json : carbone is an alternative to Py3o
This commit is contained in:
5
report_carbone/models/carbone/__init__.py
Normal file
5
report_carbone/models/carbone/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import carbone_print_by_action
|
||||
from . import carbone_field_extractor
|
||||
from . import carbone_create_report_wizard
|
||||
from . import carbone_translate
|
||||
from . import carbone_translate_line
|
||||
@@ -0,0 +1,72 @@
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class CarbonCreateReportWizard(models.TransientModel):
|
||||
_name = "carbone.create.report.wizard"
|
||||
_description = "Create Carbone report wizard helper"
|
||||
|
||||
report_type_extension = fields.Selection(
|
||||
string="Report extension",
|
||||
selection=[("docx", ".docx"), ("pptx", ".pptx"), ("xlsx", ".xlsx")],
|
||||
help="Leave empty if you already have a existing Carbone template for this report, then set the template ID",
|
||||
)
|
||||
|
||||
template_id = fields.Char(
|
||||
string="Carbone Template ID", help="Leave empty if you don't have any existing template for this report"
|
||||
)
|
||||
|
||||
input_user_model_id = fields.Many2one(
|
||||
"ir.model", string="Odoo model name", domain=[("transient", "=", False)], required=True
|
||||
)
|
||||
|
||||
action_name = fields.Char(string="Action name", required=True)
|
||||
|
||||
@api.constrains("template_id", "report_type_extension")
|
||||
def _check_template_xor_extension(self):
|
||||
"""Users are not allowed to enter both the extension of their report and a Carbone Template ID.
|
||||
This is because if the user enters an extension, it is to create a report from scratch, and we mock a call
|
||||
to Carbone to properly set up the studio.
|
||||
If the user enters a Carbone Template ID, we will rely exclusively on that, without making a mock call
|
||||
to Carbone.
|
||||
If the user enters a Carbone Template ID, we will rely exclusively on that, without making
|
||||
a mock call."""
|
||||
for record in self:
|
||||
has_template = bool(record.template_id)
|
||||
has_extension = bool(record.report_type_extension)
|
||||
|
||||
if has_template == has_extension:
|
||||
raise exceptions.ValidationError(_("You must specify either a template or an extension, but not both."))
|
||||
|
||||
def action_create_carbone_report(self):
|
||||
export_suffixe_name = self.input_user_model_id.display_name
|
||||
new_carbone_report = self.env["ir.actions.report"].create(
|
||||
{
|
||||
"name": self.action_name,
|
||||
"input_user_model_id": self.input_user_model_id.id,
|
||||
"model": self.input_user_model_id.model,
|
||||
"template_id": self.template_id,
|
||||
"report_type": "carbone",
|
||||
"file_extension": self.report_type_extension,
|
||||
"m2o_reference_id": self.env[self.input_user_model_id.model].search([], limit=1).id,
|
||||
}
|
||||
)
|
||||
export_model = new_carbone_report.retrieve_global_export_model(
|
||||
self.input_user_model_id.model, export_suffixe_name
|
||||
)
|
||||
new_carbone_report.export_model = export_model
|
||||
view_id = self.env.ref("report_carbone.act_report_carbone_view").id
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "ir.actions.report",
|
||||
"view_mode": "form",
|
||||
"view_id": view_id,
|
||||
"views": [(view_id, "form")],
|
||||
"res_id": new_carbone_report.id,
|
||||
"target": "current",
|
||||
"context": {
|
||||
"active_model": "ir.actions.report",
|
||||
"active_id": new_carbone_report.id,
|
||||
"active_ids": [new_carbone_report.id],
|
||||
"default_report_type": "carbone",
|
||||
},
|
||||
}
|
||||
196
report_carbone/models/carbone/carbone_field_extractor.py
Normal file
196
report_carbone/models/carbone/carbone_field_extractor.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import xml.etree.ElementTree as ET # type: ignore # noqa: F401
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
FIELDS_RECURSION_LIMIT = 0
|
||||
EXCLUDED_FIELDS = [
|
||||
"id",
|
||||
"create_uid",
|
||||
"create_date",
|
||||
"write_uid",
|
||||
"write_date",
|
||||
"display_name",
|
||||
"message_ids",
|
||||
"activity_ids",
|
||||
"activity_user_id",
|
||||
"message_partner_ids",
|
||||
"activity_type_id",
|
||||
"default_user_id",
|
||||
]
|
||||
|
||||
|
||||
class CarboneFieldExtractor(models.Model):
|
||||
_name = "carbone.field.extractor"
|
||||
_description = "Field extractor for exports based on views"
|
||||
|
||||
@api.model
|
||||
def get_exportable_fields_from_view(self, model_name, view_type):
|
||||
"""
|
||||
Retrieves exportable fields from a view (priority to form/list)
|
||||
|
||||
:param model_name: Model name
|
||||
:param view_type: Type of view ('list', 'form', 'search')
|
||||
:return: List of exportable fields with their info
|
||||
"""
|
||||
target_model = self.env[model_name]
|
||||
|
||||
view_info = target_model.get_view(view_type=view_type)
|
||||
|
||||
root = ET.fromstring(view_info["arch"])
|
||||
|
||||
exportable_fields = []
|
||||
processed_fields = set()
|
||||
|
||||
for field_elem in root.iter("field"):
|
||||
field_name = field_elem.get("name")
|
||||
|
||||
if field_name and field_name not in processed_fields and field_name in target_model._fields:
|
||||
field_obj = target_model._fields[field_name]
|
||||
|
||||
if self._is_field_exportable(field_obj):
|
||||
fields_info = self._get_field_export_info(field_name, field_obj, field_elem)
|
||||
exportable_fields.extend(fields_info)
|
||||
processed_fields.add(field_name)
|
||||
|
||||
return exportable_fields
|
||||
|
||||
@api.model
|
||||
def _is_field_exportable(self, field_obj):
|
||||
if field_obj.name in EXCLUDED_FIELDS:
|
||||
return False
|
||||
|
||||
return getattr(field_obj, "exportable", True)
|
||||
|
||||
@api.model
|
||||
def _get_field_export_info(self, field_name, field_obj, field_elem):
|
||||
"""
|
||||
Retrieves export information for a field
|
||||
"""
|
||||
field_info = field_name
|
||||
sub_fields = []
|
||||
if field_obj.type in ["one2many", "many2many"]:
|
||||
sub_fields = self._extract_o2m_m2m_fields(field_elem, field_obj.comodel_name, field_name)
|
||||
sub_fields.extend([field_info])
|
||||
return sub_fields
|
||||
|
||||
@api.model
|
||||
def _get_sub_field_info(self, field_name, comodel_name):
|
||||
"""
|
||||
Retrieves the name of a subfield
|
||||
"""
|
||||
target_model = self.env[comodel_name]
|
||||
if field_name not in target_model._fields:
|
||||
return None
|
||||
|
||||
field_obj = target_model._fields[field_name]
|
||||
if not self._is_field_exportable(field_obj):
|
||||
return None
|
||||
|
||||
return field_name
|
||||
|
||||
@api.model
|
||||
def _get_inline_view_fields(self, field_elem, comodel_name):
|
||||
"""
|
||||
Recovers fields from inline views (list/form in the field XML)
|
||||
"""
|
||||
inline_fields = []
|
||||
|
||||
for child_elem in field_elem:
|
||||
if child_elem.tag in ("list", "form"):
|
||||
for sub_field_elem in child_elem.iter("field"):
|
||||
sub_field_name = sub_field_elem.get("name")
|
||||
if sub_field_name:
|
||||
field_info = self._get_sub_field_info(sub_field_name, comodel_name)
|
||||
if field_info:
|
||||
inline_fields.append(field_info)
|
||||
break # Prendre la première vue trouvée
|
||||
|
||||
return inline_fields
|
||||
|
||||
@api.model
|
||||
def _extract_o2m_m2m_fields(self, field_elem, comodel_name, parent_field_name):
|
||||
"""
|
||||
Extracts the O2M/M2M relationship fields from inline views
|
||||
|
||||
:param field_elem: XML element of the O2M/M2M field
|
||||
:param comodel_name: Target model name
|
||||
:param parent_field_name: Parent field name
|
||||
:return: List of subfields with their information
|
||||
"""
|
||||
sub_fields = []
|
||||
|
||||
inline_fields = self._get_inline_view_fields(field_elem, comodel_name)
|
||||
if not inline_fields:
|
||||
return sub_fields
|
||||
|
||||
sub_fields = [f"{parent_field_name}/{sub_field_info}" for sub_field_info in inline_fields]
|
||||
|
||||
return sub_fields
|
||||
|
||||
def clean_fields(self, fields):
|
||||
"""Allows you to remove occurrences from fields that are displayed with subfields
|
||||
(order_line/name, order_line/product_id, and order_line)..
|
||||
If 'order_line' is not removed, it is the only thing Odoo will display."""
|
||||
prefixes = {f.split("/")[0] for f in fields if "/" in f}
|
||||
result = [f for f in fields if not (f in prefixes and any(ff.startswith(f + "/") for ff in fields))]
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def get_fields_from_multiple_views(self, model_name, view_types=None):
|
||||
if view_types is None:
|
||||
view_types = ["list", "form"]
|
||||
|
||||
all_fields = []
|
||||
|
||||
for view_type in view_types:
|
||||
fields = self.get_exportable_fields_from_view(model_name, view_type)
|
||||
for field_info in fields:
|
||||
if field_info not in all_fields:
|
||||
all_fields.append(field_info)
|
||||
|
||||
clean_fields = self.clean_fields(all_fields)
|
||||
|
||||
if not clean_fields:
|
||||
return self.fallback_get_fields(model_name)
|
||||
return clean_fields
|
||||
|
||||
def update_current_global_export(
|
||||
self, export: "odoo.model.ir_exports", old_fields: list[str], newer_fields: list[str]
|
||||
):
|
||||
added = [field for field in old_fields if field not in newer_fields]
|
||||
removed = [field for field in newer_fields if field not in old_fields]
|
||||
|
||||
if removed:
|
||||
to_unlink = self.env["ir.exports.line"].search([("export_id", "=", export.id), ("name", "in", removed)])
|
||||
to_unlink.unlink()
|
||||
if added:
|
||||
vals_list = [{"name": name, "export_id": export.id} for name in added]
|
||||
self.env["ir.exports.line"].create(vals_list)
|
||||
|
||||
def fallback_get_fields(self, model_name: str, depth: int = FIELDS_RECURSION_LIMIT, prefix: str = "") -> list[str]:
|
||||
"""
|
||||
Used to retrieve all fields that can be exported from a model
|
||||
:param model_name: Model to export field
|
||||
:param depth: Maximum recursion number
|
||||
⚠ Be careful to not call it with an excessif number, field number grows exponentially
|
||||
(tested with purchase.order model with default depth, it gives 76 fields, in depth=1, it increases
|
||||
to 1800 fields.
|
||||
:param prefix: Must be left empty. Use for recursion call, to have a complete field's parent name.
|
||||
:return: Fields name to export
|
||||
"""
|
||||
object_model = self.env[model_name]
|
||||
result = []
|
||||
|
||||
fields = object_model.fields_get(
|
||||
attributes=["type", "required", "relation", "exportable"],
|
||||
)
|
||||
for field_name, field in fields.items():
|
||||
if field.get("exportable") and field_name not in EXCLUDED_FIELDS:
|
||||
full_name = f"{prefix}{field_name}" if not prefix else f"{prefix}/{field_name}"
|
||||
result.append(full_name)
|
||||
|
||||
if depth > 0 and field.get("type") in ("many2one", "many2many"):
|
||||
related_model = field.get("relation")
|
||||
nested_fields = self.fallback_get_fields(related_model, depth=depth - 1, prefix=full_name)
|
||||
result.extend(nested_fields)
|
||||
return result
|
||||
76
report_carbone/models/carbone/carbone_print_by_action.py
Normal file
76
report_carbone/models/carbone/carbone_print_by_action.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import logging
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
from ..base.ir_actions_report import _tz_get
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CarbonReportPrintByAction(models.TransientModel):
|
||||
_name = "carbone.print_by_action"
|
||||
_description = "Print by action"
|
||||
|
||||
@api.model
|
||||
def _get_model(self):
|
||||
rep_obj = self.env["ir.actions.report"]
|
||||
report = rep_obj.browse(self.env.context["active_ids"])
|
||||
return report[0].model
|
||||
|
||||
@api.model
|
||||
def _get_id_record(self):
|
||||
id = self.env.context.get("record_id")
|
||||
return id
|
||||
|
||||
@api.model
|
||||
def _get_currency_id(self):
|
||||
id = self.env.context.get("currency_id")
|
||||
return id
|
||||
|
||||
name = fields.Text(string="Object Model", default=_get_model, readonly=True)
|
||||
id_object = fields.Integer(string="Object ID", default=_get_id_record)
|
||||
lang_id = fields.Many2one(
|
||||
"res.lang",
|
||||
string="Language",
|
||||
default=lambda self: self.env["res.lang"].search([("code", "=", self.env.user.lang)], limit=1),
|
||||
help="If this option is enabled, the language in which the report is printed",
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", string="Currency", default=_get_currency_id)
|
||||
tz = fields.Selection(_tz_get, string="Timezone", default=lambda self: self.env.user.tz or "UTC")
|
||||
|
||||
def to_print(self):
|
||||
rep_obj = self.env["ir.actions.report"]
|
||||
report = rep_obj.browse(self.env.context["active_id"])[0]
|
||||
ctx = dict(self.env.context)
|
||||
print_ids = self.env[self.name].browse(self.id_object)
|
||||
if not print_ids:
|
||||
raise exceptions.UserError(_("No record is retrieve with this id."))
|
||||
# report_pdf_no_attachment context key
|
||||
# forces the system not to retrieve an attachment saved in the database.
|
||||
ctx.update(
|
||||
{
|
||||
"active_id": print_ids[0],
|
||||
"active_ids": print_ids.ids,
|
||||
"active_model": report.model,
|
||||
"lang": self.lang_id.code,
|
||||
"tz": self.tz,
|
||||
"currency_id": self.currency_id.id,
|
||||
"from_print_by_action": True,
|
||||
"report_pdf_no_attachment": True,
|
||||
}
|
||||
)
|
||||
data = {
|
||||
"model": report.model,
|
||||
"id": print_ids[0],
|
||||
"ids": print_ids,
|
||||
"report_type": "carbone",
|
||||
}
|
||||
res = {
|
||||
"type": "ir.actions.report",
|
||||
"report_name": report.report_name,
|
||||
"report_type": report.report_type,
|
||||
"datas": data,
|
||||
"context": ctx,
|
||||
"target": "current",
|
||||
}
|
||||
return res
|
||||
67
report_carbone/models/carbone/carbone_translate.py
Normal file
67
report_carbone/models/carbone/carbone_translate.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CarboneTranslate(models.Model):
|
||||
_name = "carbone.translate"
|
||||
_description = "Carbone Translate"
|
||||
_rec_name = "ir_actions_report_id"
|
||||
|
||||
ir_actions_report_id = fields.Many2one(
|
||||
"ir.actions.report", string="Carbone Report", required=True, ondelete="cascade"
|
||||
)
|
||||
lang_id = fields.Many2one(
|
||||
"res.lang",
|
||||
string="Language",
|
||||
required=True,
|
||||
)
|
||||
carbone_translate_line_ids = fields.One2many(
|
||||
"carbone.translate.line", "carbone_translate_id", string="Translation lines"
|
||||
)
|
||||
_sql_constraints = [
|
||||
(
|
||||
"lang_report_uniq",
|
||||
"UNIQUE(ir_actions_report_id, lang_id)",
|
||||
"A report cannot have two translations for the same language.",
|
||||
)
|
||||
]
|
||||
|
||||
def _create_translation_lines(
|
||||
self,
|
||||
carbone_translate_to_maj: "odoo.model.carbone_translate",
|
||||
translation_lines: "odoo.model.carbone_translate_line",
|
||||
):
|
||||
existing_sources = carbone_translate_to_maj.mapped("carbone_translate_line_ids.source")
|
||||
lines = [
|
||||
{
|
||||
"source": current_translate_line.source,
|
||||
"value": "",
|
||||
"carbone_translate_id": carbone_translate_to_maj.id,
|
||||
}
|
||||
for current_translate_line in translation_lines
|
||||
if current_translate_line.source not in existing_sources
|
||||
]
|
||||
self.env["carbone.translate.line"].create(lines)
|
||||
|
||||
def button_create_update_copy_of_translate(self):
|
||||
self.ensure_one()
|
||||
create_vals = []
|
||||
|
||||
report_translate_langs = self.ir_actions_report_id.carbone_translate_ids.mapped("lang_id")
|
||||
available_languages = self.ir_actions_report_id.lang_ids
|
||||
# If there is no language at all, we will create one, with the right keys.
|
||||
for lang in available_languages:
|
||||
if lang not in report_translate_langs:
|
||||
vals = {"lang_id": lang.id, "ir_actions_report_id": self.ir_actions_report_id.id}
|
||||
lines = [
|
||||
(0, 0, {"source": translate_line.source, "value": ""})
|
||||
for translate_line in self.carbone_translate_line_ids
|
||||
]
|
||||
vals.update({"carbone_translate_line_ids": lines})
|
||||
create_vals.append(vals)
|
||||
else:
|
||||
carbone_translate_to_maj = self.ir_actions_report_id.carbone_translate_ids.filtered_domain(
|
||||
[("lang_id", "=", lang.id)]
|
||||
)
|
||||
self._create_translation_lines(carbone_translate_to_maj, self.carbone_translate_line_ids)
|
||||
if create_vals:
|
||||
self.env["carbone.translate"].create(create_vals)
|
||||
27
report_carbone/models/carbone/carbone_translate_line.py
Normal file
27
report_carbone/models/carbone/carbone_translate_line.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CarboneTranslateLine(models.Model):
|
||||
_name = "carbone.translate.line"
|
||||
_description = "Carbone Translate Line"
|
||||
_rec_name = "source"
|
||||
|
||||
carbone_translate_id = fields.Many2one(
|
||||
"carbone.translate", string="Carbone Translate", required=True, ondelete="cascade"
|
||||
)
|
||||
source = fields.Text(string="Source term")
|
||||
value = fields.Text(string="Translation Value", default="")
|
||||
_sql_constraints = [
|
||||
(
|
||||
"source_value_uniq",
|
||||
"UNIQUE(source, value, carbone_translate_id)",
|
||||
"A report cannot have two translations for the same value.",
|
||||
)
|
||||
]
|
||||
|
||||
def write(self, vals):
|
||||
# We replace occurrences of the value 'False'
|
||||
# with a null string. Carbone does not interpret 'false' in translations.
|
||||
if "value" in vals and not vals.get("value"):
|
||||
vals["value"] = ""
|
||||
return super().write(vals)
|
||||
Reference in New Issue
Block a user