[ADD] report_carbone, jsonifier, export_json : carbone is an alternative to Py3o
9
report_carbone/README.md
Normal 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
@@ -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>`__
|
||||
3
report_carbone/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
from .post_install import set_res_config_settings
|
||||
50
report_carbone/__manifest__.py
Normal 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
@@ -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",
|
||||
}
|
||||
2
report_carbone/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import main
|
||||
from . import config
|
||||
15
report_carbone/controllers/config.py
Normal 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}
|
||||
134
report_carbone/controllers/main.py
Normal 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)
|
||||
@@ -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>
|
||||
8
report_carbone/data/carbone_guide_attachment.xml
Normal 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>
|
||||
BIN
report_carbone/data/demo_template_purchase_order.odt
Normal file
BIN
report_carbone/data/placeholders/template.docx
Normal file
BIN
report_carbone/data/placeholders/template.pptx
Normal file
BIN
report_carbone/data/placeholders/template.xlsx
Normal file
293
report_carbone/docs/carbone_userguide_v18.adoc
Normal 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.
|
||||
17318
report_carbone/docs/carbone_userguide_v18.pdf
Normal file
855
report_carbone/i18n/fr.po
Normal 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é"
|
||||
3
report_carbone/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import base
|
||||
from . import res
|
||||
from . import carbone
|
||||
5
report_carbone/models/base/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import base
|
||||
from . import ir_model
|
||||
from . import ir_actions_report
|
||||
from . import ir_exports
|
||||
from . import exceptions
|
||||
45
report_carbone/models/base/base.py
Normal 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
|
||||
11
report_carbone/models/base/exceptions.py
Normal 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)
|
||||
884
report_carbone/models/base/ir_actions_report.py
Normal 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
|
||||
59
report_carbone/models/base/ir_exports.py
Normal 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
|
||||
12
report_carbone/models/base/ir_model.py
Normal 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}"
|
||||
5
report_carbone/models/carbone/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import carbone_print_by_action
|
||||
from . import carbone_field_extractor
|
||||
from . import carbone_create_report_wizard
|
||||
from . import carbone_translate
|
||||
from . import carbone_translate_line
|
||||
@@ -0,0 +1,72 @@
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class CarbonCreateReportWizard(models.TransientModel):
|
||||
_name = "carbone.create.report.wizard"
|
||||
_description = "Create Carbone report wizard helper"
|
||||
|
||||
report_type_extension = fields.Selection(
|
||||
string="Report extension",
|
||||
selection=[("docx", ".docx"), ("pptx", ".pptx"), ("xlsx", ".xlsx")],
|
||||
help="Leave empty if you already have a existing Carbone template for this report, then set the template ID",
|
||||
)
|
||||
|
||||
template_id = fields.Char(
|
||||
string="Carbone Template ID", help="Leave empty if you don't have any existing template for this report"
|
||||
)
|
||||
|
||||
input_user_model_id = fields.Many2one(
|
||||
"ir.model", string="Odoo model name", domain=[("transient", "=", False)], required=True
|
||||
)
|
||||
|
||||
action_name = fields.Char(string="Action name", required=True)
|
||||
|
||||
@api.constrains("template_id", "report_type_extension")
|
||||
def _check_template_xor_extension(self):
|
||||
"""Users are not allowed to enter both the extension of their report and a Carbone Template ID.
|
||||
This is because if the user enters an extension, it is to create a report from scratch, and we mock a call
|
||||
to Carbone to properly set up the studio.
|
||||
If the user enters a Carbone Template ID, we will rely exclusively on that, without making a mock call
|
||||
to Carbone.
|
||||
If the user enters a Carbone Template ID, we will rely exclusively on that, without making
|
||||
a mock call."""
|
||||
for record in self:
|
||||
has_template = bool(record.template_id)
|
||||
has_extension = bool(record.report_type_extension)
|
||||
|
||||
if has_template == has_extension:
|
||||
raise exceptions.ValidationError(_("You must specify either a template or an extension, but not both."))
|
||||
|
||||
def action_create_carbone_report(self):
|
||||
export_suffixe_name = self.input_user_model_id.display_name
|
||||
new_carbone_report = self.env["ir.actions.report"].create(
|
||||
{
|
||||
"name": self.action_name,
|
||||
"input_user_model_id": self.input_user_model_id.id,
|
||||
"model": self.input_user_model_id.model,
|
||||
"template_id": self.template_id,
|
||||
"report_type": "carbone",
|
||||
"file_extension": self.report_type_extension,
|
||||
"m2o_reference_id": self.env[self.input_user_model_id.model].search([], limit=1).id,
|
||||
}
|
||||
)
|
||||
export_model = new_carbone_report.retrieve_global_export_model(
|
||||
self.input_user_model_id.model, export_suffixe_name
|
||||
)
|
||||
new_carbone_report.export_model = export_model
|
||||
view_id = self.env.ref("report_carbone.act_report_carbone_view").id
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "ir.actions.report",
|
||||
"view_mode": "form",
|
||||
"view_id": view_id,
|
||||
"views": [(view_id, "form")],
|
||||
"res_id": new_carbone_report.id,
|
||||
"target": "current",
|
||||
"context": {
|
||||
"active_model": "ir.actions.report",
|
||||
"active_id": new_carbone_report.id,
|
||||
"active_ids": [new_carbone_report.id],
|
||||
"default_report_type": "carbone",
|
||||
},
|
||||
}
|
||||
196
report_carbone/models/carbone/carbone_field_extractor.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import xml.etree.ElementTree as ET # type: ignore # noqa: F401
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
FIELDS_RECURSION_LIMIT = 0
|
||||
EXCLUDED_FIELDS = [
|
||||
"id",
|
||||
"create_uid",
|
||||
"create_date",
|
||||
"write_uid",
|
||||
"write_date",
|
||||
"display_name",
|
||||
"message_ids",
|
||||
"activity_ids",
|
||||
"activity_user_id",
|
||||
"message_partner_ids",
|
||||
"activity_type_id",
|
||||
"default_user_id",
|
||||
]
|
||||
|
||||
|
||||
class CarboneFieldExtractor(models.Model):
|
||||
_name = "carbone.field.extractor"
|
||||
_description = "Field extractor for exports based on views"
|
||||
|
||||
@api.model
|
||||
def get_exportable_fields_from_view(self, model_name, view_type):
|
||||
"""
|
||||
Retrieves exportable fields from a view (priority to form/list)
|
||||
|
||||
:param model_name: Model name
|
||||
:param view_type: Type of view ('list', 'form', 'search')
|
||||
:return: List of exportable fields with their info
|
||||
"""
|
||||
target_model = self.env[model_name]
|
||||
|
||||
view_info = target_model.get_view(view_type=view_type)
|
||||
|
||||
root = ET.fromstring(view_info["arch"])
|
||||
|
||||
exportable_fields = []
|
||||
processed_fields = set()
|
||||
|
||||
for field_elem in root.iter("field"):
|
||||
field_name = field_elem.get("name")
|
||||
|
||||
if field_name and field_name not in processed_fields and field_name in target_model._fields:
|
||||
field_obj = target_model._fields[field_name]
|
||||
|
||||
if self._is_field_exportable(field_obj):
|
||||
fields_info = self._get_field_export_info(field_name, field_obj, field_elem)
|
||||
exportable_fields.extend(fields_info)
|
||||
processed_fields.add(field_name)
|
||||
|
||||
return exportable_fields
|
||||
|
||||
@api.model
|
||||
def _is_field_exportable(self, field_obj):
|
||||
if field_obj.name in EXCLUDED_FIELDS:
|
||||
return False
|
||||
|
||||
return getattr(field_obj, "exportable", True)
|
||||
|
||||
@api.model
|
||||
def _get_field_export_info(self, field_name, field_obj, field_elem):
|
||||
"""
|
||||
Retrieves export information for a field
|
||||
"""
|
||||
field_info = field_name
|
||||
sub_fields = []
|
||||
if field_obj.type in ["one2many", "many2many"]:
|
||||
sub_fields = self._extract_o2m_m2m_fields(field_elem, field_obj.comodel_name, field_name)
|
||||
sub_fields.extend([field_info])
|
||||
return sub_fields
|
||||
|
||||
@api.model
|
||||
def _get_sub_field_info(self, field_name, comodel_name):
|
||||
"""
|
||||
Retrieves the name of a subfield
|
||||
"""
|
||||
target_model = self.env[comodel_name]
|
||||
if field_name not in target_model._fields:
|
||||
return None
|
||||
|
||||
field_obj = target_model._fields[field_name]
|
||||
if not self._is_field_exportable(field_obj):
|
||||
return None
|
||||
|
||||
return field_name
|
||||
|
||||
@api.model
|
||||
def _get_inline_view_fields(self, field_elem, comodel_name):
|
||||
"""
|
||||
Recovers fields from inline views (list/form in the field XML)
|
||||
"""
|
||||
inline_fields = []
|
||||
|
||||
for child_elem in field_elem:
|
||||
if child_elem.tag in ("list", "form"):
|
||||
for sub_field_elem in child_elem.iter("field"):
|
||||
sub_field_name = sub_field_elem.get("name")
|
||||
if sub_field_name:
|
||||
field_info = self._get_sub_field_info(sub_field_name, comodel_name)
|
||||
if field_info:
|
||||
inline_fields.append(field_info)
|
||||
break # Prendre la première vue trouvée
|
||||
|
||||
return inline_fields
|
||||
|
||||
@api.model
|
||||
def _extract_o2m_m2m_fields(self, field_elem, comodel_name, parent_field_name):
|
||||
"""
|
||||
Extracts the O2M/M2M relationship fields from inline views
|
||||
|
||||
:param field_elem: XML element of the O2M/M2M field
|
||||
:param comodel_name: Target model name
|
||||
:param parent_field_name: Parent field name
|
||||
:return: List of subfields with their information
|
||||
"""
|
||||
sub_fields = []
|
||||
|
||||
inline_fields = self._get_inline_view_fields(field_elem, comodel_name)
|
||||
if not inline_fields:
|
||||
return sub_fields
|
||||
|
||||
sub_fields = [f"{parent_field_name}/{sub_field_info}" for sub_field_info in inline_fields]
|
||||
|
||||
return sub_fields
|
||||
|
||||
def clean_fields(self, fields):
|
||||
"""Allows you to remove occurrences from fields that are displayed with subfields
|
||||
(order_line/name, order_line/product_id, and order_line)..
|
||||
If 'order_line' is not removed, it is the only thing Odoo will display."""
|
||||
prefixes = {f.split("/")[0] for f in fields if "/" in f}
|
||||
result = [f for f in fields if not (f in prefixes and any(ff.startswith(f + "/") for ff in fields))]
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def get_fields_from_multiple_views(self, model_name, view_types=None):
|
||||
if view_types is None:
|
||||
view_types = ["list", "form"]
|
||||
|
||||
all_fields = []
|
||||
|
||||
for view_type in view_types:
|
||||
fields = self.get_exportable_fields_from_view(model_name, view_type)
|
||||
for field_info in fields:
|
||||
if field_info not in all_fields:
|
||||
all_fields.append(field_info)
|
||||
|
||||
clean_fields = self.clean_fields(all_fields)
|
||||
|
||||
if not clean_fields:
|
||||
return self.fallback_get_fields(model_name)
|
||||
return clean_fields
|
||||
|
||||
def update_current_global_export(
|
||||
self, export: "odoo.model.ir_exports", old_fields: list[str], newer_fields: list[str]
|
||||
):
|
||||
added = [field for field in old_fields if field not in newer_fields]
|
||||
removed = [field for field in newer_fields if field not in old_fields]
|
||||
|
||||
if removed:
|
||||
to_unlink = self.env["ir.exports.line"].search([("export_id", "=", export.id), ("name", "in", removed)])
|
||||
to_unlink.unlink()
|
||||
if added:
|
||||
vals_list = [{"name": name, "export_id": export.id} for name in added]
|
||||
self.env["ir.exports.line"].create(vals_list)
|
||||
|
||||
def fallback_get_fields(self, model_name: str, depth: int = FIELDS_RECURSION_LIMIT, prefix: str = "") -> list[str]:
|
||||
"""
|
||||
Used to retrieve all fields that can be exported from a model
|
||||
:param model_name: Model to export field
|
||||
:param depth: Maximum recursion number
|
||||
⚠ Be careful to not call it with an excessif number, field number grows exponentially
|
||||
(tested with purchase.order model with default depth, it gives 76 fields, in depth=1, it increases
|
||||
to 1800 fields.
|
||||
:param prefix: Must be left empty. Use for recursion call, to have a complete field's parent name.
|
||||
:return: Fields name to export
|
||||
"""
|
||||
object_model = self.env[model_name]
|
||||
result = []
|
||||
|
||||
fields = object_model.fields_get(
|
||||
attributes=["type", "required", "relation", "exportable"],
|
||||
)
|
||||
for field_name, field in fields.items():
|
||||
if field.get("exportable") and field_name not in EXCLUDED_FIELDS:
|
||||
full_name = f"{prefix}{field_name}" if not prefix else f"{prefix}/{field_name}"
|
||||
result.append(full_name)
|
||||
|
||||
if depth > 0 and field.get("type") in ("many2one", "many2many"):
|
||||
related_model = field.get("relation")
|
||||
nested_fields = self.fallback_get_fields(related_model, depth=depth - 1, prefix=full_name)
|
||||
result.extend(nested_fields)
|
||||
return result
|
||||
76
report_carbone/models/carbone/carbone_print_by_action.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import logging
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
from ..base.ir_actions_report import _tz_get
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CarbonReportPrintByAction(models.TransientModel):
|
||||
_name = "carbone.print_by_action"
|
||||
_description = "Print by action"
|
||||
|
||||
@api.model
|
||||
def _get_model(self):
|
||||
rep_obj = self.env["ir.actions.report"]
|
||||
report = rep_obj.browse(self.env.context["active_ids"])
|
||||
return report[0].model
|
||||
|
||||
@api.model
|
||||
def _get_id_record(self):
|
||||
id = self.env.context.get("record_id")
|
||||
return id
|
||||
|
||||
@api.model
|
||||
def _get_currency_id(self):
|
||||
id = self.env.context.get("currency_id")
|
||||
return id
|
||||
|
||||
name = fields.Text(string="Object Model", default=_get_model, readonly=True)
|
||||
id_object = fields.Integer(string="Object ID", default=_get_id_record)
|
||||
lang_id = fields.Many2one(
|
||||
"res.lang",
|
||||
string="Language",
|
||||
default=lambda self: self.env["res.lang"].search([("code", "=", self.env.user.lang)], limit=1),
|
||||
help="If this option is enabled, the language in which the report is printed",
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", string="Currency", default=_get_currency_id)
|
||||
tz = fields.Selection(_tz_get, string="Timezone", default=lambda self: self.env.user.tz or "UTC")
|
||||
|
||||
def to_print(self):
|
||||
rep_obj = self.env["ir.actions.report"]
|
||||
report = rep_obj.browse(self.env.context["active_id"])[0]
|
||||
ctx = dict(self.env.context)
|
||||
print_ids = self.env[self.name].browse(self.id_object)
|
||||
if not print_ids:
|
||||
raise exceptions.UserError(_("No record is retrieve with this id."))
|
||||
# report_pdf_no_attachment context key
|
||||
# forces the system not to retrieve an attachment saved in the database.
|
||||
ctx.update(
|
||||
{
|
||||
"active_id": print_ids[0],
|
||||
"active_ids": print_ids.ids,
|
||||
"active_model": report.model,
|
||||
"lang": self.lang_id.code,
|
||||
"tz": self.tz,
|
||||
"currency_id": self.currency_id.id,
|
||||
"from_print_by_action": True,
|
||||
"report_pdf_no_attachment": True,
|
||||
}
|
||||
)
|
||||
data = {
|
||||
"model": report.model,
|
||||
"id": print_ids[0],
|
||||
"ids": print_ids,
|
||||
"report_type": "carbone",
|
||||
}
|
||||
res = {
|
||||
"type": "ir.actions.report",
|
||||
"report_name": report.report_name,
|
||||
"report_type": report.report_type,
|
||||
"datas": data,
|
||||
"context": ctx,
|
||||
"target": "current",
|
||||
}
|
||||
return res
|
||||
67
report_carbone/models/carbone/carbone_translate.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CarboneTranslate(models.Model):
|
||||
_name = "carbone.translate"
|
||||
_description = "Carbone Translate"
|
||||
_rec_name = "ir_actions_report_id"
|
||||
|
||||
ir_actions_report_id = fields.Many2one(
|
||||
"ir.actions.report", string="Carbone Report", required=True, ondelete="cascade"
|
||||
)
|
||||
lang_id = fields.Many2one(
|
||||
"res.lang",
|
||||
string="Language",
|
||||
required=True,
|
||||
)
|
||||
carbone_translate_line_ids = fields.One2many(
|
||||
"carbone.translate.line", "carbone_translate_id", string="Translation lines"
|
||||
)
|
||||
_sql_constraints = [
|
||||
(
|
||||
"lang_report_uniq",
|
||||
"UNIQUE(ir_actions_report_id, lang_id)",
|
||||
"A report cannot have two translations for the same language.",
|
||||
)
|
||||
]
|
||||
|
||||
def _create_translation_lines(
|
||||
self,
|
||||
carbone_translate_to_maj: "odoo.model.carbone_translate",
|
||||
translation_lines: "odoo.model.carbone_translate_line",
|
||||
):
|
||||
existing_sources = carbone_translate_to_maj.mapped("carbone_translate_line_ids.source")
|
||||
lines = [
|
||||
{
|
||||
"source": current_translate_line.source,
|
||||
"value": "",
|
||||
"carbone_translate_id": carbone_translate_to_maj.id,
|
||||
}
|
||||
for current_translate_line in translation_lines
|
||||
if current_translate_line.source not in existing_sources
|
||||
]
|
||||
self.env["carbone.translate.line"].create(lines)
|
||||
|
||||
def button_create_update_copy_of_translate(self):
|
||||
self.ensure_one()
|
||||
create_vals = []
|
||||
|
||||
report_translate_langs = self.ir_actions_report_id.carbone_translate_ids.mapped("lang_id")
|
||||
available_languages = self.ir_actions_report_id.lang_ids
|
||||
# If there is no language at all, we will create one, with the right keys.
|
||||
for lang in available_languages:
|
||||
if lang not in report_translate_langs:
|
||||
vals = {"lang_id": lang.id, "ir_actions_report_id": self.ir_actions_report_id.id}
|
||||
lines = [
|
||||
(0, 0, {"source": translate_line.source, "value": ""})
|
||||
for translate_line in self.carbone_translate_line_ids
|
||||
]
|
||||
vals.update({"carbone_translate_line_ids": lines})
|
||||
create_vals.append(vals)
|
||||
else:
|
||||
carbone_translate_to_maj = self.ir_actions_report_id.carbone_translate_ids.filtered_domain(
|
||||
[("lang_id", "=", lang.id)]
|
||||
)
|
||||
self._create_translation_lines(carbone_translate_to_maj, self.carbone_translate_line_ids)
|
||||
if create_vals:
|
||||
self.env["carbone.translate"].create(create_vals)
|
||||
27
report_carbone/models/carbone/carbone_translate_line.py
Normal file
@@ -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)
|
||||
1
report_carbone/models/res/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import res_config_settings
|
||||
30
report_carbone/models/res/res_config_settings.py
Normal 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()
|
||||
8
report_carbone/post_install.py
Normal 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)
|
||||
9
report_carbone/pyproject.toml
Normal 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"
|
||||
]
|
||||
17
report_carbone/security/groups.xml
Normal 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>
|
||||
8
report_carbone/security/ir.model.access.csv
Normal 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
|
||||
|
297
report_carbone/static/description/assets/description.css
Normal 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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 892 KiB |
|
After Width: | Height: | Size: 799 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
BIN
report_carbone/static/description/assets/gifs/pop-up-worflow.gif
Normal file
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 140 KiB |
BIN
report_carbone/static/description/assets/logo/icon-use-case.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 57 KiB |
BIN
report_carbone/static/description/assets/logo/mangono-logo.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
report_carbone/static/description/assets/logo/mangono_logo.jpg
Normal file
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 119 KiB |