[ADD] report_carbone, jsonifier, export_json : carbone is an alternative to Py3o

This commit is contained in:
2026-04-21 14:59:24 +02:00
parent ae3c34257f
commit c2061984d1
216 changed files with 29344 additions and 0 deletions

File diff suppressed because it is too large Load Diff

9
report_carbone/README.md Normal file
View File

@@ -0,0 +1,9 @@
Carbone Odoo Connector
======================
Carbone Odoo Connector system for Odoo Community edition.
Configuration
=============
Add your Carbon API keys and define users who have access to the "Carbon Reports" menu with the "Carbon Report - Viewer" and "Carbon Report - Manager" groups.

55
report_carbone/README.rst Normal file
View File

@@ -0,0 +1,55 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
Carbone Odoo Connector
======================
Carbone Odoo Connector system for Odoo 18 Community edition.
Configuration
=============
Add your Carbon API keys and define users who have access to the "Carbon Reports" menu with the "Carbon Report - Viewer" and "Carbon Report - Manager" groups.
.. image:: static/description/assets/screenshots/add_api_keys.png
:scale: 36 %
.. image:: static/description/assets/screenshots/carbone_groups.png
:scale: 50 %
Company
-------
* `Mangono <https://mangono.fr/>`__
License
-------
General Public License, Version 3 (LGPL v3).
(http://www.gnu.org/licenses/lgpl-3.0-standalone.html)
Credits
-------
Developer: (V18) Mangono , Contact: contact@mangono.fr
Contacts
--------
* Mail Contact : contact@mangono.fr
* Website : https://mangono.fr
Bug Tracker
-----------
In case of trouble, please contact us via our email address.
Maintainer
==========
.. image:: static/description/assets/logo/mangono-logo-bleu.png
:scale: 10 %
:target: https://mangono.fr
This module is maintained by Mangono.
For support and more information, please visit `Our Website <https://mangono.fr/>`__
Further information
===================
HTML Description: `<static/description/index.html>`__

View File

@@ -0,0 +1,3 @@
from . import controllers
from . import models
from .post_install import set_res_config_settings

View File

@@ -0,0 +1,50 @@
{
"name": "Report Generator | Carbone.io",
"version": "18.0.1.0.8",
"author": "Mangono",
"maintainer": "Mangono",
"summary": """
Automate documents with Carbone.io the universal, most efficient no-code document generation solution. Generate
custom documents and reports from Odoo data using customizable templates in Word, Excel, PowerPoint, LibreOffice,
or Google Docs. Convert and export to 100+ formats, including PDF, DOCX, XLSX, PPTX, CSV, HTML, XML, JPG, and PNG,
with enterprise features like charts, barcodes, images, forms, e-signatures, E-reporting, Factur-X, ZUGFeRD,
i18n, multi-language support, HTML rendering, and PDF merging. Use cases: Generate invoices, contracts, quotes,
delivery notes, payslips, purchase orders, sales presentations, sales reports, brochures, datasheets, SLAs, NDAs,
meeting notes, daybook reports, sales analysis, inventory reports, and product category reports.
""",
"category": "Reporting",
"depends": ["base", "base_setup", "web", "export_json"],
"description": "Report Generator | Carbone.io",
"website": "https://carbone.io/integration/odoo.html",
"data": [
"security/groups.xml",
"security/ir.model.access.csv",
"data/carbone_guide_attachment.xml",
"data/carbone_demo_template_purchase_order.xml",
"views/base/ir_actions_report.xml",
"views/base/ir_exports.xml",
"views/res/res_config_settings_carbone.xml",
"views/carbone/carbone_print_by_action.xml",
"views/carbone/report_carbone_menu.xml",
"views/carbone/carbone_translate.xml",
"views/carbone/carbone_translate_line.xml",
"views/carbone/carbone_create_report_wizard.xml",
],
"assets": {
"web.assets_backend": [
"report_carbone/static/src/js/report/action_manager_report.js",
"report_carbone/static/src/js/report/action_create_ir_report.js",
"report_carbone/static/src/js/report/copy_export_json.js",
"report_carbone/static/src/js/report/initiate_studio.js",
"report_carbone/static/src/scss/carbone.scss",
"report_carbone/static/src/xml/list_template.xml",
],
},
"test": [],
"images": ["static/description/banner.png"],
"installable": True,
"auto_install": False,
"license": "AGPL-3",
"application": False,
"post_init_hook": "set_res_config_settings",
}

101
report_carbone/const.py Normal file
View File

@@ -0,0 +1,101 @@
ALLOWED_EXTENSIONS = {
# Document
"pdf",
"docx",
"odt",
"bib",
"doc",
"doc6",
"doc95",
"docbook",
"docx7",
"fodt",
"html",
"latex",
"ooxml",
"ott",
"psw",
"rtf",
"sdw",
"stw",
"sxw",
"text",
"txt",
"uot",
"vor",
"xhtml",
"jpg",
"jpeg",
"png",
"epub",
"md",
"svg",
"webp",
"pages",
# Spreadsheet
"xlsx",
"ods",
"csv",
"dbf",
"dif",
"fods",
"ots",
"pxl",
"slk",
"stc",
"sxc",
"uos",
"xls",
"xls5",
"xls95",
"xlt",
"xlt5",
"xlt95",
"numbers",
"parquet",
"json",
# Presentation
"pptx",
"odp",
"bmp",
"emf",
"eps",
"fodp",
"gif",
"met",
"odg",
"pbm",
"pct",
"pgm",
"ppm",
"pwp",
"ras",
"sti",
"svm",
"swf",
"sxi",
"tiff",
"uop",
"wmf",
"otp",
"potm",
"pot",
"pps",
"ppt",
# Web
"etext",
"html10",
"mediawiki",
"sdw3",
"sdw4",
"text10",
"vor4",
# Graphics
"fodg",
"odd",
"otg",
"std",
"sxd",
"pub",
"cdr",
}

View File

@@ -0,0 +1,2 @@
from . import main
from . import config

View File

@@ -0,0 +1,15 @@
from odoo import http
from odoo.http import request
class CarboneConfigParamController(http.Controller):
@http.route("/carbone_config/carbone_studio_params", type="json", auth="user")
def get_carbone_studio_params(self):
carbone_url = request.env["ir.config_parameter"].sudo().get_param("report-engine.carbone_studio_url")
carbone_js_url = request.env["ir.config_parameter"].sudo().get_param("report-engine.carbone_js_file_url")
return {"studio_url": carbone_url, "js_url": carbone_js_url}
@http.route("/carbone_config/carbone_api_key", type="json", auth="user")
def get_carbone_api_key(self):
value = request.env["res.config.settings"].sudo().retrieve_carbone_api_key(test_mode_key=True)
return {"token": value}

View File

@@ -0,0 +1,134 @@
import json
import logging
import mimetypes
import requests
import werkzeug.exceptions
from werkzeug.urls import url_decode
from odoo import _, api, exceptions, http
from odoo.http import content_disposition, request
from odoo.tools import html_escape
from odoo.addons.web.controllers.report import ReportController
_logger = logging.getLogger(__name__)
def _get_headers(extension, content, filename):
if extension == "zip":
mime_type = "application/zip"
else:
mime_type, _ = mimetypes.guess_type(f"file.{extension}")
if not mime_type:
mime_type = "application/octet-stream" # generic fallback
header = [
("Content-Type", mime_type),
("Content-Length", len(content)),
("X-Content-Type-Options", "nosniff"),
]
if filename:
header.append(("Content-Disposition", content_disposition(filename)))
return header
class CarboneReportController(ReportController):
@classmethod
def handle_response(cls, response: requests.Response, raise_exception=True) -> str | dict:
try:
response_json = response.json()
except ValueError:
response_json = {}
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
response_error_message = response_json.get("message", response.text) if response_json else response.text
error_message = f"Carbone API Error: {response.status_code}: {response_error_message}"
_logger.error("Carbone API Error %s: ", response.status_code, exc_info=e)
if raise_exception:
raise exceptions.UserError(error_message) from e
return error_message
return response_json
@api.model
def check_carbone_report(self, carbone_report: "odoo.model.ir_actions_report"):
if not carbone_report:
raise exceptions.UserError(_("No report with this name and Carbone type was found."))
if not carbone_report.name:
raise exceptions.UserError(_("Please add a name to the report in order to export it."))
@api.model
def handle_exception_error(self, e, reportname: str):
_logger.exception("Error while generating report %s", reportname)
se = http.serialize_exception(e)
error = {"code": 200, "message": "Odoo Server Error", "data": se}
res = request.make_response(html_escape(json.dumps(error)))
raise werkzeug.exceptions.InternalServerError(response=res) from e
@http.route(
[
"/report/<converter>/<reportname>",
"/report/<converter>/<reportname>/<docids>",
],
type="http",
auth="user",
website=True,
readonly=True,
)
def report_routes(self, reportname: str, docids: str | None = None, converter=None, **data):
if converter != "carbone":
return super().report_routes(reportname, docids, converter, **data)
context = dict(request.env.context)
context.update({"from_ir_report_controller": True})
if data.get("options"):
data.update(json.loads(data.pop("options")))
if data.get("context"):
context.update(json.loads(data["context"]))
request.update_context(**context)
# Retrieval of ir.actions.report
carbone_report = request.env["ir.actions.report"]._get_report_from_name(reportname)
self.check_carbone_report(carbone_report)
report_content, extension = request.env["ir.actions.report"]._render_carbone(carbone_report, docids)
filename = f"{carbone_report.report_name}.{extension}"
headers = _get_headers(extension, report_content, filename)
return request.make_response(report_content, headers)
def _call_carbone_converter(self, docids: str, reportname: str, context: str, url: "str"):
if docids:
# Generic report:
response = self.report_routes(reportname, docids=docids, converter="carbone", context=context)
else:
# Particular report:
data = dict(url_decode(url.split("?")[1]).items()) # decoding the args represented in JSON
if "context" in data:
context, data_context = json.loads(context or "{}"), json.loads(data.pop("context"))
context = json.dumps({**context, **data_context})
response = self.report_routes(reportname, converter="carbone", context=context, **data)
return response
@http.route(["/report/download"], type="http", auth="user")
def report_download(self, data, context=None):
"""
Overload of the function to handle ir.actions.reports of "Carbone" type
:param data: data: a javascript array JSON.stringified contain report internal url ([0]) and
type [1]
:param context:
:return: Response with an attachment header
"""
requestcontent = json.loads(data)
url, report_type = requestcontent[0], requestcontent[1]
if report_type != "carbone":
return super().report_download(data, context)
reportname = url
try:
reportname = url.split("/report/carbone/")[1].split("?")[0]
docids = None
if "/" in reportname:
reportname, docids = reportname.split("/")
response = self._call_carbone_converter(docids, reportname, context, url)
return response
except Exception as e:
self.handle_exception_error(e, reportname)

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="report_carbone_demo_purchase_order" model="ir.attachment">
<field name="name">Demo_template_purchase_order.odt</field>
<field name="datas" type="base64" file="report_carbone/data/demo_template_purchase_order.odt"/>
<field name="public">True</field>
</record>
</odoo>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="report_carbone_userguide_attachment" model="ir.attachment">
<field name="name">Carbone_report_guide.pdf</field>
<field name="datas" type="base64" file="report_carbone/docs/carbone_userguide_v18.pdf"/>
<field name="public">True</field>
</record>
</odoo>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,293 @@
:toc:
= Guide to creating a Carbone report in Odoo
image::../static/description/assets/logo/mangono_logo.jpg[Logo, 200, align=center]
== Creating an export template
[NOTE]
====
This section is not mandatory for setting up a Carbone report, since by default, when creating a report for the first time with an Odoo X model (sale.order, purchase.order, etc.), a "Global" export is created for the model in question.
This export model retrieves all fields visible in the list and form views of the Odoo model in question.
However, it may be necessary to create your own export model in order to retrieve other fields not displayed in the interface.
====
For this example, we assume that we want to create a report based on data from a purchase order.
You need to go to the list view of the desired universe, in this case "Purchase".
Select any record, then click on "Actions" and then "Export".
image::../static/description/assets/screenshots/select_one_record.png[]
In the pop-up window that opens, tick "JSON" in the "Export format" field.
image::../static/description/assets/screenshots/pop-up-export.png[]
Select the fields you want to include. Once you have done this, save your export template.
From the list, select "New template". Name your export template, then save this new export template.
image::../static/description/assets/screenshots/pop-up-export-2.png[]
In order to more easily find the name of the model from which we are extracting our data (in our example, purchase orders), we will click on "Export".
This will allow us to identify, first of all, whether the data extracted in the new file meets our needs, and secondly, the file name displays the name of the Odoo model (here "Purchase Order") and the technical name of the model (here "purchase.order").
image::../static/description/assets/screenshots/file-name-export.png[]
== Creating a new Odoo report
In the list of universes, click on the "Document Generation" menu, then on the "New document" button.
image::../static/description/assets/screenshots/open_carbone_menu.png[]
image::../static/description/assets/screenshots/new_button.png[]
A pop-up appears, allowing you to fill in the first mandatory fields when creating a report.
- Action name :
Name that will be displayed in Odoo when clicking on the "Print" button, when it is possible to print the report.
- Odoo model name :
Name of the Odoo model on which the report data should be based.
- Carbone Template ID :
Unique identifier generated when adding a file to your Carbone account, or in Odoo. (Not required if an extension is used)
- Report extension :
Extension of the input report. (Not mandatory if a Carbone Template ID is specified)
For now, the data to be entered are the fields "Action name", "Odoo model name", "Carbone Template ID" or
"report extension".
image::../static/description/assets/screenshots/find-your-model.png[]
[NOTE]
====
You must have either a Carbone Template ID or a report extension, but not both at the same time.
====
image::../static/description/assets/screenshots/pop-up-landing.png[]
On the left, we find fields from our report to be defined, and on the right is a Carbone Studio, directly integrated into Odoo.
You can see that the Carbone Template ID is already filled in, and that the studio has initialised itself with a file of the extension that you previously selected in the pop-up.
image::../static/description/assets/screenshots/new_report_landing.png[]
- Export Model :
Name of the export template to use to retrieve the data required for the report.
- Printed Report Name :
Name that will be given to the generated PDF report. Leave blank if there is no specific report name to be set.
[NOTE]
====
When creating a new document, a "Global" export template is automatically created for the "Odoo model name" used. By default, it contains the exportable fields displayed in the list and form views of the model. It is systematically named "Global Export For Carbone", followed by the name of the model displayed in the interface.
====
For our example, we will use our own export template created previously.
image::../static/description/assets/screenshots/find-your-export.png[]
Once our Odoo model name has been defined, a record will automatically be assigned to the "Record for preview" field. This field allows us to choose a record belonging to the Odoo model on which to export information, to help us create our file from real data.
When you click on the "Refresh Studio" button, the recording data will be automatically copied and pasted into the studio.
image::../static/description/assets/screenshots/generate-json.png[]
== Creation of the document (.odt, .docx, etc.)
Now that we have created our first draft Odoo report, we can start creating our document. Carbone will use this future document to display our data exported from Odoo.
You can either download the file already uploaded to Carbone directly in Odoo via the Edit button.
If your browser is compatible with live upload, simply open the downloaded file and
save the changes locally so that they are directly reflected in Odoo.
Otherwise, you will need to drag and drop the downloaded file into the Drag and drop your template here area.
[NOTE]
====
You can also click on "New" and then select your file from the file explorer that will be displayed.
====
image::../static/description/assets/screenshots/drag_and_drop.png[]
Once the file has been added to the studio, it will become visible, with a "Carbone" watermark.
image::../static/description/assets/screenshots/aftermath_drag_and_drop.png[]
As long as the file is active in the studio, any changes made to the file on your computer will automatically be displayed in the studio after saving.
image::../static/description/assets/screenshots/live-reload.png[]
We can now display the JSON data in our file.
image::../static/description/assets/screenshots/test-with-key.png[]
== Add the document to Carbone
[NOTE]
====
This section is not mandatory if you created your Odoo report directly from Odoo,
by choosing one of the three extensions (docx, pptx, xlsx).
====
Now that our file is configured, we can add it to Carbone.
After logging into our Carbone account and clicking on the "Open Studio button, then "Start from Scratch",
image::../static/description/assets/screenshots/landing_page_carbone.png[]
image::../static/description/assets/screenshots/create_new_template_carbone.png[]
A page will open. Enter a template name in the top left corner of the window, and open the desired odt/docx file using the "New" button on the right side of the window.
Once the file has been uploaded, click on the "Save" button, which will save our new Carbone Report.
image::../static/description/assets/screenshots/save_new_template.png[]
After creating the new template, click on the "Template ID" button to copy the Carbone ID of your report to the clipboard in the "Carbone Template ID" field on the Odoo side.
image::../static/description/assets/screenshots/copie_template_id.png[]
Once the ID has been entered, you can now add your new Odoo report to the list of printable reports for the "Purchase order" template by clicking on the "Add to “print” menu" button.
image::../static/description/assets/screenshots/copie_carbone_template_id_add_menu.png[]
image::../static/description/assets/screenshots/button_print_with_our_report.png[]
When the "Carbone Template ID" is filled in from Carbone's "Template ID", this information is no longer required to be modified in the Odoo backend.
You can manage your different report versions in Carbone or Odoo and choose which version should be effective via the "Deploy" button.
image::../static/description/assets/screenshots/manage_all_template_from_odoo.png[]
image::../static/description/assets/screenshots/deploy_button.png[]
The version printed in Odoo will always be the one deployed.
image::../static/description/assets/screenshots/different_version.png[]
[WARNING]
====
It is important to copy the Template ID, not the version ID, in order to access the report history and
deploy these reports directly from Odoo.
If a version ID is entered, an information message will appear, and the studio will not be functional.
However, report printing will still be operational, and it will be the template linked to the Version ID that will
be printed.
image::../static/description/assets/screenshots/wrong_template_id_used.png[]
====
== Edit a template
If you wish to modify an existing file in Carbone, best practice is to create a new version of the file.
To do this, first retrieve the .odt/.docx or other file associated with the Carbone report.
Either via Carbone Studio:
image::../static/description/assets/screenshots/download_carbone_report_file.png[]
Either directly in Odoo, using the same button:
image::../static/description/assets/screenshots/get_template_from_odoo.png[]
In the Carbone studio (or the one displayed in Odoo), we create a new version of the report using the "New" button.
image::../static/description/assets/screenshots/upload_carbone_report_file.png[]
Once the changes to the file have been made locally, you can click on "Save" again, and a new Carbone report associated with a new "Version ID" will be generated.
image::../static/description/assets/screenshots/new_version_for_new_report.png[]
This way, the first version, which may still be in production, remains usable until the new version is deployed.
To use a specific version, simply click on the desired template in Carbone, then "Deploy".
== Currency management
To display a specific currency in a report, you must fill in the "Currency" field.
By default, if no currency is specified, the currency used will be the one available in the Odoo template, if that template has a "currency" field. Otherwise, the currency defined at the company level is used, which can be configured in "Settings > Users & Companies > Companies".
== Language and translation management
=== Translations
It is possible to add translations to a Carbone report, which will be displayed in the report, depending on the language of the report.
For non-dynamic information in a report (title, paragraph, table column name, etc.), the text must be enclosed in the "{t(my_text)}" tag.
To add a translation, click on the "Translations" tab, then add a language.
image::../static/description/assets/screenshots/add_lang_to_translate.png[]
The available languages will be those installed in Odoo.
image::../static/description/assets/screenshots/add_source_and_translation_value.png[]
The "Create/update languages placeholder" button allows you to quickly copy/paste "source terms" from the current language into all other translations of the report, if these "source terms" are not present in the other languages. These translations will be created without a "Translation Value".
[WARNING]
====
The text of the report must be copied and pasted correctly into Odoo to maintain the correct typography, depending on the different quotation marks, apostrophes, etc. Otherwise, the translation will not be effective.
====
Previously added translations will be visible directly in the embed studio by clicking on the "{d}" button (1) and then selecting the translation data via "{t}".
To view the translation in real time, simply change the studio language using the second button (2) in the screenshot and select the target language from the list (3).
image::../static/description/assets/screenshots/see_available_translate.png[]
As for the dynamic information in the report, retrieved via the data available in Odoo, if the report does not contain any languages or only one language in the "Languages" field, the information is directly translated according to the language entered, or the user's language if no language is defined.
The translations are those defined in Odoo.
[cols="a,a"]
|===
|image::../static/description/assets/screenshots/translate_in_data_en_us.png[]
_Data translated into English_
|image::../static/description/assets/screenshots/translate_in_data_fr_fr.png[]
_Data translated into French_
|===
When the report is available in more than one language,
for example English and French, the data with translations will be replaced by a key that leads to a value in the translation dictionary, visible via the "{t}" button.
It contains translations for each language, for terms
entered manually and for terms translated automatically using Odoo's native translations.
[cols="a,a"]
|===
|image::../static/description/assets/screenshots/translate_with_key.png[]
|image::../static/description/assets/screenshots/translation_avaible_in_t_data.png[]
|===
=== Languages
The language settings for the report can be configured in several ways:
- You can define the print language in relation to another 'language' field in the template from which the information is retrieved, via the "Translations" tab, then "Path of the language field".
For example, for purchase orders, it is possible to print reports based on the customer's language by entering the Python path to the lang field, in this case `partner_id.lang`.
image::../static/description/assets/screenshots/path_of_the_language_field.png[]
image::../static/description/assets/screenshots/res_partner_lang.png[]
- Also via the "Languages" field. If nothing is defined in this field, the print language will be the language of the user printing the report. If several languages are defined, the chosen language will be the first `res.lang` in Odoo.
== User groups and rights
You can define which user(s) will be able to view or edit Carbone Reports via two new groups: "Carbone Report - Manager" and "Carbone Report - Viewer".
The "Manager" group allows users to modify Carbone reports and access the "Carbone Reports" menu.
The "Viewer" group allows users to access Carbone reports in read-only mode; no modifications are possible.

File diff suppressed because one or more lines are too long

855
report_carbone/i18n/fr.po Normal file
View File

@@ -0,0 +1,855 @@
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__credits_used
msgid "Credits used"
msgstr "Crédits utilisés"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid "\"File name\""
msgstr "\"Nom du fichier\"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "<span class=\"text-muted\">Exceeding Credits Fee:</span>"
msgstr "<span class=\"text-muted\">Frais de dépassement de crédits:</span>"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "<span class=\"text-muted\">Exceeding Template Fee:</span>"
msgstr "<span class=\"text-muted\">Frais de déplacement de modèle:</span>"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "<span class=\"text-muted\">Included Credits:</span>"
msgstr "<span class=\"text-muted\">Crédis inclus:</span>"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "<span class=\"text-muted\">Included Templates:</span>"
msgstr "<span class=\"text-muted\">Modèles inclus:</span>"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "<span class=\"text-muted\">Price HT:</span>"
msgstr "<span class=\"text-muted\">Prix HT:</span>"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid ""
"<strong>Save</strong>\n"
" this page and come back here to set up the feature."
msgstr ""
"<strong>Sauvegardez</strong>\n"
" et revenez ici pour configurer la fonctionnalité."
#. module: report_carbone
#: model:ir.model.constraint,message:report_carbone.constraint_carbone_translate_lang_report_uniq
msgid "A report cannot have two translations for the same language."
msgstr "Un rapport ne peut pas avoir plus de deux traductions pour une même langue."
#. module: report_carbone
#: model:ir.model.constraint,message:report_carbone.constraint_carbone_translate_line_source_value_uniq
msgid "A report cannot have two translations for the same value."
msgstr "Un rapport ne pas pas avoir deux traductions avec une même valeur."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "API Keys"
msgstr "Clés API"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__access_token
msgid "Access Token"
msgstr "Token d'accès"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__expire_date
msgid "Access Token expires date"
msgstr "Date d'expiration du token d'accès"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Account technical settings"
msgstr "Paramètres techniques du compte"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__is_account_stage_mode
msgid "Account test mode"
msgstr "Mode test du compte"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Add the API key to print reports easily with Carbone."
msgstr "Ajoutez la clé API pour imprimer facilement des rapports avec Carbone."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Add the API key to print reports easily with Carbone."
msgstr "Ajoutez la clé API pour imprimer facilement des rapports avec Carbone."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Link to Carbone design documentation"
msgstr "Lien vers la documentation design Carbone"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Log in to Carbone"
msgstr "Se connecter à Carbone"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Authorize Odoo to access to a Carbone account"
msgstr "Autoriser Odoo à accéder à un compte Carbone"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Billing"
msgstr "Facturation"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Billing information"
msgstr "Informations de facturation"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "Cancel"
msgstr "Annuler"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__is_carbone_plan_subscribed
msgid "Carbon plan Subscribed"
msgstr "Plan Carbone Souscrit"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__carbone_js_file_url
msgid "Carbone JS file URL"
msgstr "URL du fichier JS Carbone"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__carbone_price_name
msgid "Carbone Plan subscribed"
msgstr "Plan Carbone souscrit"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_translate__ir_actions_report_id
#: model:ir.model.fields.selection,name:report_carbone.selection__ir_actions_report__report_type__carbone
msgid "Carbone Report"
msgstr "Rapport Carbone"
#. module: report_carbone
#: model:res.groups,name:report_carbone.group_report_carbone_manager
msgid "Carbone Report - Manager"
msgstr "Rapport Carbone - Responsable"
#. module: report_carbone
#: model:res.groups,name:report_carbone.group_report_carbone_viewer
msgid "Carbone Report - Viewer"
msgstr "Rapport Carbone - Lecteur"
#. module: report_carbone
#: model:ir.ui.menu,name:report_carbone.carbone_report_menu_root
msgid "Document Generation"
msgstr "Génération de document"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__carbone_studio_url
msgid "Carbone Studio URL"
msgstr "URL Studio Carbone"
#. module: report_carbone
#: model:ir.model,name:report_carbone.model_carbone_translate
#: model:ir.model.fields,field_description:report_carbone.field_carbone_translate_line__carbone_translate_id
msgid "Carbone Translate"
msgstr "Traduction Carbone"
#. module: report_carbone
#: model:ir.model,name:report_carbone.model_carbone_translate_line
msgid "Carbone Translate Line"
msgstr "Ligne de traduction Carbone"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_res_config_settings__is_carbone_plan_subscribed
msgid "The Carbone plan linked to the account is paid"
msgstr "Le plan Carbone liée au compte est payé"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_see_prices__carbone_price_ids
msgid "Carbone prices"
msgstr "Prix Carbone"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__template_id
#: model:ir.model.fields,field_description:report_carbone.field_carbone_create_report_wizard__template_id
msgid "Carbone Template ID"
msgstr "Carbone Template ID"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Carbone.io Integration"
msgstr "Intégration Carbone.io"
#. module: report_carbone
#: model:ir.actions.act_window,name:report_carbone.action_carbone_report_template_tree_all
msgid "Printing actions"
msgstr "Actions d'impression"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "Carbone Template ID must be set for this action."
msgstr "Le Carbone Template ID doit être défini pour cette action."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Report file example"
msgstr "Exemple de fichier de rapport"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Carbone.io integration"
msgstr "Intégration Carbone.io"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Carbone.io subscription plan information"
msgstr "Informations sur le plan d'abonnement Carbone.io"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Carbone.io. plan"
msgstr "Plan Carbone.io"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Check if Carbone account connection is up"
msgstr "Vérifiez si la connexion au compte Carbone est établie"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_res_config_settings__is_account_stage_mode
msgid "Checked to use test account endpoints"
msgstr "Coché pour utilisé les accès de compte test"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__client_id
msgid "Client ID"
msgstr "Client ID"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__client_secret
msgid "Client Secret"
msgstr "Client Secret"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Client identifier for API authentication"
msgstr "Identifiant client pour l'authentification via API"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Client secret for API authentication"
msgstr "Secret client pour l'authentification API"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_create_report_wizard_form
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_print_by_action_view_form
msgid "Close"
msgstr "Fermer"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid "Create/update export template"
msgstr "Créer/mettre à jour un modèle d'export"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.report_carbone_translate_view_form
msgid "Create/update languages placeholder"
msgstr "Créer/mettre à jour des valeurs génériques pour chaque langue"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__currency_id
#: model:ir.model.fields,field_description:report_carbone.field_carbone_print_by_action__currency_id
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__currency_id
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__currency_id
msgid "Currency"
msgstr "Devise"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__default_currency_id
msgid "Default currency"
msgstr "Devise par défaut"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Documentation"
msgstr "Documentation"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__exceeding_credits_fee
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__exceeding_credits_fee
msgid "Exceeding Credits Fee"
msgstr "Frais de dépassement de crédits"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__exceeding_template_fee
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__exceeding_template_fee
msgid "Exceeding Template Fee"
msgstr "Frais de déplacement de modèle"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__export_model
msgid "Export Model"
msgstr "Modèle d'export"
#. module: report_carbone
#: model:ir.model,name:report_carbone.model_carbone_field_extractor
msgid "Field extractor for exports based on views"
msgstr "Extracteur de champs pour exports basé sur les vues"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_exports_line__field_label
msgid "Field label"
msgstr "Nom du champ"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_carbone_print_by_action__lang_id
msgid "If this option is enabled, the language in which the report is printed"
msgstr "Si cette option est activée, langue dans laquelle le rapport est imprimé"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__included_credits
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__included_credits
msgid "Included Credits"
msgstr "Crédis inclus"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__included_templates
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__included_templates
msgid "Included Templates"
msgstr "Modèles inclus"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_actions_report__currency_id
msgid ""
"Leave blank to let Odoo manage the currency to use "
"(by default the currency of the associated template, "
"if there is a currency, otherwise the company currency)."
msgstr ""
"Laisser vide pour laisser Odoo gérer la devise à utiliser (par défaut la "
"devise du modèle associé, s'il y a une devise, sinon la devise de la "
"société)"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_print_by_action__lang_id
#: model:ir.model.fields,field_description:report_carbone.field_carbone_translate__lang_id
msgid "Language"
msgstr "Langue"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_actions_report__lang_ids
msgid ""
"Language(s) into which the report must be translated. Leave blank if no "
"specific translation is required."
msgstr ""
"Langue(s) dans laquelle/lesquelles le rapport doit être traduit."
"Laisser vide si aucune traduction spécifique n'est requise."
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__lang_ids
msgid "Languages"
msgstr "Langues"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_translate__carbone_translate_line_ids
msgid "Translation lines"
msgstr "Lignes de traduction"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_res_config_settings__currency_id
msgid "Carbone plan currency"
msgstr "Devise du plan Carbone"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Manage Carbone Reports"
msgstr "Modifier les rapports Carbone"
#. module: report_carbone
#: model:ir.ui.menu,name:report_carbone.menu_ir_action_report_settings
msgid "By Carbone.io"
msgstr "Par Carbone.io"
#. module: report_carbone
#: model:ir.model,name:report_carbone.model_carbone_price
msgid "Model to manage different Carbone subscription prices"
msgstr "Modèle permettant de paramétrer différents prix d'abonnement Carbone"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__name
msgid "Name"
msgstr "Nom"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_exports_line__field_label
msgid "Name render in model view"
msgstr "Nom affiché dans la vue du modèle"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__next_billing_amount
msgid "Next billing amount"
msgstr "Montant de la prochaine facture"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__next_billing_date
msgid "Next billing date"
msgstr "Date de la prochaine facture"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_res_config_settings__access_token
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid ""
"Oauth2.0 token sent by Carbone.io, when user consents use of their account. "
"You can manually set this token for debugging"
msgstr "Token Oauth2.0 envoyé par Carbone.io lorsque l'utilisateur consent à l'utilisation de son compte."
"Vous pouvez définir manuellement ce token pour des tests."
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_print_by_action__id_object
msgid "Object ID"
msgstr "ID de l'enregistrement"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_print_by_action__name
msgid "Object Model"
msgstr "Modèle de l'enregistrement"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__input_user_model_id
#: model:ir.model.fields,field_description:report_carbone.field_carbone_create_report_wizard__input_user_model_id
msgid "Odoo model name"
msgstr "Nom du modèle Odoo"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_create_report_wizard__action_name
msgid "Action name"
msgstr "Nom de l'action"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_create_report_wizard__report_type_extension
msgid "Report extension"
msgstr "Extension du rapport"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__period
msgid "Period"
msgstr "Période"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__period_end
msgid "Period end"
msgstr "Fin de période"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__period_start
msgid "Period start"
msgstr "Début de période"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__price
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__price
msgid "Price HT"
msgstr "Prix HT"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_print_by_action_view_form
msgid "Print"
msgstr "Imprimer"
#. module: report_carbone
#: model:ir.model,name:report_carbone.model_carbone_print_by_action
msgid "Print by action"
msgstr ""
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__prod_api_key
msgid "Prod API Key"
msgstr "Clé API Prod"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__m2o_reference_id
msgid "Record for preview"
msgstr "Enregistrement pour la prévisualisation"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__redirect_url
msgid "Redirect URL"
msgstr "URL de redirection"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid "Refresh Studio"
msgstr "Actualiser le Studio"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__refresh_token
msgid "Refresh Token"
msgstr "Token d'actualisation "
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_price__is_selected
msgid "Selected"
msgstr "Sélectionné"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.carbone_see_prices_view_form
msgid "Shop with Stripe"
msgstr "Acheter avec Stripe"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__strip_id_session
msgid "Strip Session ID"
msgstr "ID Session Strip"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Subscribe"
msgstr "S'abonner"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Synchronize usage"
msgstr "Synchronizer la consommation"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__templates_used
msgid "Templates used"
msgstr "Modèles utilisés"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_translate_line__source
msgid "Source term"
msgstr "Terme source"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__stage_api_key
msgid "Test API Key"
msgstr "Clé API Test"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Test Carbone connection"
msgstr "Tester la connexion avec Carbone"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_res_config_settings__is_stage_mode
msgid "Test mode"
msgstr "Mode Test"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid "Test generation without leaving this form."
msgstr "Testez la génération sans quitter ce formulaire."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_print_by_action_view_form
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid "Test generation"
msgstr "Tester la génération"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "Test generation"
msgstr "Tester la génération"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_print_by_action__tz
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__tz
msgid "Timezone"
msgstr "Fuseau horaire"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__carbone_translate_ids
msgid "Translations available"
msgstr "Traductions disponible"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid "Translations"
msgstr "Traductions"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_actions_report__export_model
msgid ""
"Odoo export template used to retrieve information from a record when "
"printing the report. These are the same exports as on the Odoo “Export Data”"
" pop-up."
msgstr ""
"Model d'export Odoo a utilisé pour récupérer les informations d'un "
"enregistrement, lors de l'impression du rapport. Ce sont les mêmes export "
"que sur la pop-up Odoo 'Export Data'"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_res_config_settings__redirect_url
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid ""
"URL used by Carbone to redirect the user once the user consents access to "
"the use of their Carbone account by Odoo."
msgstr ""
"URL utilisée par Carbone pour redirigé l'utilisateur, "
"une fois que l'utilisateur consent à l'utilisation de son compte Carbone par Odoo."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Documentation"
msgstr "Documentation"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "An error occurred when generating the report via Carbone : %s"
msgstr "Une erreur s'est produite lors de la génération du rapport via Carbone : %s"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "Error : Model not found in registry"
msgstr "Erreur: Modèle non trouvé dans le registre"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "Unable to read file: %s"
msgstr "Impossible de lire le fichier : %s"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid ""
"No API Carbone key has been entered. Please enter it or contact your "
"administrator."
msgstr ""
"Aucune clé API Carbone n'a été saisie. Veuillez la saisir ou contacter votre"
"administrateur."
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "No %s selected for printing."
msgstr ""
"Aucun(e) %s sélectionné(e) pour l'impression."
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/carbone/carbone_print_by_action.py:0
msgid "No record is retrieve with this id."
msgstr "Aucun enregistrement n'a été trouvé avec cet id."
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_carbone_translate_line__value
msgid "Translation Value"
msgstr "Valeur de la traduction"
#. module: report_carbone
#: model:ir.model,name:report_carbone.model_carbone_see_prices
msgid "Wizard allowing subscription to a Carbone plan"
msgstr "Assistant permettant de souscrire à un forfait Carbone"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__m2o_reference_model
msgid "Reference to the model for m2o_reference"
msgstr "Reference au modèle pour m2o_reference"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/controllers/main.py:0
msgid "No report with this name and Carbone type was found."
msgstr "Aucun rapport avec ce nom et de type Carbone n'a été trouvé."
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/controllers/main.py:0
msgid "Please add a name to the report in order to export it."
msgstr "Veuillez ajouter un nom au rapport pour pouvoir l'exporter."
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__file_extension
msgid "File extension linked to Carbone Template ID"
msgstr "Extension de fichier liée à l'ID du modèle Carbone"
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__partner_lang_path
msgid "Path of the language field"
msgstr "Chemin du champ de la langue"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_actions_report__partner_lang_path
msgid ""
"If specified, the report will be printed according to the language defined in the path. "
"For example, to print the report in the customer's language for a purchase order, specify partner_id.lang_id. "
"Leave blank to print the report in the first language specified in the 'Language' field."
msgstr ""
"Si spécifié, le rapport sera imprimé en fonction de la langue du chemin "
"défini. Par exemple, pour imprimer le rapport avec la langue du client, pour"
" une commande d'achat, il faut indiquer partner_id.lang_id. Laissez-vide pour"
" imprimer le rapport avec la première langue indiqué dans le champ "
"'Languages'."
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid "Create a Carbone Account"
msgstr "Créer un compte Carbone"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid ""
"In test mode, printed reports will have a watermark, and newly created reports\n"
" will be deleted after 30 days."
msgstr ""
"En mode de test, les rapports imprimés auront un filigrane, et les nouveaux\n"
" rapports créés seront supprimés au bout de 30 jours"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid "An error occurred when uploading placeholder report Carbone : %s"
msgstr "Une erreur s'est produite lors du téléchargement du rapport temporaire Carbone : %s"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.res_config_settings_view_form_carbone_inherit
msgid ""
"In test mode, printed reports will have a watermark, and newly created reports\n"
" will be deleted after 30 days."
msgstr ""
"En mode de test, les rapports imprimés auront un filigrane, et les nouveaux\n"
" rapports créés seront supprimés au bout de 30 jours."
#. module: report_carbone
#. odoo-javascript
#: code:addons/report_carbone/static/src/xml/list_template.xml:0
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_create_report_wizard_form
msgid "New document"
msgstr "Nouveau document"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_create_report_wizard_form
msgid "Sauvegarder"
msgstr ""
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_create_report_wizard_form
msgid ""
"You can either enter a Carbone Template ID if you already have a Carbone report,\n"
" or choose the extension for your new document."
msgstr ""
"Vous pouvez soit renseigner un Carbone Template ID, si vous possédez déjà un rapport Carbone,\n"
" ou bien choisir l'extension de votre nouveau rapport."
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/carbone/carbone_create_report_wizard.py:0
msgid ""
"You must specify either a template or an extension, but not both."
msgstr ""
"Vous devez renseigner soit un template, soit une extension, mais pas les "
"deux."
#. module: report_carbone
#. odoo-javascript
#: code:addons/report_carbone/static/src/js/report/action_create_ir_report.js:0
msgid "Create new Carbone report"
msgstr "Créer un nouveau rapport Carbone"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_carbone_create_report_wizard__template_id
msgid "Leave empty if you don't have any existing template for this report"
msgstr "Laissez vide si vous n'avez pas de template existant pour ce rapport"
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_carbone_create_report_wizard__report_type_extension
msgid ""
"Leave empty if you already have a existing Carbone template for this report, then set the Template ID"
msgstr ""
"Laissez vide si vous disposez déjà d'un rapport carbone existant, en définissant le Carbone Template ID."
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_actions_report__m2o_reference_id
msgid ""
"The data from the field record will be used for document preview in Studio. "
"The record ID will be used by default for the 'test generation' pop-up."
msgstr "Les données de l'enregistrement du champ seront utilisées pour la prévisualisation du "
"document, dans le Studio. L'id de l'enregistrement sera utilisé par défaut pour le pop-up tester la génération."
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__is_available_in_print_action
msgid "Enable"
msgstr "Actif"
#. module: report_carbone
#: model_terms:ir.ui.view,arch_db:report_carbone.act_report_carbone_view
msgid ""
"Please note that you have copied the version ID and not the template ID."
msgstr "Attention, vous avez copié le version ID and non pas le template ID."
#. module: base
#: model:ir.model.fields,field_description:base.field_ir_actions_report__print_report_name
msgid "Printed Report Name"
msgstr "Nom du fichier"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid ""
"You cannot generate a document in any format other than PDF in test mode "
"and/or without a prod API key"
msgstr ""
"En mode test et/ou sans clé API de production, vous ne pouvez pas générer de "
"document dans un autre format que le PDF"
#. module: report_carbone
#. odoo-python
#: code:addons/report_carbone/models/base/ir_actions_report.py:0
msgid ""
"The extension you entered is not included in the list of extensions "
"supported by Carbone."
msgstr ""
"L'extension saisie n'est pas incluse dans la liste des extensions supportées par Carbone."
#. module: report_carbone
#: model:ir.model.fields,help:report_carbone.field_ir_actions_report__report_output_file_extension
msgid ""
"To be specified in order to generate a document in a format other than PDF "
"The output format must be included in the list of formats supported by "
"Carbone. You must provide a production API key and must not be in test mode."
msgstr ""
"À définir pour générer un document dans une autre extension que PDF. "
"L'extension de sortie doit être dans la liste des extensions supportées "
"par Carbone. Il faut obligatoirement renseigner une clé API prod et ne pas "
"être en mode de test."
#. module: report_carbone
#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__report_output_file_extension
msgid "File extension of the generated document"
msgstr "Extension du document généré"

View File

@@ -0,0 +1,3 @@
from . import base
from . import res
from . import carbone

View File

@@ -0,0 +1,5 @@
from . import base
from . import ir_model
from . import ir_actions_report
from . import ir_exports
from . import exceptions

View File

@@ -0,0 +1,45 @@
import pytz
from odoo import api, fields, models
class Base(models.AbstractModel):
"""The base model, which is implicitly inherited by all models."""
_inherit = "base"
carbone_default_currency_id = fields.Many2one(
"res.currency", string="Default currency used in Carbone Report", compute="_compute_carbone_default_currency_id"
)
def _compute_carbone_default_currency_id(self):
for rec in self:
currency = self.env.user.company_id.currency_id
if "currency_id" in rec._fields.keys():
currency = rec.currency_id
rec.carbone_default_currency_id = currency
@api.model
def _jsonify_value(self, field, value):
"""Overloading the OCA jsonifier library function :
- Datetime fields are displayed directly with the time zone set in context.
-The value of a Selection field's label is displayed, rather than the key stored in the database."""
if value is False and field.type != "boolean":
value = None
elif field.type == "date":
value = fields.Date.to_date(value).isoformat()
elif field.type == "datetime":
# Ensures value is a datetime
value = fields.Datetime.to_datetime(value)
expected_tz = self.env.context.get("tz") or self.env.user.tz
tz_pytz = pytz.timezone(expected_tz) if expected_tz else pytz.utc
value = pytz.utc.localize(value).astimezone(tz_pytz)
value = value.isoformat()
elif field.type in ("many2one", "reference"):
value = value.display_name if value else None
elif field.type in ("one2many", "many2many"):
value = [v.display_name for v in value]
elif field.type == "selection":
selection_list = field._description_selection(self.env)
value = dict(selection_list).get(value)
return value

View File

@@ -0,0 +1,11 @@
from odoo.exceptions import UserError
class MissingApiKeyError(UserError):
"""Missing Carbone API Keys error.
When you try to use Carbone API without correct key.
"""
def __init__(self, message):
super().__init__(message)

View File

@@ -0,0 +1,884 @@
import base64
import io
import json
import logging
import mimetypes
import os
import re
import zipfile
from collections import OrderedDict
from typing import Any
from urllib.parse import urljoin
import carbone_sdk
import pytz
import requests
from PIL import Image
from werkzeug import urls
from odoo import _, api, exceptions, fields, models, release
from odoo.modules import get_module_path
from odoo.tools.safe_eval import safe_eval, time
from odoo.addons.export_json.controller.main import JsonExportFormat
from ...const import ALLOWED_EXTENSIONS
from ...controllers.main import CarboneReportController
from .exceptions import MissingApiKeyError
_logger = logging.getLogger(__name__)
# put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 1086728
_tzs = [(tz, tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith("Etc/") else "_")]
MODULE_NAME = "report_carbone"
RELATIVE_PATH_PDF = "docs/carbone_userguide_v18.pdf"
RELATIVE_PATH_ODT = "data/demo_template_purchase.odt"
RELATIVE_PLACEHOLDERS_PATH = "data/placeholders"
# In Carbone API documentation, Values ≥ 42000000000 (year 3300) are treated as 'now'.
TIMESTAMP_NOW = 42000000000
def _tz_get(self):
return _tzs
def _build_zip_from_data(stream_to_ids: dict[Any, list]) -> bytes:
"""
:param stream_to_ids: dict { io.bytesIo object : [
int, str]}
:return: zip bytes
"""
buffer = io.BytesIO()
i = 1
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as zipfile_obj:
for doc_data in stream_to_ids:
content = doc_data.getvalue()
file_name = stream_to_ids[doc_data][1]
zipfile_obj.writestr(f"{i}-{file_name}", content)
i += 1
return buffer.getvalue()
class IrActionsReportCarbone(models.Model):
_inherit = "ir.actions.report"
report_type = fields.Selection(selection_add=[("carbone", "Carbone Report")], ondelete={"carbone": "set default"})
lang_ids = fields.Many2many(
"res.lang",
string="Languages",
help="Language(s) into which the report must be "
"translated. Leave blank if no specific translation "
"is required.",
)
currency_id = fields.Many2one(
"res.currency",
string="Currency",
help="Leave blank to let Odoo manage the currency to use "
"(by default the currency of the associated template, "
"if there is a currency, otherwise the company currency).",
)
tz = fields.Selection(selection=_tzs, string="Timezone")
template_id = fields.Char(string="Carbone Template ID", default=None)
export_model = fields.Many2one(
"ir.exports",
string="Export Model",
help="Odoo export template used to retrieve information from a record when "
"printing the report. These are the same exports as on the Odoo “Export Data” pop-up.",
)
hide_create_update_button = fields.Boolean(
"Hide the 'Create/update export template' button", compute="_compute_hide_create_update_button"
)
jsonify_export = fields.Json(string="Export JSON", compute="_compute_all_jsonify_export")
jsonify_translate_export = fields.Json(string="Export translate JSON", compute="_compute_all_jsonify_export")
m2o_reference_id = fields.Many2oneReference(
string="Record for preview",
model_field="m2o_reference_model",
help="The data from the field record will be "
"used for document preview in Studio. The record ID will be used by default for the 'test generation' pop-up.",
)
m2o_reference_model = fields.Char(
string="Reference to the model for m2o_reference", compute="_compute_m2o_reference_model"
)
input_user_model_id = fields.Many2one("ir.model", string="Odoo model name", domain=[("transient", "=", False)])
carbone_translate_ids = fields.One2many(
"carbone.translate", "ir_actions_report_id", string="Translations available"
)
file_extension = fields.Char(string="File extension linked to Carbone Template ID", default="docx")
is_valid_template_id = fields.Boolean(
string="Is valid templateId",
help="True if template id is a templateId,false if it is a versionId",
compute="_compute_is_valid_template_id",
store=True,
)
partner_lang_path = fields.Char(
string="Path of the language field",
help="If specified, the report will be printed according to the language defined in the path. "
"For example, to print the report in the customer's language for a purchase order, specify partner_id.lang_id. "
"Leave blank to print the report in the first language specified in the 'Language' field.",
)
is_available_in_print_action = fields.Boolean(string="Enable", compute="_compute_is_available_in_print_action")
report_output_file_extension = fields.Char(
string="File extension of the generated document",
help="To be specified in order to generate a document in a format other than PDF "
"The output format must be included in the list of formats "
"supported by Carbone. You must provide a production API key and must not "
"be in test mode.",
)
@api.model
def _setup_template_id_and_extension(self, vals):
"""If a template_id is specified during creation, only the extension is retrieved via the Carbone API
and the studio is allowed to retrieve the report automatically.
If there is no template_id, a placeholder report is added to ir.actions.report so that the studio
can be set up without the user having to specify a template_id.
"""
if vals.get("template_id"):
new_file_extension = self.get_extension_file_from_api(vals.get("template_id"), raise_error=False)
vals.update({"file_extension": new_file_extension})
else:
try:
file_extension = vals.get("file_extension", "docx")
template_name = vals.get("name") or f"PlaceholderTemplate ({file_extension})"
new_vals = self.post_template_from_api(template_name, file_extension)
vals.update(new_vals)
except Exception as e:
raise exceptions.UserError(
_("An error occurred when uploading placeholder report Carbone : %s") % e
) from e
def check_report_output_file_extension(self, vals):
report_output_file_extension = vals.get("report_output_file_extension")
if not report_output_file_extension:
return
report_output_file_extension = report_output_file_extension.lower().lstrip(".")
if report_output_file_extension not in ALLOWED_EXTENSIONS:
raise exceptions.UserError(
_("Extension you entered is not included in the list of extensions supported by Carbone.")
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("report_type") == "carbone":
if not vals.get("report_name"):
name = vals.get("name")
vals.update({"report_name": name})
self._setup_template_id_and_extension(vals)
return super().create(vals_list)
def write(self, vals):
if "template_id" in vals.keys():
new_template_id = vals.get("template_id")
new_file_extension = self.get_extension_file_from_api(new_template_id, raise_error=False)
vals.update({"file_extension": new_file_extension})
if "report_output_file_extension" in vals.keys():
self.check_report_output_file_extension(vals)
return super().write(vals)
def _compute_all_jsonify_export(self):
"""This compute allows you to create data for the jsonify_export and jsonify_translate_export fields.
- A first step to retrieve a complete JSON of the data, based on the configured export.
This JSON also contains all available translations, based on the languages active in the tool.
- The previously retrieved JSON will be used to enhance the translation JSON, which is first built with all the
carbone.translate related to the report.
- Once the translation JSON has been modified, we define the jsonify_translate_export field.
- We can then extract the keys and values containing translations from the complete JSON.
"""
for rec in self:
dict_full_data = rec._get_jsonify_export()
dict_langs = rec._get_jsonify_translate_export()
# Based on the data from dict_full_data, we add the translations.
# dict_langs et dict_full_data are modified by reference.
rec.extract_translations(dict_full_data, dict_langs)
# The translations are now correctly configured.
rec.jsonify_translate_export = json.dumps(dict_langs, indent=4)
# The data no longer has the keys with all the translations for each language.
rec.jsonify_export = json.dumps(dict_full_data, indent=4)
def extract_translations(self, data, translations, path: str = ""):
sub_pattern = ""
lang = list(translations.keys())
lang_map = {}
for code in lang:
key = code.split("-")[1].upper()
lang_map.update({key: code})
sub_pattern += f"{key}|"
sub_pattern.strip("|")
translation_pattern = re.compile(r"^(.+)_(" + sub_pattern + ")$")
keys_to_remove = []
for key, value in data.items():
current_path = f"{path}/{key}" if path else key
match = translation_pattern.match(key)
if match:
base_key = match.group(1)
lang_code = match.group(2)
locale = lang_map.get(lang_code)
if locale and locale in translations:
if base_key in data:
reference_key = data[base_key]
if isinstance(reference_key, str):
translations[locale][reference_key] = value
keys_to_remove.append(key)
elif isinstance(value, dict):
self.extract_translations(value, translations, current_path)
elif isinstance(value, list):
for _i, item in enumerate(value):
if isinstance(item, dict):
self.extract_translations(item, translations, current_path)
for key in keys_to_remove:
del data[key]
def _get_jsonify_export(self):
self.ensure_one()
if not self.export_model or not self.model or self.model not in self.env.registry or not self.m2o_reference_id:
return {}
else:
test_record = self.env[self.model].browse(self.m2o_reference_id)
export_json_instance = JsonExportFormat()
export_lines = self.export_model.export_fields
field_names = self._prepare_fields_name(export_lines)
lang_codes = self.lang_ids.mapped("code")
specific_lang = self.get_lang_to_use(test_record, parse_for_carbone=False)
if specific_lang not in lang_codes:
lang_codes.append(specific_lang)
json_data = export_json_instance.perform_json_export(
[], field_names, test_record.ids, self.env[self.model], lang_codes
)
# json_data is an str with a list of dict, we don't want to give user a list, just a dict.
json_list = json.loads(json_data)
dict_json = json_list and json_list[0] or json_list[:1]
return dict_json
def _get_jsonify_translate_export(self):
for rec in self:
all_langs = {}
available_langs = self.env["res.lang"].search([])
for lang in available_langs:
lang_code = lang.code.lower().replace("_", "-")
all_langs.update({lang_code: {}})
for carbone_translate in rec.carbone_translate_ids:
lang_code = carbone_translate.lang_id.code.lower().replace("_", "-")
lines = {}
for line in carbone_translate.carbone_translate_line_ids:
lines.update({line.source: line.value})
all_langs.update({lang_code: lines})
return all_langs
def _compute_hide_create_update_button(self):
for rec in self:
rec.hide_create_update_button = rec.get_hide_create_update_button_value()
def _compute_is_available_in_print_action(self):
for rec in self:
rec.is_available_in_print_action = rec.binding_model_id
@api.depends("model")
def _compute_m2o_reference_model(self):
for rec in self:
if rec.model not in self.env.registry:
rec.m2o_reference_model = "res.partner"
else:
rec.m2o_reference_model = rec.model
@api.depends("template_id")
def _compute_is_valid_template_id(self):
for rec in self:
if re.match(r"^[a-f0-9]{64}$", rec.template_id or ""):
rec.is_valid_template_id = False
else:
rec.is_valid_template_id = True
@api.onchange("model", "export_model", "report_type")
def onchange_hide_create_update_button(self):
self.hide_create_update_button = self.get_hide_create_update_button_value()
@api.onchange("input_user_model_id")
def onchange_user_model_id(self):
if self.report_type == "carbone" and self.input_user_model_id:
self.model = self.input_user_model_id.model
# We have to set a new record in the m2o_reference_id, because Odoo will try to display a record with
# current id (4 for example), from a existing model X, in a non-existing record with id 4 in model Y.
self.m2o_reference_id = self.env[self.model].search([], limit=1)
export_suffixe_name = self.input_user_model_id.display_name
if not self.export_model:
self.export_model = self.retrieve_global_export_model(self.model, export_suffixe_name)
@api.onchange("name")
def onchange_name(self):
"""For Carbon reports, the report_type field is not displayed.
It is not used when printing Carbon reports; it is filled in automatically."""
if self.report_type == "carbone":
self.report_name = self.name
def action_setup_carbone_studio_options(self):
"""Use to set-up JSON dicts (data, translations) for Carbone Studio, and retrieve lang in which
report will be rendered."""
self.ensure_one()
test_record = self.env[self.model].browse(self.m2o_reference_id)
lang_code = self.get_lang_to_use(test_record)
currency = self.get_currency_to_use(test_record)
timezone = self.env.context.get("tz") or self.env.user.tz
template = self.template_id
extension = self.file_extension
if not extension:
extension = self.get_extension_file_from_api(template, raise_error=False)
self.file_extension = extension
return {
"type": "ir.actions.client",
"tag": "copy_options_to_carbone",
"context": {
"record_id": self.id,
"json_data": self.jsonify_export,
"json_translate_data": self.jsonify_translate_export,
"lang": lang_code,
"timezone": timezone,
"currency": currency.name,
"template": template,
"extension": extension,
},
}
def action_refresh_carbone_studio(self):
self.ensure_one()
return {
"type": "ir.actions.client",
"tag": "action_refresh_carbone_studio",
}
def get_hide_create_update_button_value(self):
self.ensure_one()
res = False
group_viewer = "report_carbone.group_report_carbone_viewer"
if (
self.report_type != "carbone"
or (self.export_model and not self.export_model.is_global_export)
or (self.model not in self.env.registry)
or (self.env.user.has_groups(group_viewer))
):
res = True
return res
def get_currency_to_use(self, record=False) -> "odoo.model.res_currency":
self.ensure_one()
currency = record and record.carbone_default_currency_id or self.env.user.company_id.currency_id
if self.currency_id and self.currency_id != currency:
currency = self.currency_id
return currency
def _get_nested_field(self, record, field_path):
"""
Find the information in the field, based on the path entered, only if the path is valid.
"""
if not field_path:
return False
fields = field_path.split(".")
current = record
for field_name in fields:
if not current or field_name not in current._fields:
return False
current = current[field_name]
if hasattr(current, "_name") and not current:
return False
return current
def get_lang_to_use(self, record, parse_for_carbone=True) -> str:
"""
For language, the rules are as follows:
If the function call comes from the test report pop-up, we keep the language specified in the context.
Otherwise, if a language is specified in partner_lang_path, we retrieve the associated language.
Otherwise, we retrieve the first language specified in the lang_ids field.
If the language is still not specified, the language set in the context is retrieved, or else the user's
language.
:param record: "odoo.model.any"
:param parse_for_carbone: bool
:return: lang code (ex : fr_FR for French)
"""
self.ensure_one()
lang = ""
if self.env.context.get("from_print_by_action"):
lang = self.env.context.get("lang")
elif self.partner_lang_path:
lang = self._get_nested_field(record, self.partner_lang_path)
if not lang:
lang = self.lang_ids and self.lang_ids[0].code or self.env.context.get("lang") or self.env.user.lang
if parse_for_carbone:
return lang.lower().replace("_", "-")
return lang
def _prepare_fields_name(self, export_lines: "odoo.model.ir_exports_line") -> tuple[str]:
field_names = []
if "id" not in export_lines.mapped("name"):
field_names = ["id"]
for line in export_lines:
field_names.append(line.name)
return field_names
def check_required_fields(self):
"""Raise user error if template_id or export_model fields are missing."""
missing_field = []
if not self.template_id:
missing_field.append("Carbone Template ID")
if not self.export_model:
missing_field.append("export model")
if missing_field:
raise exceptions.UserError(f"Missing {', '.join(missing_field)} to generate this report")
def _get_parameters_for_render(self, context, record, parse_for_carbone=True) -> tuple:
"""Used to retrieve lang to translate report, timezone and currency, to render a
Carbone report."""
self.ensure_one()
lang = self.with_context(context).get_lang_to_use(record, parse_for_carbone)
currency_id = context.get("currency_id")
currency = self.env["res.currency"].browse(currency_id)
if not currency:
currency = self.get_currency_to_use(record)
tz = context.get("tz") or self.env.user.tz
return lang, tz, currency
def _check_no_test_mode_and_prod_api_key(self, extension: str):
"""A user error is raised if the user attempts to print a report in a format other than PDF,
and the system is in test mode and/or there is no production API key"""
self.ensure_one()
is_stage_mode = self.env["ir.config_parameter"].sudo().get_param("report-engine.is_stage_mode")
prod_api_key = self.env["ir.config_parameter"].sudo().get_param("report-engine.prod_api_key")
if extension != "pdf" and (is_stage_mode or not prod_api_key):
raise exceptions.UserError(
_(
"You cannot generate a document in any format other than "
"PDF in test mode and/or without a prod API key"
)
)
def get_report_output_file_extension(self) -> str:
self.ensure_one()
if self.report_output_file_extension:
self._check_no_test_mode_and_prod_api_key(self.report_output_file_extension)
return self.report_output_file_extension
return "pdf"
def get_default_user_agent(self) -> str:
default_user_agent = requests.utils.default_user_agent()
return f"{default_user_agent} Mangono Odoo v{release.version}"
def _retrieve_attachement(self, collected_streams, res_ids, has_duplicated_ids):
"""Copy of odoo/addons/base/models/ir_actions_report.py "_render_qweb_pdf_prepare_streams" function.
The only change was to retrieve the name of the associated
attachment when printing several reports at the same time
(we create one zip file with X files, rather than one file containing the information from X files).
"""
if res_ids:
records = self.env[self.model].browse(res_ids)
for record in records:
res_id = record.id
if res_id in collected_streams:
continue
stream = None
attachment = None
if not has_duplicated_ids and self.attachment and not self._context.get("report_pdf_no_attachment"):
attachment = self.retrieve_attachment(record)
# Extract the stream from the attachment.
if attachment and self.attachment_use:
stream = io.BytesIO(attachment.raw)
# Ensure the stream can be saved in Image.
if attachment.mimetype.startswith("image"):
img = Image.open(stream)
new_stream = io.BytesIO()
img.convert("RGB").save(new_stream, format="pdf")
stream.close()
stream = new_stream
filename = attachment and attachment.name or False
collected_streams[res_id] = {
"stream": stream,
"attachment": attachment,
"filename": filename,
}
return collected_streams
@api.model
def get_carbone_sdk(self) -> carbone_sdk.CarboneSDK:
access_token = self.env["res.config.settings"].retrieve_carbone_api_key()
if not access_token:
raise MissingApiKeyError(
_("No API Carbone key has been entered. Please enter it or contact your administrator.")
)
csdk = carbone_sdk.CarboneSDK(access_token)
csdk._api_headers.update({"User-Agent": self.get_default_user_agent()})
return csdk
def _get_json_data(self, export_json_instance, field_names, record, model, lang_codes):
json_data = export_json_instance.perform_json_export([], field_names, record.ids, self.env[model], lang_codes)
dict_full_data = json.loads(json_data)[0]
return dict_full_data
def _call_carbone_to_get_streams(self, all_res_ids_wo_stream: list, collected_streams: OrderedDict):
# Creation of Carbone and JsonExportFormat instances
csdk = self.get_carbone_sdk()
export_json_instance = JsonExportFormat()
# Recovery of the report model.
model = self.model
# Retrieving field_names from the report.
field_names = ["id"]
export_lines = self.export_model.export_fields
field_names.extend(line.name for line in export_lines)
context = dict(self.env.context)
records = self.env[model].browse(all_res_ids_wo_stream) or self.env[model]
if not records.exists():
raise exceptions.MissingError(_("No %s selected for printing." % records._description)) # noqa: UP031
for record in records:
try:
# We retrieve the translation language, time zone and currency.
lang, tz, currency = self._get_parameters_for_render(context, record, parse_for_carbone=False)
# We collect all the languages into which the report can be translated. And, if necessary,
# the target language is added to the list of possible translations for the report.
lang_codes = self.lang_ids.mapped("code")
if lang not in lang_codes:
lang_codes.append(lang)
# After that, we can transform lang code to match the Carbone language format.
lang = lang.lower().replace("_", "-")
# Creating the JSON file (data and translate).
dict_full_data = self._get_json_data(export_json_instance, field_names, record, model, lang_codes)
dict_langs = self._get_jsonify_translate_export()
# Modification by reference of dicts.
self.extract_translations(dict_full_data, dict_langs)
output_file_extension = self.get_report_output_file_extension()
tuple_pdf = csdk.render(
self.template_id,
{
"data": dict_full_data,
"convertTo": output_file_extension,
"translations": dict_langs,
"lang": lang,
"timezone": tz,
"currencySource": currency.name,
"currencyTarget": currency.name,
},
)
pdf_content_stream = io.BytesIO(tuple_pdf[0])
filename = self._retrieve_carbone_filename(record, output_file_extension)
collected_streams[record.id]["stream"] = pdf_content_stream
collected_streams[record.id]["filename"] = filename
collected_streams[record.id]["out_file_extension"] = output_file_extension
except Exception as e:
raise exceptions.UserError(
_("An error occurred when generating the report via Carbone : %s") % e
) from e
return collected_streams
def _render_carbone_prepare_streams(self, res_ids=None):
has_duplicated_ids = res_ids and len(res_ids) != len(set(res_ids))
collected_streams = OrderedDict()
if res_ids:
collected_streams = self._retrieve_attachement(collected_streams, res_ids, has_duplicated_ids)
res_ids_wo_stream = [res_id for res_id, stream_data in collected_streams.items() if not stream_data["stream"]]
all_res_ids_wo_stream = res_ids if has_duplicated_ids else res_ids_wo_stream
is_carbone_needed = not res_ids or res_ids_wo_stream
if is_carbone_needed:
collected_streams = self._call_carbone_to_get_streams(all_res_ids_wo_stream, collected_streams)
return collected_streams
def _render_carbone_handler_create_attachment(self, has_duplicated_ids, collected_streams):
"""Copy of odoo/addons/base/models/ir_actions_report.py _render_qweb_pdf() ir.attachment handler"""
# Generate the ir.attachment if needed.
if not has_duplicated_ids and self.attachment and not self._context.get("report_pdf_no_attachment"):
attachment_vals_list = self._prepare_pdf_report_attachment_vals_list(self, collected_streams)
if attachment_vals_list:
attachment_names = ", ".join(x["name"] for x in attachment_vals_list)
try:
self.env["ir.attachment"].create(attachment_vals_list)
except exceptions.AccessError:
_logger.info(
"Cannot save PDF report %r attachments for user %r",
attachment_names,
self.env.user.display_name,
)
else:
_logger.info("The PDF documents %r are now saved in the database", attachment_names)
def _render_carbone(self, report_ref, docids: str | list, data=None) -> tuple[bytes, str]:
context = dict(self.env.context)
report_sudo = self._get_report(report_ref)
report_sudo.env.context = context
# docids can be either a string, if the function call comes from
# a "Print" button, on a list the call does not come from the button.
res_ids = self.get_ids(docids)
has_duplicated_ids = res_ids and len(res_ids) != len(set(res_ids))
report_sudo.check_required_fields()
collected_streams = report_sudo._render_carbone_prepare_streams(res_ids)
report_sudo._render_carbone_handler_create_attachment(has_duplicated_ids, collected_streams)
stream_to_ids = {
v["stream"]: [k, v.get("filename", False), v.get("out_file_extension", False)]
for k, v in collected_streams.items()
if v["stream"]
}
streams_to_dl = list(stream_to_ids.keys())
if not context.get("from_ir_report_controller") or len(streams_to_dl) == 1:
pdf_content = streams_to_dl[0].getvalue()
# stream_to_ids[streams_to_dl[0]] contains [record_id, filename, extension]
extension = stream_to_ids[streams_to_dl[0]][2]
return pdf_content, extension
zip_content = _build_zip_from_data(stream_to_ids)
return zip_content, "zip"
def _retrieve_carbone_filename(self, records, output_file_extension: str) -> str:
self.ensure_one()
filename = f"{self.name}.{output_file_extension}"
if records:
if self.print_report_name and not len(records) > 1: # print_report_name is not mandatory
report_name = self._sanitize(safe_eval(self.print_report_name, {"object": records, "time": time}))
filename = f"{report_name}.{output_file_extension}"
return filename
@api.model
def _sanitize(self, name: str) -> str:
"""Avoid slash in filename, otherwise zip will create subdirectory."""
return name.replace("/", "_").replace(":", "_").replace("\\", "_").replace(" ", "_")
@api.model
def _get_report_from_name(self, report_name: str) -> "odoo.model.ir_actions_report":
"""Override to first search for an ir.actions.report by the "Carbone" report type and name,
before just searching by the report name. Allows you to have reports of different types,
"Carbone" and others."""
report_obj = self.env["ir.actions.report"]
domain = [
("report_type", "=", "carbone"),
("report_name", "=", report_name),
]
context = self.env["res.users"].context_get()
res = report_obj.with_context(context).sudo().search(domain, limit=1)
if not res:
return super()._get_report_from_name(report_name)
return res
@api.model
def get_ids(self, docids: str | list) -> list[int]:
if isinstance(docids, list):
return docids
return [int(x) for x in docids.split(",")]
@api.model
def check_model_in_registry(self, model_name: str):
if model_name not in self.env.registry:
raise exceptions.UserError(_("Error : Model not found in registry"))
def action_carbon_print_by_action_window(self):
self.ensure_one()
self.check_model_in_registry(self.model)
record = self.env[self.model].browse(self.m2o_reference_id)
context = self.env.context.copy()
if record:
context.update({"record_id": record.id, "currency_id": self.get_currency_to_use(record).id})
return {
"name": _("Test generation"),
"view_mode": "form",
"res_model": "carbone.print_by_action",
"type": "ir.actions.act_window",
"context": dict(context),
"target": "new",
}
def create_global_export(self, model_name: str, export_suffixe_name: str) -> "odoo.model.ir_exports":
fields = self.env["carbone.field.extractor"].get_fields_from_multiple_views(self.model)
to_create_ir_export_line = [(0, 0, {"name": field}) for field in fields]
to_create_ir_export_line.insert(0, (5, 0, 0))
return self.env["ir.exports"].create(
{
"name": f"Global Export For Carbone - {export_suffixe_name}",
"resource": model_name,
"export_fields": to_create_ir_export_line,
"is_global_export": True,
}
)
def action_download_carbone_documentation(self):
ir_attachment_name = "Carbone_report_guide.pdf"
attachment_xml_id = "report_carbone.report_carbone_userguide_attachment"
return self.download_carbone_file(ir_attachment_name, attachment_xml_id, MODULE_NAME, RELATIVE_PATH_PDF)
def action_download_carbone_file_sample(self):
ir_attachment_name = "Demo_template_purchase_order.odt"
attachment_xml_id = "report_carbone.report_carbone_demo_purchase_order"
return self.download_carbone_file(ir_attachment_name, attachment_xml_id, MODULE_NAME, RELATIVE_PATH_ODT)
def download_carbone_file(
self, ir_attachment_name: str, attachment_xml_id: str, module_name: str, relative_path: str
) -> dict:
attachment = self.env.ref(attachment_xml_id, raise_if_not_found=False)
if not attachment:
attachment = self.env["ir.attachment"].search([("name", "=", ir_attachment_name)], limit=1)
if not attachment:
try:
module_path = get_module_path(module_name)
pdf_path = os.path.join(module_path, relative_path)
with open(pdf_path, "rb") as pdf_file:
pdf_content = pdf_file.read()
except Exception as e:
raise exceptions.UserError(_("Unable to read file: %s") % str(e)) from e
datas = base64.b64encode(pdf_content)
attachment = self.env["ir.attachment"].create(
{
"name": ir_attachment_name,
"datas": datas,
"public": True,
}
)
return {
"type": "ir.actions.act_url",
"target": "new",
"url": f"/web/content/{attachment.id}",
}
def retrieve_global_export_model(self, model_name: str, export_suffixe_name: str) -> "odoo.model.ir_exports":
"""Retrieve or create a global export model, when user change Odoo model from ir.actions.report."""
global_export = self.env["ir.exports"].search(
[("resource", "=", model_name), ("is_global_export", "=", True)], limit=1
)
if not global_export:
return self.create_global_export(model_name, export_suffixe_name)
return global_export
def button_create_update_ir_export(self):
if self.model not in self.env.registry or not self.input_user_model_id:
return
global_export = self.env["ir.exports"].search(
[("resource", "=", self.model), ("is_global_export", "=", True)], limit=1
)
if not global_export:
export_suffixe = self.input_user_model_id.display_name
self.export_model = self.create_global_export(self.model, export_suffixe)
else:
carbone_extractor = self.env["carbone.field.extractor"]
last_update_fields = carbone_extractor.get_fields_from_multiple_views(self.model)
old_fields = global_export.export_fields.mapped("name")
if last_update_fields == old_fields:
return
carbone_extractor.update_current_global_export(global_export, last_update_fields, old_fields)
@api.model
def post_template_from_api(self, template_name: str, file_extension: str) -> dict:
module_path = get_module_path(MODULE_NAME)
relative_file_path = f"{RELATIVE_PLACEHOLDERS_PATH}/template.{file_extension}"
file_path = os.path.join(module_path, relative_file_path)
with open(file_path, "rb") as f:
file_content = f.read()
files = {
"template": (
os.path.basename(file_path),
file_content,
mimetypes.guess_type(file_path)[0] or "application/octet-stream",
)
}
data = {
"name": template_name,
"versioning": "true",
"deployedAt": TIMESTAMP_NOW,
}
response = self.call_carbone_endpoint("template", method="POST", files=files, data=data)
return {"template_id": response["data"]["id"], "file_extension": file_extension}
def get_extension_file_from_api(self, template_id: str, raise_error=True) -> str | bool:
# If we are in install mode, for unit test for example, and we have to init a ir.actions.report from an XML
# file, we don't wan't to call Carbone's API to retrieve extension.
if self.env.context.get("install_mode"):
return ".docx"
endpoint = "templates"
params = {
"search": template_id,
"limit": 1,
"includeVersions": "true",
}
res = self.call_carbone_endpoint(endpoint, params, raise_error)
data_list = res.get("data")
if not data_list:
return False
return data_list[0].get("type")
def call_carbone_endpoint(self, endpoint: str, params=None, raise_exception=True, method="GET", **kwargs):
api_token = self.env["res.config.settings"].retrieve_carbone_api_key()
if not api_token:
if raise_exception:
raise MissingApiKeyError(
_("No API Carbone key has been entered. Please enter it or contact your administrator.")
)
else:
return False
api_endpoint = self.env["ir.config_parameter"].sudo().get_param("report-engine.carbone_studio_url")
response = requests.Response
# We have to specified carbone-version 5
headers = {"Authorization": "Bearer " + api_token, "carbone-version": "5"}
url = f"{api_endpoint}/{endpoint}"
if params:
url = urljoin(url, "?" + urls.url_encode(params))
if method == "GET":
response = requests.get(url, headers=headers, **kwargs)
elif method == "POST":
response = requests.post(url, headers=headers, **kwargs)
handled_response = CarboneReportController.handle_response(response, raise_exception=raise_exception)
return handled_response

View File

@@ -0,0 +1,59 @@
from odoo import fields, models
class IrExportsCarbone(models.Model):
_inherit = "ir.exports"
is_global_export = fields.Boolean(string="Global Export")
class IrExportsLineCarbone(models.Model):
_inherit = "ir.exports.line"
field_label = fields.Char(string="Field label", help="Name render in model view", compute="_compute_field_label")
def _get_fields_model_data(self, model_name: str):
model = self.env[model_name]
return model.fields_get(attributes=["string", "relation"])
def _get_fields_model_data(self, model_name: str):
model = self.env[model_name]
return model.fields_get(attributes=["string", "relation"])
def _get_field_label_recursive(self, field_path, current_model):
"""
Recursively retrieves the label of a field for relations
:param field_path: Field path (ex: 'company_id/name' ou 'partner_id/country_id/name')
:param current_model: Current model name
:return: Field label
"""
path_parts = field_path.split("/")
fields = self._get_fields_model_data(current_model)
current_field = path_parts[0]
field_info = fields.get(current_field)
if not field_info:
return field_path
if len(path_parts) == 1:
return field_info.get("string", current_field)
relation_model = field_info.get("relation")
if not relation_model:
return field_path
remaining_path = "/".join(path_parts[1:])
return self._get_field_label_recursive(remaining_path, relation_model)
def _compute_field_label(self):
for rec in self:
field_label = ""
current_model = self.export_id.resource
fields = self._get_fields_model_data(current_model)
if rec.name and "/" in rec.name:
field_label = self._get_field_label_recursive(rec.name, current_model)
else:
field_get_information_field = fields.get(rec.name)
if field_get_information_field:
field_label = field_get_information_field.get("string")
rec.field_label = field_label

View File

@@ -0,0 +1,12 @@
from odoo import api, models
class IrModelCarbone(models.Model):
_inherit = "ir.model"
@api.depends("name", "model")
def _compute_display_name(self):
if not self.env.context.get("carbone_report_display_name"):
return super()._compute_display_name()
for rec in self:
rec.display_name = f"{rec.name} - {rec.model}"

View 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

View File

@@ -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",
},
}

View 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

View 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

View 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)

View 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)

View File

@@ -0,0 +1 @@
from . import res_config_settings

View File

@@ -0,0 +1,30 @@
from odoo import fields, models
class CarboneResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
carbone_studio_url = fields.Char("Carbone Studio URL", config_parameter="report-engine.carbone_studio_url")
carbone_js_file_url = fields.Char("Carbone JS file URL", config_parameter="report-engine.carbone_js_file_url")
is_stage_mode = fields.Boolean(string="Test mode", config_parameter="report-engine.is_stage_mode")
prod_api_key = fields.Char(string="Prod API Key", config_parameter="report-engine.prod_api_key")
stage_api_key = fields.Char(string="Test API Key", config_parameter="report-engine.stage_api_key")
def open_ir_actions_reports(self):
return self.env["ir.actions.actions"]._for_xml_id("report_carbone.action_carbone_report_template_tree_all")
def retrieve_carbone_api_key(self, test_mode_key=False):
"""Depending on the test mode checkbox or 'test_mode_key' parameter, either the production key or
the staging key is returned."""
stage_mode = self.env["ir.config_parameter"].sudo().get_param("report-engine.is_stage_mode")
if stage_mode or test_mode_key:
return self.env["ir.config_parameter"].sudo().get_param("report-engine.stage_api_key")
return self.env["ir.config_parameter"].sudo().get_param("report-engine.prod_api_key")
def action_download_carbone_documentation(self):
ir_action_report = self.env["ir.actions.report"]
return ir_action_report.action_download_carbone_documentation()
def action_download_carbone_file_sample(self):
ir_action_report = self.env["ir.actions.report"]
return ir_action_report.action_download_carbone_file_sample()

View File

@@ -0,0 +1,8 @@
STUDIO_JS_URL = "https://bin.carbone.io/studio/5.1.1/carbone-studio.min.js"
API_REPORT_URL = "https://api.carbone.io"
def set_res_config_settings(env):
env["ir.config_parameter"].set_param("report-engine.carbone_studio_url", API_REPORT_URL)
env["ir.config_parameter"].set_param("report-engine.carbone_js_file_url", STUDIO_JS_URL)
env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True)

View File

@@ -0,0 +1,9 @@
[build-system]
requires = ["addon-odoo-wheel>=0.4.3"]
build-backend = "addon_odoo_wheel.builder"
[tool.addon-odoo-wheel]
dependencies = [
"mangono-addon-export_json~=18.0",
"carbone_sdk~=1.0"
]

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="group_report_carbone_viewer" model="res.groups">
<field name="name">Carbone Report - Viewer</field>
<field name="category_id" ref="base.module_category_usability"/>
<field name="implied_ids" eval="[(4, ref('base.group_allow_export'))]"/>
</record>
<record id="group_report_carbone_manager" model="res.groups">
<field name="name">Carbone Report - Manager</field>
<field name="implied_ids" eval="[(4, ref('report_carbone.group_report_carbone_viewer'))]"/>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
report_carbone.access_carbone_print_by_action,access_carbone_print_by_action,report_carbone.model_carbone_print_by_action,base.group_user,1,1,1,1
access_ir_actions_report_carbone_manager,ir_actions_report_carbone_manager,base.model_ir_actions_report,report_carbone.group_report_carbone_manager,1,1,1,1
report_carbone.access_carbone_create_report_wizard,access_carbone_create_report_wizard,report_carbone.model_carbone_create_report_wizard,report_carbone.group_report_carbone_manager,1,1,1,1
report_carbone.access_carbone_field_extractor,access_carbone_field_extractor,report_carbone.model_carbone_field_extractor,base.group_user,1,1,1,1
report_carbone.access_carbone_translate,access_carbone_translate,report_carbone.model_carbone_translate,base.group_user,1,1,1,1
report_carbone.access_carbone_translate_line,access_carbone_translate_line,report_carbone.model_carbone_translate_line,base.group_user,1,1,1,1
"access_ir_model_user","ir_model_all","model_ir_model",base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 report_carbone.access_carbone_print_by_action access_carbone_print_by_action report_carbone.model_carbone_print_by_action base.group_user 1 1 1 1
3 access_ir_actions_report_carbone_manager ir_actions_report_carbone_manager base.model_ir_actions_report report_carbone.group_report_carbone_manager 1 1 1 1
4 report_carbone.access_carbone_create_report_wizard access_carbone_create_report_wizard report_carbone.model_carbone_create_report_wizard report_carbone.group_report_carbone_manager 1 1 1 1
5 report_carbone.access_carbone_field_extractor access_carbone_field_extractor report_carbone.model_carbone_field_extractor base.group_user 1 1 1 1
6 report_carbone.access_carbone_translate access_carbone_translate report_carbone.model_carbone_translate base.group_user 1 1 1 1
7 report_carbone.access_carbone_translate_line access_carbone_translate_line report_carbone.model_carbone_translate_line base.group_user 1 1 1 1
8 access_ir_model_user ir_model_all model_ir_model base.group_user 1 0 0 0

View File

@@ -0,0 +1,297 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
a {
color: "#5e17eb";
}
body {
font-family: "Inter", sans-serif;
line-height: 1.7;
color: #001e2b;
background: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
border-radius: 0;
margin-bottom: 40px;
position: relative;
}
.header-title-container {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
margin-bottom: 20px;
}
.header-icon {
width: 300px;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.header-icon img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.header h1 {
color: #5e17eb;
font-size: 3em;
font-weight: 700;
letter-spacing: -0.02em;
}
.header .subtitle {
font-size: 1.8em;
opacity: 0.85;
margin-bottom: 15px;
font-weight: 250;
color: #e6fffa;
}
.badge {
display: inline-block;
background: transparent;
border: 1px solid rgba(245, 243, 237, 0.3);
padding: 10px 24px;
border-radius: 2px;
margin: 10px 8px;
font-size: 0.85em;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.section {
margin-bottom: 80px;
padding: 0;
background: transparent;
border-radius: 0;
box-shadow: none;
}
.section h2 {
color: #001e2b;
font-size: 2.2em;
margin-bottom: 30px;
border-bottom: 1px solid #001e2b;
padding-bottom: 15px;
font-weight: 400;
letter-spacing: -0.01em;
}
.section h3 {
color: #001e2b;
font-size: 1.4em;
margin-top: 40px;
margin-bottom: 20px;
font-weight: 500;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 40px;
}
.feature-card {
background: #ffffff;
padding: 35px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.feature-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.feature-card h4 {
color: #001e2b;
font-size: 1.15em;
margin-bottom: 12px;
display: flex;
align-items: center;
font-weight: 500;
}
.feature-card .icon {
font-size: 1.4em;
margin-right: 12px;
opacity: 0.8;
}
.feature-card p {
color: #5a5a5a;
line-height: 1.6;
font-size: 0.95em;
}
.screenshot-placeholder {
background: #ffffff;
border-radius: 0;
padding: 0;
text-align: center;
margin: 30px 0;
color: #8a8a8a;
font-size: 1em;
font-weight: 400;
letter-spacing: 0.02em;
overflow: hidden;
display: flex;
flex-direction: column;
}
.screenshot-placeholder img {
width: 100%;
height: auto;
object-fit: contain;
display: block;
}
.benefits-list {
list-style: none;
padding: 0;
}
.benefits-list li {
padding: 20px 0;
border-bottom: 1px solid #e8e6df;
display: flex;
align-items: start;
}
.benefits-list li:before {
content: "—";
color: #5e17eb;
font-weight: 400;
font-size: 1.3em;
margin-right: 20px;
margin-top: 2px;
}
.tech-specs {
background: #ffffff;
padding: 30px;
border-radius: 0;
margin-top: 30px;
border: 1px solid #e8e6df;
}
.tech-specs table {
width: 100%;
border-collapse: collapse;
}
.tech-specs td {
padding: 15px 0;
border-bottom: 1px solid #e8e6df;
font-size: 0.95em;
}
.tech-specs td:first-child {
font-weight: 500;
color: #001e2b;
width: 240px;
}
.tech-specs td:last-child {
color: #5a5a5a;
}
.cta-section {
background: #001e2b;
color: #f5f3ed;
padding: 70px 60px;
border-radius: 0;
text-align: center;
margin-top: 80px;
}
.cta-button {
display: inline-block;
background: transparent;
color: #f5f3ed;
border: 2px solid #f5f3ed;
padding: 16px 45px;
border-radius: 2px;
text-decoration: none;
font-weight: 400;
font-size: 1em;
margin-top: 25px;
transition: all 0.3s ease;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.cta-button:hover {
background: #f5f3ed;
color: #001e2b;
transform: none;
box-shadow: none;
}
.doc-ressource-list {
list-style: none;
padding: 0;
margin-left: 40px;
line-height: 2;
}
.doc-ressource-list li:before {
content: "—";
color: #5e17eb;
font-weight: 400;
font-size: 1.3em;
margin-right: 20px;
margin-top: 2px;
}
.highlight-box {
background: #5e17eb;
border-left: 3px solid #001e2b;
padding: 25px 30px;
margin: 30px 0;
border-radius: 0;
color: #001e2b;
}
.highlight-box strong {
color: #001e2b;
}
.green-horizontal {
width: 70px;
height: 1px;
background-color: #5e17eb;
position: relative;
flex-shrink: 0;
right: 90px;
}
.text-item .text-bottom {
font-weight: 700;
position: relative;
z-index: 1;
}
.text-item .text-item .text-bottom {
font-size: 44px;
}
.title-item h3 {
margin: 0;
}
.title-item {
display: flex;
align-items: center;
}
.footer {
text-align: center;
padding: 40px 20px;
color: #666;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Some files were not shown because too many files have changed in this diff Show More