From 7162b84404a9d6d41a043ced892a2d51b3c6987b Mon Sep 17 00:00:00 2001 From: Kitti U Date: Mon, 24 Aug 2020 17:58:31 +0700 Subject: [PATCH] [13.0][MIG] excel_import_export, excel_import_export_demo --- excel_import_export/README.rst | 202 ------------ excel_import_export/__manifest__.py | 7 +- .../i18n/excel_import_export.pot | 30 +- excel_import_export/i18n/zh_CN.po | 294 ++++++------------ excel_import_export/models/__init__.py | 14 + excel_import_export/models/common.py | 2 +- excel_import_export/models/styles.py | 4 +- excel_import_export/models/xlsx_export.py | 14 +- excel_import_export/models/xlsx_import.py | 134 ++++---- excel_import_export/models/xlsx_report.py | 8 +- excel_import_export/models/xlsx_template.py | 288 ++++++++++++++--- excel_import_export/readme/HISTORY.rst | 8 + excel_import_export/readme/INSTALL.rst | 2 +- excel_import_export/readme/ROADMAP.rst | 1 - excel_import_export/readme/USAGE.rst | 27 ++ .../static/description/common_wizard.png | Bin 0 -> 12058 bytes .../static/description/index.html | 71 ++--- .../static/description/xlsx_template.png | Bin 0 -> 81396 bytes .../src/js/report/action_manager_report.js | 59 ++-- .../views/xlsx_template_view.xml | 87 +++++- excel_import_export/wizard/__init__.py | 1 + .../wizard/export_xlsx_wizard.py | 10 +- .../wizard/import_xlsx_wizard.py | 16 +- .../wizard/report_xlsx_wizard.py | 22 ++ .../wizard/report_xlsx_wizard.xml | 31 ++ 25 files changed, 704 insertions(+), 628 deletions(-) delete mode 100644 excel_import_export/README.rst create mode 100644 excel_import_export/static/description/common_wizard.png create mode 100644 excel_import_export/static/description/xlsx_template.png create mode 100644 excel_import_export/wizard/report_xlsx_wizard.py create mode 100644 excel_import_export/wizard/report_xlsx_wizard.xml diff --git a/excel_import_export/README.rst b/excel_import_export/README.rst deleted file mode 100644 index bb2b29a1c..000000000 --- a/excel_import_export/README.rst +++ /dev/null @@ -1,202 +0,0 @@ -========================== -Excel Import/Export/Report -========================== - -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! This file is generated by oca-gen-addon-readme !! - !! changes will be overwritten. !! - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png - :target: https://odoo-community.org/page/development-status - :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 -.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/12.0/excel_import_export - :alt: OCA/server-tools -.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-excel_import_export - :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/12.0 - :alt: Try me on Runbot - -|badge1| |badge2| |badge3| |badge4| |badge5| - -The module provide pre-built functions and wizards for developer to build excel import / export / report with ease. - -Without having to code to create excel file, developer do, - -- Create menu, action, wizard, model, view a normal Odoo development. -- Design excel template using standard Excel application, e.g., colors, fonts, formulas, etc. -- Instruct how the data will be located in Excel with simple dictionary instruction or from Odoo UI. -- Odoo will combine instruction with excel template, and result in final excel file. - -**Table of contents** - -.. contents:: - :local: - -Installation -============ - -To install this module, you need to install following python library, **xlrd, xlwt, openpyxl**. - -Then, simply install **excel_import_export**. - -For demo, install **excel_import_export_demo**. - -Usage -===== - -This module contain pre-defined function and wizards to make exporting, importing and reporting easy. - -At the heart of this module, there are 2 `main methods` - -- ``self.env['xlsx.export'].export_xlsx(...)`` -- ``self.env['xlsx.import'].import_xlsx(...)`` - -For reporting, also call `export_xlsx(...)` but through following method - -- ``self.env['xslx.report'].report_xlsx(...)`` - -After install this module, go to Settings > Excel Import/Export > XLSX Templates, this is where the key component located. - -As this module provide tools, it is best to explain as use cases. For example use cases, please install **excel_import_export_demo** - -**Use Case 1:** Export/Import Excel on existing document - -This add export/import action menus in existing document (example - excel_import_export_demo/import_export_sale_order) - -1. Create export action menu on document, with res_model="export.xlsx.wizard" and src_model="", and context['template_domain'] to locate the right template -- actions.xml -2. Create import action menu on document, with res_model="import.xlsx.wizard" and src_model="", and context['template_domain'] to locate the right template -- action.xml -3. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import -- .xlsx -4. Create instruction dictionary for export/import in xlsx.template model -- templates.xml - -**Use Case 2:** Import Excel Files - -With menu wizard to create new documents (example - excel_import_export_demo/import_sale_orders) - -1. Create report menu with search wizard, res_model="import.xlsx.wizard" and context['template_domain'] to locate the right template -- menu_action.xml -2. Create Excel Template File (.xlsx), in the template, name the underlining tab used for import -- .xlsx -3. Create instruction dictionary for import in xlsx.template model -- templates.xml - -**Use Case 3:** Create Excel Report - -This create report menu with criteria wizard. (example - excel_import_export_demo/report_sale_order) - -1. Create report's menu, action, and add context['template_domain'] to locate the right template for this report -- .xml -2. Create report's wizard for search criteria. The view inherits ``excel_import_export.xlsx_report_view`` and mode="primary". In this view, you only need to add criteria fields, the rest will reuse from interited view -- -3. Create report model as models.Transient, then define search criteria fields, and get reporing data into ``results`` field -- .py -4. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results -- .xlsx -5. Create instruction dictionary for report in xlsx.template model -- templates.xml - -**Note:** - -Another option for reporting is to use report action (report_type='excel'), I.e., - -.. code-block:: xml - - - -By using report action, Odoo will find template using combination of model and name, then do the export for the underlining record. -Please see example in excel_import_export_demo/report_action, which shows, - -1. Print excel from an active sale.order -2. Run partner list report based on search criteria. - -Known issues / Roadmap -====================== - -- Module extension e.g., excel_import_export_async, that add ability to execute as async process. -- Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action. - -Changelog -========= - -12.0.1.0.5 (2019-12-19) -~~~~~~~~~~~~~~~~~~~~~~~ - -* Speed up export when dealing with many rows - -12.0.1.0.4 (2019-08-28) -~~~~~~~~~~~~~~~~~~~~~~~ - -* Fix style sum in footer - -12.0.1.0.3 (2019-08-09) -~~~~~~~~~~~~~~~~~~~~~~~ - -* Add report action for report_type = 'excel' - -12.0.1.0.2 (2019-08-07) -~~~~~~~~~~~~~~~~~~~~~~~ - -* Small fix, to ensure that system parameter 'path_temp_file' (ir.config_parameter) is readable - -12.0.1.0.1 (2019-06-24) -~~~~~~~~~~~~~~~~~~~~~~~ - -* Fix wizard on v12 can't download sample template file - https://github.com/OCA/server-tools/issues/1574 - -12.0.1.0.0 (2019-02-24) -~~~~~~~~~~~~~~~~~~~~~~~ - -* Start of the history - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues `_. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed -`feedback `_. - -Do not contact contributors directly about support or help with technical issues. - -Credits -======= - -Authors -~~~~~~~ - -* Ecosoft - -Contributors -~~~~~~~~~~~~ - -* Kitti Upariphutthiphong. (http://ecosoft.co.th) -* Saran Lim. (http://ecosoft.co.th) - -Maintainers -~~~~~~~~~~~ - -This module is maintained by the OCA. - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. - -.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px - :target: https://github.com/kittiu - :alt: kittiu - -Current `maintainer `__: - -|maintainer-kittiu| - -This module is part of the `OCA/server-tools `_ project on GitHub. - -You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/excel_import_export/__manifest__.py b/excel_import_export/__manifest__.py index 212408875..7e52a8149 100644 --- a/excel_import_export/__manifest__.py +++ b/excel_import_export/__manifest__.py @@ -4,22 +4,23 @@ { "name": "Excel Import/Export/Report", "summary": "Base module for developing Excel import/export/report", - "version": "12.0.1.0.4", + "version": "13.0.1.0.0", "author": "Ecosoft,Odoo Community Association (OCA)", "license": "AGPL-3", "website": "https://github.com/OCA/server-tools/", "category": "Tools", "depends": ["mail"], - "external_dependencies": {"python": ["xlrd", "xlwt", "openpyxl",],}, + "external_dependencies": {"python": ["xlrd", "xlwt", "openpyxl"]}, "data": [ "security/ir.model.access.csv", "wizard/export_xlsx_wizard.xml", "wizard/import_xlsx_wizard.xml", + "wizard/report_xlsx_wizard.xml", "views/xlsx_template_view.xml", "views/xlsx_report.xml", "views/webclient_templates.xml", ], "installable": True, - "development_status": "beta", + "development_status": "Beta", "maintainers": ["kittiu"], } diff --git a/excel_import_export/i18n/excel_import_export.pot b/excel_import_export/i18n/excel_import_export.pot index d2843ba6e..f6d2eaf9f 100644 --- a/excel_import_export/i18n/excel_import_export.pot +++ b/excel_import_export/i18n/excel_import_export.pot @@ -193,18 +193,6 @@ msgstr "" msgid "Allow Choose Template" msgstr "" -#. module: excel_import_export -#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__attachment_ids -msgid "Attachments" -msgstr "" - #. module: excel_import_export #: model:ir.model,name:excel_import_export.model_xlsx_styles msgid "Available styles for excel" @@ -383,13 +371,7 @@ msgid "Error filling data into Excel sheets\n" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:212 -#, python-format -msgid "Error importing data" -msgstr "" - -#. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:224 +#: code:addons/excel_import_export/models/xlsx_import.py:219 #, python-format msgid "Error importing data\n" "%s" @@ -670,7 +652,7 @@ msgid "Invalid declaration, %s has no valid field type" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:222 +#: code:addons/excel_import_export/models/xlsx_import.py:217 #, python-format msgid "Invalid file style, only .xls or .xlsx file allowed" msgstr "" @@ -737,7 +719,7 @@ msgid "No Delete" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:254 +#: code:addons/excel_import_export/models/xlsx_import.py:249 #, python-format msgid "No data_dict['__IMPORT__'] in template %s" msgstr "" @@ -818,7 +800,7 @@ msgid "Post Import Hook" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:237 +#: code:addons/excel_import_export/models/xlsx_import.py:232 #, python-format msgid "Post import operation error\n" "%s" @@ -961,8 +943,8 @@ msgid "Template with CSV Quoting = False, data must not contain the same char as msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_export.py:228 -#: code:addons/excel_import_export/models/xlsx_import.py:249 +#: code:addons/excel_import_export/models/xlsx_export.py:230 +#: code:addons/excel_import_export/models/xlsx_import.py:244 #, python-format msgid "Template's model mismatch" msgstr "" diff --git a/excel_import_export/i18n/zh_CN.po b/excel_import_export/i18n/zh_CN.po index 7fec0fdef..f8f0aa3fb 100644 --- a/excel_import_export/i18n/zh_CN.po +++ b/excel_import_export/i18n/zh_CN.po @@ -1,6 +1,6 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * excel_import_export +# * excel_import_export # msgid "" msgstr "" @@ -32,8 +32,7 @@ msgstr "'%s' 找不到工作表" #: model:ir.model.fields,help:excel_import_export.field_export_xlsx_wizard__state #: model:ir.model.fields,help:excel_import_export.field_import_xlsx_wizard__state #: model:ir.model.fields,help:excel_import_export.field_xlsx_report__state -msgid "" -"* Choose: wizard show in user selection mode\n" +msgid "* Choose: wizard show in user selection mode\n" "* Get: wizard show results from user action" msgstr "" "* 选择:用户选择模式下的向导显示\n" @@ -46,35 +45,24 @@ msgstr "单元格:Excel工作表中的数据位置(例如:A1,B1,... #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Continue: If not selected, start rolling with specified first row " -"cells. If selected, continue from previous one2many field" -msgstr "" -"继续: 如果未选中,则使用指定的第一行单元格开始滚动。 如果选中,则从之" -"前的one2many字段继续" +msgid "Continue: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field" +msgstr "继续: 如果未选中,则使用指定的第一行单元格开始滚动。 如果选中,则从之前的one2many字段继续" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Field Cond.: Python code in ${...} to manipulate field " -"value, e.g., if field = product_id, value will represent " -"product object, e.g., ${value and value.uom_id.name or \"\"}" +msgid "Field Cond.: Python code in ${...} to manipulate field value, e.g., if field = product_id, value will represent product object, e.g., ${value and value.uom_id.name or \"\"}" msgstr "" -"字段条件: Python代码${...} 操纵字段值, 例如, if field = " -"product_id, value 将代表产品对象, 例如, ${value and value." -"uom_id.name or \"\"}" +"字段条件: Python代码${...} 操纵字段值, 例如, if field = product_id, " +"value 将代表产品对象, 例如, ${value and value.uom_id.name or \"\"" +"}" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Field Cond.: Python code in ${...} value will represent " -"data from excel cell, e.g., if A1 = 'ABC', value will represent " -"'ABC', e.g., ${value == \"ABC\" and \"X\" or \"Y\"} thus can " -"change from cell value to other value for import." +msgid "Field Cond.: Python code in ${...} value will represent data from excel cell, e.g., if A1 = 'ABC', value will represent 'ABC', e.g., ${value == \"ABC\" and \"X\" or \"Y\"} thus can change from cell value to other value for import." msgstr "" -"字段条件: Python代码 ${...} 值将代表来自excel 单元格的数" -"据, 例如, if A1 = 'ABC', value 代表'ABC',例如, ${value == " -"\"ABC\" and \"X\" or \"Y\"} 因此可以从单元格值更改为其他值以进行导入." +"字段条件: Python代码 ${...} 值将代表来自excel 单元格的数据, 例如, if A1 = " +"'ABC', value 代表'ABC',例如, ${value == \"ABC\" and \"X\" or " +"\"Y\"} 因此可以从单元格值更改为其他值以进行导入." #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -83,18 +71,13 @@ msgstr "字段: 要导入的记录的字段,例如product_id" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Field: Field of the record, e.g., product_id.uom_id.name. They are " -"orm compliant." +msgid "Field: Field of the record, e.g., product_id.uom_id.name. They are orm compliant." msgstr "字段: 记录的字段,例如product_id.uom_id.name。他们是合规的。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"No Delete: By default, all one2many lines will be deleted before " -"import. Select this, to avoid deletion" -msgstr "" -"不删除: 默认情况下,导入前将删除所有one2many行。选择此项,以避免删除" +msgid "No Delete: By default, all one2many lines will be deleted before import. Select this, to avoid deletion" +msgstr "不删除: 默认情况下,导入前将删除所有one2many行。选择此项,以避免删除" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -103,12 +86,8 @@ msgstr "备注:" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Row Field: Use _HEAD_ for the record itself, and one2many field (e." -"g., line_ids) for row data" -msgstr "" -"行字段: 使用_HEAD_表示记录本身,使用one2many字段(例如line_ids)表示行" -"数据" +msgid "Row Field: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data" +msgstr "行字段: 使用_HEAD_表示记录本身,使用one2many字段(例如line_ids)表示行数据" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -117,34 +96,22 @@ msgstr "工作表: excel工作表的名称(例如,工作表1)或索 #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to " -"export data to" -msgstr "" -"工作表: 要将数据导出到的Excel工作表的名称(例如,工作表1)或索引(例" -"如,1)" +msgid "Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to" +msgstr "工作表: 要将数据导出到的Excel工作表的名称(例如,工作表1)或索引(例如,1)" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Style w/Cond.: Conditional style by python code in #?...?, e.g., apply style for specific product, #?value.name == \"ABC\" " -"and #{font=bold;fill=red} or None?" +msgid "Style w/Cond.: Conditional style by python code in #?...?, e.g., apply style for specific product, #?value.name == \"ABC\" and #{font=bold;fill=red} or None?" msgstr "" -"样式条件: python代码中的条件样式 #?...?, 例如,为特定产品" -"应用样式, #?value.name == \"ABC\" and #{font=bold;fill=red} or None?" +"样式条件: python代码中的条件样式 #?...?, 例如,为特定产品应用样式, #" +"?value.name == \"ABC\" and #{font=bold;fill=red} or None?" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Style: Default style in #{...} that apply to each cell, " -"e.g., #{align=left;style=text}. See module's style.py " -"for available styles." +msgid "Style: Default style in #{...} that apply to each cell, e.g., #{align=left;style=text}. See module's style.py for available styles." msgstr "" "样式: 默认样式#{...} 适用于每个单元格, 例如, " -"#{align=left;style=text}. 请参阅模块的style.py 可用的样" -"式。" +"#{align=left;style=text}. 请参阅模块的style.py 可用的样式。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -153,20 +120,16 @@ msgstr "总和: 在最后一行添加总和值, @{sum}" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"\n" +msgid "\n" "{\n" " '__EXPORT__': {\n" " 'sale_order': { # sheet can be name (string) or index (integer)\n" " '_HEAD_': {\n" -" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;" -"style=text}',\n" +" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;style=text}',\n" " 'B3': 'name${value or \"\"}#{align=left;style=text}',\n" " },\n" -" 'line_ids': { # prefix with _CONT_ to continue rows from " -"previous row field\n" -" 'A6': 'product_id.display_name${value or \"\"}" -"#{style=text}',\n" +" 'line_ids': { # prefix with _CONT_ to continue rows from previous row field\n" +" 'A6': 'product_id.display_name${value or \"\"}#{style=text}',\n" " 'C6': 'product_uom_qty${value or 0}#{style=number}',\n" " 'E6': 'price_unit${value or 0}#{style=number}',\n" " 'G6': 'price_subtotal${value or 0}#{style=number}',\n" @@ -175,8 +138,7 @@ msgid "" " },\n" " '__IMPORT__': {\n" " 'sale_order': { # sheet can be name (string) or index (integer)\n" -" 'order_line': { # prefix with _NODEL_ to not delete rows before " -"import\n" +" 'order_line': { # prefix with _NODEL_ to not delete rows before import\n" " 'A6': 'product_id',\n" " 'C6': 'product_uom_qty',\n" " 'E6': 'price_unit${value > 0 and value or 0}',\n" @@ -193,14 +155,14 @@ msgstr "" " '__EXPORT__': {\n" " 'sale_order': { # 工作表名称可以是(字符串)或索引(整数)\n" " '_HEAD_': {\n" -" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;" -"style=text}',\n" +" 'B2': 'partner_id.display_name${value or \"\"" +"}#{align=left;style=text}',\n" " 'B3': 'name${value or \"\"}#{align=left;style=text}',\n" " },\n" " 'line_ids': { # prefix with _CONT_ to continue rows from " "previous row field\n" -" 'A6': 'product_id.display_name${value or \"\"}" -"#{style=text}',\n" +" 'A6': 'product_id.display_name${value or \"\"}#{style=text}'," +"\n" " 'C6': 'product_uom_qty${value or 0}#{style=number}',\n" " 'E6': 'price_unit${value or 0}#{style=number}',\n" " 'G6': 'price_subtotal${value or 0}#{style=number}',\n" @@ -234,8 +196,7 @@ msgstr "model: 活动模型,例如., self.env['my.model']" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"object: record object or line object depends on Row Field" +msgid "object: record object or line object depends on Row Field" msgstr "object: 记录对象或行对象依赖于 行字段" #. module: excel_import_export @@ -252,11 +213,8 @@ msgstr "value: 值来自 字段" #. openerp-web #: code:addons/excel_import_export/static/src/js/report/action_manager_report.js:49 #, python-format -msgid "" -"A popup window with your report was blocked. You may need to change your " -"browser settings to allow popup windows for this page." -msgstr "" -"您的报告的弹出窗口被阻止。您可能需要更改浏览器设置以允许此页面的弹出窗口。" +msgid "A popup window with your report was blocked. You may need to change your browser settings to allow popup windows for this page." +msgstr "您的报告的弹出窗口被阻止。您可能需要更改浏览器设置以允许此页面的弹出窗口。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -283,18 +241,6 @@ msgstr "添加工作表部分" msgid "Allow Choose Template" msgstr "添加表单部分" -#. module: excel_import_export -#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__attachment_ids -#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__attachment_ids -msgid "Attachments" -msgstr "" - #. module: excel_import_export #: model:ir.model,name:excel_import_export.model_xlsx_styles msgid "Available styles for excel" @@ -302,8 +248,7 @@ msgstr "excel的可用样式" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template_import__no_delete -msgid "" -"By default, all rows will be deleted before import.\n" +msgid "By default, all rows will be deleted before import.\n" "Select No Delete, otherwise" msgstr "" "默认情况下,导入前将删除所有行。\n" @@ -326,8 +271,7 @@ msgstr "CSV引用" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template__post_import_hook -msgid "" -"Call a function after successful import, i.e.,\n" +msgid "Call a function after successful import, i.e.,\n" "${object.post_import_do_something()}" msgstr "" "成功导入后调用函数,即,\n" @@ -347,7 +291,8 @@ msgid "Cell" msgstr "单元格" #. module: excel_import_export -#: selection:export.xlsx.wizard,state:0 selection:import.xlsx.wizard,state:0 +#: selection:export.xlsx.wizard,state:0 +#: selection:import.xlsx.wizard,state:0 #: selection:xlsx.report,state:0 msgid "Choose" msgstr "选择" @@ -466,8 +411,7 @@ msgstr "文件必须处于草稿状态" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_import.py:89 #, python-format -msgid "" -"Error deleting data\n" +msgid "Error deleting data\n" "%s" msgstr "" "删除数据时出错\n" @@ -476,29 +420,16 @@ msgstr "" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:141 #, python-format -msgid "" -"Error filling data into Excel sheets\n" +msgid "Error filling data into Excel sheets\n" "%s" msgstr "" "将数据填充到Excel工作表时出错\n" "%s" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:212 -#, fuzzy, python-format -#| msgid "" -#| "Error importing data\n" -#| "%s" -msgid "Error importing data" -msgstr "" -"导入数据时出错\n" -"%s" - -#. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:224 +#: code:addons/excel_import_export/models/xlsx_import.py:219 #, python-format -msgid "" -"Error importing data\n" +msgid "Error importing data\n" "%s" msgstr "" "导入数据时出错\n" @@ -553,25 +484,17 @@ msgstr "导出" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Export Instruction is how to write data from an active data record to " -"specified cells in excel sheet.\n" -" For example, an active record can be a " -"sale order that user want to export.\n" -" The record itself will be mapped to the " -"header part of excel sheet. The record can contain multiple one2many fields, " -"which will be written as data lines.\n" -" You can look at following instruction as " -"Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row " -"sections (one2many fields)." +msgid "Export Instruction is how to write data from an active data record to specified cells in excel sheet.\n" +" For example, an active record can be a sale order that user want to export.\n" +" The record itself will be mapped to the header part of excel sheet. The record can contain multiple one2many fields, which will be written as data lines.\n" +" You can look at following instruction as Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row sections (one2many fields)." msgstr "" "导出指令是如何将数据从活动数据记录写入Excel工作表中的指定单元格。\n" -" 例如,活动记录可以是用户想要导出的销售订" -"单。\n" -" 记录本身将映射到Excel工作表的标题部分。该记" -"录可以包含多个one2many字段,这些字段将写为数据行。\n" -" 您可以将以下指令看作Excel工作表,每个工作表" -"有1个标题部分(_HEAD_)和多个行部分(one2many字段)。" +" 例如,活动记录可以是用户想要导出的销售订单。\n" +" " +"记录本身将映射到Excel工作表的标题部分。该记录可以包含多个one2many字段,这些字段将写为数据行。\n" +" " +"您可以将以下指令看作Excel工作表,每个工作表有1个标题部分(_HEAD_)和多个行部分(one2many字段)。" #. module: excel_import_export #: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__field_name @@ -616,20 +539,15 @@ msgstr "以下是每列的详细说明:" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Following show very simple example of the dictionary construct.\n" -" Normally, this will be within templates." -"xml file within addons." +msgid "Following show very simple example of the dictionary construct.\n" +" Normally, this will be within templates.xml file within addons." msgstr "" "下面显示了字典构造的非常简单的示例。\n" -" 通常,这将位于插件中的templates.xml文件" -"中。" +" 通常,这将位于插件中的templates.xml文件中。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"For code block ${...} and #?...?, following object " -"are available," +msgid "For code block ${...} and #?...?, following object are available," msgstr "对于代码块 ${...}#?...?, 以下对象可用," #. module: excel_import_export @@ -638,7 +556,8 @@ msgid "For code block ${...}, following object are available," msgstr "对于代码块${...}, 以下对象可用," #. module: excel_import_export -#: selection:export.xlsx.wizard,state:0 selection:import.xlsx.wizard,state:0 +#: selection:export.xlsx.wizard,state:0 +#: selection:import.xlsx.wizard,state:0 #: selection:xlsx.report,state:0 msgid "Get" msgstr "获取" @@ -706,8 +625,7 @@ msgstr "如果部分类型是行,则此字段是必需的" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:137 #, python-format -msgid "" -"IllegalCharacterError\n" +msgid "IllegalCharacterError\n" "Some exporting data contain special character\n" "%s" msgstr "" @@ -745,24 +663,16 @@ msgstr "导入文件(*.xlsx)" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"Import Instruction is how to get data from excel sheet and write them to an " -"active record.\n" -" For example, user create a sales order " -"document, and want to import order lines from excel.\n" -" In reverse direction to exporting, data " -"from excel's cells will be mapped to record fields during import.\n" -" Cells can be mapped to record in header " -"section (_HEAD_) and data table can be mapped to row section (one2many " -"field, begins from specifed cells." +msgid "Import Instruction is how to get data from excel sheet and write them to an active record.\n" +" For example, user create a sales order document, and want to import order lines from excel.\n" +" In reverse direction to exporting, data from excel's cells will be mapped to record fields during import.\n" +" Cells can be mapped to record in header section (_HEAD_) and data table can be mapped to row section (one2many field, begins from specifed cells." msgstr "" "导入指令是如何从Excel工作表中获取数据并将其写入活动记录。\n" -" 例如,用户创建销售订单文档,并希望从excel导" -"入订单行。\n" -" 与导出相反,excel单元格中的数据将在导入期间" -"映射到记录字段。\n" -" 单元格可以映射到标题部分(_HEAD_)中的记" -"录,数据表可以映射到行部分(one2many字段,从指定的单元格开始)。" +" 例如,用户创建销售订单文档,并希望从excel导入订单行。\n" +" 与导出相反,excel单元格中的数据将在导入期间映射到记录字段。\n" +" " +"单元格可以映射到标题部分(_HEAD_)中的记录,数据表可以映射到行部分(one2many字段,从指定的单元格开始)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard @@ -771,41 +681,27 @@ msgstr "导入成功!" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"In header section part, map data fields (e.g., number, partner_id.name) into " -"cells (e.g., B1, B2)." -msgstr "" -"在头部分,将数据字段(例如,number, partner id.name)映射到单元格(例如,B1, " -"B2)。" +msgid "In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2)." +msgstr "在头部分,将数据字段(例如,number, partner id.name)映射到单元格(例如,B1, B2)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"In header section, map cells (e.g., B1, B2) into data fields (e.g., number, " -"partner_id)." +msgid "In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id)." msgstr "在头部分,将单元格(如B1、B2)映射到数据字段(如number、合作伙伴id)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"In row section, data list will be rolled out from one2many row field (e.g., " -"order_line), and map data field (i.e., product_id.name, uom_id.name, qty) " -"into the first row cells to start rolling (e.g., A6, B6, C6)." +msgid "In row section, data list will be rolled out from one2many row field (e.g., order_line), and map data field (i.e., product_id.name, uom_id.name, qty) into the first row cells to start rolling (e.g., A6, B6, C6)." msgstr "" -"在行部分中,数据列表将从one2many行字段(例如,order_line)推出,并将数据字段" -"(即product_id.name,uom_id.name,qty)映射到第一行单元格以开始滚动(例如," -"A6,B6,C6)。" +"在行部分中,数据列表将从one2many行字段(例如,order_line)推出,并将数据字段(即product_id.name,uom_id.name," +"qty)映射到第一行单元格以开始滚动(例如,A6,B6,C6)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "" -"In row section, data table from excel can be imported to one2many row field " -"(e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) " -"to fields (e.g., product_id, uom_id, qty)" +msgid "In row section, data table from excel can be imported to one2many row field (e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) to fields (e.g., product_id, uom_id, qty)" msgstr "" -"在行部分中,可以通过将第一行上的单元格(例如,A6,B6,C6)映射到字段(例如," -"product_id,uom_id,qty),将来自excel的数据表导入到one2many行字段(例如," -"order_line)" +"在行部分中,可以通过将第一行上的单元格(例如,A6,B6,C6)映射到字段(例如,product_id,uom_id,qty),将来自excel的数据表导" +"入到one2many行字段(例如,order_line)" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -834,7 +730,7 @@ msgid "Invalid declaration, %s has no valid field type" msgstr "声明无效,%s没有有效的字段类型" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:222 +#: code:addons/excel_import_export/models/xlsx_import.py:217 #, python-format msgid "Invalid file style, only .xls or .xlsx file allowed" msgstr "文件样式无效,仅允许.xls或.xlsx文件" @@ -854,8 +750,7 @@ msgstr "样式类型%s的值%s无效" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:134 #, python-format -msgid "" -"Key Error\n" +msgid "Key Error\n" "%s" msgstr "" "关键字错误\n" @@ -894,8 +789,7 @@ msgstr "最后更新时间" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template__gname -msgid "" -"Multiple template of same model, can belong to same group,\n" +msgid "Multiple template of same model, can belong to same group,\n" "result in multiple template selection" msgstr "" "同一模型的多个模板,可以属于同一组,\n" @@ -907,7 +801,7 @@ msgid "No Delete" msgstr "不删除" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:254 +#: code:addons/excel_import_export/models/xlsx_import.py:249 #, python-format msgid "No data_dict['__IMPORT__'] in template %s" msgstr "模版%s中没有 data_dict['__IMPORT__']" @@ -988,10 +882,9 @@ msgid "Post Import Hook" msgstr "导入后挂钩" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:237 +#: code:addons/excel_import_export/models/xlsx_import.py:232 #, python-format -msgid "" -"Post import operation error\n" +msgid "Post import operation error\n" "%s" msgstr "" "发布导入操作错误\n" @@ -1130,14 +1023,12 @@ msgstr "模板名称" #. module: excel_import_export #: code:addons/excel_import_export/models/common.py:242 #, python-format -msgid "" -"Template with CSV Quoting = False, data must not contain the same char as " -"delimiter -> \"%s\"" +msgid "Template with CSV Quoting = False, data must not contain the same char as delimiter -> \"%s\"" msgstr "CSV Quoting =False的模板,数据不能包含与分隔符相同的字符->\"%s\"" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_export.py:228 -#: code:addons/excel_import_export/models/xlsx_import.py:249 +#: code:addons/excel_import_export/models/xlsx_export.py:230 +#: code:addons/excel_import_export/models/xlsx_import.py:244 #, python-format msgid "Template's model mismatch" msgstr "模板的模型不匹配" @@ -1160,14 +1051,8 @@ msgstr "选定的重定向操作不适用于模型%s" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_ir_actions_report__report_type -msgid "" -"The type of the report that will be rendered, each one having its own " -"rendering method. HTML means the report will be opened directly in your " -"browser PDF means the report will be rendered using Wkhtmltopdf and " -"downloaded by the user." -msgstr "" -"将呈现的报告类型,每个报告都有自己的呈现方法。HTML表示报告将直接在浏览器中打" -"开PDF表示报告将使用Wkhtmltopdf呈现并由用户下载。" +msgid "The type of the report that will be rendered, each one having its own rendering method. HTML means the report will be opened directly in your browser PDF means the report will be rendered using Wkhtmltopdf and downloaded by the user." +msgstr "将呈现的报告类型,每个报告都有自己的呈现方法。HTML表示报告将直接在浏览器中打开PDF表示报告将使用Wkhtmltopdf呈现并由用户下载。" #. module: excel_import_export #: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:89 @@ -1177,8 +1062,7 @@ msgstr "此导入操作在此文档上下文中不可用" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template__show_instruction -msgid "" -"This is the computed instruction based on tab Import/Export,\n" +msgid "This is the computed instruction based on tab Import/Export,\n" "to be used by xlsx import/export engine" msgstr "" "这是基于选项卡导入/导出的计算指令,\n" diff --git a/excel_import_export/models/__init__.py b/excel_import_export/models/__init__.py index 52501c936..04a47be59 100644 --- a/excel_import_export/models/__init__.py +++ b/excel_import_export/models/__init__.py @@ -7,3 +7,17 @@ from . import xlsx_import from . import xlsx_template from . import xlsx_report from . import ir_report + +# +# +# INSERT INTO "purchase_order_line" ( +# "id", "create_uid", "create_date", +# "write_uid", "write_date", "date_planned", +# "display_type", "name", "order_id", +# "price_unit", "product_qty", "product_uom", +# "sequence") VALUES ( +# nextval('purchase_order_line_id_seq'), 2, (now() at time zone 'UTC'), +# 2, (now() at time zone 'UTC'), '2020-10-05 09:39:28', +# NULL, '[FURN_0269] Office Chair Black', 8, +# '11111.00', '5.000', 1, +# 10) diff --git a/excel_import_export/models/common.py b/excel_import_export/models/common.py index e0314904f..296ddd982 100644 --- a/excel_import_export/models/common.py +++ b/excel_import_export/models/common.py @@ -102,7 +102,7 @@ def get_field_style_cond(field): def fill_cell_style(field, field_style, styles): - field_styles = field_style.split(";") + field_styles = field_style.split(";") if field_style else [] for f in field_styles: (key, value) = f.split("=") if key not in styles.keys(): diff --git a/excel_import_export/models/styles.py b/excel_import_export/models/styles.py index 2c753f345..48d2bdaa6 100644 --- a/excel_import_export/models/styles.py +++ b/excel_import_export/models/styles.py @@ -8,7 +8,7 @@ from odoo import api, models _logger = logging.getLogger(__name__) try: - from openpyxl.styles import colors, PatternFill, Alignment, Font + from openpyxl.styles import PatternFill, Alignment, Font except ImportError: _logger.debug('Cannot import "openpyxl". Please make sure it is installed.') @@ -23,7 +23,7 @@ class XLSXStyles(models.AbstractModel): return { "font": { "bold": Font(name="Arial", size=10, bold=True), - "bold_red": Font(name="Arial", size=10, color=colors.RED, bold=True), + "bold_red": Font(name="Arial", size=10, color="FF0000", bold=True), }, "fill": { "red": PatternFill("solid", fgColor="FF0000"), diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py index 2aac49b90..a8023f140 100644 --- a/excel_import_export/models/xlsx_export.py +++ b/excel_import_export/models/xlsx_export.py @@ -51,6 +51,7 @@ class XLSXExport(models.AbstractModel): """ line_field, max_row = co.get_line_max(line_field) line_field = line_field.replace("_CONT_", "") # Remove _CONT_ if any + line_field = line_field.replace("_EXTEND_", "") # Remove _EXTEND_ if any lines = record[line_field] if max_row > 0 and len(lines) > max_row: raise Exception(_("Records in %s exceed max records allowed") % line_field) @@ -89,10 +90,7 @@ class XLSXExport(models.AbstractModel): elif style is False: style = field_style_dict[field[0]] # Use default style vals[field[0]].append((value, style)) - return ( - vals, - aggre_func_dict, - ) + return (vals, aggre_func_dict) @api.model def _eval_style_cond(self, model, record, value, style_cond): @@ -185,6 +183,7 @@ class XLSXExport(models.AbstractModel): fields = ws.get(line_field, {}).values() vals, func = self._get_line_vals(record, line_field, fields) is_cont = "_CONT_" in line_field and True or False # continue row + is_extend = "_EXTEND_" in line_field and True or False # extend row cont_set = 0 rows_inserted = False # flag to insert row for rc, field in ws.get(line_field, {}).items(): @@ -200,9 +199,9 @@ class XLSXExport(models.AbstractModel): new_rc = False row_count = len(vals[field]) # Insert rows to preserve total line - if not rows_inserted: + if is_extend and not rows_inserted: rows_inserted = True - st.insert_rows(row + 1, amount=row_count - 1) + st.insert_rows(row + 1, row_count - 1) # -- for (row_val, style) in vals[field]: new_row = row + i @@ -220,6 +219,7 @@ class XLSXExport(models.AbstractModel): new_row += 1 f_rc = "{}{}".format(col, new_row) st[f_rc] = "={}({}:{})".format(f, rc, new_rc) + styles = self.env["xlsx.styles"].get_openpyxl_styles() co.fill_cell_style(st[f_rc], style, styles) cont_row = cont_row < new_row and new_row or cont_row return @@ -255,7 +255,7 @@ class XLSXExport(models.AbstractModel): content = BytesIO() wb.save(content) content.seek(0) # Set index to 0, and start reading - out_file = base64.encodestring(content.read()) + out_file = base64.encodebytes(content.read()) if record and "name" in record and record.name: out_name = record.name.replace(" ", "").replace("/", "") else: diff --git a/excel_import_export/models/xlsx_import.py b/excel_import_export/models/xlsx_import.py index 9af7b4b4f..7c4914938 100644 --- a/excel_import_export/models/xlsx_import.py +++ b/excel_import_export/models/xlsx_import.py @@ -89,18 +89,40 @@ class XLSXImport(models.AbstractModel): if line_field in record and record[line_field]: record[line_field].unlink() # Remove _NODEL_ from dict - for s, sv in data_dict.items(): - for f, fv in data_dict[s].items(): + for s, _sv in data_dict.copy().items(): + for f, _fv in data_dict[s].copy().items(): if "_NODEL_" in f: new_fv = data_dict[s].pop(f) data_dict[s][f.replace("_NODEL_", "")] = new_fv except Exception as e: raise ValidationError(_("Error deleting data\n%s") % e) + @api.model + def _get_end_row(self, st, worksheet, line_field): + """ Get max row or next empty row as the ending row """ + _x, max_row = co.get_line_max(line_field) + test_rows = {} + max_end_row = 0 + for rc, _col in worksheet.get(line_field, {}).items(): + rc, key_eval_cond = co.get_field_condition(rc) + row, col = co.pos2idx(rc) + # Use max_row, i.e., order_line[5], use it. Otherwise, use st.nrows + max_end_row = st.nrows if max_row is False else (row + max_row) + for idx in range(row, max_row and max_end_row or st.nrows): + cell_type = st.cell_type(idx, col) # empty type = 0 + r_types = test_rows.get(idx, []) + r_types.append(cell_type) + test_rows[idx] = r_types + empty_list = filter(lambda y: all(i == 0 for i in y[1]), test_rows.items()) + empty_rows = list(map(lambda z: z[0], empty_list)) + next_empty_row = empty_rows and min(empty_rows) or max_end_row + return next_empty_row + @api.model def _get_line_vals(self, st, worksheet, model, line_field): """ Get values of this field from excel sheet """ vals = {} + end_row = self._get_end_row(st, worksheet, line_field) for rc, columns in worksheet.get(line_field, {}).items(): if not isinstance(columns, list): columns = [columns] @@ -108,10 +130,11 @@ class XLSXImport(models.AbstractModel): rc, key_eval_cond = co.get_field_condition(rc) x_field, val_eval_cond = co.get_field_condition(field) row, col = co.pos2idx(rc) - out_field = "{}/{}".format(line_field, x_field) + new_line_field, _x = co.get_line_max(line_field) + out_field = "{}/{}".format(new_line_field, x_field) field_type = self._get_field_type(model, out_field) vals.update({out_field: []}) - for idx in range(row, st.nrows): + for idx in range(row, end_row): value = co._get_cell_value(st.cell(idx, col), field_type=field_type) eval_context = self.get_eval_context(model=model, value=value) if key_eval_cond: @@ -123,6 +146,53 @@ class XLSXImport(models.AbstractModel): vals.pop(out_field) return vals + @api.model + def _process_worksheet(self, wb, out_wb, out_st, model, data_dict, header_fields): + col_idx = 1 + for sheet_name in data_dict: # For each Sheet + worksheet = data_dict[sheet_name] + st = False + if isinstance(sheet_name, str): + st = co.xlrd_get_sheet_by_name(wb, sheet_name) + elif isinstance(sheet_name, int): + st = wb.sheet_by_index(sheet_name - 1) + if not st: + raise ValidationError(_("Sheet %s not found") % sheet_name) + # HEAD updates + for rc, field in worksheet.get("_HEAD_", {}).items(): + rc, key_eval_cond = co.get_field_condition(rc) + field, val_eval_cond = co.get_field_condition(field) + field_type = self._get_field_type(model, field) + value = False + try: + row, col = co.pos2idx(rc) + value = co._get_cell_value(st.cell(row, col), field_type=field_type) + except Exception: + pass + eval_context = self.get_eval_context(model=model, value=value) + if key_eval_cond: + value = str(safe_eval(key_eval_cond, eval_context)) + if val_eval_cond: + value = str(safe_eval(val_eval_cond, eval_context)) + out_st.write(0, col_idx, field) # Next Column + out_st.write(1, col_idx, value) # Next Value + header_fields.append(field) + col_idx += 1 + # Line Items + line_fields = filter(lambda x: x != "_HEAD_", worksheet) + for line_field in line_fields: + vals = self._get_line_vals(st, worksheet, model, line_field) + for field in vals: + # Columns, i.e., line_ids/field_id + out_st.write(0, col_idx, field) + header_fields.append(field) + # Data + i = 1 + for value in vals[field]: + out_st.write(i, col_idx, value) + i += 1 + col_idx += 1 + @api.model def _import_record_data(self, import_file, record, data_dict): """ From complex excel, create temp simple excel and do import """ @@ -130,9 +200,9 @@ class XLSXImport(models.AbstractModel): return try: header_fields = [] - decoded_data = base64.decodestring(import_file) + model = record._name + decoded_data = base64.decodebytes(import_file) wb = xlrd.open_workbook(file_contents=decoded_data) - col_idx = 0 out_wb = xlwt.Workbook() out_st = out_wb.add_sheet("Sheet 1") xml_id = ( @@ -143,53 +213,9 @@ class XLSXImport(models.AbstractModel): out_st.write(0, 0, "id") # id and xml_id on first column out_st.write(1, 0, xml_id) header_fields.append("id") - col_idx += 1 - model = record._name - for sheet_name in data_dict: # For each Sheet - worksheet = data_dict[sheet_name] - st = False - if isinstance(sheet_name, str): - st = co.xlrd_get_sheet_by_name(wb, sheet_name) - elif isinstance(sheet_name, int): - st = wb.sheet_by_index(sheet_name - 1) - if not st: - raise ValidationError(_("Sheet %s not found") % sheet_name) - # HEAD updates - for rc, field in worksheet.get("_HEAD_", {}).items(): - rc, key_eval_cond = co.get_field_condition(rc) - field, val_eval_cond = co.get_field_condition(field) - field_type = self._get_field_type(model, field) - value = False - try: - row, col = co.pos2idx(rc) - value = co._get_cell_value( - st.cell(row, col), field_type=field_type - ) - except Exception: - pass - eval_context = self.get_eval_context(model=model, value=value) - if key_eval_cond: - value = str(safe_eval(key_eval_cond, eval_context)) - if val_eval_cond: - value = str(safe_eval(val_eval_cond, eval_context)) - out_st.write(0, col_idx, field) # Next Column - out_st.write(1, col_idx, value) # Next Value - header_fields.append(field) - col_idx += 1 - # Line Items - line_fields = filter(lambda x: x != "_HEAD_", worksheet) - for line_field in line_fields: - vals = self._get_line_vals(st, worksheet, model, line_field) - for field in vals: - # Columns, i.e., line_ids/field_id - out_st.write(0, col_idx, field) - header_fields.append(field) - # Data - i = 1 - for value in vals[field]: - out_st.write(i, col_idx, value) - i += 1 - col_idx += 1 + # Process on all worksheets + self._process_worksheet(wb, out_wb, out_st, model, data_dict, header_fields) + # -- content = BytesIO() out_wb.save(content) content.seek(0) # Set index to 0, and start reading @@ -235,7 +261,7 @@ class XLSXImport(models.AbstractModel): _("Invalid file style, only .xls or .xlsx file allowed") ) except Exception as e: - raise ValidationError(_("Error importing data\n%s") % e) + raise e @api.model def _post_import_operation(self, record, operation): diff --git a/excel_import_export/models/xlsx_report.py b/excel_import_export/models/xlsx_report.py index fdc741458..e963752bf 100644 --- a/excel_import_export/models/xlsx_report.py +++ b/excel_import_export/models/xlsx_report.py @@ -11,8 +11,8 @@ class XLSXReport(models.AbstractModel): _name = "xlsx.report" _description = "Excel Report AbstractModel" - name = fields.Char(string="File Name", readonly=True, size=500,) - data = fields.Binary(string="File", readonly=True,) + name = fields.Char(string="File Name", readonly=True, size=500) + data = fields.Binary(string="File", readonly=True) template_id = fields.Many2one( "xlsx.template", string="Template", @@ -20,7 +20,7 @@ class XLSXReport(models.AbstractModel): ondelete="cascade", domain=lambda self: self._context.get("template_domain", []), ) - choose_template = fields.Boolean(string="Allow Choose Template", default=False,) + choose_template = fields.Boolean(string="Allow Choose Template", default=False) state = fields.Selection( [("choose", "Choose"), ("get", "Get")], default="choose", @@ -41,7 +41,6 @@ class XLSXReport(models.AbstractModel): defaults["template_id"] = len(templates) == 1 and templates.id or False return defaults - @api.multi def report_xlsx(self): self.ensure_one() Export = self.env["xlsx.export"] @@ -51,7 +50,6 @@ class XLSXReport(models.AbstractModel): "type": "ir.actions.act_window", "res_model": self._name, "view_mode": "form", - "view_type": "form", "res_id": self.id, "views": [(False, "form")], "target": "new", diff --git a/excel_import_export/models/xlsx_template.py b/excel_import_export/models/xlsx_template.py index e34528042..622e2ce68 100644 --- a/excel_import_export/models/xlsx_template.py +++ b/excel_import_export/models/xlsx_template.py @@ -7,7 +7,7 @@ from ast import literal_eval from os.path import join as opj from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.modules.module import get_module_path from . import common as co @@ -24,18 +24,18 @@ class XLSXTemplate(models.Model): _description = "Excel template file and instruction" _order = "name" - name = fields.Char(string="Template Name", required=True,) + name = fields.Char(string="Template Name", required=True) res_model = fields.Char( string="Resource Model", help="The database object this attachment will be attached to.", ) - fname = fields.Char(string="File Name",) + fname = fields.Char(string="File Name") gname = fields.Char( string="Group Name", help="Multiple template of same model, can belong to same group,\n" "result in multiple template selection", ) - description = fields.Char(string="Description",) + description = fields.Char(string="Description") input_instruction = fields.Text( string="Instruction (Input)", help="This is used to construct instruction in tab Import/Export", @@ -45,8 +45,12 @@ class XLSXTemplate(models.Model): compute="_compute_output_instruction", help="Instruction on how to import/export, prepared by system.", ) - datas = fields.Binary(string="File Content",) - to_csv = fields.Boolean(string="Convert to CSV?", default=False,) + datas = fields.Binary(string="File Content") + to_csv = fields.Boolean( + string="Convert to CSV?", + default=False, + help="Convert file into CSV format on export", + ) csv_delimiter = fields.Char( string="CSV Delimiter", size=1, @@ -67,10 +71,10 @@ class XLSXTemplate(models.Model): help="Optional for CSV, default is full quoting.", ) export_ids = fields.One2many( - comodel_name="xlsx.template.export", inverse_name="template_id", + comodel_name="xlsx.template.export", inverse_name="template_id" ) import_ids = fields.One2many( - comodel_name="xlsx.template.import", inverse_name="template_id", + comodel_name="xlsx.template.import", inverse_name="template_id" ) post_import_hook = fields.Char( string="Post Import Function Hook", @@ -89,8 +93,36 @@ class XLSXTemplate(models.Model): domain=[("type", "=", "ir.actions.act_window")], help="Optional action, redirection after finish import operation", ) + # Utilities + export_action_id = fields.Many2one( + comodel_name="ir.actions.act_window", ondelete="set null", + ) + import_action_id = fields.Many2one( + comodel_name="ir.actions.act_window", ondelete="set null", + ) + use_report_wizard = fields.Boolean( + string="Easy Reporting", + help="Use common report wizard model, instead of create specific model", + ) + result_model_id = fields.Many2one( + comodel_name="ir.model", + string="Report Model", + help="When use commone wizard, choose the result model", + ) + result_field = fields.Char(compute="_compute_result_field",) + report_menu_id = fields.Many2one( + comodel_name="ir.ui.menu", string="Report Menu", readonly=True, + ) + report_action_id = fields.Many2one( + comodel_name="ir.actions.report", string="Report Action", + ) + + def _compute_result_field(self): + for rec in self: + rec.result_field = ( + ("x_%s_results" % rec.id) if rec.result_model_id else False + ) - @api.multi @api.constrains("redirect_action", "res_model") def _check_action_model(self): for rec in self: @@ -105,13 +137,13 @@ class XLSXTemplate(models.Model): ) @api.model - def load_xlsx_template(self, tempalte_ids, addon=False): - for template in self.browse(tempalte_ids): + def load_xlsx_template(self, template_ids, addon=False): + for template in self.browse(template_ids): if not addon: addon = list(template.get_external_id().values())[0].split(".")[0] addon_path = get_module_path(addon) file_path = False - for root, dirs, files in os.walk(addon_path): + for root, _dirs, files in os.walk(addon_path): for name in files: if name == template.fname: file_path = os.path.abspath(opj(root, name)) @@ -126,9 +158,11 @@ class XLSXTemplate(models.Model): rec._compute_input_export_instruction() rec._compute_input_import_instruction() rec._compute_input_post_import_hook() + if vals.get("result_model_id"): + rec._update_result_field_common_wizard() + rec._update_result_export_ids() return rec - @api.multi def write(self, vals): res = super().write(vals) if vals.get("input_instruction"): @@ -136,9 +170,89 @@ class XLSXTemplate(models.Model): rec._compute_input_export_instruction() rec._compute_input_import_instruction() rec._compute_input_post_import_hook() + if vals.get("result_model_id"): + for rec in self: + rec._update_result_field_common_wizard() + rec._update_result_export_ids() return res - @api.multi + def unlink(self): + self.env["ir.model.fields"].search( + [ + ("model", "=", "report.xlsx.wizard"), + ("name", "=", self.mapped("result_field")), + ] + ).unlink() + return super().unlink() + + def _update_result_field_common_wizard(self): + self.ensure_one() + _model = self.env["ir.model"].search([("model", "=", "report.xlsx.wizard")]) + _model.ensure_one() + _field = self.env["ir.model.fields"].search( + [("model", "=", "report.xlsx.wizard"), ("name", "=", self.result_field)] + ) + if not _field: + _field = self.env["ir.model.fields"].create( + { + "model_id": _model.id, + "name": self.result_field, + "field_description": "Results", + "ttype": "many2many", + "relation": self.result_model_id.model, + "store": False, + "depends": "res_model", + } + ) + else: + _field.ensure_one() + _field.write({"relation": self.result_model_id.model}) + _field.compute = """ +self['%s'] = self.env['%s'].search(self.safe_domain(self.domain)) + """ % ( + self.result_field, + self.result_model_id.model, + ) + + def _update_result_export_ids(self): + self.ensure_one() + results = self.env["xlsx.template.export"].search( + [("template_id", "=", self.id), ("row_field", "=", self.result_field)] + ) + if not results: + self.export_ids.unlink() + self.write( + { + "export_ids": [ + (0, 0, {"sequence": 10, "section_type": "sheet", "sheet": 1}), + ( + 0, + 0, + { + "sequence": 20, + "section_type": "row", + "row_field": self.result_field, + }, + ), + ( + 0, + 0, + { + "sequence": 30, + "section_type": "data", + "excel_cell": "A1", + "field_name": "id", + }, + ), + ], + } + ) + + @api.onchange("use_report_wizard") + def _onchange_use_report_wizard(self): + self.res_model = "report.xlsx.wizard" if self.use_report_wizard else False + self.redirect_action = False + def _compute_input_export_instruction(self): self = self.with_context(compute_from_input=True) for rec in self: @@ -166,11 +280,16 @@ class XLSXTemplate(models.Model): if "_CONT_" in row_field: is_cont = True row_field = row_field.replace("_CONT_", "") + is_extend = False + if "_EXTEND_" in row_field: + is_extend = True + row_field = row_field.replace("_EXTEND_", "") vals = { "sequence": sequence, "section_type": (row_field == "_HEAD_" and "head" or "row"), "row_field": row_field, "is_cont": is_cont, + "is_extend": is_extend, } export_lines.append((0, 0, vals)) for excel_cell, field_name in lines.items(): @@ -184,7 +303,6 @@ class XLSXTemplate(models.Model): export_lines.append((0, 0, vals)) rec.write({"export_ids": export_lines}) - @api.multi def _compute_input_import_instruction(self): self = self.with_context(compute_from_input=True) for rec in self: @@ -230,7 +348,6 @@ class XLSXTemplate(models.Model): import_lines.append((0, 0, vals)) rec.write({"import_ids": import_lines}) - @api.multi def _compute_input_post_import_hook(self): self = self.with_context(compute_from_input=True) for rec in self: @@ -238,7 +355,6 @@ class XLSXTemplate(models.Model): input_dict = literal_eval(rec.input_instruction.strip()) rec.post_import_hook = input_dict.get("__POST_IMPORT__") - @api.multi def _compute_output_instruction(self): """ From database, compute back to dictionary """ for rec in self: @@ -259,6 +375,8 @@ class XLSXTemplate(models.Model): row_field = line.row_field if line.section_type == "row" and line.is_cont: row_field = "_CONT_%s" % row_field + if line.section_type == "row" and line.is_extend: + row_field = "_EXTEND_%s" % row_field row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field @@ -305,6 +423,101 @@ class XLSXTemplate(models.Model): inst_dict[itype] = rec.post_import_hook rec.instruction = inst_dict + def add_export_action(self): + self.ensure_one() + vals = { + "name": "Export Excel", + "res_model": "export.xlsx.wizard", + "binding_model_id": self.env["ir.model"] + .search([("model", "=", self.res_model)]) + .id, + "binding_type": "action", + "target": "new", + "view_mode": "form", + "context": """ + {'template_domain': [('res_model', '=', '%s'), + ('fname', '=', '%s'), + ('gname', '=', False)]} + """ + % (self.res_model, self.fname), + } + action = self.env["ir.actions.act_window"].create(vals) + self.export_action_id = action + + def remove_export_action(self): + self.ensure_one() + if self.export_action_id: + self.export_action_id.unlink() + + def add_import_action(self): + self.ensure_one() + vals = { + "name": "Import Excel", + "res_model": "import.xlsx.wizard", + "binding_model_id": self.env["ir.model"] + .search([("model", "=", self.res_model)]) + .id, + "binding_type": "action", + "target": "new", + "view_mode": "form", + "context": """ + {'template_domain': [('res_model', '=', '%s'), + ('fname', '=', '%s'), + ('gname', '=', False)]} + """ + % (self.res_model, self.fname), + } + action = self.env["ir.actions.act_window"].create(vals) + self.import_action_id = action + + def remove_import_action(self): + self.ensure_one() + if self.import_action_id: + self.import_action_id.unlink() + + def add_report_menu(self): + self.ensure_one() + if not self.fname: + raise UserError(_("No file content!")) + # Create report action + vals = { + "name": self.name, + "report_type": "excel", + "model": "report.xlsx.wizard", + "report_name": self.fname, + "report_file": self.fname, + } + report_action = self.env["ir.actions.report"].create(vals) + self.report_action_id = report_action + # Create window action + vals = { + "name": self.name, + "res_model": "report.xlsx.wizard", + "binding_type": "action", + "target": "new", + "view_mode": "form", + "context": { + "report_action_id": report_action.id, + "default_res_model": self.result_model_id.model, + }, + } + action = self.env["ir.actions.act_window"].create(vals) + # Create menu + vals = { + "name": self.name, + "action": "{},{}".format(action._name, action.id), + } + menu = self.env["ir.ui.menu"].create(vals) + self.report_menu_id = menu + + def remove_report_menu(self): + self.ensure_one() + if self.report_action_id: + self.report_action_id.unlink() + if self.report_menu_id: + self.report_menu_id.action.unlink() + self.report_menu_id.unlink() + class XLSXTemplateImport(models.Model): _name = "xlsx.template.import" @@ -318,15 +531,15 @@ class XLSXTemplateImport(models.Model): ondelete="cascade", readonly=True, ) - sequence = fields.Integer(string="Sequence", default=10,) - sheet = fields.Char(string="Sheet",) + sequence = fields.Integer(string="Sequence", default=10) + sheet = fields.Char(string="Sheet") section_type = fields.Selection( [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")], string="Section Type", required=True, ) row_field = fields.Char( - string="Row Field", help="If section type is row, this field is required", + string="Row Field", help="If section type is row, this field is required" ) no_delete = fields.Boolean( string="No Delete", @@ -334,9 +547,9 @@ class XLSXTemplateImport(models.Model): help="By default, all rows will be deleted before import.\n" "Select No Delete, otherwise", ) - excel_cell = fields.Char(string="Cell",) - field_name = fields.Char(string="Field",) - field_cond = fields.Char(string="Field Cond.",) + excel_cell = fields.Char(string="Cell") + field_name = fields.Char(string="Field") + field_cond = fields.Char(string="Field Cond.") @api.model def create(self, vals): @@ -348,9 +561,7 @@ class XLSXTemplateImport(models.Model): if self._context.get("compute_from_input") and vals.get("field_name"): field_name, field_cond = co.get_field_condition(vals["field_name"]) field_cond = field_cond and "${%s}" % (field_cond or "") or False - vals.update( - {"field_name": field_name, "field_cond": field_cond,} - ) + vals.update({"field_name": field_name, "field_cond": field_cond}) return vals @@ -366,25 +577,30 @@ class XLSXTemplateExport(models.Model): ondelete="cascade", readonly=True, ) - sequence = fields.Integer(string="Sequence", default=10,) - sheet = fields.Char(string="Sheet",) + sequence = fields.Integer(string="Sequence", default=10) + sheet = fields.Char(string="Sheet") section_type = fields.Selection( [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")], string="Section Type", required=True, ) row_field = fields.Char( - string="Row Field", help="If section type is row, this field is required", + string="Row Field", help="If section type is row, this field is required" ) is_cont = fields.Boolean( - string="Continue", default=False, help="Continue data rows after last data row", + string="Continue", default=False, help="Continue data rows after last data row" ) - excel_cell = fields.Char(string="Cell",) - field_name = fields.Char(string="Field",) - field_cond = fields.Char(string="Field Cond.",) - is_sum = fields.Boolean(string="Sum", default=False,) - style = fields.Char(string="Default Style",) - style_cond = fields.Char(string="Style w/Cond.",) + is_extend = fields.Boolean( + string="Extend", + default=False, + help="Extend a blank row after filling each record, to extend the footer", + ) + excel_cell = fields.Char(string="Cell") + field_name = fields.Char(string="Field") + field_cond = fields.Char(string="Field Cond.") + is_sum = fields.Boolean(string="Sum", default=False) + style = fields.Char(string="Default Style") + style_cond = fields.Char(string="Style w/Cond.") @api.model def create(self, vals): diff --git a/excel_import_export/readme/HISTORY.rst b/excel_import_export/readme/HISTORY.rst index c6e2a27cf..8aa1176a5 100644 --- a/excel_import_export/readme/HISTORY.rst +++ b/excel_import_export/readme/HISTORY.rst @@ -1,3 +1,11 @@ +13.0.1.0.0 (2020-08-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Migration to Odoo 13 +* Enhancement on usability, to be more configurable instead of coding + * Import/Export now have "Add/Remove Export Action", "Add/Remove Import Action" + * "Easy Report" option which allow user to create export directly by config only. + 12.0.1.0.5 (2019-12-19) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/excel_import_export/readme/INSTALL.rst b/excel_import_export/readme/INSTALL.rst index f64a90e5b..f53012c54 100644 --- a/excel_import_export/readme/INSTALL.rst +++ b/excel_import_export/readme/INSTALL.rst @@ -2,4 +2,4 @@ To install this module, you need to install following python library, **xlrd, xl Then, simply install **excel_import_export**. -For demo, install **excel_import_export_demo**. +For demo, install **excel_import_export_demo** diff --git a/excel_import_export/readme/ROADMAP.rst b/excel_import_export/readme/ROADMAP.rst index 21040a2d2..304d073d1 100644 --- a/excel_import_export/readme/ROADMAP.rst +++ b/excel_import_export/readme/ROADMAP.rst @@ -1,2 +1 @@ - Module extension e.g., excel_import_export_async, that add ability to execute as async process. -- Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action. diff --git a/excel_import_export/readme/USAGE.rst b/excel_import_export/readme/USAGE.rst index ef4a1f291..7be6909c8 100644 --- a/excel_import_export/readme/USAGE.rst +++ b/excel_import_export/readme/USAGE.rst @@ -1,3 +1,6 @@ +Concepts +-------- + This module contain pre-defined function and wizards to make exporting, importing and reporting easy. At the heart of this module, there are 2 `main methods` @@ -13,6 +16,9 @@ After install this module, go to Settings > Excel Import/Export > XLSX Templates As this module provide tools, it is best to explain as use cases. For example use cases, please install **excel_import_export_demo** +Use Cases +--------- + **Use Case 1:** Export/Import Excel on existing document This add export/import action menus in existing document (example - excel_import_export_demo/import_export_sale_order) @@ -59,3 +65,24 @@ Please see example in excel_import_export_demo/report_action, which shows, 1. Print excel from an active sale.order 2. Run partner list report based on search criteria. + +Easy Reporting Option +--------------------- + +Technically, this option is the same as "Create Excel Report" use case. But instead of having to write XML / Python code like normally do, +this option allow user to create a report based on a model or view, all by configuration only. + +1. Goto > Technical> Excel Import/Export > XLSX Templates, and create a new template for a report. +2. On the new template, select "Easy Reporting" option, then select followings + - Report Model, this can be data model or data view we want to get the results from. + - Click upload your file and add the excel template (.xlsx) + - Click Save, system will create sample export line, user can add more fields according to results model. +3. Click Add Report Menu, the report menu will be created, user can change its location. Now the report is ready to use. + + .. figure:: ../static/description/xlsx_template.png + :width: 800 px + +Note: Using easy reporting mode, system will used a common criteria wizard. + + .. figure:: ../static/description/common_wizard.png + :width: 800 px diff --git a/excel_import_export/static/description/common_wizard.png b/excel_import_export/static/description/common_wizard.png new file mode 100644 index 0000000000000000000000000000000000000000..0fb954079013d81e1dd8ab76ecef616c6c1e221f GIT binary patch literal 12058 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 literal 0 HcmV?d00001 diff --git a/excel_import_export/static/description/index.html b/excel_import_export/static/description/index.html index e0b8816d6..17eebfbfb 100644 --- a/excel_import_export/static/description/index.html +++ b/excel_import_export/static/description/index.html @@ -379,35 +379,34 @@ ul.auto-toc {

Table of contents

-

Installation

+

Installation

To install this module, you need to install following python library, xlrd, xlwt, openpyxl.

Then, simply install excel_import_export.

For demo, install excel_import_export_demo.

-

Usage

+

Usage

This module contain pre-defined function and wizards to make exporting, importing and reporting easy.

At the heart of this module, there are 2 main methods

    @@ -463,53 +462,47 @@ Please see example in excel_import_export_demo/report_action, which shows,

-

Known issues / Roadmap

+

Known issues / Roadmap

  • Module extension e.g., excel_import_export_async, that add ability to execute as async process.
  • Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action.
-

Changelog

+

Changelog

-

12.0.1.0.5 (2019-12-19)

-
    -
  • Speed up export when dealing with many rows
  • -
-
- -
-

12.0.1.0.3 (2019-08-09)

+
+

12.0.1.0.3 (2019-08-09)

  • Add report action for report_type = ‘excel’
-
-

12.0.1.0.2 (2019-08-07)

+
+

12.0.1.0.2 (2019-08-07)

  • Small fix, to ensure that system parameter ‘path_temp_file’ (ir.config_parameter) is readable
-
-

12.0.1.0.1 (2019-06-24)

+
+

12.0.1.0.1 (2019-06-24)

-
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed @@ -517,22 +510,22 @@ If you spotted it first, help us smashing it by providing a detailed and welcome

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Ecosoft
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose diff --git a/excel_import_export/static/description/xlsx_template.png b/excel_import_export/static/description/xlsx_template.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bdc37f9fa748ee645ed440aef858a3cf8d806a GIT binary patch literal 81396 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

1Ift&K$+I(_J)KGp+?9z{Q&)h7w3AXv>qn-0% zNsVf}?(g2EoTqs!&wnPnS+8NH@bkoKiVBvpLR-DQ+7RAGmqFa?4eo&}bafzGAA;p0 zgo653t4E)rj$z{&MTfrQ+h{SS!T`zb zO{$$GJ6eM1>(9AHh@)5$?#Tpw=yZBEROak6z%>c{{CNg zJG5NYG9=O6@4m(2)$$^IsKSzO>^q8Vav+`u|?Dt1g?}>u}f`p4-kA zKHib0e?Hm8<;zfS)lUJSCun~@?#E)RL}2mOP|+p;jqYdR+w}yV&jlP1yLFj+xZD;F z7n5FWw+j}NvQ=nI%5d8VFq4Xz7Fg0xc{jvzkpmfmmyEr+-#)_S2L_g#S19 zh4<9nTWB@X;{hfg3)1+@Ujdl`GTv%O?qo=1nHd9|2l*KPo{^tNwFkR%t`Y|>SR~(p zA53hdbEKJN&bjW(!TM+S<5k8smtTMCo0|V{PcU;El{iTU=KDqkB@|wV|@OW}dPkqA}a)5s>l53j^ zI+Y+^DM8U|!ysQ}J(5bEbu9D)Hj6XeaU|cKRmEI_y*j0nfUU}Nz#~&EcXpe*@?bFQ z2O!JxwV#u57wu*G2ogB7dfT2IDld^xbiBZjQBAlR3!Oi#meGWZm+fB5F^WHS7WXk#XJN;+eZg8C>*RydZdTS?2Q>LOrO&BRSA zzkM(kdVk$`b@_siB`z_1tUl_f6AD>4{!>cn0zq%c$ZbT54)7jsdn?oTr*$=vTa4E; z=WwdcyZRLvPxkO9x@P8JkOKhdJi03AOPgx6of$Kr_Cq`}KOvFOOZ$Mf?W8o@_(KGB zIkXec$ZIGa^-IK~PySk#6II9_;n>mlYM_9kK}EM^_wWyl+t>Z8H6c>k0Rsex|~n5MuEKb zE|o!%nrOWi7A5P>6{HMAoDt9rnu}%AnN71r2Z0qyIGD%3xg4rqYski`N}4g{dasB7 zs)*d*gPLD3GY<+~V}Vnt&s=J1GIh=SQkgBh(%5X1^#>Y#ScwMI-NGcm-0Sszz2pNC6VfDnIQ;K)~y&m1rN8giFYzfQ7msP{?BY$ zXZ#ITuiG<;sXF6U+nFCx&>b~bmE=6c@NScO@qv_KJ{jKi9}ffVXlxG14d+QEuIhSk z-Nuh&wwTCcmsXzD0|rJs%wXSNz20KwrYyHLLWD6c8ZUUB*7oLQf*@po|5&1ZoYvGf zq^Gb?8tDSWHk)2Le*>08LPG~8*R1Q;CzsrZK%a9I{C@saPc3+Fx%9EAU9>1D1BXcW z#u)c_PU-wG@CP(7iNE*MK{MGb7C)2I4LNw$p912nu?7jX3r1UI(dS;jGn5v~L}POI zJar>lYOcGN0xB{vr3zcT9%f~=QBvEhgz zDRg!|g(7}G*W7B6K(HfoYxQYhP)iLM!pcBWp~&yBgtqw99=Y*JMC#*KlO`^j(Sd&r z5|7VcKkM_Y#+2E;I*YHN$=?p37S%s=JQs|1eh3PCkt$-dFPTtem3@tte?SD(?QSP5 zz?kXO?5w2UM7B<+i@hqEbfQK45rWlWYeente6l4foy5CyyM=swyM3VCecLq4$H=sP zfS!}(zInQ%K%UF}=j3zNi*HX6{LW#k25%>>L}UV+6+J@}akH;2# z!+IH^Db4K6o%j6~2pe^gz2|SOC*gzuclX_BCRJmst=z_^v}f4v`%3q18#w!pD@E^9 z%A@Y+fwbb6Aj%^d1JQTrEa-zKSh=>a5K#Rwm_{wbZsS>FytMkxK>aGRzwGjmvyS~V zTO9}&*yHDR`jmG#-G|9Z69(8x38=!6Up2_&!_`0lIfZ}k-Q`>g=;WeNDX?6Usk{Z; zU;_NZ@re5~VqWbHli&dUu!a|3Lba5X)9<js**@SYYBp@S`lct>99LQ%OpS*-Cx|ysL7u`bk2x z?-W8%Ab=U&=OnIrDFnj&GVU(}?|Q~RF%5~4c(9ukm8Vt6aGYhg~2Qh)Gxa7PXs z3l3p{XKpz0UZ3T~mPC6#qkD?Rb-~!3rxqG#j2NHsdk_b3|hT!5-WQ8r4XaQeg zNFZ2yBEi{hext4x=Q5QR%}eU-cna--FT!oHTr9lXAk=S;gEtfi8kVxhfik%_pwUJ1;7BZQqpW8TdV6F8IlHR` zGQhxAbSqsM#eimktA79ub`wwztvDweU}HHM#`tX>9W?Qtl_;80zD>EBoLjRMCSqND zGG_)B)l(3q4Rs`E=k&jkPoD1w_w4(~@V;L3MDjiB9~fzSYEN%t#yl@#AfN-@5g(Dr zqWt`9PLzkgUL{tgbEa>Zwi&n~Eg;>+4Wh~Y2 z!M*>Fy0;E$>-+jf>9;~#pcF4&LUAkZ(3Svgu>!@dKyfRs0a}U$D^T2uyGzheG)QrW zKyY^r7Ve?n=l8xd_r7!InP=`lmkBe6WM`eV_ma<&eKu7Hzx#r6WmEle`dLymY96S~ ztRugr5#V(xuRz8%I;N&^gI?*7vryuIF8XzQk-uU;Bu{um{1(h^{w#HLRonL7%ZICZ z>5h5=dJoo%F{ewLRybX|J9qb=A>2Pd1t@=9M9(05@7>y4+Q z$C^L%r&Z#A+Hzno#rw}t4h?T@@`|SZYPJ2CmGAMU~(tRGPup8BEMKINxLa&g|) zte&g7THhBV>4ir+Jt{9(rr%e~!P%jLb1H5@Z!)5#GL`@&Li&{CfqD5=6@4kOmg)t6 ziSDtgN5zL`v20u6FfS^aW}Akh&xGu~T3cHmVqparr|17FS$=c1nEnb-_G>~OqLL(u zV0yy{HNj*lXJf)|!T)g?VFNa1ULKsQP4+FDl&3Rz_R7D_uMW&-Deo(HKr!@k*B)U4LqU~elC z2c7{^=ZGOT1h~Au z%x>dgZ;P$Eo_qogK4rG_TStUNY)~oHsv$N!!Wi3yELJz)@oI!vRxc+=UJ4z(TCRVm zr(a!v$OS)5b3*-WnGo7{B%!=f+2mn&H#ZC{0pFEk`u>Ci1=(CCCzs?i4+`3vQ7v|0 zng5a^GRE!~_}~Pse?U#erLB z)T>FAPc$Swo4`Ge(m#0JX?W&ZDynN{?m`l?Q|4lQIQ+pzn2tx&GyT18B}a!k+l9N8 zSwIl8Jq{Uz<>(d19+?2J@kdfZe{HX5I@Qslj0DzXEoU2cDbkx?J$GtzY2R?@LhEv| z`3L5*J3N+E^FH%(F{+recVVQUcE&T11&$eJ-1MiFTI>vDHaq(q(=aVKFVaX~nW0V+ z82~)t1-HKVb+{r~e~SEduq4}+&@*tF(jWxec&pU`;~9Nv2p*8L(bPo0%r8Z1*;}|O$ikVc4XEn7 zHL}#SkxdX){D=JO*V^#8%7!N00h)Y!XIGn>vMF9CZ!=bjLNO)~PwP{Wypx$jJ+&PM z&{~0u_o~pu+*0WYGI%;<>3R2tIEiecm+bd|Cyl$U$7J@fA!5LsfbEu29;2hbZDuQ2 z)~cG_7gD;(v|;AaWCOiEJJHGbH7|;5XPxYg`DJ??y}0D0jx#eCWo1p-OR8)xTCWhs z2hp9<_y-!4mK-sc>|tMnR}&ZwBFCq<@g*v20*ds-&fcI3e|K%)_he5x%ZBKh=nKG^ zxORpaXSumlbxLSW=lx9e6AQQ>n1y<)4!+3MOhX(#{1zi}f%6scNPZ`cUx1)TgY2MnuA7`q|R9 zsOs4bLnV43#4-ng%E)8FU-i~14!~xV4OkD5$x*~uZ^Z4zqa+f=fZh#?ICjVJN$OAH z@e>9|?}L>gy68Bihqb2`p~=;5rxSKNRgD-AHSpy6b7^F5bAEw{Q;%KywlL8HXjj?B z>eMA_`h3XXu_XwBeFPkk@r<-kYSUH27W4!*~M;aMX?=b<@?Y}T}6>owXrBY<`gLm{5>nLMTr3IFqgp8cJPfc?{&H)dU16CK?VNhTa~)gmrAcm`O7e zW#rh+aWvk)`!q2$K8fE-spur+r%hxw4reILxO5u6d8lESWYoGSyM-Hh7W0H_U`k8B zY_GCa28R2PZ-ah<^ZFf>?gN-g6xEw&Pno$VzxRQ5957;x+DSnZ?PqX+3Ax9YsvZuQ zjR!cXe5KNB2H{=|^+w14G~RvyA=Tph{o zsWUBYfAhfa@oQr7kQuj>pdcgt!%Bq~S`+U&QAl&a1$Lrsis~o*rzjKaAr)he8yC0b zrs#j*Ti^%l#(O?s1O1vyU*mx_&qVF32T!QUMV3mY96>TqYHNQ(@Sn`WtBW=(3Dq9l z4}+qU{L7Wo@0OM#9|U0fAZPcrLN1>vRV9?urMFDiHyt zs`F;$=B&xul^w&uom*+^GNskj2)tjIwvLUDtHr_MGoe@CE^QgxCQP1W0olUr*W(Qyj^_9($(kU zJo|ML67^AStoxVFu@npW?a1E|cS~^(A4oCvYY9Xe)h>@t#5BMl+Rl7DJYJK(9T9AB zq$}%lrv>@DFwxub^RnMqUp5Ywt$)vI5tZv@ZAO1~jo$lZn3;=`u=8Gi`S`)@sD0xW zA&J`yEUdWzd#``58u5bh|6cjR=>DB6*1o`t|23=~`|pRpXMUjn9q&d-Nc-=Voz&vp zzgP1%Z~vdf4)!n^5~WSkc1oE$YizZ#|9*$2>i^4o{D)z*EnaxUxY&&(Ev*H-yf1mS z@*w8s#s~gI;B03o>c1HG3VeW&~Qj%9d}7XU@d;D9V-o!3qmQQ_mc z0gHWh?&tO`*Y2=!Gx(W){sBgT=z%k`qEG!<)65u7;H0_1MNV~|0vIg%fl}1;jmPn5 zdOnu34w*U)l?Jj(I-+YwQ{^h0=C=tkg71LA;xs@(w1L>X+AxU#z9C(R1aq6HPokyGZ z6{gYH5E9&Q4-FCVPc*aWEhl%6(>H7_|Cw_gU#_E7O3cJrkAAb$)Xwz9DAm12tGIve zyhDG8Q9k?+kJYF`hp?G6n0U55>p8&?6Gg!Ct~i@R&2Q!C>XJAx}o+LY62!JK;8u;L}Zx4|L-tc-b|bwN5pPyqY`h5wqkl^b!Bx z1-n|8_g+R#NDN~}Chq&V)p@&s*G6+zwsu52jwfqrO5Kbp3M90T)b~7cszV3;m8>HUQrNnYRtC{dOU4O;TaIm zbMID7y(N3OC1yvr+9CMN9AkeQc8F;}r+>i4=~N1+C%#Om!{+kvN0S0B2!wZ9ioYUw z+8stL=JZi_@@-^UyUtZc2c@znPDT^bcpiO3**%-Ig zmdb!ft|WiRvxB;7xuIJ=8Z?u3DHKFYY{MR)y}@@T%g>JTGuBj$Qbt~*)M$#Mey$-#?lfxQMHmCa?P(4>MT&nLH*jpi;yX;roP%-tkR3*WVv zgLRM!tpyK>>mSJQeQau89msAXJ#+KoN>?fkTgvp-Y1#;Fmp|{W?;7-dH^w z39kwYGH;EN!FQsm&J|YdDzopV1A&H}`1Z`B9eU%lDF_ZiaK@~Xl$OQS(!FO$9?FmJ95fCBTYi$bMUg%A0l~>;3C&UFi(d=VY)_0| znKyd-dQ{|8CD;kvPIq3mcr_XyKfJ;CHCeghkw*5VlcQPSv0De_m*wcW2v4y$IpP=V zkP$yHxPN=LYL&|%bz>)LyD2hhfL8B(A#elHAqG*Ny7uv0?~ngUa97L`cZo6aE`49r5lNf7goG&xt{AUHVEw?{T!Nq}5eJ8vh)x zXifwVE$U9oU2x*Sgr0!&C*)O{x0ucMtsnd~`_9(}lKn*zh0we{jso=kOJ{SEm9^4) zZr)Bh1qIZ1F1`=at(I|52W%oUcTclNRU0yo3?`Ou_Ley=$A&5&n?|UJP`};x{W|Oc z@9)!~yq*0%eU7(UV>S1Miz->m`zD*q^g&_XZyt?{Vb6HpkgcCDOKcPgPZi?vQz%kP z=6gu6w#nYJ@@Qc9JkirTk>!QK$uMzv%v?kSw?{an@g*?{VEz2TpU`= zNlJQ?M3hn~gG6U{$l08+;uy4cKpTHov4Ywe@0kVM#Fqyo<{ckos@vk{qj5GUUML0T zGmf$w3ks?lU8Z)iI58fu4bc{&eD{HzfQpGO)eGvhtGtRzeDh%1|aO=(Ml^MwA zP;xsD@R<#=FI-cKi0)L7Rb+0Hk-4mzv{cL^D^;`|DwMF(E-qdat7`7z=r5HV zT5BqCx6(=1(e3xUe?k_k`p-WKnKB8ve-z2bRjP7Zr-hqD3{*88^~rN0@aAXmZq)!? zZYiOut3v61*;sO6GiHlYqZH->{X($Kl^KqBh#%MS>}nB;gA;@(GR`<$QQ-}y6u^<ZS z@^|8rA`Ba{`?v%&*`#E10@T=NZ{DfA`l!<7n@Hi=`KUQQpUButZwgx}FR^@Um6GW) zD2Ok$q7>;YboPtQ-2<^>8J+8IW;2ymF6j`h3z)DC)8pE=jffJZXBn z4!OZQ3{qwTUQ+t>6L_Ui^@;9ZZGy0*8?*AhY+OM7KKi! zkKFFX1#jQ*gndL1x~jU`*>z|_e+MQgGc%yEzjI3n>o-om!HUGuP%TQl{5d*MB1+X0 zg;__pGr*L{9i)=-uNRx}`xOn~zwno6{u2%EOTdZ$2JKkBasTVR|9MCorlg}onf;d= zta%0VrT>?h_kVu3t7h~f&ijA4{+)#869gy3G1;ka6w5;ZjknUW(ATC+>=ZoSB@_1moMbS1%<|Cj1rjb>F|&PO^yh++G)P zYAISmTHRc$W&RYi6LZ5`O6ztdkrPg-W10S6cN;Ro2CC0h^=qH#b@HLek99eabjP=o z9!xt&qn)&D4%!8GDyK)1`&C8MeI7#gub&^h9@rNC6949wlrJTpNvr1LI}m6)w)p&` z0tHpZ55&OxuOvU}xQe_G3UXM#vG#m2IEf7jgH>iWtb5)EK0S7<&Q(->_^|>De;Vz} zL~u*#>XRCW_QkKHg~a~Y%9|cP*I6l48I^C z=eHc4uC|Jx5f^dUp3KR~!I!-Hf`UV!8S(Nt?nkTCqAuIj6Z%QKW{PTRV^fu8fDd9+ zOxD!ZMNUo%*)O)6^v0fFT+~!o+sxGTnO5=(3PwSol;Ag*OGE@Mt%%clhIB}01TAWyP>=8EwE-Z1*RRpp<%){WWO*>!bOJ1#TU0tAm9JtBw=u-k-k_Hv0`VvXoYp@O2zwl_xBBDYQwf!Mp6#{;;&bUYq9@CZ)eD^$+k<5?hb*gY zXX{UxB+t9|7uqB-=p{jvmtzN%gosHI39~cM(Mg)w0VwY`x*s8JUA?_A^DTaHTn4tV z8I;FXvBAy7#Ov3uugjuV&R-4sNxWI~+m#5oU3#~)*x1;{#>VOCX!pmtFDLg<5{ybS`2y)mQ&F|{!qLT`s68iEB;A6Qf z5|Bn|K>=ap)z#JMCZPLf7dHNd#y*@5w6|)1JZ}j8TXk?tsIRZz(m7b}j-Gwz6@W`l zNI@Y2NCXOn?##eYjsxP_#p+AZ<4#k-(pgHs2|`_ zeCA0w-JWthTz(fO!)w_3ovZok5Ph)Z?CeY$vI3~g>{L+qXOS=Szrz9L-$U7zYX;y^ zl2TBxf>d+lffE|UJx{j*&ZS?7iFtMXpaa&cw6O4UHOX!c5Xf%(yN?y~5c4I!LU@Rt z2=1gj<=={nAc&E%)3nkC(9GLmYpI^>q;l`xb)O%t;XUwc`LwQxC`cfg`Rr;;dzW%V%QcB-BJDsUk zV>^4Xk(;KEfbZ6CaVl}K>SE8N-d!-3xd7k^SbC@D8joDxdd;U76a@W@i!<;n&&y*@ z55uny7Pzf6`A*;f2K(?~ZRbA<3JST3DH*>!3ZpuNB-T}LweW7<687BKtuJ>gKUhLY zqUZfkb(3kB$l)cJE7|yf1A4SK@lQDdK+k!emURy7<&o;>}&!<2!(3-FgS61BPME0nfT%)_gEb zEBk&QYr8xK$=1jGOVWh&6klB2PTzN~z(sGVsrz?WW*9#FlKVwt;VmYStjKcK?XRTfR~Ou+Pqhlk6`$_57q!$&5Hb!0p(a%m3;M|y#RoL+gMs&?6={c?absdL%TmG0=$(QBlJCYs`FKI)ipF=H8qM! z0ycn?O6}yDsj;b?vhM5a1HRa_6fPR@gc|TM+A1nMHq%u-Jv|M3@XKP@9ARWsR1_dD z6c9*IpWQl7zzJr`Mud~|%7C|BJRKb?)eCJ50Kaq>upz+kMN(|LqZsJuZmr{HIa-O7 zL;HBjEUmhxraw*6r!$mX2WESHJff85v6=7di`6iz543~;oerlGu7I%`?BIEsZ~N)D z14^{Z`!>iMm_Jr2ujOc-gkJeyq^K(|A2O{Be*5-qP>?mU*8T=_?Kmt+T@nx5X<^u1!cg9x z=H}~A5;pK>K%}m7?rS8T_>Ugdt;Fg8frhxG_xVa3fFM*aKl!f7=8l|?>)35L)vxr% z2?`355ffKwcmeLhWi7=u;QM!CGBQCR2<&d{3lfk(hhzWsK3t@vq^5inGrV!OiNcPi zf;zR^09XT{0a{#JgMC?O1u%i1k8c~*>;vF?WMm{rMN6v=K!^DFX9Ninkb3>niLDbr zMdkohsj?UWq|*t2q}`m@!1&|Gk1Z`N#nSeg&gNeGJ?QQjm!SwV1)P({c2ixQFe__8 zR@O3r<3a%REVKm@5#4I+c0z;anXQk{O}1Rz#pUJY#YKrhGiJuF`Q#5YBt(@M0OWJP z9p);fVde@x@$J9n0s(=Dn0@^f@I(sMZ!2~KxELRw6R`BN7Jo8^%yZ-uhOh6$=gKGW z0L}{veVo)MV7~|^5Qbbh>FCUy?##%^$w_BrXG`af=#`HHUVL+FOL`eu=a^dkzaeEe zZ5r^2fExjnumYGYfE2%eJ@;#wcWDrJ97q-(K_Zh=QfBp0!j7w|TAi7!Y*=pzd?fLFV@W z?=cRJD4;QW>b2_w{_yz`+05`I#$n+>i>+VPet-~+QK|KOF}>mz0!5J@FE$t=Lq2agTsNSxpT*Z6N8&udJ;6rj*l(mIHD-d*An@wxmFU z;C>stW8EeQ&k+9>+wW3}dmL|$6|!LeA{*-bf9a#N0X~Wwz)Rv=rTZ;8ap&0V1@O+u z&qsgX!O|7{ChUd3{ucoOt^7ZATmI*n=l`K~^WP-q1HGLwmN! zAKzQm=wxAfTUf2`Fp~Y`Br2XcJ)M}uck9)^?IKiM0jvA5G7Cq{(OR?T0_rBU7W=nz zrUuz9F)xG8h`2=~_g&#fxL4r}Hv>13W@*{KfhG92>x8B`zwAZbG;PmuxFYSxOlTk< zQ+-l1{`o@x?>y!&oiUBpC{C33%n6c&ZMpeI$IFYE2}|(Xzx}THONYa(tAmINmtrc( zxs!h+FB2LMq3A=HJC-1jRo=32F^9Q6q`jf@!Bq0`=_U%fmGKE0P;>}$D?2y_dtx0w zreU}pHW*i96OB#tqdR&fJC3tM9EJqlTn#wv&u7o+92^|3Nrt4+-hc2*1IR4@QvH2? zbl5K%mBNt*ahW>KV?IAAU5S(hD`!=xBqoBvvFwpoUI)3~9{ifvfAu%K(09J5YCKK+ zY}Ok|AIErc0i9xxkmQuS-pF5X8 z!l0C#5Wa3ue4Uqtj8#jQ(0jN)bZ@Ioq5M%q_8O^ZZ55DFHw*db3t0cCb2PztO_ePS zPySI>K0NGI(c|zzx2YjaANbNf4933@kK zvZ*KN=-dwq1Jd>gMhR30P(AleYTXz)OuJ!OLiFCn*QH3Bn$WOq)|-Q9^z1 zjAxm~)#s#f=4KtwE)CC_bC%7bIH}`x)V3^7y=_@o%Nvjdl|sCR!B(o>+Kvx32mwRe zdsc+~TOpj4=YoHxl4VL%7nA|cUbg9i(xMLbn6xb2Z4adi582cZ0aYqu=5Ee=*J@tXLw$vVs_SyQF1D4Pf3SS_UOV#oonY-V0^Sm#>$#I^sN30|NMBkZ z|IuhzUK*S=ZB)F?2b)tqHqwrAcR)W3lOZY+ln;k|QagR%87GxTT>S7>Cx_VkoP&jm z>y(%{c>K5g_!H?`7ILK7Zn6jUee9J7kogy%!XnIUhezthrwh}9Hkm!RISetMA!VJi znL|oJ`WJ6KcAuCbq&aa>OG}mO47ou;GpWM8yz%AUCiW9ERc^gT&+;AtF#gfwEZRTi6u;*cRP-BVbl#w;~u$%Oeo zd@UE<<*ucFbnUjhOt>27*@f~ykeuW(taX2%%)gkYqv(5h{n=4vnr%WzmB4;V@N+3N zB87^gXvqBA$-r6qo(@72o{PC$H4Kzf=f%+!j>ydOUqDJ zLhHsWSeccq%VFDwf1Lhmwn(8~zh#57l;U{nnEN^TYDt4|em`u)D6MDr$9!J6gPd($ z_WbPVx5XlJ9gjCgIV&oTh00=hNj*-6PI}K3Fji6H{#jY&q-oi!(tm3oeyT`G75n0L5fQHkrqu(B8rza zZ!T+ETcb(>WK?#9gXKo%Y<)svH;bxB)wOYMe#TWWrsu$2X%$796e9syIB$~ZlvIC~ zsNgZK%xuf9))Aa8raW>$b#?VBg$=n#sKr693qqM3IZfW2D0;qwH@%Q_g~%u`Y;vmG zYY)@D>tT0sspX{-$Zb%4^|2=JoXsRxSBUO?<39=|w+p z_LqzY6hUBlJ$Q!QOpc@Ogs%zS3YYC5fhdHW28P9Cx-iJmcP=TDsYFZ~8 zIn)X&UpQ_UX}s>9uI-olr!e*y!JhH#4?ZGE;X(1t%49W*UFKLmbn9q4CiqE8_24GZ zwOU#tlcglCUVWuC6!1o;I8W`IB_gR$x;t)#gGBg|oH~iH^i_4vO82{v%oB`rcqzNP z9r3hP%D9ihTMFfi10orMV92ye%^MPBxwH_S>J|n}z_JF8N+<{e#XyO>ce)_#-7AE% z$o8ySbE#jWlN|n(&}~j-HV4(z@CZ--k_Mlz1w9HLzc)TQ=d3)k=ay+2oh+7ir?laO z2?=Cnb7*PrhlHCgv3pr>6u_uKAZULTJDp+dYx{{Q7%3_7i~~*OyS>vU2T?0aH3EX0 zPc{hi?72iZJDp>o9C&D!G>yvcRCg+4!ZD=aBVx4*P2!aD7F_a<9x+2d&{)_C8JxX)w8+qW6nAVp8^MPY(Zc{~0Pv zXXWXr+WNlO(7N|zf`iSu!8+g}3Q%bMtD(@ruJ|nHam_fLEJm*LuhvPdJ*}Y%10DB6 zg2sXK|2i)2!6i{%?8d|9EBz?GxeQLEH#+triU0b z2y-!1Kx7L?1WI@$e;8|Yl-CU|l!kc1^X4C^UT%lxIuIGDlZtexMI~v(Bu~=}*6vI- zE3!F2?`LA57E$hereF*PZ@v+uRCkhzwlJN@ zT`Lf57dQBE_I+cqB{xSh)f#$9><(P7n~@%u#4EXe(&;XXOejK*6fcNV(S z15n(IS{3~FdC1RGKlKpa;c-zg_>GZzvzC)Z49e`YwPJ|UpPJLn%g(W=##UAW0&-d+ zPnKro;(-@tdtZXhjWOlF*2x_M*^-i}?G)KugavbQ?ZORUStAfaqy8=~O~{5kFVFko zrA6LiPJ5EvT)Vej4b6~POLvcKrd@7iv&%>!OXqJK-l(*0dEVN-0dNI2e@i~eq^#?ZX z&pUUSpJB~?1<<_IBJMNEl}6)h0(;S7u98sxXBkTxCYb!L)sLm+v0FQ#$qs5@r!%v) zYs?`=;LkGML0(6_NJ9`=wpYWDGaiGr#|Os zKRt)@Lr=7Vh^F^oyZ=hFA|KM*-x#W~T@d7AFQb3U;fuka^^D>&z&sz%G1v1z4ktP! z<6swuZR4DOx;zCG*+8~t3u%uz^?>~be*~#Ope5CD=O9lpwvH?K^EJAZptmN9rx%H0#bq@jsfknEx86qc#55UaFD?6P!{JC5Xjcz zCkVt@n7!S@Y1-0RqOWA2XUn zWP!1c_i4@5wwZjel8K3=A^iB%-Zi1G02;LZ$6oIYO4zn26m3*qV1!cJBs6vvnP5%~ zvGICS|M1DrkUd*-YkWIo&9ELP0YUNNw6^BtdEu%{9yExxpl$al_uLm27U<$sYF23QHU$M2<=w$HmD|hR5dBquM#ppl1DW zQWD#BuW)DQ)Eie3D$d9swp6b^6}iVod47!XXlT-Yp#uK6#hJtk!pIyg7KWA+SqMas z>8$vhez0$>y)E~?x{+G+OA4?x_1gS{9_25nL{{XNz4Vd#nmx==U2~O7ss$_IaN}Kh zqrn!}wm?Xr+=?4eI6H5Q`@p9hnkSq51Qvg>==n>8Ds`2%KjXOFs;NxZd8`h3jcL$S zU2+HsGH=60$+|LhRMlryjnsDW^UDHg0~5dL&bqQHHdxyA?Cz6r1dAG*N!^a zVJ6Lj?jlGM(rFPriZSOJY;5H0W%o!%H{<|rUl^9Egn>GYjmz&Sz$w&XN?!mh_+o;9 zyo;;&028W^RQX4nKzYZfhoxvo(~r#FpowQ!opOuIpmsvfx-yep*E4S?@yX|o-(K+DNUx40!-SF6nwhB@m`0ng;Jk)jT1N1MwWx)Bf{E{=;1!yT)JlmTJEAS0%pd)x9H z)o!~buQKhg{F~y9#Ke2IV+my^tkkS4J_1IPGim;B4{kjU=snx9l;aqa01g&Mzo*HP zm%Ms((VZc-x#j6*LY7WFvWSEL#&LGu%1Mzms^f?;SC$5IGq4MuOdU`z#(IXMQZKD- z0MyJ1`mquRoYAMW_{co>l4+ju6*Q``NOC&U?Rv?!{FVc8Nx2+VmCQ-wh3#@DK z`?o>X2HfJpDg*>;7eBMeI7@E|!>>>;Rz3y=t;go(f=?F?C}|WG9upDR+sMngRdl(# zKVO`VO(&~bGt2K1SSYE7$FN)eBC|QD7MiNq(Q^kF;j=2obvjrmy)G|V2A#rgJ}tI zZyVilTGiggN}ChT863GVhJ$YWcIm+Ta!r}mR+VWHK=dFkWBuRYWXl$q@g%(J`Bhof|^@C z@i^I=6yj)f%J=&S346!VO*Z!%a`ZqlyOg;=89K4$9to!vY-h-6Vx2dNb(@;aJmc!| z$8UA5HNRx`8hVNf!Y0$9Z-23zH1quVD&E8(KSc0;VXffj+6#t)cibKX#)m}qBJk~7 z6ZW74w2YsD)nr6nYBkz|+-_#fBhXnY+*vBu1hmMj->uNJQy~vdXs9=@jJo>bVHmT? z+}+3Cy(6UIs$;&aQ9dNY+pJ6`fO zCn$(8Z%jJ=4}SLN)WQ{kb0Plt>ssny&V4hU>t=9DeB6^#r@AJ*@fww|G@d# zB(^<8ymng0lHk(~8|S0#9gKd8g=n_-K`m7;xt;rRnrgm{)}@5&26s=^vMen;lM2dU z*4F#pJ?W$Ei>8b|I*VN~d%-0@{)U+0Lt#iz@Quwlqs~UXTN6{Q%9UrKa1{3nZFCq0 z2p%z&*N>#b7y-a&rD7c zsuKZTulfqj7$dg}QMxWwnF%a3t`D_!TEz`%+U_g*d1)MKs4cDh^J&UK4f^MrU<{;s zqaKK^31-{erG4asLxx$P8Q-7DGy28W-qvzo(e7JVT4oS?F9RJ+5haTW4`rY$B`4Q= zFwL98Ld49c`x+wpjy7!%czV=WYl=4qHB;yxFog8|byHr_+9ZZnxO2Li-qPLa;H z!nZ}@-}vF}*ssn;b$H_tGFhL82#Jo zT=m|%Yk1>Ppjn8eHV{h%Y$6nrV9UE51h1Q#;$FY@y9+eu8aJ;3q-LP5X;ik*1`hUl zQL+HnvMZV9_bkXuFI?$6ZX=~pI8OG3J|x7%&t6FV<5622Pzr+|g=br*9fjgPCMT;# z?|#3(^x)(R5w%b5@hh9D)g9TZ*0ot~{_j@PWTUP3ATr0}6=RViO%*W;U^w+(WWZ+Fdr7&nA+t{>mPd$l}O>uYCY5YqNDDAq5JF6 zleCy0H7)66+BkQSCuJVOo|B}5UDm;le?i`)jrb-(`NC;o5%>%UgaWq+R zSIziohp-S-zpI?;gxJI|icX~R>XM~_*0LGRjbYU)`_l5wABS`R4}X{oJ$4g!jxUEe zVRp(tUtb}^nDxRte{|Iy$!D(2*PL)I6HgrPzY@VHZhVbRgaR#Ec9Loi$vG=?)Q^OQ zKIdztXIQgCZl9N?&C)JY z;xQj_!bo&0QDu+*TS%tE=kF}@TTtV^#ImWgo5jfeLmwTzFK@xDH;1Ep;o|ld3u(Af z)1`CgaxFCwezPmiosz>aRMQ8M-=CjIHjTLCDHBm#4%!iA`qc(yD0st6X9=Z0=GsVE z4nx0xGTytPuI^7vtJYGX5nusz8hkEwB(Ik25bV#t>P4nWS?@)%G%QYnw=G ztj5sKL{6__&1T&{lW-U~ex5&bXs1*vNi1PkUSW4Rr~N26R1OnkadQNy@zrJ9lBG-I zNpq45O_9xbXt*vK7SSF?JKC`qiG6p@-E!yDgJ9~gwV;&N4-pxa<(3R_RJ#VGiXnnYFLb87p_GI%1FB$lsY=5_WlaFR_ve7fI8CU z+{bBM0SJ@n&$y?bD_hw}OSctQngv?*Q%z}q(Cw=}dhr~J4!O4}k6#KMnd*hBNP~-6 z0T=Wf_8`nxWIkYxwb6$zV=3+0Cq!;Mg& zUMQgZtguZjD&eJCDS1X3RT{j9s_{664YB{B@K8Om4QL(hpWimklsO74&zdCnHNNsny?>N2lr%!|^AndOg$VvQ@wfh`uMkLy`R zcOP3dHO<3qlxrRr*M!Yh1*Lq;V6Zj1V|SmgxcxorOMjsZIn$-6mlQ_$gL|~Tk29Q$ zcZg8~ff0cV$mT4hiCqzgxb_9We#V_Gq4m{@KaJp0h=zm9A(Wh#R?EPhVhYyFKOWl#Rxg zEQX41w%01ndaFDdvfb{8UXGc%6*@-q)}%e~kguaEgwPrEmSfI>=%XhDkI7oBrZ7eC zIZw#~^HTM*!x|+$HQz*^9iL)iNl=oP8!&l&+#<`s*g zdq>ooj%dB_B{B4CLjl-+V0mq=bhK+;z?K84Z*~%i=v&T8F>TZFCy5Kc+EK8%ldRHm zxYkD!!)R)rw#Xf4i{ab6h`&y zoVwJ{Xg;ud8;6AxCGOQLs79hJ#s4gt+c%zK3^u4h)z6Sj>>u)U}wiC?1lkh1su>8 z#;4U?oR7(wmZ1xWJ_mk*3OL$BZKoE$W4B#-K4|6n$}Q)Rs#B8G{RGw1WdH7nh>$d! zsq1$dPxXDUealxYFj1ZPImrT@T*`snQ-}P*)b|Q{srivD#wAu1y0F9Gm3*8LRWeM9FZmxw?(GXKbNqSPHz+8Ya>5lP7yH?I4D>0Z+4ckY12)85eM(uY(^kS6EY}>Yt#*J+>Zrs>zY_8b0jc;Y|eLu%{ywCgZ zoqyMbxhAeT#yQS$jv6T)QiOPw;*nu3z%LKK!q2X8cX<3_UK7eVc4cP8C1fx9eqt#v zMSiYH70=2NTWKqGE=50h-6UD=zh&34b@Ep5ZKvI00|u@WW=0lpj%;MLc5C0Bf-RnY zj3`vCGU%@FypVKmKv<>8;>A}l-*r2o^`@7s_`3A^Ikp`Jj%v zrOyF9XS~SBCdEu|rxh&nveQ|MfgDqU0-bc*qLkgsdm+dyxtOm+t3C|lifuctn|*kn z*6ZtMm0~!fE^cev!z3SgBR=Dk69na=BSj)9;H!8vz0Z zf#_3Q>hnm>`_Zspr+C^I__rygn4ky6ISe5*VxcDwE`Aus?^GuaO|5_!iB2vS7N=V_ z78K{!)&LCbVtb?0`X@C<-+EW0w5e0_omE?Q5_G9gfV_jm$lZ~>pu4IQFQXvfX|G5R z0FdLRYg7>Y<6F!z|7Ef`1)}FFPD==F?*sS-^%?}g{_xu?`BYfGp5xJ*di*!sHN5r? z8oK@F1rmP2!JhIuxVa=^Pp~cqGGAL*fM6575m@Q{T(3gWI!=LB3Kk7mK{Y2sp@-D% zoGt)XLR<LizGY~^+%@=eH(=xRF zkcezhhN)+!^vxZ+-HTa3R+n(ek~N+2ng=}Wa$aIU2EgHsv8vOubr~*Lh5NEGc+?1> zV&-ZQ@YC8JRV?am$&7fA>V6fNOpI(ZD==9`tb7|Ohm4v`~NPE9H@UU7*H=v5 zk(0z-eP<)90l@9UtBdxtaeAUpBhfGbQy!YDAIrN$-sBK~4qrrnKK;R}MWWCQ7_MXA zMrP16ipvQ%R+I5<3_YoQr&fqq~Du~J%9TUB9m_HJ#HE zF2FukPI2(14U&00p4a%9g`<>u`D0X-J6NSLNCcws7msRA{XqYHjmrr*$dFqee z%?+?PD*yCD%>ZZU{bi@k)pzF|mQ_*(MSw2Il`5jR%-)hh=;IDS85*h)9RHF6v~47% z*qLsA4vLMX53E2SeZK;21O`A=?_pBu0zS`)giwL6Yz9JVt_-pyeOV36oN~l|(%+{j z?S9f_j3z_|b1A;MCP3Bd9w=#V1tIB4%?Zd5Iq4z1HRE*srUfC6fm2$e$G}d9%*Vm5p^ib8LOjmBB4iN2;+;ryDuU-C&Ap~6| zUqG4w=~_nk0A$d3@h4*NJdsTi;1cv4vtnu;quCaMKBhEAk7&on4=F%~qcE%<*?R%b zGfoBtc}t1p33PRjYqxNG4|)@iI0$#}?jH$&(B}nCZ^@?8gKt#&@D|sp6!_AMVRr#+ z2Rt_TE|5c8QQm%#L1*dJ3+9Ua1-uRC&?>F7Hwl*fZBI$xqNyUJYoJ$JV|{P?(b*ml zOwS<1+>q=6tdPq{-i-epueQ5eDCsbUb^}#S;~Zya%&nGU5yJZ-T_fH(k#;W`Vj1;0 zY{s-Lu{Z-%GSI2kIX<4pLGaF*3ps8I?~L;@Oy zG`Py9#IoW3p3K)5kwsxhVw1BY-+Z~UqRMXeoQYNEp>!;(Z1E{nr!b>oE*r*)ABPB# z`+ooF*Y;qgj#%X&W$LHmwv#zUL6?!a}L8kBEd)T*@d5bzSF-!7sdDI*l4;iWg0J_f>42?BAfnPETzox8a z+}(Uf035Cu&!sWHhFtYq4E+Q~76uvf6uY#isht5{0{+JT+M#P0nR6>8S+fh`MTz%M zs*!CS^RhU?tx8PteLcEP2p{M+Y1FIrEG+LbtwvF;+KoioF6h1T*EX{mCsBPtnim+9 zp-jUrhV?%cPs<%QW>!t=sO^1-p)qAqfP-_~%kvNu%=Psud26gXvE0S=$mA0FRHz91 zTpq&Co_w~_V$eFZ-7BjWtjjX$)GX&fqpVHBC6nZJ9GxaT*LvgkyIz-@hk^j2X-tK? zOta;8Nr9oDx8QYQ?|RC3#ZUl|S}nQAy{AcjpQ_#`8_nFY=LgS${Ufz-Iwj>bI*;X0 z!clE(DS1sn51-b04Sry9D@iMV;cN$s+}<)&6|IUpv09N@E5iOlz&1e+n{IPR0pzFAXc(Jmo+QSRb^cDCUT`fhc4HdH55Uk~S8&A<@2~p}2KZ<;1 z+Qnt?5*OD^2q*oo<)aVoJ_yzW@_0aWd{WxUSTr)Kk9&-WcRxdP$@E{oT0* zuqqR?GtUIc#)~6JYQG=9mQ9^jyUuM%(>>5Y1n8ukyPmo7b<0G?Wf4J!36W zeiD0owr8Cf#qR`Mj9*QnOvTm3KV@INlu<+YA<`oXpl8+aaG4zJ_26{2cwBDOcfZtU z>?_ekW7dX+(Yue;9ekFVI%_*oX$ecAjTJG^rk<4i!2(cEbyTr#FASY4|2iav3JBN% zA~mSs{v=(L9Ag%10xpSFDh(L&V}dHnAX0+V)uqJhi@=B@~t$`6A%wzGX$?h zehm6sgEW%Q!rxGhau~iogxB=hBAkuW+vkzi<^(oWIz`th6H7L(;x1Jw>Z&`X3#=)`BcKG3H;?ymXbbFq7!{9%eThzHW7ReayXRVy z2-W07XA@(}KNdOxu@*}!NNBGv=oAe}w28%OSzFW=edor(fYf-UJ5#qkcvj5h!$5dW zSu&G@zZRptNF3B(%unoh>uq4)cJ^ox*c>r8(V0th5}ucWWkXaV2Zo=}!u;j-`xBaL ze@#8h*})pQZ?$X-RUEI*r1_xwjcK!!X!6~gG`_m6?)*~@6sdrce0toaHd$*Av_z0; z5+6Hs7nWFRQp1)RGnAGrcHjJdC6$31JAFE8P|B5I^GSJDX}m|^a$2_9m7=MurFbQw zV8XVG*1Q#D<<;EcT(w70v!Ej8P6tNTqmbivhvKl4qs{)`-tk?<> zjrm+o+4<18z3*?+sPfC!%M5nnug-~{J+!v`r>DEqDhm2IRqgAwltpCEiT+Pz7nOMl z1R?!TSC-VcF4w^umXv)Flt20#9k$s>CW|ZS>koM~Ay~ptIMPP&@CQ(lBc6}aIWC_; z3dn=z!fq}fbM~(qawHq(<^L*#2*KjoP;CkS@2CI!MW7@7e;Uf5$ACQQ|6Lk86+*`M zzn@@9^P`AWz({w#Xtp>XBGET{T$`5C|99V#+F-xbm*=Qq}Eb)qaArT!JGOb#r$n{r>h2MvCIh%Xc4Qe8KOiS&o`FRM5 zRh5<589#*cPg%S zI-1#RR!M1jW45@A6oAT;p`@1E!hX&j_J~kQMBj5Y7pVC1#^p4EICq0f8RY$}^E2P! zoxV3twRI*Cz;+{8uUdwT?HjSoVmmyW(ZyH8WoAGH(WRsHP&S=>nVyLgAK+8M`!PCK zI{6OXgLH#7vv!ljjC!!JN~Ii}M(k(QUumtjJ2RrV$LgPvLbe-nGqz8OaS@-bv|4S= z2^#`45Y{VfPLrt&x0;?OU7PCuGz~*vw-EvmWi}Jy|Z1C`V%r!1$6C;%9pq2`4wETapa0 z2fnct*EKg(@(M1lt-%fM(`(&@L2h7ekqNgByrs$W(n*^$*XNjT8s*`-kLEO7p*o^C zf$GmNs%Wp&Vaia<%-Ya%OrDwWQ*c++4RBrS9Eg=~|hhsa1$^_#M~Bu<%Y>9t&+ym`vx*9x@ko zLMV=gUlFi3xIK;(Gt7$Pjof9}v~_wH**3Fj!@4h7Nx7+}RUq>TjE~94=Brg9-P+rX zRp{agoa)!zx2$=TkR`x!fAA_|bO^YaQuTb+IHysjYB>GFZ^ z>v|UbUksq@JGDA--T8W*OK0`i531AQ{T!`i5{PSoOfo>WsuBWiU2P9P@S|p=l z!rP3qOhGKak_pG=k3=Qe0u;~&^ZxcGozafc!$_|Ua9=%>B`wxzI4KFbU=l`o7HkOL zA`5(Uf-6=XF{ogmduhLX@(_r==FPp7fJ=BSCr+|mTz8oB&h=&80?;QnJV z0>(mIDv^eN*UsXy-0%r{eQ|N=1Ub8BsNcB(c)(7))oaMXIkV_%uXf7WWigABf;Yir zFf#}5*Xr=xKb?@s`yq%Pi!aK_w=>Tz)A_&&@ax{yV+tnwi3(U(2OjvVx4U~-a`e*p z06QFmt-LJ$#|0Qv24>>H0DcTpCKrFt>#gE_mqQ-#AX4JAZR@r#E$ri-_Z|+cmNc=~ zS~bNFTIn%UaXNiE%!poMr$=uiFy!22sT}H z3mExC9AXlWXN(d-mL_fnbl)$RvAtCG;QgAVlY=WNrHyG>i3_$!=IQ-|rf`yqe9z=GXU4`IQw`{{kD-&=O-=4d z?h)qIzN%4}QaKnN074a`9F>kU2# zVJ83b;`_nB?pRl8`3_V6wC?%xCWb?2+*2$HH4m)MHn-Z;;(=8j2+~_3U9QeQ)s^UR z%!)tTCj+e>a)Rvv08tDK0RP!|k0R7!qM_wFr8VNMmk%Ur12)w3CV2T79WK!63_<7N zi0LW##}C8}2QSYb2KmegJ>L+vrTbh=xV`8r#3)Qp=0e)J8=H9B_Q^JS^WF62px>?^ zi6$Y4_pXll);3Qd^~**l$_ca;%qaa1dH*@Zfud-{jw5I?iPd~`4Kt=g0L(kl(VYbyk^#<^fKHNHqDT9xBX5~%uHWPT zYD#D&#Y@sx2A9!1|076VG)5^IcP#79H#i0hy>9kpR@=FcC0)?-&2<<6Ee41U4dA zR5&qD*cAD=JQX$@zG>!%{Gfp*AE#iA0`1IZ=bBzCqnz_a<8@rirL_eXgUf zg{CH0=sm0Qecfn$JH`cWQdOq}un!o`ipQMqkKDtBUpSB7w{N7Ik9yF0F4^=qFa$O# zg-MeX-}Qz|FNw)Ta(e6RfD4@IO=zHV4@yK?C}2h1bWK5i0gtrLqqZc;Y{%gU6)nFB zhf)*K4wH)`ZPMslnnxrYVH0r|yYi5PB}zjhL_Fv4Bn>^Ob?6O#3@ipqa#`WcCdtQg zQ=z0j4v(I-c^iYG7aDeUVa)>TV(gXm{?bo18%As=7~#;2UKXBF3suznFpFdHg~{0yv|`O$Y;U*HHr;aA=X}hqJE>I0 zC$!TDhW+9En3dRBF#W!%&bbLx&YKi`hpTMITi3URMr*-w_=iH2!#aE2CuZMyE`nB5XUnJq~1{CG+N?w5HXzwiorS%qB-h;oUEC3M)OzyCQWe_?An- zHD0(~r}nD?qY=GpLfO^)$iPXP7T$c3^X5rf` z#o1`&leMTTM*`k(m9js!g!O#?TK7Au0w8~3P{#GwR{NCRVCM_e?k*L>zyR^KV)BYr zmBRl#8pjCGSDJLk6mR$5(&uVO;Mainod0G=hkA7YA0Q~w5V4qMr99vBqr~OlX~2im zaizldiplL}IDlN&fSj{?9Fh8oRy%Bf7-mFK%PQlA&G+AsU7$fa9+O`@{_wwr(mJK14f%dwCes|XSqxN zRS8|G`NvZNRYXT7`a7Pr(28Swf6=`D&WDasZBM#4+MUYu@yIJ1keu5m6>w6wa*(i( z;%8JxrGNY%7^h}=m0+LeKVIxDEg=(x?8q^E;lT)(2l@J+O6i1PbB}pH%s94>u;pOZ zeh-wgs^PvY0f#?B$0t6wX^PRDm|z4noY2;@y#Ldq=#L&XzqY^g`Mzvd>5f|Vy}d3p zqWC>wczT9RL`D8>^LV(7p`QHD!NZ06=lZPM8~t6$6&K(w86P&n-C5sw*6*r;6g}h| zeB7U)9{t4#3;T6M@$cwF;(ZL{N&~%iS5NTok{{iSJu7njpj+JEeYAOBiepd=jhO6+ z>NKUZrLiPF{2!1{>GRKfc+`96I2xz5L z#Q(de@1J{+PS;~*XMYU*E}Q$r0v(ECv@e^Ow7Jr}iGTp5zBasOCcgu!GW`^~6Ay>(pwxYaN`Mo6tmo4N_^*p=AZ<97V+ z2mdF7U#!-ihLY%%&5woljmm_UpfF5Sd@u?*CY-2IP$WbB8N1UZSu%Ye@f)KtsJLG& z|7UcH&!n8VwPr@6pGyBL96Olx6Z9t4&&V+uWF}8~&%@Aqs{}&Liku2zwylq;Ll;Ga zR`XwmZ13Jae8sYrOj^Mq=8o=kl}9|w8N0zvf7OWh=gzo&*;M}lB4I~n-v{9bMe`_B z$9)SrA6sD?Icd{PB?a=aJA$<)$7KnP>4;`LY87nIpL7)au!vNm_T#gaBPw~+c$KuC zbdwg3HU>cQvh{}uc_RM3b!E#O;BFv)RmQMr*bMNKf7EC0Xo70~kiriFj2*HX+ z^oj@^m13xb61Z}pCiil#C8H|ZSCP^NXLPk%YKN^|F2EdZZ7rWh^;2*DUwk0M4f9g> zsuTONiWxPsocP$MATD+P213rNS#Q$)vs0eMFJ3pH^FG8JPyR+3Ebn3o2{cv1^vux; zRpT)%`@8sDSVtwid*pPsA-i>HTuG4r+k>jFL`GgIoz{|6(HZ3lI5=;PPv$Kc5@=L4 zu3jrV@88zf7s3iCGI_bLwCnNCJ7ZeUi_Vcqjk}T#^Y|j=X_ZFBCDZWfTfW^lG@2Y& z!5?#V2NmvB;hj8tE=&5oN}fXL8%{5M!^5+ATQB*l^oF>`r-JE+ki(HNFVdeg=42~V zvci$do>N0P|fuB2537@?Mq*l32y+R={T6uXkdLJTsSzG3YWn=7VdnNNs&b2B` zYlY9uU4#asP%o-gDs?m$-{TEbN+kI8S75A0b@VY(4)$vPFk9(WSaQuLgVN825+A9Z zgf1D4UR5kc;A-inZC>z$l$6wXvBtW-3~Ol_R9ZRGXaMvvBOH?lV8KZI>6YzGDGX5F zNm369xh5m!)D^$C#5+Wsl)2S&(>r$?Sk_&|TCt=|#BH<<-WdkFDA#Q!)cgdD+Nw2M z_Ukkq1NSiW%;@}HX^@G(J2JmrqHttA4Ddti-rOK+ocx88&ImbOzRV|{Y79kX)@Rg7 zWIt51V{XVg+t~gb+1hupR*6?zzpO@yXhD znVxo*1lNnNNv`~X1^-hV9cdiYLvl0wm~ThJT*PPvaa z4({Kmd`z5_ip_viE0~neNE*A7 zGR5ECcZNp!@^mgCyUR4s{4HH)y+60vY8o8<3f%3Rz@8vV`P~TN-}psoyeP>H%-_uo z4?kla(QDDt=FXN|wn}Z)38*u=!T&DO4&e{Hk=;9iRrQWhC(3DV@8d3q4I%2X9h?&h zKfg39F2u@cztN!#LS;q-Evn_+%qqi?2F+0I+M5)Ic%?$CbruN!nLT9XqWz-qmZiuf zEw{Pz1sA^?N30Qtn(%hI`rA*Oy)dYv=8Ai@gnaWa5MO$`kqg<>Dc~UqPDTqOj6?uk z*Qih|4K$8UnK;%ws3h3@rs?|+1Av?9M-Xj9lN0&-gTk6cI_ zcF>fa%)+3V-CA5vJ7Q5rn1b=rYYAxHihWkBqW1IkUgDY?a>?ASsM}!=yt>nHPG}GL zmR_g){sgMJ2QbSd#pnAo28#w8i=BX+?7o?#-WTpFyK?1g=tpV;b{i5v% zy2|!PS6$W8{4VRPd%`~4krm$G+8T2IqaRVGbMGt&0MB%w)zy9EC$whf8Rk$t2N>+T$FVWg>Zb8<`0EPc=BnUV#VPav*~bWPvrfL_Qj%aA489KG`H3H(Uo0}R)~NYR64|u(i4PD! zJVk|kJQhk58AGck8=>^lyy{&rIyq7unZMh_%j)C82u>V?40i79lfV6}c= z(T_C{1BH;mO~0B5a+%jZJ2ZKg&Kgv*EWb8CN+^H{Wc04Wbkws_);imhHhPtRaZwnU z=XF_To9z~(ikLdW4sOY^72t4fg9U&W7`^_WZ*0Bi8cKz*$ZlIcg{ClsgAl;Xibq5G zWwWpU#bS43D=X`zFmvKZ7|QuXU-0QvMl`ErmT3$Jy)F73@|m$24oalS00e*~8|yKZ zfc~EUnGYriy#yBE4Wo7!6F2Ps%$7&O^;n{A0oeEoJV&0MGqx8Lh^6-u^j+%k97Tr! zrxzr@g#d3V&jBTL;z~2Xl{U_bRawqc2LQ}bUTaZstyXV8Tpfvfs|ukucO zE-h+bvd}zkBSQHWq)9IBHED~j*EG;@wPZByVgUluwwHz~R{`$s7`I62xe6&^4;t1E zGQ4{~X{;8?lSr5ERlN2?SXoKlu#o?ttkwW;;^`+!Sjr<%z`IP8eW{{TDF6lO9@XyM zhR1@SHDeEQzlEHMkD5qGs5IEw3wCII_TflBXxuBGy4l}1Z=p- z;+QDIpH*uI4mWls()f4Xl=0l!l_CJDaZMEU?`hii4pa9k>J>eIKw3b&XOacd>9&l- zjQGe64oinEyS%Ngs%b}Mno#JAi9sCAsAB*U0!qhJ%Vlm{jZwqZ75xV?Y9lzkmc}!6 zl~%c5JYT-;!WF`*iF)@BSSUv=M>k~fRhMGy=qad|m}<0W>FJtrc`IX+!rdI65Qx%A zY!V3#c8dGZUC*zSybmOG2zhT#19~R<4sFcL0a+d*~H{DNlfWqvBB;T za8~?bb?y*W{go+>VoGmx&YJ@x#VNp7WZ6*|*N}~(7MZe@qEjWfPO{oUQRM$p2W9Yo zrMtd5$!wZjn`eObD(?b7+jEaY-2ZUnSn;_)!CWOr2XmDxhH1)c#jlR+FmGGcnz5~R zNxLd~+(+scgGA8L4GV=d{)bg};;={n?mDgi=Q}DoS+#~WyRR!j?z4yYT3$#Ua5@C3 znp}L4MoUfpqVp%|VOVC_hzGS4JRo2Jt7VYACaY|ORV^5@ne#Fr38`rNH@1~zGOV-{ z(ybX;DdD9?b#IEZ%5G8nZjM$G&TjYsotac7|&&w|2501%O zpY95eI$NCQoat>LZ+3z1-MgYFz|ZkG9pf}oKs?YI9R#CvQ?WYj^y{3$Aey>(ifFGC zEQQ}K)dUjtaV*HPwiMI*=`soR|296lxp*z4^I|))$>p%I-o*8Yj7R!YEmW6NLbtk^ z>}|>%lN!$nV(D3xMW0hG&2!z==DSp^I?@&6QYWxFn?J!m0bui$wKu9;WQjF|tQjXg zOH#rVse{J8^k(3%!YhPM9b6iGE0v#T5BFKO^wrbbMB0yGcfEu0H6~ATh`(sC6aT49 z8^*xs6U%fM{*$+eR4}2I%}l;-L2g*GZOlwZ02Sa)hYd3l4bft2Ev&0_8FCbcYrSZ9 zqrt)~RLP-nmt75Kx|>WpF06KahoyN6KYukKDrm3Lsc4;+hi*Nz{8k$;+M9lx>z-uP z?kmNPegood;%Qvo0j-nOD=gLX;@Ww(7vsZRZZ*QY;p_@6?)#HZxp5hw?h1bbwRK2o z5i-V@%@&*g zfJqrxU*##kLQGkdm%BXFq?@Ek%=G*E)-;s%y9*NViVZPe6^!PT2DV1Fu1%@cF+t$w zZ_i0F$2V2m&g5z)c+s-+J1vrB zH(~rm2HZHkFe0%gq9TO1-sVSl{M$?m+6V_;I@AA?V|7SOzG$`7zPbf$09w+^?$fkit-QYF5Athus#Q7d znRI;HnL11c6=Lmav*aexFBAm7afY~<-!VOl9;Bh8Nw*r-QL%*qq*!$3HuHB;gaJo6 zO4j#eGV#HJ%=e&^+EfVRQ2ALUji&fBrckxCUuJg;$cjhcx`=gwhap?j&{g-PKhgrX z>T78~SE;lDn(z>bm&^}LK@-=z;&QnqS{lr~lA`D9ahoXh1but=(cZ?UhIsAY>2$JZ zS{;40o`a>cI%q#1Wkdz8ytC?Umd`!Ph4_wepW1T%Da)H*Iok=2m$!{(bNSBrfdB@r z9R455%+q^mYiRc*!150`&Wz8>5);4m%5l{9E84dvB7mIl@Q zeZA#G0og5SQxUWpNJ%Y9X;)?!cO`ibL?d?~7{Bl~nLg<6(UmTPc(SV5fThd?tM4Bk z$mlCpdVKsNsXaD6!Z_*$#~La@Kv%sC{*|OiJ$1}Agywu%5$<&Q6HC%bpA*?I4yzxk z7A7xUrv_u{-8N3Vf^Q8;Dc5`2Xx>+ZFMY+l+E%~uEJ;W);Fnm8GSsDbR~XO{nI9XwN^5|Ouv>=?~KpB3erf~jYmJAZ+ z{YMs(+EdU0!PG3E{i8S0iFuj(f2b}X2;e*Z2T|o;_>VLwlI^DaU&@GMM)r`Shm33~ zH0VcT5fD!Bzia+)e#(E(8?4v`m6}k73tFizlFgD#0`Y4F|3_8>##dkcxcw)M_A%Tmx0mg#m!` z3kI?BY{wBFE@b$PTC)M?*D>-lsD8zOR2qSyG8wT65Ge=%T`#2Ld4-C9haG(QlC+WD z!F`3cUb8Uv>a?F%RI8Mb-4COk?OHG}cj@GS(81MgMvU&I%c}7(pDi127urLM*^S6D z`SrK&$9y=btEL9TH*vX4A_fP95HS18J8t z+Cz&cHG%A%mPbY1I zxpN_(e&sc<;d1|uSL92}dkB;FrUFOa)a7I2=fsn8h}AEvWR;r6a2GF#ybX z^epT|g<}Jdx2Kfa7zGY++ZgZbfXQd&%vyxr{+SsW>3l4o2G}aRXDFg1Vx4ExPTSsJ z;aW^2HjTKx>JGDK?mJh}DKVcdsi~zjj3~|X=8zE0+3Yv4>k?#KPVS(B0W7Y{7srx{ zZ9pgBNd*iGvXA2F*>`*3@c*~~Xc59J=)RfX6#P4Cn={r}f`fVuE8kWa3>CGzg(v;& z>s|s<2yv2AZWi*Y0c}WPYZJ^3Y3^Y6ISx<6ycB#2J%n6V?|EX4)lF~JkvssO=y;!} zQ@`rvPbIbu4F^9+)MJWNwoXU?x&z}DpRnoKe!Xq8DOCyF2L7!P`vL=IUvGT=T>ECH z+r2PZSB-Py;~-mMsIKth10aSCiZ|YcfZCgh(g{NfcvZIfK2~k%hC=fysJoRvv@Ukr zahsMC>bT|vO7p15%x5e4aC+^q9y-qrqq{;y+&HtwiRv%CPa=BBl1Z zw42<|+4g(Kfr2_9j0DPNM{XI8rU|?JyV=G0Kh`GO+N$f)9+-$B8idwdQC_$ajz}h# zEb=j_&(t#^M1+q0hM>$eqK2H;>b#p3q5uC2a)PTqKee>P%SY?IwbMk2zGDP1aPQfo z5be64gP~~>h*@;+f#`C*G3Pc859`Js$^5@kSvg|?#@`wl4J6O_ifZTN9Tpry8 z>Kt&c)~o0mT|B^GzwDb6%OHbW*AIoTvB{c^43{bk>FE{`4~|~2dA;Rtbk=0H5R1OQ z2L63D!$?TxQaf@yvmN1D0Uf{tBk?iMCdDQ$@7fs;F~Sz16OzP|wRGvjJ`B>GJozM$ zfVT0@Wy&vXZ)9bcr0enNdPD*<&?@Jd-w0zaCfsI#2v6$p~AeDP|hj zL0Y3*VNbDs(|i2r_tPkzmFkJmft<-!m40%6TQVPRb(3@AZfkl<|(WTU3=_pwvc zm-VNf4!grHA}2p=CpZ}NXI-HO7pl;pckOj&7nM_a0LEEhBPFSI6$a$6#8J=e!% z+YU+txC#rmBjCrPu;2`|FQa^L@ zM;QWcJQwdvmvBGQ5r3Yma5Kh5@R^izFCC=eS-&g{4=QmeRG4-zoc%Z+@EJ;PUt6nbcXJ?ouB*j(bEO}vJhx9n8WZAgC-CTHurVtO3y z&jSt=bdHlBK+0mTs8}!r{~%6 z^0%%Te#eMFXnL;O)b_WX63#?L{py@>ESaf>Rd6o7bbd7h08D!k(|c%-J$Bs;QBnq) zsjd-I0Jc<=^4T-WxQ&DWcO663LV$pW1!569Tc)NWF}>opaO%uhzA;3^I7@bN9oS#qgxMVoSo zLJjBuBg-|9jJ+&W{U;0va)f?UAgjqGagDl|w(_&{Q-1}RJ2gFbH74&FC+l*Fxf-2d zouk1)0KC@@9r7b!`fIkq{BbA3y`_;;TKQ4-L0OdS%okTUyxk5;i%aP-k(4LjxiMP8 zFf>sRGrPI}&CH{LNjW=TP*oL{eS?wl&_m<0h+%!rj?zz>7=(r1faMPK4|F$|m z_Zb@s30a;1)iEaLHK)=*jP%EZN=c{>y_Q^>Bh z!}u%)9hlZPn}K(Ec>0^$5UPtEh?^yO>vFd5l5Tac*37995O3lg_^s^f+pH_dG=Y8W zKUfOA1fC9L| zz>C7_0~)tMp(OWE5CrrtFx7bzM3=oEXp~qJh=sMx%>yoqPPw%V6LO066C4bN+v#xY z(^OaeZ>m&IpnFzQE{?u^sEEHd=qwtSQ70y*hYIKqHXFO1x8u>gOXIrC2JG{U)Dh@hkZuQ&5IYHM@S9!JMV$0H`Z_khk<#`j{N z0z2OEY&K+ec6CVFm&%SS+u7)RonW8siUSOHPlE$OqKtdMf|? zWWly!pslE0`b3;&sNkL1MEi#;IEm*G9}+BM?51}Zg?=ViZ)KNj=C#8GHX;?%B-VWKF>nIqi@qD-UvbwCdUeees-Xr`9qs)NXgyHag z)mG=$-|TZeci0yyGZ_l4S{X)}k_$u6mZ&h(kfHWL$8vcsQ^o_A$Mdu4_kDDljj!>M zX20{lKkEv1H_*XI=Npzq5gsd;J%IA0*46z;EbTZoFIAeC7fKqZ+Ph~c%!ZjQwZfK; zJGdT}+}tRnv2;Xz^7?+{T^Z$^SeR2rA_hmrZeCp*o z;;UC{d-re7F@UxYW=i{UKmqZrC2CmJqf=&9O*2k3(vhk7Fs5+ zqZCViv)fa1BQ9mMCIHQ+Hrmre7c8ZsC^d?++qDAa=b5trVZG!dF39*H$$G^ed0woL2Y1U|0!+PZycAeEW1RNC>9GDg1njQbAsy2Q}1gg>+O{6 z%ptkQNSNY+XIql%t*^E{e%C1H_^UZYZoZObg}vT68ZjuP;_l~FLk~2wYmJ%o+YOw8 z<8PimRTB}x#C*DlCJqPgv^QkBWw7$uzrK>8kX! z{DL|ze}+!{rDCWWxg&8@8bywHINk)xt|st`sx)orUs2)>t+~c5pNXepd>aZr3hQra zJfRxYSTn!JrJV54zoQ_F*B*On5nXXO=+n|h=drBH(kJ_ZW;V`d8_^F1ge%-j=@r6YRAEU`w<%IyO3e^I6UMYiW zhhAdSmz*3v)uoYm`}YYUZd{CIUuly-h+0dL9gj6tS@`kGb)Ub%w#dQ~z6y*^IN`I-)YaB5=Q^fZu3MUTv{Wf1*ny*{^qQ!^KDk&Z=%U1?kt#W>c>y;g` z1Pn$NoUEMXKy0z?_PdNJmr8mHep_nTT;Mh{t4ht{{pGDV`Lgxv(I;H{Z1CZ>C7V3{ zDc6N}S3!5tFZ-QC^Y9U6y*Zd|4z@B58>ch z=j{FLUHftT{IP+_z-3BRqOQql_U1-$K-g_4B&ADp;*zppcT7?4fKj@Jn_f!j#E%z+ixXga7oV5COc=`DI(@qvip+ zj~^q!E`A`eE0XV9MA|vrq*?J>Xk#bjmM~*_txoS#)+`w?j4}vm$y1uKL7=E{7QSu| zjOUKCu{m0|$4$*6!VC#&SZuGZ22_l(E|~S;^_n^XKM;rwXb28yMdN*Ik^S?ldv@5E z4D(mT7sq*;M4X(x#MC81C>M-x{t46GuXCj4 z_dJ(Az7kVAFT6nJC3PVROIQ&s15bi&$fozl7o{}c)&z!wu74>OlG~oaOZ-p@)P$Sv zcxvj>W|mc%Y~YXM8k_Ato5^>e`1nZD(SL}-AujLf3ft&=VbPgukL8nm@OVf26$#Ec z0~q}}2i&bcoduAWfiw4gH8i2;(p}vi-0FvnCpoH397xSM4WG|gOMTLd{^rRCZycB_ z@R*M=5K_d&OILJ3<@TRe)$m|O*dPaj?N9xDz%b$7%FUw}OV8Sj=q`MHkeOqu{c(|& z4JYs}udkNwgzmo}lMT!K`qLF^%Z<&`rVO;@UhYmwPx~3Mz8(AECdqjK$a;@e9vj-c z6TzDTrfi_0riR^|-JqG7nXG(KE#LYw&tgf8_cAg$q#DK7O9VRumTBm2O}$Hlczg;E z!ESE#a}ItrwovK+kZpa9#i2!iMiB@t!;gOJ(kzBhyghmF}p8 z>pP=G`WB#ND=`#67U7W*?WzK&$e;|*C6)|{1_Wcs`H3MHP_{Movk6PHN-U4Ad)M5; zDm0Zer9UXN_Eu{Pj3F%tA&AQWXGrQ8dWe*j!!Oe2LwRpGiqnOK+5Os9tT-AC60pZI ze~%z(zkl(X()u=WsC{=m7{3`Zom zL`ax$>fmy&V`{vm1#;(fKPO$VMp1pB3tr~rTfOgQ;ABY7`9iQ)I`MZ;#CA+mIwU!* z`ExlIf`It?-1_6==4@63%P8V^@dFXoR2T0p9|61O@kAwCZ(kY_*L!MzO%FT-N`<6; zh>-s!<2~yVf`apmi02g7-VVlicA7~oXS^dH&la31mEn3o>i_g%K9C+Zr81;1A>qjDAjH$>WvR(uSH@1HIH2 znU*7)SKM7NNR5-xnD(v(qiFPL{gN;ni{Zcl=M`KT)l76SlSkp^MQ|Kxl^|Sei@&Fo zx`$$)Ogpbn{|$loqeoy@?RTdqKZAJ9J@y;Us$iFAElPJ$49b_oZ$bLhyAzR&)H;GGUDN0t2tnoRTre&Eng2>*q%R z>nP-S4TCHW_MGd}$>eyazLK9B9ha)nXNm7psy=G+IDcH|nedP=8zZ(8_TTiPga?$c zyB*liPHCOQ)_h2v;%}xZxJ-we08?}Ghkx;-+FxO|^Yh|_NKP9$8~s6WBvBh3BOgk-75j3jJG%zz&?Lv9F`03N$ibxz+*gGa0!SeaMyTqII7nTV! zcNxK8x)gUqU=DZoRcEPfhMH{v4B~Ta6*ZsNQq1mp!WZ40Ikc3Ur-LjPNs4(ecK{M) z)4ScB6EhRV!TIx=6+d8cw$Kt!kYL!u*iW(6r7LhJx1PwJCu4syJVy6RIw% zg8WGl=;{$Pl(^onwM2CSg(bd;ub&g^?7vE9#y8U#L2x#hd1n5^(=cbNI z37n3(a=ul=B#g)@b7;H)r3`O0g}guw7fqL|+o4Lu@!Fg^>*;iM5z7?fsVgfB{l&xE z$--gk?A;rP6;mRjR##qsIjj9|e`Ik)`I_BL7FAqG!b z-cDjt=tq6$$q`CIo#oD5~{lnXv^ab|y#uRC2^Ay(oW=GH_)|(#7 zA-F^%RrN1n_1 z?%=2y)QoHb{oN4s6{FrpG1O1%1wHdC%XANCq|*yDqL>^5?t+j|`PUhuhhDsSMM%FY zB?t~3I9Kr*kSV%r?e8|E_SQJFR|z4>u}Eu1QKX;`7FWe4>ORWUb3xP6upSj!)S*@E z=;BF!L$QnQhQ6D?RUc)IT7uC@b*(N$-x&~*2DvS!G}rWBg~tBw_U;Jga!_=PQZ(0W zbX!UfN{d#s?dd&D+{jVoQgH-TaCFgMMAS}prtMOAYBhcs5~HWsB~dl6rkb;Atgtv+ z?R>@DL!!=*R_rMw%T86-s+;4~Y1?I6MEpwPs$D(yU2^&HlRhdlufp`0YXqrQn)!=YN!xsl)Aj3imr8xrovXW%fn9Xz(46xYC{=2Jq@?%J0?;Hj>d?oFf16#p-iRR zFO!MeTWysqh`yoSw`9;NDq9nI!mp^Qnat6Quq@akYn}S24sYOxtPUj~9N?oh7~H3M z!f)qUQN13R3=uX5WF0Fzz9>8p@80a)(q>G(D^^E=vR;_<^z=|8oC2vhA-gBUf0_fq zjh2bAw*kl{Biw~{jaQ9yxy~1F z1OF57$DtD`AS`BjDk9rw?RIXy7t5bqV?}0ai-IkB%|E_xQAAWqN{aR)0IJTyuF95L z@l_5l?5J7UMvlDQ6~}^i77xL)ssgy|cYzZTuVY6Rle)qYFETSI`a-%lBwF$q80sH5 z)f5FFrM&u+#I@HqcKe$2^moaak|8$*L;sj2E`@w|kzfgSTv-f)8q7Bw}O<$)yw7c;#j&&C?1PpKW-=0dPk>s z)1fj?+@9aZ_~;E2vqPfYO=gpxoejsxH=d?cKpV6GWe!9mkFgX%0u&_CSZdi9R^M8E zC(Z)E?Pr+{)$_?k)FoH|X9TICu+;HUwQuJcSj0(+r>9SEZ!i0+wT1?fOoZY|%d|Id zzI+e`hI4DYpU9QYr;v|K`UsZRP|&6EVfa5LBY~Zud9?JR>zS9_{;F#X2#aeX0UT@1 zq97+|to!$!NwBGtj$ibZQ3mzrIfN}{J5YS$SS0N0dn;0pR8D3g1}oB4=S7Wmh^qT9 zFel00Mse{g0lU1a&mx*H0bR*t@t?W-Y>3vk=?TY4f}E4j5(*-T>1bwEEKhz3(|XtE zmdM#j@e-`a-0{IC#U*gRgOWvR5Y)N7Xn{=yX7uK!cD&zRgk|4G`3SW zp4u=IlTuQUpe6^JTAChD%N+Z;lFicvOA=#RJ34)kX*jk9>iPSSV_GZ4ad7KUgu#n& z>g1VouHLhnq6C(_;$Fa^g5|_boHmK+!WpVpBI*-xya$#S8aznz8TAn`ku@^&ZuL6Znyw14?K|Y=S6aoUomZPXNx~&XA}ghC55M&{Zw}D=CYjyt4Bk zqFquP@owF1=g4VxdwXI`|0(-9Ah0A}UUBc+)97mkB-2;+Z@h*n1d(&7(rG*g~-u_c*H@8{5*lANJ5^mv#W@VTK>ytDZh}# zM3q(%2XzKL6bas7^e)W{31L96QV<_j7e3aWv(0Mtkf-aqJ=%6)+WjmsUQ zg(x>60HcQR*Btm5DdgAw;L(>`$u}e9RcgQR~>M)W*=&-^We$ zs=5@VQ~RR9Dz^?%8iW;2i=qR*JoXN$mSU4#IV>TwJe%}^e~9o@d?|r-GG!il3O7#u z%BiAqloRf%ZjlvpP1GZM3{Bro*S1$iOiMnFV3R0YSeky@xo(;``jJ^ZYhg(REG1D* zx#s&SurIJTk~j{Z9jvNQGIk`0 z2giproX(;o@A!QYeG^e!%m5LTR6vs%8=2CaUjbCn>1sb)TiMpBQq7+twDC`k7EbPW zmeMl@ma2)f6*%v|?zHgLd5w?HIbQ7t#?3L^9z!#DTuz3Vv}UU|4GpGak+iX;fJQ6U zccDv>1WWKhrmr{aF7}81ToL$(%hQnYa8SkbYar&NAgrPIE4ls#C5$1gUsNn*%=5PO zV%G-Zct;o(zMvs`w5C!M>KKEP9dXI<91zCDaaq+e@}$s+ylhR@unVqI?Xzq9mh-}p zZl<;kWZBiw5W>7F0v|VWJm5$w<}?V+WF}Qvk()Y5bJDh7+1)7-K(#z929;6Dm(Cwx zr{BWS2iA0f&^(g*%FVb9J_0)O7ye@O{T{}20A!w|lG`!;3K(lVZi$=WYZXRYZ@G4E zW_u`B#Ft!gnq(z)3+o0Pqk7HKYm7hP@+2KkQ2&HQzBLVwZ$ycQ( zAtJH5`c7cp#6DG`xjHv2^sxE#B-GRhRCOY+pon9b(Xg$pO)CyU^scIJWxBJGfO1$R z86pWtZpO&=4Hn7wuyu(+_ZR!6avi9+!8k$7CB-GY<=l~iK&wf=f2p7E|2O!||9`c55Ppc5q?qpN;jtN!mrFu{uA1uc&OpW< zg(f?8I!{vUwG{GFRwS)E4>NRI$&PN;y`9BEtIol-;>oKA1P4i~5SxC)$cdyA& zJ^=xSpk`s36fB}Trmq}9Wy`tter8T3vIUXq@9pF*?=%!uY>Rr-^-Bnnqg@hf`?f-|n{6Tjf+L^9oI+5mbW-GWIC zOX?IK91-biH4I-?b``JDR`FE9Vf!}BM&@Da@8=t8vaL0beeG{8qGbnmO@v)k}U4JDaOn_TE|kh%(~s!i(LX<&vD0 zURzEU$7nm{yh2y-lXwGkhZJ&jL4hf| zQ&d6AG-w~|dZnx_WH0s$(j3REyDHP)jt1~j`Zz%+oVRNf6?V$P!f%t(aqm#Hrr*E$ z#iutvCfAKdIT$D>cSFOgp?cmO$VgXB>W16OYn91c$1OunG{#woK7!E<$ zuyFS;!77TnEhn$9e_lj{T@+JZrMawztT*McICQ6x1f!jpFuk$pxqIvB!`Vfxhtu2O zza=#hfP#H9_SCN?zxcPdBVWtGPhdmFka)0*q*UWA9oFl@a93407ds?nkB2b<;@b@>zk6(RCsqwpGU(Bcx4c1_Um3jfk$Nrr@JJacEz3ioMZq1GmC-ghR=#u^_pK0J-NRNQ0I?#zr-b_ z#s*aN#jfbPTw1BwZmw?~x#ypKBZ_k_GG*$Z-YeE4zA-0kiZk4htZb^6#|sWOoASXW z!MG;+$8JsHk{w|F)^*?`z+KU3DVyMG>t|B_ba`5gZ~56PQF06O>dpt;eUi{Kt#TS$ zbX0YJlJ+l5FZm0C6)Oc-sX`^)dxSKnss642!=LRaLNqC#Ctfm}(XpuVUm58eHJ+2l@>!YqRMHKhn#c%e#$1DqXS(=zQ!J3InJ8k(N6+I{M?)1n+@{MA47{Rh&pyQAgF zpzFg1eNx`g;3WEXDb$C#V+SCBSp?LW9h3c(@7gw@57v=k>YOg}{!R63UYR6ZvbWW* zq1=ZI$7Yq|2t#+O3sub&-Z6*P>xeCZDr%#L3v65yE8D`I$3*p66v)@NSPd8q^h~Du zL!y^==%TW`-}Ab{D$z^8sW_&he5<{n6v{e#QxSlBmq+}_FONsDM;aaD4FHvV<}1~W&;J*LL=PWcSE+B*prKz}j=Auqoynp-=q;)S zm?yfafyuMa!u>?>%i|wZ0Ij?}#=`1LsDl7Y+q$OyC&g@f7Q|`GYe&cJyD?XlC;aP= zIhCTD+ALiZv}hmPI$sO*UF zG^0xo&r*3uk)>s<($Ca7`XfD|x!0;Nm&I1`ufE7ReYp0cW&+Rfc($p z^Qe)=&Vp^~o*q2ssGmrlB&j_m{up4Is&opfEPb4+!GO?LI^f6S)@9qnwA4K&GY0jC z-{a$&hl|x{OXZe^OMd>U&e~*7X-p1JOgx5s7`mWMrP4Ec@WOmg=!vbYWGiodf3_CN zi%z#aGBSX0Oc91y&KP!-QyYK8eRJAOyM5K)@B;T@jUd8MePKW|L~P*D=2b(A17nE9 zje9wXMbf=y^Bs|PI!~@DuwLVDsSV!FxXZZuj)|MJ=fDL>;;>#lj*v_NB_Y(Df0??f zOMaa<58F|Klu#Y*AC@Jeq5nz%k!N#HjlFO5x?;NPQ3;2!FBn! z_6!1xeE9S~YTW-QZHQ||UjOjUAsYYxT3v{Pz;%$Vp4A4TOuYZ%p#!N`2n9PxU%jJp zinVJ-ry#a6tS za-e4jdz{lZ<3puxQ=!dga@iVl^Ionfuic(jg%}(qxs~~v&$j1Xybk|_%&9;uGE>l>PMR{w^Q>p6QKetzWv>> zTH(G2)!g=neP%hMNMKwU5G2NcYop;-Vd3coq)|uEMw@BRch1kVmyKd=qBhRQ!!3{z zl;W~<9zYA~LSq=Fp(|pZ zmo5-8=EDW3Y71I$&e)Q5XtbMc`6eBN4Ib0vxlz+?Eg&$U;LO+r3n|Ilt$yi-la4pfm7<~5@=tMQyYUK- zf$ds^?oXeP%b&3PS*|DD-wYGXtDfbgfJ0#?%e0H4^lvDQ7@$fifvOAv1r3}56%Y^5833BK!$TFhIx+Dt@WH-Xooif1SJi7VXT@m^|mT)8m3Agek z|AB2E)=D-HgtJRh*6`cfX5^|v%~VyV?GJy(yrAuwO20D914>}y>VBkqW6n0S$GB|X zT%2dKvAKcwqZRQrk{9{zkeYg|f%>+@#!80{-=`xh_46k?`Q#M*>hRm^pO|SCQ2=D` z4sJ|>JzIbK!-jR%%NIh8Vzay7J6uHBC4!Ei zo<40QWBo=z7M`cno>!gZ9!=$4^A(##0q^{l;bv;Y-s|B`Pml9GyEBztdxI&egY9*uDVyH){`~x?GF1%h=;VF6 z3RV^!QP1vqm`!P_Ov07Ue#a86LeWwz?&d~;YMlK@brzOqrEe1EU8|W&4j{-Qv~ZTM z{*U%9WO2kqrG`MRl`uo?O#a1YVXuoKg^dz-2{Q`=B7n#8TJueX#aasHPYp&@T1R~~ zhn&Ejncx|f-Be_3wJJ|jG-n&RSs)$@1)IBEijxohO#BsfPV!7_QdDL4R5h#c+(gah z>h5BPP475}kFsWK-hJJMS?%f&#BQqsnkj{g%k&?Xm9l4M6QWdJ7jT|q_VEj}pI?Yq zt6~KvJ$o3@L`+PQ>KxWX@g2M_qf<+9FnB)eZc`c~c}U7lmw#i(qv_OWF$qg5Hsg}l z5s3fx;v5Jy@&lQr-EBFL;JJNuy;IC)%9MhLpWA zj;9OyxT!lGR^IWWzuD3ML5sKb7yHx(QE1c&Y^5M>4^rv`VG`}x=3KSVT^_4KudA(c zzT?VtuL?d2Hn=QeEIU}RHC}zX*ec$ywM_k6v94f9JtY1HnCd0?67^ahql+=NU)>>* z_)ihWSijA5;KNunQ?C>ax z;A5ir2zF%)dKKixt%$S|IulKw`(1YwR)&X6WYt#@!YQ6?R?ou9(>;nEE9@5y=bM=aiyyiBDxdqr?9|yl zB0GXzw>bE7s-!{lhdoQPht#>9)ojmTY~ls}4c`*m=2Yh_v7Z;WJ;?Z74)LTP%&gfP zuuJ-8KNt;5Q80J9NIOZ&fcphD8dI8b-JG!p;CnjU(K%lB4B_1DU7xa}Nqb7eQm{T* zdI&Tr6LM-76Jz{9xq&3unzMWGIF+?%mta{(3D)ATXxQ*734K_M$zztUI^tH-aH)s* zuf1$5w@ozvs`L76=CnUC)ieYQc#iz?@LgkBCVJryf?pBU0p!3%m;puCNn%d<-WzEF za{2zm`LC+#EywNbia7OA4Tm441%j^VVOk+&XC6%gpVU-6i?e@j^t31(OiZ5Bv{7m} zlTu&_6hI1YEbDBJ61Tfg=>?ahfaup;jZHyGS4lN1vk3u%2WF#0`?c9Q-j;8ZQ= zDnfDk_*he7(@7etffgR^^BkN#bu7Y5+s*Z<&(b^{46fdZf5t0_^bc_6aYgZz`)F8{Ktc?}t+94%t}pOTcv~DEfYRGsWXIa@yB*&rLj( zjJa6NORZXr_diB@EUL6_+V9mtAv&G&@b@$WQ|#!D9eZKdX{}qauVF`j7lDxvLIV^k zhUT_sg!f9D`Blg^9$QH+2RzNdn0;V7MoPr#MEJBJx69GFD3-Yt4)8#CY6617hr=FKw~d2?)LF zfm@JRO-lb|Pi|&H>~OElST^KfE9!7)BjU&SuN2iUuUP>WNb?i-!GHJV{|7>ReOLp! zyQW`Uv_QkDUf&G)r>6U1pdgQS1cAvlx2fRerHVDw-MGRPI@e4<`f)}>rv zO#4smYW%;Ek7OeJUVXPW3_d;#DCl2ZT~lJz)jbk<>c}WF$=FVXOPp{a7ynNN==42f zH=^(x{?Ac<4>%SX6feg@8}_d~uq=caQ+}`Zt{U~bEtG%Y^X6dhb`;-d{-F`I79$w{ zSP|a|Q2jApzQQCiVxRv{e@v0li%li@7l~&_yz_DK$w6sD6@t9~x&SQ55#Z2A5jUX{ z(*OKNZL9y(B+1qWO)817M1Qm9f3#6}ANhx9V_BDh;Kr^$exhjVKg8R8`d7T_{5-G+ zAu}@(?+x|8qVGQX=NhL{fX6I}OSU^yx9c4GuV}u2zw$DP!~}Z~ic#Dxdzp`n)QccS z;bBOZXAySIAB6wGYwRSykooRn$RL&EHraI#gZs$d$y*&eK+VVvJ@Ao6g#1SLv=8`-CC+n+4JZ9(x1*;+>gRD6&C4#s9%BanBj}IX5nwFY) zL}Mx%5UDAYDaDb41sJqz3;tI=rl$z_-WXAF`6Ugs<|x0=_Zba?5bU3e%Kvc@VP5_C z@7hIFx8`$gsW^sboI70}@t3Na%&}c|Fm?|V>}G_Z)6k|Z-9{f&d$qQ02eWtU*4X$s zJAaJDVE+3(k?xn9?~s)sFP-?w=7{6cAHjbN*86X-jxvRNBOo+dqGvb6W4HQr7W402 z+RvFnFw{>tKuUVWdN(2&i163-4Z({GSfG|R!NdLESz0&{>j4pLE0o-FdKrg^cFxZ5 zA$I$<=!Uib&-01tJwkP1PY)gXyyPzIdOD04k0HT5T;FMNbb5!ixz1 zWW#yrnBx)bezr+Ss`0L2Suog`yzCdFp;4{dFDIKC&+Q09K-L%l1EUC4NkTuvze9&e zr+`?Lp8Fq0q%9Gf)`=tOlh?CkiBkODe}Vd6Yy_StUn)uOK*+}Xzjz6m|k0XGJQN-EPo zwMV)&oHd&$RP?rP2Nu+TXfllc3R2pIIf}M2m(L0^(v}^N(HS#pCuS=obapo@khyK&lxhIh)X%k{S85o3a zf;yVpCYt0*)LE~f{gy05BR6?1N`KH(rj0g(oj$F-fsi3pn;`c4NTGwJ!|5Y5vS?aJfRnAyt(Avj*}EI1 zRkK@{`+($V=G^aBKLyEn*0jl4S|8W@^J=BBN1NJoU-?uwo829(VBxZ9%&WX2i$t}A z%JC-TbfO%5n8Gm%V7pT>a{wSOVsFmrn4Wd;-Y-t#6{iULik|{%i({LzSLf?%na^LCs1KC@amAWsGZXdW z8U+L{+T?31l{7|g@KAL_=!2D{abAZF$j3LS8GS6PkC7;0VgM!fNe?iXk*1F3uU}Tb z+Lb4}6ga?RVrM{D-*l<3wgop;$3DGC{Hz{a`TOYC%ADbGcrT>0lksKw{*cJr+(Q`p zayh|h0B7phRGy%aQ|YK%E)V-6`)yEp!)CLA3bifr z+Zl(3!$pVU%cS^)lmu1e4>gBpX%@&ArVG5e<_ABl&1B_)z*Vuk#Aq!ARl|TFL>gU; zx9cPyV^xv?DA~k8p^a+YBunAPm3-V>vhrGn<;Tf#K&C-1rzJrruZV{F(bw` zDZ7{uF<1BPzPMT20B`n)$d^jB)T~S7feXb_r=jJ+ejdMu&?=LL6I8hkR>a3s16pr{ z&`2$)=G^QWzMz> z7Vml|8+ZzCjH^u&CTW5-n>2ZT6uGgpUg1(>PWHK`h%mU9Y8PGagC{(7j_YY>Q)=cf zz|*~{P1wXaB_Hbo^vB57h!roK)%Vm%Xlg~#H<6ft+Jq#y8{Wrt1`>W5-{zuIk4@1I zi!^I27A({1T&nGqC0~WBusq>W>o=&Yh~l0Jx+t=wEuc_Q_Wm+%{NBZ^uF)r3rl_Z9 zD#hS&tAdSmo(E*}I)1K=I3%(L3-vLXl0aXBMR?z88EZwbK+v$7h+UzbnN-+q{a0dy z7n&9K)X);t9q{pFjy!YiQFdxAvP;ab{jU}*Fu40u>@J{OXhO%*; z{tjm$dM|bD)fXQc38Brl_`T4bq)Ky?gL*3A+po_HD*WnB^MBq%?P*sMxr&tCfO8O8 zMIBBBLh$+Y=bMeo{PKLw{Qt|+FFd_iN!7SIxMR@QxC8zBre(cj5o~KiA1=@V#F5w zVjjr!Le4O<9A*3Jjr=^jpwlV7Y5EU;w$nSs74dZ-sw;=iA9d|d#Rj{N$PSA_G;po@ z;!5iaW@(8dRY<6ICW|P)CuNCf6jgG*cDpV`>RGa*6rxMP=2*Q_Wshl0R-U~e zBa@}`PudWSmJXG#l38xc3kQ0)iq>NQODFwVb*;?xm$d0#p zUlhi?vl+E^dQr$l_2B$P8b*=wYr6XD%D`~@rhdGpk5C3octl=mhWw$);Pv!}TE*gE zOA4T)bc+wh&#&a~fOBCsX#;|(9;OPgr>j{G5n;+b5-R*;dgh3n(zT3nJsQYK_$CoYh&G3& zId_M(R$Px@YIRqr)6K?EeHbY1d~B|$Vu3%MByK>6j8@MKU-b9$FtqGc$g<6~E>&R8 zLQ-+M(z%2FCruq3-*zb-23t_-n8Br`C==PiOcy_H>2sFRz#xMt_5Kd2CH1xeYbF%L z!4D*|pUsw1>b;4|gZl4jM|MIsjBFz-#U`(~aNAx3p!#|UPdlS4Sw=oY{4+}z8yi+w z1_w(`HPsiB#?iCdFgU#;CqMR<=9NlK4qQrMPaT!Noz+I*@e=e45Xe|kL?wgPRg?Xg z^222rD`%&`-O}Phm!QJt9m!oc)2El~pCvf4emPYE36T99Co(r{v29?O;{5U)nNPNwLxY-;tZ@or%=H2bjz~sWyX#`FNKxJKY zX~a99M0c2c1{RS^<6oE9TC?L&Pem_I$yE~g609L8Fs>2I*`%wMbn|lvEVj?MYN{CN z;+h6NQ1qIFcnD}f>q>*syACM5bOnu^oi_L>rH_G@fLKO1m?FCS)2sr$P#9oSpk{-Zv#C5j*p>tX(CP*9D3@Y)m)G zv}?2Z1u7T91_)N$3LI5Ok)=^t&d~Y>E8jr&%jX84f(l$nhqQ1Ys?IP2Pa=i4hlQg{ zpD2asS1nGiM9iz^1YuQ`>VgGRyAwA3nsSHG-MK$@L4$+{t<$9{jE3xhU9fSbun7{lwlVMkqTY zbxof1-DM-Qo&;Nx{pqZinvGa^XIBoKloMbk8LS~d1)q!;0(ePfjqXUU$ja8IX`+%a zI~8lG4A!Qm8lpBqR1fUORI(-lK6V8=v$2Yi0Ru+!S@8uvSylFOCV?lccdkX_q`%^& zwM`&jh3qjplEWqfMBuzju?;0O*CR6#+$4d0h5I{&A7!pC0;?H3N~N;kSQ$n;CuwWt z9>9Q9yGh~f`tr)!klU4pcs(TzJBEwV0Puo?n7~>D*EB0PP$V}bDrzReZ%d$Evsm9y zv4&w=iKWeJ9Oy778pb5^jm@>}sojWkKlwPP#&_^qWP_J4&J?6h7ElFNt-n4PhF=9X z+K516t2IqEg1ZXS?AZ?9yBrS9P}F)`;|-nJyrEN<*EXx0Y6#z%XWz7}5Dap%?{)7y zzv>itj~!=5w{ew6aopNDL)DT_&v2XHJV_^HyDMHhd%X$YpG>e@k>*L8Axv&4$^ZRz z@EIsX(LJ<_%;WYWTfV#z$69~;z&Qyudw#tWVi|lV7HV%!G$6O6jNUbJPz={i zh$y}>-K=V~!yuk7dp9RrY>`KHpqR5*)wSr^OUu(xUP%*5vWQ-nMjWbfeqkG3U8g&~VQ_I+ zsIZg)@@#iELYaW}^vDSAiYO%bDJpUXHaNvY`wBQG~zkid22ISJqw`lOmOb_1v(A0sHin( z*e}u{2~iS4hUnJ%pF?@$C8Azw<*5P0iMnj2-!|&R@`?11b?nu5j^0=3&V-jKg%yfo zX-Tm7%?9#PzpwO*7uJ_>`OFB!H2h#92xGQ>7N)kgS{GoP#x1dGJF_nk+S~2)o?%!q zu_{BmX@N<9%g6SmUl!<2|BI8H^C0gFnp%M^_skgCGyId$$Z~Hr>^>u8uG5*vt`xxu zY$jmMS#BI+d!od6+(Ps6H-`l_>f1xzm!x{5t1vMInHGx#(DfdtLg%Pnmrmsg#V^^9WZ8wQYrDPKHyE0o~2ta{k_vehTq>!P2l zG^o-Np4%PrX<~M$Yg#Vcr1d%V+G=461Tl!Cl&V9O_;M1nwtD@4PE#?;&`eS8y$b}Nvk z;S;H&rpUIAP%`+HB*->Y4oN9XPEHO> z;mA5mI;GLB=hJ{u&4NYMocp{(?>xjI0O86XZY1oFzwmo(-@7+M7W8Iqc81s8Y;&j0 zdZ-DMvxI?tOVXEoa~H7c0l$oW-;0J3Al_6Xxg?k&n z!y{qmzZXW39W2~=zgY0!$44%^aJgT?YVJ7BmOlFlt$I0h4Go1pcnq3D{o7F%{n}CX zrNmdsjLL}r_B*8}-fMda+m=e=3_EY;`h{q4d%U-XBJv&yZnhuAggt-awyYuIHhfUt zm>ZaXk6<^$9T*Nw@Rs?vo*aH&^*WuN(~Lh?SA(r7zLCjO7{7o?a0z~*(?PxK3V$4x$jW!XOZ=*ptB$Bq)@)P6Rm;yZ-^|Cy3cuJ zRmlGMs^J%$O3D0Cf^%kc9vh4dCb^nka!suQ(T-xttQ)M5qpD<5Uw8MP=;47 z3~z5Z=m=uyF8)hv-yPOe_pFPb3ZD%=6hx(IL=g}WfzUxE6scn9RRs*vW9R{b1qdJ@ zARry7(xpQJMCpR`gx&F7E)<(pbnboh6$gMP;Vnfyo- zL1;U~=9z0nQdj7%N_xyF>X3QZ)8SvIniT*U3=C-i&n22J8NT0QGcgGD>w;UTjBjR} zLLM~D&d%tAf?5T;lv((aYBc4JMt}7L*d#eS?eEv+mHOSkqHUIs7M552`{CezWsI6V zkM=2Ml7JF79-1$3+V^q@OP%Iit4x?;6*yG=r(8lDMf`ajpz!9|3@9~I`|(f z>12?%Z!b@ZqTfMA!grAr%PU73i`%~csS@D%bR)6u1_%ir>t>dsjMHbMUo8wWZX zQ}YVc6wY>xn>&PM(d@VUb+Gy0sqg>Np#8hcA8JX zTEny=?qzz@Em2Wi2P{$RX2;Q}>CH>C$Jb^)w_DcSYMXI2etA*&N3rW8Ru_cL#L7D* zb^%L_XZ0f1a)Wmi7=6!7&`60ecIo^}2le)%R{sIS^5;K&If3V5VPC*~jj=wh<))7^ zIum1fX@2VsTrOWT)MJHO!FR`Lw`MQSHnh@ib$KPD37lOnCJWzg?%TlduF{4(Pb<2A zS7HRp89qx>D|HXSz=^`b|^0$F` z#J6|#Jg-;$mupKsi-xu$Wg!AP3EN)m?E9~3ga*4!^a~{eu?{Qr4D@G}WYK8x_)5_ zHvLEP{xbScnFMmH%b9n&JzP`d=x2GL$0$h`t@oH2TEg58i~}47pS}Pmw-E85124pf z!h-y`SA86~K|i2vn71us4DpHg*Y%N-ti{zEBL#zOABNDyY7a(x`Y&KY@kYPuu(Bmt zrh3H8H68_LN2PjwoBFEujlI{fgHUuCY($e&SsWG_VP`Uzr+bl(bDxX2X{$2-WAkB2 zZqf>6sL1PZ;B-<3I5Ao^QkTNoc2s{n%@~%vUM)o?Tgs=qS{wv^(;49;V)e`1EWUk((;Z$q zdPXGm4_PPRN7FUqRrs=!L!NLM$axr7z?H)7ti?ku*p=oEb?!2Lo-^)ljBV+gh3PC8 ziI`PCY3Zo@Y!to9TilMg32R8mQZlHpr-v4PSriC~M-Ew#lFHKCMiaK2G8eR$b1!Cl zt%h1y^ut8ddkq(a&AvSI!`GeKI7>= zQseLWR>G6;e3K&GWa$P^GqEsPpRd#CtCeVdHqz%>h1J*wpGtLKjmOOnY4La=nUy}v^z+zL+9Y4$j`_Dz=>3$2Rr$o7P6W{u3>6ov!wNdSvITP`&D3M)LMrum`D z^cdhae=d={Z#K!I`V1Ymn=bG!eu|sVFA^&6Je-2G=C!86xoKt9LLoax^Q# z-}TGSn9A^`v^5U5)shI#LrV5+Yz%Tx#DbP9+-ekL<$u$*#nQoZ#Ugd1akE_Wo!(CL zwq0utOF80VkNKy!i6kBFEu{m8u~!XCP@b;u`)#j(%soSMCxc}Tdh~vq4B)P&PgaOo z=fV~@btVX+4FzbkPy*=u6@Qp-oksJparVQ9JUmV87EXwj!!KVi^7uoadg%3kc>BHp zH4&wNU1@Fh$~47WP|kr}#y7NM?@~9#1q$5nV?>UpeWa8R8d(VJT5B}FiLtya5bAU| z+Jds=xwO$i1A+Q|pGPkW)v+dNbH{_1QpIKp>Dv=3DF5nCA~eXg`N&xA-Br?HC`k{$ zoEq0l7`Se6p&yMD>C`LRrHP(Sd^~&X@HvI1 zl1n|1vl2|C&hAMED1^%(t=R8yz`i=utLybihuvW|JqLHw%3IDV`aYSKr7L4DMr_#Z zCv8$4EkchA2wl5ZuprQGk5PStH1>yxaEZ})hO0SnPrf?pYaepYW7Td(XsmK`$vFCg z%J;suiM29KwnQr+pITNbLp-SawwjwCMqcQ#&6OqFj-W(I(RUH*XhR_7 zKbMOG2AXqm?-O_EjR~BbvE7)(V@ol`t`FnUXjwtwS-9;E&^pOSO)nV2;_sHMmn}|r z%vhapFFAY}2s*~N^>ITv9CX)9f0RB~Af78|;ge{+J(?J1Xrr!v*AL^d`)>Imj#r7A zu(3-v>goO>WN#7qc4L+_fBvXGEA9N%8Mlsf7{E19=T%rQZzq~G_*NReJEqUnkpJJH zi(QX8sfaSkEe@xm#}ID%z1Hp0ccfYm60H+OTbOAz)I9q=0rxQMkp-c^$!rqHJmnMq za36o`9djTavl$y3A3v3`cIC>;+KT*~L3+_@)_0o>bKrK0WR#G!pYX7Te4J|@4nCah zj!Np~X**zx_eUf(glQZB6MvYy{oD+ctQq%JEeW%A?@TU)}Eq9~TWaL9+amw@G)IyfMflx!w=j>DPe{$NN?0YZnD5tzIN8S z>z`-{7gJmpaUX2`OL_5U$CXo(ZzW8LZ=WhMbR$+wcSk!CDOMEi{0-`Z>P(EA(N9=l z_N&MrK7z$5YxfOylUw*7KUa_nXgH_T(EHbfEl*KiB^1OUrBz?qKX|LS?4*l-!e*|8 zXfs-R3l^S$v(eJR{6}9ucAht_S7pB|`hi%smY2MaiV8zAYzk_3O6)W4mf`8R%Rscq z_YiVlzd~MGHa{EXYBpHku-C}|^(yZoiH4#vervCvw(Fvn@B9TsfxV`4qgD#L%RwFK z!^VDJsLs^xxNIMp4`RObYaz7#F>jpawar&7pp92XuEt;yvs`LVi7NTX*Ax8@KlyAU zek>jTTIG=2CGYccF=};E=c{XoNCT5sG_-xN!>cZa4)~SRo>&t=v{Ab!=B0uTf7}!nm$|2bpFG>4%9H{}q2oVq&W!l%TwG39-#vcB8n#o+n=a-Oa z0t+#stMknf<2UqI1>nsQUcYqeP5G8nkLk&n$+R*sNc``BOMoaM4z(Fbn4zzVA|McR zW+V3~VKI}br(`uaUasQXr+4PS;<;z`+b`va;XW3Q`Q(?#6a#*#K|f60&db+4S);kv zINN*imCc0(6+qsK@a;%nY9;@gthm@#AF3h2!Z=nQRF_G5Z#t$zl(uy_glYp&XIgb5 z^ThgTQ|Ko8n1zA@0D=CT;rTi=;<>OoFVeXkf#6LWy@ zsT}i7t^Bh7<=O_UNMu!CWr97ka9mF@-^iSa`+8;s*M|8rR%nX8V^|5UP^*df1Q_P2 z99FojZEp_@ji&mPq0jmdZd-NdUr=WFH+Zx(kSl8OQs@7|N7WSEKD08^ca=BZu>i2n zijJG2tpKKG2sj)ZvO@WVH8|L{&pmH0$g4y-$a%MMdp;6jkb1jSUK=T2|LYztP?Hv} z3`2c6SUUU_`BgA>8Po7{rr*VRz*FxCL3OMZ>J*v#F8hB!zh?*av}}6!>m6W#`x0BJ z&&quM2E>k{9;J_orgAC_z0NNfyW545?sEqW0&nu4NGpK1ySgfd?HEHq#8Htm14DdM zr6~i08)W>by(jSbcN6>Hrhv%H53q<|(qz<87~^_fC5Tht4_VpsyuH;&5EIB2)#U#V zB~FKrhtA~uZP(mXOFd@Vor`_s3Kj;zt;8;y5fUb62pX*d=LL{PQ<6M%`u4n4yQ(be+)}dlOD?jq1{XP!Be>Jp1>V4PG_pDwM%k`p zV1Hx+pi1M!bWmQk;pmkX_i2``P-y(Bx0qpb?J6M(WlS|*iSdbgVx48F95wysNMmDA zmQto!Y;F6^96YRl)aMjh^z0C8m>-%dt;=O6Ti0D8d^06$e=s zom8N?qMKH%gTIVV>%re_>JPvCFSdD>f>t$$UCQSvD;Z8(k3)QaYhec-8_KlcotLdv zi6)P1&2D)3vRF(_-I;Z7e~>+^9U1}jgf8mF4-r}EfrKma0 zHEhg`4*Tj}YYxd?2T4lEx>JdM`76f2gf~_{MLs28`}z891iV}b`Gf+) zxr;`z3%5-_TKr6Z%m4v`NjVR$?Z_o=pe6L6z1*nFJ+pmQT4utTi*N~cAIV4}{ zTEAnFm8X!B<-?RAjyL2Si!hXE-GUxkD+bQ7W9%af!H!NS@azFxy=g!YEo#YA@{#d8>`o1X=WWqaZORd$OkAM-Q? zXICJ3@H2#E`zX6L7&~HnJ_h{r)lqWG#iF#fas6Jac(X!)3zV#$By*i`3$(-TjWVfN zr1g%>&Jeg=)tsZn*D!Ndp=h^@X8wcuqPKI+K|0tB9n7AjU?uvn7G7`9nr+hT-%$+A zEkDHt^N=6Rb1U{hcvAcP<4}VC8GXTFq9V!Y4uP-t&q@mzHY8M9a$u zeHUOzW@S^TK|yv6H7n&58-?aQtLH?m^tfo$iV-o{$})bK8Ha2r@zLqgeUU)v|+wv9$q)dggJUgHv6W1@OW$!XaaDf%YqNBf7|G!Yi z$pH0EdBq-B7}||=o+qA4y+EtGc?Qgk1nsXcE8z^UUh6wWd)8RMjs>*};dG>I9;nVD zP0pa|tMu5PEQ-t16ldVT1Y`gIhzgGV-Dv(txAWp|!Yz&f&c^)_B1@#%w!v?&(==a5 z8GE}cJ(ydwoiw&>0eBmY>`l1>s1QdQ#;WXu;wV04=+1Q+ATwqA8>#Mz_Qd>xsh$TS zzV$bGK*)K5VKwgg^p8kf!oz>~8?o!TuE?hh4F30~Ms<$DaoLN%-HAE#5HJ~fS>Yu| zX;|R%-}4Lq0t0}9T8$W6&H3GK!TD;eh6+G5059aXkpHpQ`X_M#2rgEgpc=R+mSO2L zElg4#R*B{1`=>=cNLHCvr8EWusxHyI&MD7ddC6Vq1t-%GlMFc`<*Xb_P(tex6V9UT3ZIQjHRwl&b5rx=~NywR~*sGks7r5RIVpC;P_t4DGkQI}x## zf<1f-g+M~9aR^FKKZZA#8vN23g?KT~8Xu4Ei%*0T!AD79b71etJxH_9lPTD%Pwuvq z(4?`cq=FJ-6Mf5hj5Bd1^^4$*r|)rqR5g91+Z44wQy8{Z>IaR#p~`f4Dan0qt>n*! z7E}|x$?`nc-eYm+iKCWN0R=yGPccMccCy{C2Zyhk)I6X2gzVCTDRQ{0olc`IZjG<; zRUzGaauBets=_+$@(x{mJ~n)rwDzGlOebGRvs#|KWF&{Xkg2$w{WCAdWdMzq7%Kyp z{5Y70RAR=OKV6?9ao+|Fz8TA9Tj^Gs179H}$K3U`L7QV--`^neAId(bP42ABWXla$ zejM65&AC~jV>D+pmP5KVv7aIbLAAN?XlW~8=3l#mIl_BlZAG(~cP%1+-dIYO$v>uJmy%0^ZHJ-VPqnt!W2bVk(^-9S$M@N=p9pt`niO}B7Q zdZBtwvQp5ZBLE+Gyyrf$ohxPaIc4xRVQO52`PU-516e!;;k2QvU~go9u4_s z49!@t$tz{R;N8QC&6$}LyDa|&7;wy;S!j}eDXZTCAB$#R2>#-YlG{=&XUA*D9}!*I=AY&Q>GLOh zMzC$Zo?bA!&2M6UDe)3Xn0`rknrCN{mrJ2K{F~hOHVRO?I(0Sru&;5!rdf(*SGrm- z*PV7zHkO*fh^1T#HFwE%UXDm4p&Jg2wK zbPMa&V*vXRup1zz7|_L}<>M9Nl%(&o@wN}}hFWcfUQpVs_al)|n+;Y@~Pcm6`wJnY?^ z)#bIrNq!Z%Bo(@av@^onrvGgwl)rvrXZ=H$O6<(jr`CttnY!EJQ*Lc?qE!*d(c$;G z?y`|CsM~AJ=}c*r+MULPPDO{=T@3^$vBH@*$?U0Cva8!@Mfh=>xuA1GE5a-itMVc- zZ91h2hwZ(IjkOeF2`YtX^lRS6U8g-se)UP>L+F(2QZFOR4R`KTtg|MZjE~M6pvPYN zSSce_V3{0TCBOTksa+51IN-0SYa})|TR#YvTK+*>d@}{!-ha|xkr}yzAXzGP_Lq&@ z#(uW#(dpEbMhOyck8Q5J>PDod8Y=C7Kh=e8^LzZ0t&}uz$D`zMn?Hx3Bqf;bLD_4x z2JM*05aTs)!ZB7=;kco_u2unL%5~m^G6!{ISMnz&pc!tN!Th6Ir<7N5g9P7aFuyc|9$h)v{K&0NCrvtyH*U%lQ*t{WrD2yAqTnRNoUDLWs}4Uj z>gxTFHg>nQ>(3YYjt%CsE=8D6Tg_sZ)zo&*?cyC@BOwX8+>`q$=;43jhZ)7Rq${<=)Cd$shm=`$>V`mc-_HsD($BUv6^iPObyEu<>ea5 z-7ROyiA#B*$P|&h=)G2x2}LRL|5`89?M#uX!>&{FG3-fT#~SZp4ycu)?4UlmrL|U# zDrzszbXrQ@;hfp-kItQ*@JriktI#l!x`rPnQMCXZ1s}lIUQQ_3{ za9obr;Zn6GNvh4%1N9`W;d#;FNm@1z6;O~@RyCQo1m5q1;c7~PRhK8nkcHV!Xt3jf zu8+Q%ATvH5=gH^Q_`V@ZD&%HX&%0m9V8$wU?^LHIy7he8*|cHp>-28|$`_z3*(QT= z*4*H0eQe$_C|C00{bM`9^zzLxr&G?Wyg{PZN_Y;%0Gc(S1 zudNWIFj%`;gZp_biuEt-D!%DT1WhVqB@6~FleJZLxoaKtXRa2ZtJ>PvyJIVN&j@`X zD<}WG+1J@%z*C~3_x za!QmoBCKk>yk4XxX(_pb~&iq@&nIulz3TXL_agw>&}6d35u1#)S?NTvzoN#h(o zQ%LLAT~_6WaOa%dI<%kBo^|5OtPQKt1>crXxzOeGNlzjN{AdgJZVrvGiaTx3DWd-&$Yu=Z3ran@Z48)%cTc zyfV|1G8^|(BdqIp?2}6Ik`mI#v1!PyD9n@n1Ff3O{ivjkSWFmrd4;!-E4301^$!OK z`}|5Qqs1sy|6J{1N2jc*5&w*%ovW(Vdw+r?;MYBy65JkIN9g_qcUt%TSR6GKc+zo~ z`tT@s9>UTqg7+)upZZvLcx@m-oEQ@r$+jZ*-qpqCNqSD{MeA=-*e ziUc!5X(yE!>E(Bh8b_|O8@YdLBa}>`wu{HKqwe1iibMG%1_@>hMb=2L`aQOo)6^hB zn)laAwj9jpp*-@N5xprbfpRIJuAO~${$-JaH&sP;R4nH_uV-i)PV5UJK-~-x_z_7y zrC8%f`X-yJJ`qx1UPUT$+^u`0Pfwf=S`M3kyiswjhxkq`(D-ybmuILn%Ss3LM&G2y zMqT%PxZk_r(+j4{X3xJ{xPlxX6w+JqCY%cSNQ*%Zc6T716o;(%X`f@>x4n{-zml>u z^0YuRi#{A(H1-k~GkP-@ZKs1>wr3F)P#C-$6}k}4n2@^SY=&|2ELCnch_|Y-rGElT znVBRadN8Z|J0SWM3=7}-0dP*C=$Q3mr9Pn?l}YVWBm`RBuhil^C^u2McU7FLzv=I z0oLM$UEb6StO{#U^dA3;GKxpI7+)bQ9o0F4vT3}IE18g>rvzr4!f2L=S$;@VJ!>Y( z$1jdtDpd7c?|OOx8kD<-WcHJoSfgF_9>Y^(C!{cvQo-6>$>*2&XW&om*0xDV@pw^T zq+7?BUi{emg?P2Il&dc~@!FaTV03B0i|)@)-%v%(7i!5iHRLx;7EmRBHC!Ke|BdCq z3bmU-)O$mA=7^Lx{k&Z8CtGnffyM9d(XKkB&)ZjJg!m)z^$Fgbvx*MU-IxyzO{x>O zgZ!Iv%@C~(|L_Y)bXomj`H$&2JCHx zDVx2evHsJX+&6^Z=yd5wvP^TzP}qz2IJ8}mYVZqt zY__>uiD38(OVh3oP9_suNm=D0w)d9bkw7y)rR|*?mY@4Vx9jwp?kbI5GQwX?QHC-_ z_+ULB$*qdW5ANR_NTvw2x*R%?b!C&TQ<#^F<$7&6`YN2ob(yhJO8or(ArKGA?BL+b z#1unT<%0YMg?>eMO+zyEW*6{!W7N7AZ-P>p2lju6Eq+I?bxkA>5QVEtiQLNWDJ&aD zU9o#N-0kh{Gfpd#wwDItMPzdKZy(i{VI#+M5-8*T7E_U9tMlo=K%itxUy$H#fr2kd zRWD&bA7!KFJPmUVMHJm}Hm7`J2E5NzC(r$^aRo;IPt!;LswDd-m%IlB%0LBULw_^K x0J{sYxkvhes@8vHo&Jl<{zXCmlONeSJeDeidhc;b { + self.call("crash_manager", "rpc_error", error); + reject(); + }, + complete: framework.unblockUI, + }); + if (blocked) { + // AAB: this check should be done in get_file service directly, + // should not be the concern of the caller (and that way, get_file + // could return a deferred) + var message = _t( + "A popup window with your report was blocked. You " + + "may need to change your browser settings to allow " + + "popup windows for this page." + ); + this.do_warn(_t("Warning"), message, true); + } }); - if (blocked) { - // AAB: this check should be done in get_file service directly, - // should not be the concern of the caller (and that way, get_file - // could return a deferred) - var message = _t( - "A popup window with your report was blocked. You " + - "may need to change your browser settings to allow " + - "popup windows for this page." - ); - this.do_warn(_t("Warning"), message, true); - } - return def; }, _triggerDownload: function(action, options, type) { diff --git a/excel_import_export/views/xlsx_template_view.xml b/excel_import_export/views/xlsx_template_view.xml index ac0727148..697993003 100644 --- a/excel_import_export/views/xlsx_template_view.xml +++ b/excel_import_export/views/xlsx_template_view.xml @@ -16,6 +16,25 @@

+
+

@@ -35,6 +54,20 @@ name="csv_quote" attrs="{'invisible': [('to_csv', '=', False)]}" /> + + + + @@ -46,6 +79,26 @@ +
+
+ @@ -83,6 +136,11 @@ attrs="{'required': [('section_type', 'in', ('head', 'row'))], 'invisible': [('section_type', 'not in', ('head', 'row'))]}" /> + + +
+
+ @@ -246,7 +326,9 @@
  • Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet
  • Row Field: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data
  • + >Row Field: Use _HEAD_ for the record itself, and one2many field for row data, e.g., order_line, line_ids[max_row] where [max_row] is optional number of rows to import
  • No Delete: By default, all one2many lines will be deleted before import. Select this, to avoid deletion
  • XLSX Templates ir.actions.act_window xlsx.template - form tree,form

    diff --git a/excel_import_export/wizard/__init__.py b/excel_import_export/wizard/__init__.py index 136b4ff0c..ffdeb0d0a 100644 --- a/excel_import_export/wizard/__init__.py +++ b/excel_import_export/wizard/__init__.py @@ -1,2 +1,3 @@ from . import export_xlsx_wizard from . import import_xlsx_wizard +from . import report_xlsx_wizard diff --git a/excel_import_export/wizard/export_xlsx_wizard.py b/excel_import_export/wizard/export_xlsx_wizard.py index be63c745f..368a7edb4 100644 --- a/excel_import_export/wizard/export_xlsx_wizard.py +++ b/excel_import_export/wizard/export_xlsx_wizard.py @@ -12,8 +12,8 @@ class ExportXLSXWizard(models.TransientModel): _name = "export.xlsx.wizard" _description = "Wizard for exporting excel" - name = fields.Char(string="File Name", readonly=True, size=500,) - data = fields.Binary(string="File", readonly=True,) + name = fields.Char(string="File Name", readonly=True, size=500) + data = fields.Binary(string="File", readonly=True) template_id = fields.Many2one( "xlsx.template", string="Template", @@ -21,9 +21,9 @@ class ExportXLSXWizard(models.TransientModel): ondelete="cascade", domain=lambda self: self._context.get("template_domain", []), ) - res_id = fields.Integer(string="Resource ID", readonly=True, required=True,) + res_id = fields.Integer(string="Resource ID", readonly=True, required=True) res_model = fields.Char( - string="Resource Model", readonly=True, required=True, size=500, + string="Resource Model", readonly=True, required=True, size=500 ) state = fields.Selection( [("choose", "Choose"), ("get", "Get")], @@ -49,7 +49,6 @@ class ExportXLSXWizard(models.TransientModel): defaults["res_model"] = res_model return defaults - @api.multi def action_export(self): self.ensure_one() Export = self.env["xlsx.export"] @@ -61,7 +60,6 @@ class ExportXLSXWizard(models.TransientModel): "type": "ir.actions.act_window", "res_model": "export.xlsx.wizard", "view_mode": "form", - "view_type": "form", "res_id": self.id, "views": [(False, "form")], "target": "new", diff --git a/excel_import_export/wizard/import_xlsx_wizard.py b/excel_import_export/wizard/import_xlsx_wizard.py index 3c785814d..7736b27dc 100644 --- a/excel_import_export/wizard/import_xlsx_wizard.py +++ b/excel_import_export/wizard/import_xlsx_wizard.py @@ -12,19 +12,19 @@ class ImportXLSXWizard(models.TransientModel): _name = "import.xlsx.wizard" _description = "Wizard for importing excel" - import_file = fields.Binary(string="Import File (*.xlsx)",) + import_file = fields.Binary(string="Import File (*.xlsx)") template_id = fields.Many2one( "xlsx.template", string="Template", required=True, - ondelete="set null", + ondelete="cascade", domain=lambda self: self._context.get("template_domain", []), ) - res_id = fields.Integer(string="Resource ID", readonly=True,) - res_model = fields.Char(string="Resource Model", readonly=True, size=500,) - datas = fields.Binary(string="Sample", related="template_id.datas", readonly=True,) + res_id = fields.Integer(string="Resource ID", readonly=True) + res_model = fields.Char(string="Resource Model", readonly=True, size=500) + datas = fields.Binary(string="Sample", related="template_id.datas", readonly=True) fname = fields.Char( - string="Template Name", related="template_id.fname", readonly=True, + string="Template Name", related="template_id.fname", readonly=True ) attachment_ids = fields.Many2many( "ir.attachment", @@ -108,7 +108,6 @@ class ImportXLSXWizard(models.TransientModel): defaults["res_model"] = res_model return defaults - @api.multi def get_import_sample(self): self.ensure_one() return { @@ -116,14 +115,12 @@ class ImportXLSXWizard(models.TransientModel): "type": "ir.actions.act_window", "res_model": "import.xlsx.wizard", "view_mode": "form", - "view_type": "form", "res_id": self.id, "views": [(False, "form")], "target": "new", "context": self._context.copy(), } - @api.multi def action_import(self): self.ensure_one() Import = self.env["xlsx.import"] @@ -149,7 +146,6 @@ class ImportXLSXWizard(models.TransientModel): "type": "ir.actions.act_window", "res_model": self._name, "view_mode": "form", - "view_type": "form", "res_id": self.id, "views": [(False, "form")], "target": "new", diff --git a/excel_import_export/wizard/report_xlsx_wizard.py b/excel_import_export/wizard/report_xlsx_wizard.py new file mode 100644 index 000000000..4647153b5 --- /dev/null +++ b/excel_import_export/wizard/report_xlsx_wizard.py @@ -0,0 +1,22 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +import ast + +from odoo import fields, models + + +class ReportXLSXWizard(models.TransientModel): + _name = "report.xlsx.wizard" + _description = "Generic Report Wizard, used with template reporting option" + + res_model = fields.Char() + domain = fields.Char(string="Search Criterias") + + def action_report(self): + action_id = self._context.get("report_action_id") + action = self.env["ir.actions.report"].browse(action_id) + res = action.read()[0] + return res + + def safe_domain(self, str_domain): + return ast.literal_eval(str_domain or "[]") diff --git a/excel_import_export/wizard/report_xlsx_wizard.xml b/excel_import_export/wizard/report_xlsx_wizard.xml new file mode 100644 index 000000000..7686749c5 --- /dev/null +++ b/excel_import_export/wizard/report_xlsx_wizard.xml @@ -0,0 +1,31 @@ + + + + + report.xlsx.wizard + report.xlsx.wizard + + + + + + +

    + +
    + +