diff --git a/report_carbone/README.rst b/report_carbone/README.rst index 40ebc85..057978c 100644 --- a/report_carbone/README.rst +++ b/report_carbone/README.rst @@ -11,11 +11,8 @@ 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 % +More information on Odoo App Store : +`report_carbone `__ Company @@ -29,7 +26,7 @@ General Public License, Version 3 (LGPL v3). Credits ------- -Developer: (V18) Mangono , Contact: contact@mangono.fr +Developer: Mangono Contacts -------- diff --git a/report_carbone/__manifest__.py b/report_carbone/__manifest__.py index 9542cae..d5e78cd 100644 --- a/report_carbone/__manifest__.py +++ b/report_carbone/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Report Generator | Carbone.io", - "version": "18.0.1.0.8", + "version": "18.0.1.1.4", "author": "Mangono", "maintainer": "Mangono", "summary": """ @@ -15,11 +15,13 @@ "category": "Reporting", "depends": ["base", "base_setup", "web", "export_json"], "description": "Report Generator | Carbone.io", - "website": "https://carbone.io/integration/odoo.html", + # Page produit Mangono plutôt que page de l'éditeur : c'est le lien que la + # place de marché affiche, et une visite acquise sur notre module a plus de + # valeur chez nous. La page renvoie vers carbone.io/integration/odoo.html. + "website": "https://mangono.fr/connecteurs-odoo/carbone.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", diff --git a/report_carbone/controllers/main.py b/report_carbone/controllers/main.py index 01b4e2e..8fae765 100644 --- a/report_carbone/controllers/main.py +++ b/report_carbone/controllers/main.py @@ -90,7 +90,11 @@ class CarboneReportController(ReportController): carbone_report = request.env["ir.actions.report"]._get_report_from_name(reportname) self.check_carbone_report(carbone_report) - report_content, extension, filename = request.env["ir.actions.report"]._render_carbone(carbone_report, docids) + report_content, filename, extension = request.env["ir.actions.report"]._render_carbone_full( + carbone_report, docids + ) + if not filename: + filename = f"{carbone_report.report_name}.{extension}" headers = _get_headers(extension, report_content, filename) return request.make_response(report_content, headers) @@ -129,5 +133,5 @@ class CarboneReportController(ReportController): response = self._call_carbone_converter(docids, reportname, context, url) return response - except Exception as e: + except Exception as e: # noqa: BLE001 self.handle_exception_error(e, reportname) diff --git a/report_carbone/data/carbone_guide_attachment.xml b/report_carbone/data/carbone_guide_attachment.xml deleted file mode 100644 index a8f2e31..0000000 --- a/report_carbone/data/carbone_guide_attachment.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - Carbone_report_guide.pdf - - True - - diff --git a/report_carbone/docs/carbone_userguide_v18.adoc b/report_carbone/docs/carbone_userguide_v18.adoc index ddfaef7..579b4dd 100644 --- a/report_carbone/docs/carbone_userguide_v18.adoc +++ b/report_carbone/docs/carbone_userguide_v18.adoc @@ -284,6 +284,10 @@ 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. +== Nested template + +If you are using Carbone's appendTemplate feature, to access your variables in complement ({c.}) instead of data ({d.}), you must check the “Use complement datasâ€� box in the Advanced Properties tab. + == 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". diff --git a/report_carbone/docs/carbone_userguide_v18.pdf b/report_carbone/docs/carbone_userguide_v18.pdf index 0983731..9f857ce 100644 --- a/report_carbone/docs/carbone_userguide_v18.pdf +++ b/report_carbone/docs/carbone_userguide_v18.pdf @@ -3,17 +3,17 @@ 1 0 obj << /Title (Guide to creating a Carbone report in Odoo) /Creator -/Producer (Asciidoctor PDF 2.3.19, based on Prawn 2.4.0) -/ModDate (D:20260205101921+01'00') -/CreationDate (D:20260205123421+01'00') +/Producer (Asciidoctor PDF 2.3.23, based on Prawn 2.4.0) +/ModDate (D:20260427142746+02'00') +/CreationDate (D:20260427142749+02'00') >> endobj 2 0 obj << /Type /Catalog /Pages 3 0 R /Names 10 0 R -/Outlines 121 0 R -/PageLabels 134 0 R +/Outlines 124 0 R +/PageLabels 138 0 R /PageMode /UseOutlines /OpenAction [7 0 R /FitH 841.89] /ViewerPreferences << /DisplayDocTitle true @@ -48,7 +48,7 @@ endobj >> endobj 6 0 obj -<< /Length 11327 +<< /Length 12094 >> stream q @@ -78,14 +78,14 @@ ET 0.0 0.0 0.0 scn q -150.0 0.0 0.0 150.0 222.64 329.33 cm +150.0 0.0 0.0 150.0 222.64 310.85 cm /I1 Do Q 0.2 0.2 0.2 scn 0.2 0.2 0.2 SCN BT -48.24 289.034 Td +48.24 270.554 Td /F2.0 22 Tf <4372656174696e6720616e206578706f72742074656d706c617465> Tj ET @@ -95,8 +95,8 @@ ET q 0.5 w 0.93333 0.93333 0.93333 SCN -102.25929 271.81 m -102.25929 169.13 l +102.25929 253.33 m +102.25929 150.65 l S Q 0.2 0.2 0.2 scn @@ -107,7 +107,7 @@ Q 0.2 0.2 0.2 SCN BT -60.24 216.396 Td +60.24 197.916 Td /F2.0 10.5 Tf [<4e4f> 20.01953 <5445>] TJ ET @@ -120,7 +120,7 @@ ET 2.22872 Tw BT -114.25929 255.846 Td +114.25929 237.366 Td /F1.0 10.5 Tf [<546869732073656374696f6e206973206e6f74206d616e6461746f727920666f722073657474696e67207570206120436172626f6e65207265706f72742c2073696e63652062> 20.01953 <792064656661756c742c>] TJ ET @@ -135,7 +135,7 @@ ET 3.71163 Tw BT -114.25929 240.066 Td +114.25929 221.586 Td /F1.0 10.5 Tf <7768656e206372656174696e672061207265706f727420666f72207468652066697273742074696d65207769746820616e204f646f6f2058206d6f64656c202873616c652e6f726465722c> Tj ET @@ -150,7 +150,7 @@ ET 1.74156 Tw BT -114.25929 224.286 Td +114.25929 205.806 Td /F1.0 10.5 Tf <70757263686173652e6f726465722c206574632e292c20612022476c6f62616c22206578706f7274206973206372656174656420666f7220746865206d6f64656c20696e207175657374696f6e2e2054686973> Tj ET @@ -165,7 +165,7 @@ ET 0.09335 Tw BT -114.25929 208.506 Td +114.25929 190.026 Td /F1.0 10.5 Tf <6578706f7274206d6f64656c2072657472696576657320616c6c206669656c64732076697369626c6520696e20746865206c69737420616e6420666f726d207669657773206f6620746865204f646f6f206d6f64656c> Tj ET @@ -180,7 +180,7 @@ ET 2.35715 Tw BT -114.25929 192.726 Td +114.25929 174.246 Td /F1.0 10.5 Tf [<696e207175657374696f6e2e20486f77657665722c206974206d61> 20.01953 <79206265206e656365737361727920746f2063726561746520796f7572206f776e206578706f7274206d6f64656c20696e>] TJ ET @@ -193,7 +193,7 @@ ET 0.2 0.2 0.2 SCN BT -114.25929 176.946 Td +114.25929 158.466 Td /F1.0 10.5 Tf [<6f7264657220746f207265747269657665206f74686572206669656c6473206e6f7420646973706c61> 20.01953 <79656420696e2074686520696e746572666163652e>] TJ ET @@ -204,7 +204,7 @@ ET 0.2 0.2 0.2 SCN BT -48.24 145.166 Td +48.24 126.686 Td /F1.0 10.5 Tf [<46> 40.03906 <6f722074686973206578616d706c652c20776520617373756d6520746861742077652077616e7420746f206372656174652061207265706f7274206261736564206f6e20646174612066726f6d2061207075726368617365206f726465722e>] TJ ET @@ -215,7 +215,7 @@ ET 0.2 0.2 0.2 SCN BT -48.24 117.386 Td +48.24 98.906 Td /F1.0 10.5 Tf [<59> 69.82422 <6f75206e65656420746f20676f20746f20746865206c6973742076696577206f6620746865206465736972656420756e6976657273652c20696e2074686973206361736520225075726368617365222e>] TJ ET @@ -226,7 +226,7 @@ ET 0.2 0.2 0.2 SCN BT -48.24 89.606 Td +48.24 71.126 Td /F1.0 10.5 Tf [<53656c65637420616e> 20.01953 <79207265636f72642c207468656e20636c69636b206f6e2022> 80.07812 <41> 20.01953 <6374696f6e732220616e64207468656e20224578706f7274222e>] TJ ET @@ -660,7 +660,7 @@ ET BT 48.24 496.496 Td /F1.0 10.5 Tf -<557365722067726f75707320616e6420726967687473> Tj +<4e65737465642074656d706c617465> Tj ET 0.0 0.0 0.0 SCN @@ -669,9 +669,9 @@ ET 0.66275 0.66275 0.66275 SCN BT -165.85062 496.496 Td +133.78362 496.496 Td /F1.0 10.5 Tf -<2e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e20> Tj +<2e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e20> Tj ET 0.0 0.0 0.0 SCN @@ -692,6 +692,51 @@ BT <3138> Tj ET +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn + +BT +48.24 478.016 Td +ET + +0.2 0.2 0.2 scn +0.2 0.2 0.2 SCN + +BT +48.24 478.016 Td +/F1.0 10.5 Tf +<557365722067726f75707320616e6420726967687473> Tj +ET + +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn +0.66275 0.66275 0.66275 scn +0.66275 0.66275 0.66275 SCN + +BT +165.85062 478.016 Td +/F1.0 10.5 Tf +<2e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e202e20> Tj +ET + +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn + +BT +534.62112 478.016 Td +/F1.0 2.625 Tf + Tj +ET + +0.2 0.2 0.2 scn +0.2 0.2 0.2 SCN + +BT +535.301 478.016 Td +/F1.0 10.5 Tf +<3138> Tj +ET + 0.0 0.0 0.0 SCN 0.0 0.0 0.0 scn q @@ -732,21 +777,21 @@ endobj /F1.0 14 0 R >> /XObject << /I1 12 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> >> -/Annots [101 0 R 102 0 R 103 0 R 104 0 R 105 0 R 106 0 R 107 0 R 108 0 R 109 0 R 110 0 R 111 0 R 112 0 R 113 0 R 114 0 R 115 0 R 116 0 R 117 0 R 118 0 R 119 0 R 120 0 R] +/Annots [102 0 R 103 0 R 104 0 R 105 0 R 106 0 R 107 0 R 108 0 R 109 0 R 110 0 R 111 0 R 112 0 R 113 0 R 114 0 R 115 0 R 116 0 R 117 0 R 118 0 R 119 0 R 120 0 R 121 0 R 122 0 R 123 0 R] >> endobj 8 0 obj << /Type /Font /BaseFont /736ee2+NotoSerif-Bold /Subtype /TrueType -/FontDescriptor 139 0 R +/FontDescriptor 143 0 R /FirstChar 32 /LastChar 255 -/Widths 141 0 R -/ToUnicode 140 0 R +/Widths 145 0 R +/ToUnicode 144 0 R >> endobj 9 0 obj @@ -758,7 +803,7 @@ endobj >> endobj 11 0 obj -<< /Names [(__anchor-top) 135 0 R (_add_the_document_to_carbone) 47 0 R (_creating_a_new_odoo_report) 25 0 R (_creating_an_export_template) 13 0 R (_creation_of_the_document_odt_docx_etc) 39 0 R (_currency_management) 73 0 R (_edit_a_template) 66 0 R (_language_and_translation_management) 76 0 R (_languages) 92 0 R (_translations) 77 0 R (_user_groups_and_rights) 100 0 R (toc) 9 0 R] +<< /Names [(__anchor-top) 139 0 R (_add_the_document_to_carbone) 47 0 R (_creating_a_new_odoo_report) 25 0 R (_creating_an_export_template) 13 0 R (_creation_of_the_document_odt_docx_etc) 39 0 R (_currency_management) 73 0 R (_edit_a_template) 66 0 R (_language_and_translation_management) 76 0 R (_languages) 92 0 R (_nested_template) 100 0 R (_translations) 77 0 R (_user_groups_and_rights) 101 0 R (toc) 9 0 R] >> endobj 12 0 obj @@ -1183,17 +1228,17 @@ V endstream endobj 13 0 obj -[7 0 R /XYZ 0 317.33 null] +[7 0 R /XYZ 0 298.85 null] endobj 14 0 obj << /Type /Font /BaseFont /2c3dcc+NotoSerif /Subtype /TrueType -/FontDescriptor 143 0 R +/FontDescriptor 147 0 R /FirstChar 32 /LastChar 255 -/Widths 145 0 R -/ToUnicode 144 0 R +/Widths 149 0 R +/ToUnicode 148 0 R >> endobj 15 0 obj @@ -1632,7 +1677,7 @@ endobj /Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /XObject << /I2 15 0 R /I3 18 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> /Font << /F1.0 14 0 R >> @@ -2645,7 +2690,7 @@ endobj /Contents 20 0 R /Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /XObject << /I4 19 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> /Font << /F1.0 14 0 R >> @@ -3162,7 +3207,7 @@ endobj /XObject << /I5 22 0 R /I6 26 0 R /I7 27 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> /Font << /F2.0 8 0 R /F1.0 14 0 R @@ -3712,7 +3757,7 @@ endobj >> /XObject << /I8 30 0 R /I9 31 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> >> >> @@ -5079,7 +5124,7 @@ endobj /Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /XObject << /I10 32 0 R /I11 35 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> /Font << /F1.0 14 0 R /F2.0 8 0 R @@ -5526,7 +5571,7 @@ endobj >> /XObject << /I12 38 0 R /I13 40 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> >> >> @@ -7036,7 +7081,7 @@ endobj /XObject << /I14 41 0 R /I15 45 0 R /I16 46 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> /Font << /F1.0 14 0 R /F2.0 8 0 R @@ -8250,7 +8295,7 @@ endobj /XObject << /I17 50 0 R /I18 52 0 R /I19 53 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> >> >> @@ -9269,7 +9314,7 @@ endobj /I21 57 0 R /I22 58 0 R /I23 60 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> >> >> @@ -10887,7 +10932,7 @@ endobj /XObject << /I24 61 0 R /I25 64 0 R /I26 65 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> /Font << /F1.0 14 0 R /F2.0 8 0 R @@ -11446,7 +11491,7 @@ endobj /I28 70 0 R /I29 71 0 R /I30 72 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> /Font << /F1.0 14 0 R /F2.0 8 0 R @@ -12235,7 +12280,7 @@ endobj >> /XObject << /I31 78 0 R /I32 79 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> >> >> @@ -12813,7 +12858,7 @@ endobj /F2.0 8 0 R >> /XObject << /I33 82 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> >> >> @@ -13500,7 +13545,7 @@ endobj /Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /XObject << /I34 85 0 R /I35 87 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> /Font << /F3.0 86 0 R /F1.0 14 0 R @@ -13909,11 +13954,11 @@ endobj << /Type /Font /BaseFont /4617f2+NotoSerif-Italic /Subtype /TrueType -/FontDescriptor 147 0 R +/FontDescriptor 151 0 R /FirstChar 32 /LastChar 255 -/Widths 149 0 R -/ToUnicode 148 0 R +/Widths 153 0 R +/ToUnicode 152 0 R >> endobj 87 0 obj @@ -14556,7 +14601,7 @@ endobj /Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /XObject << /I36 90 0 R /I37 91 0 R -/Stamp2 137 0 R +/Stamp2 141 0 R >> /Font << /F2.0 8 0 R /F1.0 14 0 R @@ -15525,11 +15570,11 @@ endobj << /Type /Font /BaseFont /af3d9b+mplus1mn-regular /Subtype /TrueType -/FontDescriptor 151 0 R +/FontDescriptor 155 0 R /FirstChar 32 /LastChar 255 -/Widths 153 0 R -/ToUnicode 152 0 R +/Widths 157 0 R +/ToUnicode 156 0 R >> endobj 94 0 obj @@ -15776,7 +15821,7 @@ endobj /Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] /XObject << /I38 94 0 R /I39 97 0 R -/Stamp1 136 0 R +/Stamp1 140 0 R >> /Font << /F1.0 14 0 R >> @@ -16294,7 +16339,7 @@ DG endstream endobj 98 0 obj -<< /Length 3139 +<< /Length 4063 >> stream q @@ -16411,6 +16456,58 @@ ET BT 48.24 718.254 Td /F2.0 22 Tf +<4e65737465642074656d706c617465> Tj +ET + +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn +0.2 0.2 0.2 scn +0.2 0.2 0.2 SCN + +1.02702 Tw + +BT +48.24 689.066 Td +/F1.0 10.5 Tf +[<496620796f7520617265207573696e6720436172626f6e65d57320617070656e6454> 29.78516 <656d706c61746520666561747572652c20746f2061636365737320796f7572207661726961626c657320696e20636f6d706c656d656e7420287b632e7d29>] TJ +ET + + +0.0 Tw +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn +0.2 0.2 0.2 scn +0.2 0.2 0.2 SCN + +0.72715 Tw + +BT +48.24 673.286 Td +/F1.0 10.5 Tf +[<696e7374656164206f66206461746120287b642e7d292c20796f75206d75737420636865636b2074686520d255736520636f6d706c656d656e74206461746173d320626f7820696e207468652041> 20.01953 <6476616e6365642050726f70657274696573>] TJ +ET + + +0.0 Tw +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn +0.2 0.2 0.2 scn +0.2 0.2 0.2 SCN + +BT +48.24 657.506 Td +/F1.0 10.5 Tf +<7461622e> Tj +ET + +0.0 0.0 0.0 SCN +0.0 0.0 0.0 scn +0.2 0.2 0.2 scn +0.2 0.2 0.2 SCN + +BT +48.24 613.394 Td +/F2.0 22 Tf <557365722067726f75707320616e6420726967687473> Tj ET @@ -16422,7 +16519,7 @@ ET 1.76498 Tw BT -48.24 689.066 Td +48.24 584.206 Td /F1.0 10.5 Tf [<59> 69.82422 <6f752063616e20646566696e6520776869636820757365722873292077696c6c2062652061626c6520746f2076696577206f72206564697420436172626f6e65205265706f727473207669612074776f206e65772067726f7570733a>] TJ ET @@ -16435,7 +16532,7 @@ ET 0.2 0.2 0.2 SCN BT -48.24 673.286 Td +48.24 568.426 Td /F1.0 10.5 Tf <22436172626f6e65205265706f7274202d204d616e616765722220616e642022436172626f6e65205265706f7274202d20566965776572222e> Tj ET @@ -16448,7 +16545,7 @@ ET 1.80419 Tw BT -48.24 645.506 Td +48.24 540.646 Td /F1.0 10.5 Tf <54686520224d616e61676572222067726f757020616c6c6f777320757365727320746f206d6f6469667920436172626f6e65207265706f72747320616e6420616363657373207468652022436172626f6e65205265706f72747322> Tj ET @@ -16461,7 +16558,7 @@ ET 0.2 0.2 0.2 SCN BT -48.24 629.726 Td +48.24 524.866 Td /F1.0 10.5 Tf <6d656e752e> Tj ET @@ -16474,7 +16571,7 @@ ET 0.15582 Tw BT -48.24 601.946 Td +48.24 497.086 Td /F1.0 10.5 Tf <5468652022566965776572222067726f757020616c6c6f777320757365727320746f2061636365737320436172626f6e65207265706f72747320696e20726561642d6f6e6c79206d6f64653b206e6f206d6f64696669636174696f6e7320617265> Tj ET @@ -16487,7 +16584,7 @@ ET 0.2 0.2 0.2 SCN BT -48.24 586.166 Td +48.24 481.306 Td /F1.0 10.5 Tf <706f737369626c652e> Tj ET @@ -16532,7 +16629,7 @@ endobj /F4.0 93 0 R /F2.0 8 0 R >> -/XObject << /Stamp2 137 0 R +/XObject << /Stamp2 141 0 R >> >> >> @@ -16541,6 +16638,9 @@ endobj [99 0 R /XYZ 0 746.55 null] endobj 101 0 obj +[99 0 R /XYZ 0 641.69 null] +endobj +102 0 obj << /Border [0 0 0] /Dest (_creating_an_export_template) /Subtype /Link @@ -16548,7 +16648,7 @@ endobj /Type /Annot >> endobj -102 0 obj +103 0 obj << /Border [0 0 0] /Dest (_creating_an_export_template) /Subtype /Link @@ -16556,7 +16656,7 @@ endobj /Type /Annot >> endobj -103 0 obj +104 0 obj << /Border [0 0 0] /Dest (_creating_a_new_odoo_report) /Subtype /Link @@ -16564,7 +16664,7 @@ endobj /Type /Annot >> endobj -104 0 obj +105 0 obj << /Border [0 0 0] /Dest (_creating_a_new_odoo_report) /Subtype /Link @@ -16572,7 +16672,7 @@ endobj /Type /Annot >> endobj -105 0 obj +106 0 obj << /Border [0 0 0] /Dest (_creation_of_the_document_odt_docx_etc) /Subtype /Link @@ -16580,7 +16680,7 @@ endobj /Type /Annot >> endobj -106 0 obj +107 0 obj << /Border [0 0 0] /Dest (_creation_of_the_document_odt_docx_etc) /Subtype /Link @@ -16588,7 +16688,7 @@ endobj /Type /Annot >> endobj -107 0 obj +108 0 obj << /Border [0 0 0] /Dest (_add_the_document_to_carbone) /Subtype /Link @@ -16596,7 +16696,7 @@ endobj /Type /Annot >> endobj -108 0 obj +109 0 obj << /Border [0 0 0] /Dest (_add_the_document_to_carbone) /Subtype /Link @@ -16604,7 +16704,7 @@ endobj /Type /Annot >> endobj -109 0 obj +110 0 obj << /Border [0 0 0] /Dest (_edit_a_template) /Subtype /Link @@ -16612,7 +16712,7 @@ endobj /Type /Annot >> endobj -110 0 obj +111 0 obj << /Border [0 0 0] /Dest (_edit_a_template) /Subtype /Link @@ -16620,7 +16720,7 @@ endobj /Type /Annot >> endobj -111 0 obj +112 0 obj << /Border [0 0 0] /Dest (_currency_management) /Subtype /Link @@ -16628,7 +16728,7 @@ endobj /Type /Annot >> endobj -112 0 obj +113 0 obj << /Border [0 0 0] /Dest (_currency_management) /Subtype /Link @@ -16636,7 +16736,7 @@ endobj /Type /Annot >> endobj -113 0 obj +114 0 obj << /Border [0 0 0] /Dest (_language_and_translation_management) /Subtype /Link @@ -16644,7 +16744,7 @@ endobj /Type /Annot >> endobj -114 0 obj +115 0 obj << /Border [0 0 0] /Dest (_language_and_translation_management) /Subtype /Link @@ -16652,7 +16752,7 @@ endobj /Type /Annot >> endobj -115 0 obj +116 0 obj << /Border [0 0 0] /Dest (_translations) /Subtype /Link @@ -16660,7 +16760,7 @@ endobj /Type /Annot >> endobj -116 0 obj +117 0 obj << /Border [0 0 0] /Dest (_translations) /Subtype /Link @@ -16668,7 +16768,7 @@ endobj /Type /Annot >> endobj -117 0 obj +118 0 obj << /Border [0 0 0] /Dest (_languages) /Subtype /Link @@ -16676,7 +16776,7 @@ endobj /Type /Annot >> endobj -118 0 obj +119 0 obj << /Border [0 0 0] /Dest (_languages) /Subtype /Link @@ -16684,136 +16784,161 @@ endobj /Type /Annot >> endobj -119 0 obj +120 0 obj << /Border [0 0 0] -/Dest (_user_groups_and_rights) +/Dest (_nested_template) /Subtype /Link -/Rect [48.24 493.43 161.8395 507.71] +/Rect [48.24 493.43 129.2055 507.71] /Type /Annot >> endobj -120 0 obj +121 0 obj << /Border [0 0 0] -/Dest (_user_groups_and_rights) +/Dest (_nested_template) /Subtype /Link /Rect [535.301 493.43 547.04 507.71] /Type /Annot >> endobj -121 0 obj -<< /Type /Outlines -/Count 12 -/First 122 0 R -/Last 133 0 R ->> -endobj 122 0 obj -<< /Title -/Parent 121 0 R -/Count 0 -/Next 123 0 R -/Dest [7 0 R /XYZ 0 841.89 null] +<< /Border [0 0 0] +/Dest (_user_groups_and_rights) +/Subtype /Link +/Rect [48.24 474.95 161.8395 489.23] +/Type /Annot >> endobj 123 0 obj -<< /Title -/Parent 121 0 R -/Count 0 -/Next 124 0 R -/Prev 122 0 R -/Dest [7 0 R /XYZ 0 841.89 null] +<< /Border [0 0 0] +/Dest (_user_groups_and_rights) +/Subtype /Link +/Rect [535.301 474.95 547.04 489.23] +/Type /Annot >> endobj 124 0 obj -<< /Title -/Parent 121 0 R -/Count 0 -/Next 125 0 R -/Prev 123 0 R -/Dest [7 0 R /XYZ 0 317.33 null] +<< /Type /Outlines +/Count 13 +/First 125 0 R +/Last 137 0 R >> endobj 125 0 obj -<< /Title -/Parent 121 0 R +<< /Title +/Parent 124 0 R /Count 0 /Next 126 0 R -/Prev 124 0 R -/Dest [24 0 R /XYZ 0 538.06216 null] +/Dest [7 0 R /XYZ 0 841.89 null] >> endobj 126 0 obj -<< /Title -/Parent 121 0 R +<< /Title +/Parent 124 0 R /Count 0 /Next 127 0 R /Prev 125 0 R -/Dest [37 0 R /XYZ 0 575.33351 null] +/Dest [7 0 R /XYZ 0 841.89 null] >> endobj 127 0 obj -<< /Title -/Parent 121 0 R +<< /Title +/Parent 124 0 R /Count 0 /Next 128 0 R /Prev 126 0 R -/Dest [44 0 R /XYZ 0 133.76742 null] +/Dest [7 0 R /XYZ 0 298.85 null] >> endobj 128 0 obj -<< /Title -/Parent 121 0 R +<< /Title +/Parent 124 0 R /Count 0 /Next 129 0 R /Prev 127 0 R -/Dest [63 0 R /XYZ 0 216.43245 null] +/Dest [24 0 R /XYZ 0 538.06216 null] >> endobj 129 0 obj -<< /Title -/Parent 121 0 R +<< /Title +/Parent 124 0 R /Count 0 /Next 130 0 R /Prev 128 0 R -/Dest [69 0 R /XYZ 0 131.61034 null] +/Dest [37 0 R /XYZ 0 575.33351 null] >> endobj 130 0 obj -<< /Title -/Parent 121 0 R -/Count 2 -/First 131 0 R -/Last 132 0 R -/Next 133 0 R +<< /Title +/Parent 124 0 R +/Count 0 +/Next 131 0 R /Prev 129 0 R -/Dest [75 0 R /XYZ 0 762.33 null] +/Dest [44 0 R /XYZ 0 133.76742 null] >> endobj 131 0 obj -<< /Title -/Parent 130 0 R +<< /Title +/Parent 124 0 R /Count 0 /Next 132 0 R -/Dest [75 0 R /XYZ 0 716.81 null] +/Prev 130 0 R +/Dest [63 0 R /XYZ 0 216.43245 null] >> endobj 132 0 obj -<< /Title -/Parent 130 0 R +<< /Title +/Parent 124 0 R /Count 0 +/Next 133 0 R /Prev 131 0 R -/Dest [89 0 R /XYZ 0 459.08649 null] +/Dest [69 0 R /XYZ 0 131.61034 null] >> endobj 133 0 obj -<< /Title -/Parent 121 0 R -/Count 0 -/Prev 130 0 R -/Dest [99 0 R /XYZ 0 746.55 null] +<< /Title +/Parent 124 0 R +/Count 2 +/First 134 0 R +/Last 135 0 R +/Next 136 0 R +/Prev 132 0 R +/Dest [75 0 R /XYZ 0 762.33 null] >> endobj 134 0 obj +<< /Title +/Parent 133 0 R +/Count 0 +/Next 135 0 R +/Dest [75 0 R /XYZ 0 716.81 null] +>> +endobj +135 0 obj +<< /Title +/Parent 133 0 R +/Count 0 +/Prev 134 0 R +/Dest [89 0 R /XYZ 0 459.08649 null] +>> +endobj +136 0 obj +<< /Title +/Parent 124 0 R +/Count 0 +/Next 137 0 R +/Prev 133 0 R +/Dest [99 0 R /XYZ 0 746.55 null] +>> +endobj +137 0 obj +<< /Title +/Parent 124 0 R +/Count 0 +/Prev 136 0 R +/Dest [99 0 R /XYZ 0 641.69 null] +>> +endobj +138 0 obj << /Nums [0 << /P (1) >> 1 << /P (2) >> 2 << /P (3) @@ -16835,10 +16960,10 @@ endobj >>] >> endobj -135 0 obj +139 0 obj [7 0 R /XYZ 0 841.89 null] endobj -136 0 obj +140 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 595.28 841.89] @@ -16866,7 +16991,7 @@ Q endstream endobj -137 0 obj +141 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 595.28 841.89] @@ -16894,7 +17019,7 @@ Q endstream endobj -138 0 obj +142 0 obj << /Length1 12108 /Length 7660 /Filter [/FlateDecode] @@ -16936,10 +17061,10 @@ Z ò˜Þ~�µž>àlƒÑ!h,œ°ýü#p›‚±>´Þ“|ÿÐ *?€¼ o裀›¡]M»"ãÏ÷öñ´ PÉL,ØdËJW�çõöB §ÆøyfÐN MñÒ®îƒÖþû9�¼ŸˆçˆV=€ya¬ž!^—1^F(æÃµü„ÆÂ×õx~W²o�äPL²…�Px*¡¾[ѵüm®+–Y@×)àµî^kLò@WX7û A Í‚.Äï=¼5ÄË�<ˆ�ñl¤OûWâ0ïaÀ'ù,"vÎìã£H¬ãy»Á³›àÝÎÏ:¹Fò¦5ò�rµ(ä½ú£™�‡ø ­B1ý¢2½üS«¢›�{7À$ ëx]»y=¶ÔƒšAë^x¼ÖK<ÛoA.~l;PXX“í@uò#Zx8Ú×ȯ¼6ÔïVè!> endobj -140 0 obj +144 0 obj << /Length 1278 /Filter [/FlateDecode] >> @@ -16959,10 +17084,10 @@ x ¿âíJ¼âíJ¼â특âíâ¬x»¯x»¯x‡�+Þ!Êw¹âB®x‡b®x‡�+Þ�ïrÅ;àà2äÍkçΜñJ¼Y³e¼Y†Œ7+|œÆx‹ oSŠŒ7+[Æ›ețךyÞ¢oVûòæµ– -ã­Œ‹·é€"¯åþR¦W�*–Ó4Xq”êC™^Jú[(£à®^ƒ1»¢y]•¡Èk¹}¼YM-x e#��e®ÊÙ_y+ë¡hÜŠ7k¶:û«/ âåæZå5«dUýµl¼uz5n¥¿[ç¾ÛeBø÷&Si|y»(ô%q& %+S%ñ*Aª¾ÛÊBijPUÃ6\¥h,ž(¶+L¥ºÝ,¥‚4G©5Þ<¥ô�…ÂcD› [˧©ñ¡*t› [ jj¼aijü¼IµÙxʪw¹g¶FÉ›wžØ¶¥§Rô>¶Þís¡hâ>½:­O¯VRÇËí¾³P*#àÙi|e0¼Uýí,ì*o§¿Uáûì/ãÎþ*[§¿U�ê,”¦Nö¹°•m€lšx°:š&\}M³�¹»)üÒðL2„´\%CHËF7æÕ§‰ÇÊ»BŽLÉ…’)æjæ€Æ»J1:%¦H߯:=Së©ÿÛ³zýr»m�éükÀó¹žÌOçþí¿‡ë媳ôû7±Ùð endstream endobj -141 0 obj +145 0 obj [259 600 600 600 600 600 600 600 399 399 600 600 293 600 293 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 752 600 667 600 652 600 769 600 400 600 600 653 600 788 787 600 600 707 600 652 747 600 1066 600 600 600 600 600 600 600 600 600 599 648 526 648 570 407 560 666 352 600 600 352 985 666 612 645 600 522 487 404 666 600 855 645 579 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600] endobj -142 0 obj +146 0 obj << /Length1 17504 /Length 10958 /Filter [/FlateDecode] @@ -17015,10 +17140,10 @@ Cn_ QØ“ ¬¤yÿ÷�Z°€z°€&˜·¬¦ l¥¬½9À>6¡nÔƒzQ:„1º„þ ]F/ùÍNŒfd”fø²8)bûÿ•}/É endstream endobj -143 0 obj +147 0 obj << /Type /FontDescriptor /FontName /2c3dcc+NotoSerif -/FontFile2 142 0 R +/FontFile2 146 0 R /FontBBox [-518 -250 1246 1047] /Flags 6 /StemV 0 @@ -17029,7 +17154,7 @@ endobj /XHeight 1098 >> endobj -144 0 obj +148 0 obj << /Length 1278 /Filter [/FlateDecode] >> @@ -17038,10 +17163,10 @@ x ¿âíJ¼âíJ¼â특âíâ¬x»¯x»¯x‡�+Þ!Êw¹âB®x‡b®x‡�+Þ�ïrÅ;àà2äÍkçΜñJ¼Y³e¼Y†Œ7+|œÆx‹ oSŠŒ7+[Æ›ețךyÞ¢oVûòæµ– -ã­Œ‹·é€"¯åþR¦W�*–Ó4Xq”êC™^Jú[(£à®^ƒ1»¢y]•¡Èk¹}¼YM-x e#��e®ÊÙ_y+ë¡hÜŠ7k¶:û«/ âåæZå5«dUýµl¼uz5n¥¿[ç¾ÛeBø÷&Si|y»(ô%q& %+S%ñ*Aª¾ÛÊBijPUÃ6\¥h,ž(¶+L¥ºÝ,¥‚4G©5Þ<¥ô�…ÂcD› [˧©ñ¡*t› [ jj¼aijü¼IµÙxʪw¹g¶FÉ›wžØ¶¥§Rô>¶Þís¡hâ>½:­O¯VRÇËí¾³P*#àÙi|e0¼Uýí,ì*o§¿Uáûì/ãÎþ*[§¿U�ê,”¦Nö¹°•m€lšx°:š&\}M³�¹»)üÒðL2„´\%CHËF7æÕ§‰ÇÊ»BŽLÉ…’)æjæ€Æ»J1:%¦H߯:=Së©ÿÛ³zýr»m�éükÀó¹žÌOçþí¿‡ë媳ôû7±Ùð endstream endobj -145 0 obj +149 0 obj [259 500 408 500 500 500 742 220 346 346 500 500 250 310 250 288 559 559 559 559 559 559 559 559 559 559 286 286 500 500 559 500 500 705 653 613 727 623 589 713 792 367 356 500 623 937 763 742 604 500 655 543 612 716 674 1046 660 625 500 500 500 500 500 458 500 562 613 492 613 535 369 538 634 319 500 584 310 944 645 577 613 613 471 451 352 634 579 861 578 564 500 428 500 428 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 361 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 259 500 500 500 500 500 500 500 450 450 250 250 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500] endobj -146 0 obj +150 0 obj << /Length1 8188 /Length 4887 /Filter [/FlateDecode] @@ -17074,10 +17199,10 @@ B Œ5„²•ˆûP¶R±BèêÚ×yŸ±EÚÆÂêîàÙ°*[¹|Œ÷‹U6†VX¥}Ó}Hu͇RQ%Ï„½¼‹,?—µce·àÜÆ½çXÞ2ÇBR®¶VñªþO"óò9ÆWYFSâKÙôð±wˆ�÷ÝŽ0…Ž3ŒÅ± !¾»¶�g†×ãÞéıqX¸nR(îÏ6¤š¹†�Ã)ž•ïÂVèÆ¹9L†ÙöaT©êD8¶+á+!ÊcŒð<†‘Ê*<µªØ®ñý¯êJ±F¡9=‰r�~¾™$륇'R½ó‡y-§:UêçUú?Ìsa;a†ÏÖén®œÞ]£Há1Œ(1¥vkì/èêåû!Š³Î†ù3­‚Ç6„3Ëqù¬ò[ø9 ’Ÿâ=Wÿ5*‡ŸŸ–á)zvÆŠOŽ&hÆNmA ØY'®Æmʉ+ „dÄÿÓ$€= endstream endobj -147 0 obj +151 0 obj << /Type /FontDescriptor /FontName /4617f2+NotoSerif-Italic -/FontFile2 146 0 R +/FontFile2 150 0 R /FontBBox [-421 -250 1238 1047] /Flags 70 /StemV 0 @@ -17088,7 +17213,7 @@ endobj /XHeight 1098 >> endobj -148 0 obj +152 0 obj << /Length 1278 /Filter [/FlateDecode] >> @@ -17097,10 +17222,10 @@ x ¿âíJ¼âíJ¼â특âíâ¬x»¯x»¯x‡�+Þ!Êw¹âB®x‡b®x‡�+Þ�ïrÅ;àà2äÍkçΜñJ¼Y³e¼Y†Œ7+|œÆx‹ oSŠŒ7+[Æ›ețךyÞ¢oVûòæµ– -ã­Œ‹·é€"¯åþR¦W�*–Ó4Xq”êC™^Jú[(£à®^ƒ1»¢y]•¡Èk¹}¼YM-x e#��e®ÊÙ_y+ë¡hÜŠ7k¶:û«/ âåæZå5«dUýµl¼uz5n¥¿[ç¾ÛeBø÷&Si|y»(ô%q& %+S%ñ*Aª¾ÛÊBijPUÃ6\¥h,ž(¶+L¥ºÝ,¥‚4G©5Þ<¥ô�…ÂcD› [˧©ñ¡*t› [ jj¼aijü¼IµÙxʪw¹g¶FÉ›wžØ¶¥§Rô>¶Þís¡hâ>½:­O¯VRÇËí¾³P*#àÙi|e0¼Uýí,ì*o§¿Uáûì/ãÎþ*[§¿U�ê,”¦Nö¹°•m€lšx°:š&\}M³�¹»)üÒðL2„´\%CHËF7æÕ§‰ÇÊ»BŽLÉ…’)æjæ€Æ»J1:%¦H߯:=Së©ÿÛ³zýr»m�éükÀó¹žÌOçþí¿‡ë媳ôû7±Ùð endstream endobj -149 0 obj +153 0 obj [259 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 725 623 589 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 579 600 486 579 493 600 556 599 304 600 600 304 600 599 574 600 600 467 463 368 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600] endobj -150 0 obj +154 0 obj << /Length1 3980 /Length 2563 /Filter [/FlateDecode] @@ -17126,10 +17251,10 @@ x Ó1oQ¼ºœÝ[|öªÂöŸ{xß¡àÞ‡<¢Ê^Øï†ÛUæ�ÙÆßËvB3¥Ì¥O9¬1,¤ë×´úC(w¹þë�Šrý|0SÚ9ˆÆ�Ð^4o?Ú9…FO‹S¿›¥ƒö3Ÿ endstream endobj -151 0 obj +155 0 obj << /Type /FontDescriptor /FontName /af3d9b+mplus1mn-regular -/FontFile2 150 0 R +/FontFile2 154 0 R /FontBBox [0 -270 1000 1025] /Flags 4 /StemV 0 @@ -17140,7 +17265,7 @@ endobj /XHeight 0 >> endobj -152 0 obj +156 0 obj << /Length 1278 /Filter [/FlateDecode] >> @@ -17149,11 +17274,11 @@ x ¿âíJ¼âíJ¼â특âíâ¬x»¯x»¯x‡�+Þ!Êw¹âB®x‡b®x‡�+Þ�ïrÅ;àà2äÍkçΜñJ¼Y³e¼Y†Œ7+|œÆx‹ oSŠŒ7+[Æ›ețךyÞ¢oVûòæµ– -ã­Œ‹·é€"¯åþR¦W�*–Ó4Xq”êC™^Jú[(£à®^ƒ1»¢y]•¡Èk¹}¼YM-x e#��e®ÊÙ_y+ë¡hÜŠ7k¶:û«/ âåæZå5«dUýµl¼uz5n¥¿[ç¾ÛeBø÷&Si|y»(ô%q& %+S%ñ*Aª¾ÛÊBijPUÃ6\¥h,ž(¶+L¥ºÝ,¥‚4G©5Þ<¥ô�…ÂcD› [˧©ñ¡*t› [ jj¼aijü¼IµÙxʪw¹g¶FÉ›wžØ¶¥§Rô>¶Þís¡hâ>½:­O¯VRÇËí¾³P*#àÙi|e0¼Uýí,ì*o§¿Uáûì/ãÎþ*[§¿U�ê,”¦Nö¹°•m€lšx°:š&\}M³�¹»)üÒðL2„´\%CHËF7æÕ§‰ÇÊ»BŽLÉ…’)æjæ€Æ»J1:%¦H߯:=Së©ÿÛ³zýr»m�éükÀó¹žÌOçþí¿‡ë媳ôû7±Ùð endstream endobj -153 0 obj +157 0 obj [500 364 364 364 364 364 364 364 364 364 364 364 364 364 500 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 500 364 500 364 364 500 500 364 500 364 500 364 364 500 364 500 364 500 364 500 500 500 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364 364] endobj xref -0 154 +0 158 0000000000 65535 f 0000000015 00000 n 0000000236 00000 n @@ -17161,158 +17286,162 @@ xref 0000000617 00000 n 0000000668 00000 n 0000000940 00000 n -0000012320 00000 n -0000012841 00000 n -0000013013 00000 n -0000013055 00000 n -0000013104 00000 n -0000013510 00000 n -0000267397 00000 n -0000267440 00000 n -0000267608 00000 n -0000394441 00000 n -0000395685 00000 n -0000396037 00000 n -0000512129 00000 n -0000656606 00000 n -0000658404 00000 n -0000658745 00000 n -0000720048 00000 n -0000723861 00000 n -0000724236 00000 n -0000724283 00000 n -0000763620 00000 n -0000832023 00000 n -0000834986 00000 n -0000835350 00000 n -0000879419 00000 n -0000961918 00000 n -0001219564 00000 n -0001223257 00000 n -0001223623 00000 n -0001280065 00000 n -0001284728 00000 n -0001285094 00000 n -0001378133 00000 n -0001378180 00000 n -0001601058 00000 n -0001745581 00000 n -0001750546 00000 n -0001752764 00000 n -0001753142 00000 n -0001934547 00000 n -0002102333 00000 n -0002102380 00000 n -0002104549 00000 n -0002104915 00000 n -0002284696 00000 n -0002289694 00000 n -0002353667 00000 n -0002425225 00000 n -0002427967 00000 n -0002428345 00000 n -0002453468 00000 n -0002545202 00000 n -0002622014 00000 n -0002623746 00000 n -0002835398 00000 n -0002869957 00000 n -0002873370 00000 n -0002873748 00000 n -0002903525 00000 n -0002936938 00000 n -0002936985 00000 n -0002952566 00000 n -0002956001 00000 n -0002956391 00000 n -0003077778 00000 n -0003089449 00000 n -0003121141 00000 n -0003121188 00000 n -0003124405 00000 n -0003124771 00000 n -0003124815 00000 n -0003124859 00000 n -0003138355 00000 n -0003193881 00000 n -0003198585 00000 n -0003198939 00000 n -0003369714 00000 n -0003372692 00000 n -0003373059 00000 n -0003480729 00000 n -0003480904 00000 n -0003594018 00000 n -0003597358 00000 n -0003597737 00000 n -0003702899 00000 n -0003819488 00000 n -0003819535 00000 n -0003819710 00000 n -0003879980 00000 n -0003880352 00000 n -0003880706 00000 n -0004042493 00000 n -0004045685 00000 n -0004046040 00000 n -0004046085 00000 n -0004046225 00000 n -0004046367 00000 n -0004046506 00000 n -0004046647 00000 n -0004046797 00000 n -0004046949 00000 n -0004047091 00000 n -0004047233 00000 n -0004047361 00000 n -0004047490 00000 n -0004047622 00000 n -0004047755 00000 n -0004047905 00000 n -0004048054 00000 n -0004048181 00000 n -0004048307 00000 n -0004048429 00000 n -0004048552 00000 n -0004048688 00000 n -0004048824 00000 n -0004048902 00000 n -0004049179 00000 n -0004049370 00000 n -0004049601 00000 n -0004049832 00000 n -0004050135 00000 n -0004050370 00000 n -0004050557 00000 n -0004050760 00000 n -0004051053 00000 n -0004051211 00000 n -0004051360 00000 n -0004051558 00000 n -0004051876 00000 n -0004051920 00000 n -0004052194 00000 n -0004052468 00000 n -0004060220 00000 n -0004060439 00000 n -0004061794 00000 n -0004062710 00000 n -0004073761 00000 n -0004073975 00000 n -0004075330 00000 n -0004076246 00000 n -0004081224 00000 n -0004081448 00000 n -0004082803 00000 n -0004083718 00000 n -0004086372 00000 n -0004086585 00000 n -0004087940 00000 n +0000013087 00000 n +0000013624 00000 n +0000013796 00000 n +0000013838 00000 n +0000013887 00000 n +0000014320 00000 n +0000268207 00000 n +0000268250 00000 n +0000268418 00000 n +0000395251 00000 n +0000396495 00000 n +0000396847 00000 n +0000512939 00000 n +0000657416 00000 n +0000659214 00000 n +0000659555 00000 n +0000720858 00000 n +0000724671 00000 n +0000725046 00000 n +0000725093 00000 n +0000764430 00000 n +0000832833 00000 n +0000835796 00000 n +0000836160 00000 n +0000880229 00000 n +0000962728 00000 n +0001220374 00000 n +0001224067 00000 n +0001224433 00000 n +0001280875 00000 n +0001285538 00000 n +0001285904 00000 n +0001378943 00000 n +0001378990 00000 n +0001601868 00000 n +0001746391 00000 n +0001751356 00000 n +0001753574 00000 n +0001753952 00000 n +0001935357 00000 n +0002103143 00000 n +0002103190 00000 n +0002105359 00000 n +0002105725 00000 n +0002285506 00000 n +0002290504 00000 n +0002354477 00000 n +0002426035 00000 n +0002428777 00000 n +0002429155 00000 n +0002454278 00000 n +0002546012 00000 n +0002622824 00000 n +0002624556 00000 n +0002836208 00000 n +0002870767 00000 n +0002874180 00000 n +0002874558 00000 n +0002904335 00000 n +0002937748 00000 n +0002937795 00000 n +0002953376 00000 n +0002956811 00000 n +0002957201 00000 n +0003078588 00000 n +0003090259 00000 n +0003121951 00000 n +0003121998 00000 n +0003125215 00000 n +0003125581 00000 n +0003125625 00000 n +0003125669 00000 n +0003139165 00000 n +0003194691 00000 n +0003199395 00000 n +0003199749 00000 n +0003370524 00000 n +0003373502 00000 n +0003373869 00000 n +0003481539 00000 n +0003481714 00000 n +0003594828 00000 n +0003598168 00000 n +0003598547 00000 n +0003703709 00000 n +0003820298 00000 n +0003820345 00000 n +0003820520 00000 n +0003880790 00000 n +0003881162 00000 n +0003881516 00000 n +0004043303 00000 n +0004047419 00000 n +0004047774 00000 n +0004047819 00000 n +0004047864 00000 n +0004048004 00000 n +0004048146 00000 n +0004048285 00000 n +0004048426 00000 n +0004048576 00000 n +0004048728 00000 n +0004048870 00000 n +0004049012 00000 n +0004049140 00000 n +0004049269 00000 n +0004049401 00000 n +0004049534 00000 n +0004049684 00000 n +0004049833 00000 n +0004049960 00000 n +0004050086 00000 n +0004050208 00000 n +0004050331 00000 n +0004050460 00000 n +0004050589 00000 n +0004050725 00000 n +0004050861 00000 n +0004050939 00000 n +0004051216 00000 n +0004051407 00000 n +0004051638 00000 n +0004051869 00000 n +0004052172 00000 n +0004052407 00000 n +0004052594 00000 n +0004052797 00000 n +0004053090 00000 n +0004053248 00000 n +0004053397 00000 n +0004053581 00000 n +0004053779 00000 n +0004054097 00000 n +0004054141 00000 n +0004054415 00000 n +0004054689 00000 n +0004062441 00000 n +0004062660 00000 n +0004064015 00000 n +0004064931 00000 n +0004075982 00000 n +0004076196 00000 n +0004077551 00000 n +0004078467 00000 n +0004083445 00000 n +0004083669 00000 n +0004085024 00000 n +0004085939 00000 n +0004088593 00000 n +0004088806 00000 n +0004090161 00000 n trailer -<< /Size 154 +<< /Size 158 /Root 2 0 R /Info 1 0 R >> startxref -4088855 +4091076 %%EOF diff --git a/report_carbone/i18n/fr.po b/report_carbone/i18n/fr.po index 7a98799..c50836f 100644 --- a/report_carbone/i18n/fr.po +++ b/report_carbone/i18n/fr.po @@ -754,6 +754,10 @@ msgstr "Nouveau document" msgid "Sauvegarder" msgstr "" +#. module: report_carbone +#: model:ir.model.fields,field_description:report_carbone.field_ir_actions_report__use_complement +msgid "Use complement datas" +msgstr "Utilisation des compléments" #. module: report_carbone #: model_terms:ir.ui.view,arch_db:report_carbone.act_carbone_create_report_wizard_form diff --git a/report_carbone/models/base/base.py b/report_carbone/models/base/base.py index 78ce65e..e086c36 100644 --- a/report_carbone/models/base/base.py +++ b/report_carbone/models/base/base.py @@ -15,7 +15,7 @@ class Base(models.AbstractModel): 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(): + if "currency_id" in rec._fields: currency = rec.currency_id rec.carbone_default_currency_id = currency diff --git a/report_carbone/models/base/ir_actions_report.py b/report_carbone/models/base/ir_actions_report.py index 1cd6dd5..298a37c 100644 --- a/report_carbone/models/base/ir_actions_report.py +++ b/report_carbone/models/base/ir_actions_report.py @@ -18,6 +18,7 @@ from werkzeug import urls from odoo import _, api, exceptions, fields, models, release from odoo.modules import get_module_path +from odoo.tools.misc import clean_context from odoo.tools.safe_eval import safe_eval, time from odoo.addons.export_json.controller.main import JsonExportFormat @@ -129,6 +130,7 @@ class IrActionsReportCarbone(models.Model): "supported by Carbone. You must provide a production API key and must not " "be in test mode.", ) + use_complement = fields.Boolean(string="Use complement datas") @api.model def _setup_template_id_and_extension(self, vals): @@ -174,11 +176,11 @@ class IrActionsReportCarbone(models.Model): return super().create(vals_list) def write(self, vals): - if "template_id" in vals.keys(): + if "template_id" in vals: 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(): + if "report_output_file_extension" in vals: self.check_report_output_file_extension(vals) return super().write(vals) @@ -195,6 +197,9 @@ class IrActionsReportCarbone(models.Model): dict_full_data = rec._get_jsonify_export() dict_langs = rec._get_jsonify_translate_export() + local_context = rec._get_env_context_datas_to_export() + dict_full_data.update(local_context) + # 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) @@ -228,11 +233,10 @@ class IrActionsReportCarbone(models.Model): 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 + if locale and locale in translations and 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): @@ -285,6 +289,52 @@ class IrActionsReportCarbone(models.Model): return all_langs + def _get_env_context_datas_to_export(self) -> dict: + """Return auxiliary data to inject into the Carbone export JSON: the current context + cleaned (clean_context, keeping JSON-serialisable values only), enriched with some + user and company information.""" + self.ensure_one() + user = self.env.user + company = user.company_id + + clean = clean_context(self.env.context) + safe_context = {k: v for k, v in clean.items() if self._is_json_serializable(v)} + + return { + "local_context": { + "context": safe_context, + "user": { + "name": user.name, + "login": user.login, + "email": user.email or "", + "phone": user.phone or "", + "lang": user.lang or "", + "tz": user.tz or "", + }, + "company": { + "name": company.name, + "vat": company.vat or "", + "email": company.email or "", + "phone": company.phone or "", + "website": company.website or "", + "street": company.street or "", + "street2": company.street2 or "", + "zip": company.zip or "", + "city": company.city or "", + "country": company.country_id.name or "", + "contact_address": company.partner_id.contact_address or "", + }, + }, + } + + @staticmethod + def _is_json_serializable(value) -> bool: + try: + json.dumps(value) + return True + except (TypeError, ValueError): + return False + def _compute_hide_create_update_button(self): for rec in self: rec.hide_create_update_button = rec.get_hide_create_update_button_value() @@ -541,12 +591,6 @@ class IrActionsReportCarbone(models.Model): csdk = carbone_sdk.CarboneSDK(access_token) csdk._api_headers.update({"User-Agent": self.get_default_user_agent()}) - api_url = ( - self.env["ir.config_parameter"] - .sudo() - .get_param("report-engine.carbone_studio_url") - ) - csdk.set_api_url(api_url) return csdk def _get_json_data(self, export_json_instance, field_names, record, model, lang_codes): @@ -572,7 +616,7 @@ class IrActionsReportCarbone(models.Model): 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 + raise exceptions.MissingError(_("No %s selected for printing.") % records._description) for record in records: try: @@ -589,6 +633,7 @@ class IrActionsReportCarbone(models.Model): # Creating the JSON file (data and translate). dict_full_data = self._get_json_data(export_json_instance, field_names, record, model, lang_codes) + dict_full_data.update(self._get_env_context_datas_to_export()) dict_langs = self._get_jsonify_translate_export() # Modification by reference of dicts. self.extract_translations(dict_full_data, dict_langs) @@ -597,7 +642,8 @@ class IrActionsReportCarbone(models.Model): tuple_pdf = csdk.render( self.template_id, { - "data": dict_full_data, + "complement": self.use_complement and dict_full_data or "", + "data": self.use_complement and "" or dict_full_data, "convertTo": output_file_extension, "translations": dict_langs, "lang": lang, @@ -651,7 +697,17 @@ class IrActionsReportCarbone(models.Model): 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, str | bool]: + def _render_carbone(self, report_ref, docids: str | list, data=None) -> tuple[bytes, str]: + """Odoo's generic `_render()` dispatches to `_render_` and unpacks its + result as a 2-element tuple `(content, report_type)` (see e.g. + `mail.template._render_field()` which calls `ir.actions.report._render()` directly + for non qweb-pdf/html report types). Keep that contract here and expose the filename + separately through `_render_carbone_full()` for callers that need it (our own + controller).""" + content, _filename, extension = self._render_carbone_full(report_ref, docids, data=data) + return content, extension + + def _render_carbone_full(self, report_ref, docids: str | list, data=None) -> tuple[bytes, str | bool, str]: context = dict(self.env.context) report_sudo = self._get_report(report_ref) @@ -676,22 +732,21 @@ class IrActionsReportCarbone(models.Model): streams_to_dl = list(stream_to_ids.keys()) if not context.get("from_ir_report_controller") or len(streams_to_dl) == 1: - stream = streams_to_dl[0] - pdf_content = stream.getvalue() - filename = stream_to_ids[stream][1] - extension = stream_to_ids[stream][2] - return pdf_content, extension, filename + pdf_content = streams_to_dl[0].getvalue() + # stream_to_ids[streams_to_dl[0]] contains [record_id, filename, extension] + filename = stream_to_ids[streams_to_dl[0]][1] + extension = stream_to_ids[streams_to_dl[0]][2] + return pdf_content, filename, extension zip_content = _build_zip_from_data(stream_to_ids) - return zip_content, "zip", f"{report_sudo.name}.zip" + return zip_content, False, "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}" + if records and 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 @@ -758,11 +813,6 @@ class IrActionsReportCarbone(models.Model): } ) - 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" @@ -845,6 +895,132 @@ class IrActionsReportCarbone(models.Model): response = self.call_carbone_endpoint("template", method="POST", files=files, data=data) return {"template_id": response["data"]["id"], "file_extension": file_extension} + def _carbone_template_exists(self, template_id: str) -> bool | None: + """Check whether a template is already known by Carbone. + + Returns: + True — API responded and template found. + False — API responded but template not found (or API-level error). + None — API unreachable after all retry attempts. + + Retries up to 3 times on network-level errors with exponential backoff + (1 s, 2 s) before giving up. + """ + self.ensure_one() + max_attempts = 3 + for attempt in range(1, max_attempts + 1): + try: + res = self.call_carbone_endpoint( + "templates", + params={"search": template_id, "limit": 1, "includeVersions": "true"}, + raise_exception=False, + ) + return bool(res and res.get("data")) + except requests.exceptions.RequestException as exc: + _logger.warning( + "Carbone /templates unreachable (attempt %d/%d): %s", + attempt, + max_attempts, + exc, + ) + if attempt < max_attempts: + time.sleep(2 ** (attempt - 1)) + return None + + def deploy_carbone_template(self, template_path: str, force: bool = False) -> str | bool: + """Associate a Carbone template to this report at deployment time. + + Idempotent "set once" helper, meant to be called from a deployment + script (``mangono_migration/post_.py``): + + * does nothing if no Carbone API key is configured; + * does nothing if the report already has a ``template_id`` (so it can + be replayed at every update without creating duplicates); + * otherwise computes the content-based template id; if that template + is already known by Carbone there is nothing to do, otherwise the + file is uploaded via ``call_carbone_endpoint`` with ``deployedAt`` + so it is immediately available in production, and the returned id + is stored on the report. Setting ``template_id`` triggers + :meth:`write`, which fills ``file_extension`` from the Carbone API. + + :param template_path: absolute path to the template file on disk. + :return: the Carbone template id, or ``False`` if skipped. + + Example (from a ``mangono_migration`` post script):: + + import os + from odoo.modules import get_module_path + from odoo.addons.mangono_migration.mangono_migration import migrate_mangono + + + # allowed_to_fail=True: an unreachable Carbone API must not block the update. + @migrate_mangono(allowed_to_fail=True) + def post_t1234(self): + report = self.env.ref("my_module.my_carbone_report") + path = os.path.join(get_module_path("my_module"), "data/templates/my_template.docx") + report.deploy_carbone_template(path) + """ + self.ensure_one() + + # Garde A: an API key must be configured, otherwise we silently skip. + if not self.env["res.config.settings"].retrieve_carbone_api_key(): + _logger.info("Carbone API key missing, skip template deployment for report %r", self.name) + return False + + # Garde B: "set once" — never overwrite an existing association. + if self.template_id and not force: + return False + + if not os.path.exists(template_path): + raise exceptions.UserError(_("Carbone template file not found: %s") % template_path) + + csdk = self.get_carbone_sdk() + # SHA256 content hash — used only to check whether the template is already known by Carbone. + version_id = csdk.generate_template_id(template_path) + template_exists = self._carbone_template_exists(version_id) + if template_exists is None: + _logger.warning( + "Carbone API unreachable: could not check template existence for report %r (id=%s). " + "Deployment skipped, template_id unchanged.", + self.name, + self.id, + ) + return False + if not template_exists: + with open(template_path, "rb") as f: + file_content = f.read() + files = { + "template": ( + os.path.basename(template_path), + file_content, + mimetypes.guess_type(template_path)[0] or "application/octet-stream", + ) + } + response = self.call_carbone_endpoint( + "template", + method="POST", + files=files, + data={ + "name": os.path.basename(template_path), + "versioning": "true", + "deployedAt": TIMESTAMP_NOW, + }, + ) + # With versioning=True, data.id is the stable template_id (not a SHA256 version hash). + template_id = response["data"]["id"] + _logger.info("Carbone template %s uploaded and deployed for report %r", template_id, self.name) + else: + # Template already on Carbone: retrieve its stable template_id via the templates list. + res = self.call_carbone_endpoint( + "templates", + params={"search": version_id, "limit": 1, "includeVersions": "true"}, + ) + template_id = res["data"][0]["id"] + _logger.info("Carbone template %s already present on Carbone, nothing to upload", template_id) + # write() override fills file_extension from the Carbone API. + self.template_id = template_id + return template_id + 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. @@ -859,9 +1035,6 @@ class IrActionsReportCarbone(models.Model): } res = self.call_carbone_endpoint(endpoint, params, raise_error) - if not isinstance(res, dict): - return False - data_list = res.get("data") if not data_list: return False diff --git a/report_carbone/models/carbone/carbone_field_extractor.py b/report_carbone/models/carbone/carbone_field_extractor.py index 5540801..8ab406d 100644 --- a/report_carbone/models/carbone/carbone_field_extractor.py +++ b/report_carbone/models/carbone/carbone_field_extractor.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # type: ignore # noqa: F401 +import xml.etree.ElementTree as ET # type: ignore from odoo import api, models diff --git a/report_carbone/models/carbone/carbone_translate.py b/report_carbone/models/carbone/carbone_translate.py index 15173d2..8633798 100644 --- a/report_carbone/models/carbone/carbone_translate.py +++ b/report_carbone/models/carbone/carbone_translate.py @@ -17,7 +17,7 @@ class CarboneTranslate(models.Model): carbone_translate_line_ids = fields.One2many( "carbone.translate.line", "carbone_translate_id", string="Translation lines" ) - _sql_constraints = [ + _sql_constraints = [ # noqa: RUF012 ( "lang_report_uniq", "UNIQUE(ir_actions_report_id, lang_id)", diff --git a/report_carbone/models/carbone/carbone_translate_line.py b/report_carbone/models/carbone/carbone_translate_line.py index 4e10948..478de16 100644 --- a/report_carbone/models/carbone/carbone_translate_line.py +++ b/report_carbone/models/carbone/carbone_translate_line.py @@ -11,7 +11,7 @@ class CarboneTranslateLine(models.Model): ) source = fields.Text(string="Source term") value = fields.Text(string="Translation Value", default="") - _sql_constraints = [ + _sql_constraints = [ # noqa: RUF012 ( "source_value_uniq", "UNIQUE(source, value, carbone_translate_id)", diff --git a/report_carbone/models/res/res_config_settings.py b/report_carbone/models/res/res_config_settings.py index 24b1bc1..d049072 100644 --- a/report_carbone/models/res/res_config_settings.py +++ b/report_carbone/models/res/res_config_settings.py @@ -1,3 +1,5 @@ +import os + from odoo import fields, models @@ -14,12 +16,20 @@ class CarboneResConfigSettings(models.TransientModel): 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.""" + """Depending on the ‘test mode’ checkbox or ‘test_mode_key’ parameter, either the production key or + the staging key is returned. + + The value set in the UI (ir.config_parameter) takes priority. If absent, the environment variable + CARBONE_STAGE_API_KEY or CARBONE_PROD_API_KEY is used as a fallback. + """ 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") + return self.env["ir.config_parameter"].sudo().get_param("report-engine.stage_api_key") or os.environ.get( + "CARBONE_STAGE_API_KEY" + ) + return self.env["ir.config_parameter"].sudo().get_param("report-engine.prod_api_key") or os.environ.get( + "CARBONE_PROD_API_KEY" + ) def action_download_carbone_documentation(self): ir_action_report = self.env["ir.actions.report"] diff --git a/report_carbone/pyproject.toml b/report_carbone/pyproject.toml index f495dab..98a3396 100644 --- a/report_carbone/pyproject.toml +++ b/report_carbone/pyproject.toml @@ -1,9 +1,10 @@ [build-system] -requires = ["addon-odoo-wheel>=0.4.3"] +requires = ["addon-odoo-wheel>=0.4.4"] build-backend = "addon_odoo_wheel.builder" [tool.addon-odoo-wheel] dependencies = [ "mangono-addon-export_json~=18.0", - "carbone_sdk~=1.0" + "carbone_sdk==1.1.1" ] +whl-exclude = ["docs"] diff --git a/report_carbone/static/src/js/report/initiate_studio.js b/report_carbone/static/src/js/report/initiate_studio.js index b34a7fb..7b9e191 100644 --- a/report_carbone/static/src/js/report/initiate_studio.js +++ b/report_carbone/static/src/js/report/initiate_studio.js @@ -279,6 +279,7 @@ class CarboneStudioManager { } return { + complement: JSON.parse(this.optionsList[0]), data: JSON.parse(this.optionsList[0]), translations: JSON.parse(this.optionsList[1]), lang: this.optionsList[2], diff --git a/report_carbone/tests/test_ir_actions_report.py b/report_carbone/tests/test_ir_actions_report.py index 5479d70..26f780d 100644 --- a/report_carbone/tests/test_ir_actions_report.py +++ b/report_carbone/tests/test_ir_actions_report.py @@ -1,11 +1,11 @@ +import inspect import json -from unittest.mock import MagicMock, Mock, patch - -from requests import Response +from unittest.mock import MagicMock, Mock, mock_open, patch from odoo import exceptions from odoo.tests import common, tagged +from odoo.addons.base.models.ir_actions_report import IrActionsReport from odoo.addons.export_json.controller import main as controller_module from ..models.base.exceptions import MissingApiKeyError @@ -65,6 +65,8 @@ class TestIrActionsReport(common.TransactionCase): self.docids = f"{self.partner_demo.id},{self.partner_admin.id}" self.env["ir.config_parameter"].set_param("report-engine.stage_api_key", "mock_key") self._setup_patcher(IrActionsReportCarbone, "call_carbone_endpoint", Mock(return_value={})) + # Force template_id on every record found (guards against stale DB data from previous runs) + self.report.template_id = "mock_carbone_template_id" def test_generate_report_carbone_without_api_keys(self): """MissingApiKeyError has to be raised when we try to generate a carbone report without an API key.""" @@ -91,6 +93,58 @@ class TestIrActionsReport(common.TransactionCase): ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name) ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + def test_render_carbone_signature_matches_odoo_render_dispatch_contract(self): + """`ir.actions.report._render()` is Odoo's generic dispatcher: for a report of type + "xxx" it looks up `_render_xxx` and calls it as `render_func(report_ref, res_ids, + data=data)`. `_render_carbone` must stay callable that way, and its result must still + be unpackable as a 2-element `(content, report_type)` tuple like every other + `_render_` method (see `_render_qweb_pdf`, used here as the reference contract). + If Odoo ever changes that convention, this test breaks and flags the incompatibility.""" + dispatch_source = inspect.getsource(IrActionsReport._render) + self.assertIn( + "render_func(report_ref, res_ids, data=data)", + dispatch_source, + "odoo.addons.base.models.ir_actions_report.IrActionsReport._render no longer calls " + "render_func(report_ref, res_ids, data=data): the assumed dispatch contract for " + "_render_carbone has changed upstream.", + ) + + carbone_signature = inspect.signature(IrActionsReportCarbone._render_carbone) + reference_signature = inspect.signature(IrActionsReport._render_qweb_pdf) + + for signature, label in ((carbone_signature, "_render_carbone"), (reference_signature, "_render_qweb_pdf")): + params = list(signature.parameters.values()) + self.assertGreaterEqual(len(params), 3, f"{label} must accept (self, report_ref, res_ids/docids, ...)") + self.assertEqual(params[0].name, "self") + self.assertIn( + params[1].kind, + (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY), + f"{label}'s 2nd parameter (report_ref) must be positional", + ) + self.assertIn( + params[2].kind, + (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY), + f"{label}'s 3rd parameter (res_ids/docids) must be positional", + ) + self.assertIn("data", signature.parameters, f"{label} must accept a 'data' parameter") + self.assertEqual( + signature.parameters["data"].default, + None, + f"{label}'s 'data' parameter must default to None", + ) + + # Simulate the exact call performed by the generic dispatcher, unbound (self is the + # positional receiver): render_func(report_ref, res_ids, data=data). + ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name) + bound = carbone_signature.bind( + ir_actions_report_carbone, ir_actions_report_carbone, "1,2", data={"some": "data"} + ) + bound.apply_defaults() + + with patch.object(IrActionsReportCarbone, "_render_carbone_full", return_value=(b"content", "file.pdf", "pdf")): + content, report_type = IrActionsReportCarbone._render_carbone(*bound.args, **bound.kwargs) + self.assertEqual((content, report_type), (b"content", "pdf")) + def test_get_report_from_name(self): # ir_actions_report found with name and report_type == "carbone': res = self.report_object._get_report_from_name(self.report_name) @@ -114,9 +168,9 @@ class TestIrActionsReport(common.TransactionCase): with ( patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_render_report), patch("carbone_sdk.CarboneSDK.get_report", return_value=mock_get_report), + self.assertRaises(exceptions.UserError) as error, ): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), @@ -127,21 +181,12 @@ class TestIrActionsReport(common.TransactionCase): def test_failed_generate_report_carbone_status_code_400(self): ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name) - def post(url, **kwargs): - response = Response() - if url.startswith("https://api.carbone.io/render/mock_carbone_template_id"): - response.status_code = 400 # NotFileError - response._content = json.dumps( - { - "success": False, - "error": "NotFileError", - } - ).encode("utf-8") - return response - - with patch("requests.post", post): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + mock_result = {"success": False, "error": "NotFileError"} + with ( + patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result), + self.assertRaises(exceptions.UserError) as error, + ): + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), "An error occurred when generating the report via Carbone : Carbone SDK render error: NotFileError", @@ -150,21 +195,12 @@ class TestIrActionsReport(common.TransactionCase): def test_failed_generate_report_carbone_status_code_500(self): ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name) - def post(url, **kwargs): - response = Response() - if url.startswith("https://api.carbone.io/render/mock_carbone_template_id"): - response.status_code = 500 # GenerateReportError - response._content = json.dumps( - { - "success": False, - "error": "GenerateReportError", - } - ).encode("utf-8") - return response - - with patch("requests.post", post): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + mock_result = {"success": False, "error": "GenerateReportError"} + with ( + patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result), + self.assertRaises(exceptions.UserError) as error, + ): + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), "An error occurred when generating the report via Carbone : Carbone SDK render error: GenerateReportError", @@ -180,9 +216,9 @@ class TestIrActionsReport(common.TransactionCase): patch("carbone_sdk.CarboneSDK.generate_template_id", return_value="no_template_carbone_id"), patch("carbone_sdk.CarboneSDK.add_template", return_value=mock_result), patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result), + self.assertRaises(exceptions.UserError) as error, ): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), "An error occurred when generating the report via Carbone : Carbone SDK render error:mock_error_message", @@ -192,9 +228,8 @@ class TestIrActionsReport(common.TransactionCase): # Error Carbone SDK render error: failled to generate the template id ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name) - with patch("os.path.exists", return_value=True): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + with patch("os.path.exists", return_value=True), self.assertRaises(exceptions.UserError) as error: + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), "An error occurred when generating the report via Carbone :" @@ -225,9 +260,9 @@ class TestIrActionsReport(common.TransactionCase): with ( patch("carbone_sdk.CarboneSDK.generate_template_id", return_value="no_template_carbone_id"), patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result), + self.assertRaises(exceptions.UserError) as error, ): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), "An error occurred when generating the report via Carbone : Carbone SDK render error: render_id empty", @@ -237,14 +272,160 @@ class TestIrActionsReport(common.TransactionCase): # Default SDK's error ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name) mock_result = None - with patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result): - with self.assertRaises(exceptions.UserError) as error: - ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) + with ( + patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result), + self.assertRaises(exceptions.UserError) as error, + ): + ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids) self.assertEqual( str(error.exception), "An error occurred when generating the report via Carbone : Carbone SDK render error: something went wrong", ) + # region local_context + + def test_is_json_serializable(self): + """_is_json_serializable accepts JSON primitives and rejects non-serialisable values + (e.g. recordsets), so the Carbone payload never breaks csdk.render's JSON encoding.""" + report = self.report + self.assertTrue(report._is_json_serializable("string")) + self.assertTrue(report._is_json_serializable(42)) + self.assertTrue(report._is_json_serializable({"a": [1, 2, True, None]})) + self.assertFalse(report._is_json_serializable(self.partner_demo)) # recordset + self.assertFalse(report._is_json_serializable(object())) + + def test_get_env_context_datas_to_export(self): + """_get_env_context_datas_to_export returns a JSON-safe `local_context` block: default_* + keys and non-serialisable context values are filtered out, user/company info is added.""" + report = self.report.with_context( + default_should_be_dropped=1, + custom_key="kept", + non_serialisable=self.partner_demo, # recordset -> not JSON serialisable + ) + result = report._get_env_context_datas_to_export() + + # Structure. + self.assertIn("local_context", result) + local_context = result["local_context"] + self.assertEqual(set(local_context), {"context", "user", "company"}) + + # clean_context: default_* dropped, plain key kept. + context = local_context["context"] + self.assertNotIn("default_should_be_dropped", context) + self.assertEqual(context.get("custom_key"), "kept") + + # Non-serialisable values are filtered out. + self.assertNotIn("non_serialisable", context) + + # User / company info is populated from the environment. + self.assertEqual(local_context["user"]["name"], self.env.user.name) + self.assertEqual(local_context["user"]["login"], self.env.user.login) + self.assertEqual(local_context["company"]["name"], self.env.user.company_id.name) + + # The whole block must be JSON serialisable (it is sent to csdk.render, which JSON-encodes it). + json.dumps(result) + + # endregion local_context + + # region deploy_carbone_template + + def _clear_template_id(self): + """Return the demo report with its template_id reset (call_carbone_endpoint + is mocked in setUp, so the write() override does not hit the network).""" + report = self.report + report.template_id = False + return report + + def test_carbone_template_exists(self): + report = self.report + # call_carbone_endpoint is mocked to {} in setUp -> no data -> not found. + self.assertFalse(report._carbone_template_exists("whatever")) + + with patch.object(IrActionsReportCarbone, "call_carbone_endpoint", return_value={"data": [{"id": "x"}]}): + self.assertTrue(report._carbone_template_exists("x")) + + def test_deploy_carbone_template_no_api_key(self): + """Garde A: without a configured API key, nothing happens.""" + self.env["ir.config_parameter"].set_param("report-engine.stage_api_key", False) + self.env["ir.config_parameter"].set_param("report-engine.prod_api_key", False) + report = self._clear_template_id() + + with patch.object(IrActionsReportCarbone, "get_carbone_sdk") as mock_sdk: + self.assertFalse(report.deploy_carbone_template("/tmp/template.docx")) + mock_sdk.assert_not_called() + self.assertFalse(report.template_id) + + def test_deploy_carbone_template_set_once(self): + """Garde B: a report that already has a template_id is left untouched.""" + self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True) + report = self.report + report.template_id = "mock_carbone_template_id" + self.assertTrue(report.template_id) + + with patch.object(IrActionsReportCarbone, "get_carbone_sdk") as mock_sdk: + self.assertFalse(report.deploy_carbone_template("/tmp/template.docx")) + mock_sdk.assert_not_called() + + def test_deploy_carbone_template_file_not_found(self): + self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True) + report = self._clear_template_id() + + with patch("os.path.exists", return_value=False), self.assertRaises(exceptions.UserError): + report.deploy_carbone_template("/tmp/missing.docx") + + def test_deploy_carbone_template_already_on_carbone(self): + """Template already known by Carbone: stable template_id retrieved via GET, no upload.""" + self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True) + report = self._clear_template_id() + + mock_csdk = MagicMock() + mock_csdk.generate_template_id.return_value = "hash_id" + + def mock_endpoint(endpoint, params=None, raise_exception=True, method="GET", **kwargs): + return {"data": [{"id": "stable_template_id", "type": "docx"}]} + + with ( + patch("os.path.exists", return_value=True), + patch.object(IrActionsReportCarbone, "get_carbone_sdk", return_value=mock_csdk), + patch.object(IrActionsReportCarbone, "_carbone_template_exists", return_value=True), + patch.object(IrActionsReportCarbone, "call_carbone_endpoint", side_effect=mock_endpoint), + ): + res = report.deploy_carbone_template("/tmp/template.docx") + + self.assertEqual(res, "stable_template_id") + # No upload: the template already existed on Carbone, SDK add_template never called. + mock_csdk.add_template.assert_not_called() + self.assertEqual(report.template_id, "stable_template_id") + + def test_deploy_carbone_template_uploads_when_missing(self): + """Template unknown by Carbone: upload via call_carbone_endpoint with deployedAt.""" + self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True) + report = self._clear_template_id() + + mock_csdk = MagicMock() + mock_csdk.generate_template_id.return_value = "hash_id" + + def mock_endpoint(endpoint, params=None, raise_exception=True, method="GET", **kwargs): + if method == "POST" and endpoint == "template": + return {"data": {"id": "new_template_id"}} + return {} + + with ( + patch("os.path.exists", return_value=True), + patch("builtins.open", mock_open(read_data=b"template_content")), + patch.object(IrActionsReportCarbone, "get_carbone_sdk", return_value=mock_csdk), + patch.object(IrActionsReportCarbone, "_carbone_template_exists", return_value=False), + patch.object(IrActionsReportCarbone, "call_carbone_endpoint", side_effect=mock_endpoint), + ): + res = report.deploy_carbone_template("/tmp/template.docx") + + self.assertEqual(res, "new_template_id") + # SDK add_template must NOT be used — upload goes through call_carbone_endpoint. + mock_csdk.add_template.assert_not_called() + self.assertEqual(report.template_id, "new_template_id") + + # endregion deploy_carbone_template + # region utils def get_not_use_id(self, model_name: "str") -> int: diff --git a/report_carbone/views/base/ir_actions_report.xml b/report_carbone/views/base/ir_actions_report.xml index 0496855..417e4ab 100644 --- a/report_carbone/views/base/ir_actions_report.xml +++ b/report_carbone/views/base/ir_actions_report.xml @@ -1,5 +1,11 @@ + + Open carbone documentation + https://carbone.io/documentation/design/overview/getting-started.html + new + + act_report_carbone_view ir.actions.report @@ -21,9 +27,8 @@ class="oe_stat_button" />