From 9f54d66b58b7229448fc3b9c63b463ae6f347658 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 12 May 2025 15:08:04 +0200 Subject: [PATCH 1/8] [REF] sale_quotation: refactor the quotation mechanism. * Extracts the shop_order_mode of products in a new module * Enables to choose a combined shop order mode (quotation or direct sale) * Adds a migration script to translate the products and templates order mode infos from the old to the new fields --- sale_quotation/README.rst | 24 +- sale_quotation/__manifest__.py | 8 +- sale_quotation/i18n/sale_quotation.pot | 111 +---- .../migrations/16.0.0.3.0/pre-migrate.py | 47 ++ sale_quotation/models/__init__.py | 2 - sale_quotation/models/product_product.py | 13 - sale_quotation/models/product_template.py | 51 --- sale_quotation/models/sale_order.py | 8 +- sale_quotation/readme/CONTRIBUTORS.rst | 3 +- sale_quotation/readme/DESCRIPTION.rst | 23 +- sale_quotation/static/description/index.html | 12 +- sale_quotation/static/description/product.png | Bin 156727 -> 0 bytes sale_quotation/tests/__init__.py | 2 +- sale_quotation/tests/test_product.py | 58 --- sale_quotation/views/product_view.xml | 12 - .../addons/shopinvader_product_order_mode | 1 + setup/shopinvader_product_order_mode/setup.py | 6 + .../shopinvader_product_order_mode_on_request | 1 + .../setup.py | 6 + shopinvader_product_order_mode/README.rst | 60 +++ shopinvader_product_order_mode/__init__.py | 1 + .../__manifest__.py | 19 + .../i18n/shopinvader_product_order_mode.pot | 81 ++++ .../models/__init__.py | 2 + .../models/product_product.py | 50 +++ .../models/product_template.py | 39 ++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 416 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_shop_order_mode.py | 77 ++++ .../views/product_product.xml | 25 ++ .../views/product_template.xml | 21 + .../README.rst | 60 +++ .../__init__.py | 1 + .../__manifest__.py | 17 + ...pinvader_product_order_mode_on_request.pot | 72 +++ .../models/__init__.py | 3 + .../models/product_product.py | 19 + .../models/product_template.py | 19 + .../models/sale_order.py | 21 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 416 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_quotation.py | 102 +++++ 46 files changed, 1608 insertions(+), 307 deletions(-) create mode 100644 sale_quotation/migrations/16.0.0.3.0/pre-migrate.py delete mode 100644 sale_quotation/models/product_product.py delete mode 100644 sale_quotation/models/product_template.py delete mode 100644 sale_quotation/static/description/product.png delete mode 100644 sale_quotation/tests/test_product.py delete mode 100644 sale_quotation/views/product_view.xml create mode 120000 setup/shopinvader_product_order_mode/odoo/addons/shopinvader_product_order_mode create mode 100644 setup/shopinvader_product_order_mode/setup.py create mode 120000 setup/shopinvader_product_order_mode_on_request/odoo/addons/shopinvader_product_order_mode_on_request create mode 100644 setup/shopinvader_product_order_mode_on_request/setup.py create mode 100644 shopinvader_product_order_mode/README.rst create mode 100644 shopinvader_product_order_mode/__init__.py create mode 100644 shopinvader_product_order_mode/__manifest__.py create mode 100644 shopinvader_product_order_mode/i18n/shopinvader_product_order_mode.pot create mode 100644 shopinvader_product_order_mode/models/__init__.py create mode 100644 shopinvader_product_order_mode/models/product_product.py create mode 100644 shopinvader_product_order_mode/models/product_template.py create mode 100644 shopinvader_product_order_mode/readme/CONTRIBUTORS.rst create mode 100644 shopinvader_product_order_mode/readme/DESCRIPTION.rst create mode 100644 shopinvader_product_order_mode/static/description/index.html create mode 100644 shopinvader_product_order_mode/tests/__init__.py create mode 100644 shopinvader_product_order_mode/tests/test_shop_order_mode.py create mode 100644 shopinvader_product_order_mode/views/product_product.xml create mode 100644 shopinvader_product_order_mode/views/product_template.xml create mode 100644 shopinvader_product_order_mode_on_request/README.rst create mode 100644 shopinvader_product_order_mode_on_request/__init__.py create mode 100644 shopinvader_product_order_mode_on_request/__manifest__.py create mode 100644 shopinvader_product_order_mode_on_request/i18n/shopinvader_product_order_mode_on_request.pot create mode 100644 shopinvader_product_order_mode_on_request/models/__init__.py create mode 100644 shopinvader_product_order_mode_on_request/models/product_product.py create mode 100644 shopinvader_product_order_mode_on_request/models/product_template.py create mode 100644 shopinvader_product_order_mode_on_request/models/sale_order.py create mode 100644 shopinvader_product_order_mode_on_request/readme/CONTRIBUTORS.rst create mode 100644 shopinvader_product_order_mode_on_request/readme/DESCRIPTION.rst create mode 100644 shopinvader_product_order_mode_on_request/static/description/index.html create mode 100644 shopinvader_product_order_mode_on_request/tests/__init__.py create mode 100644 shopinvader_product_order_mode_on_request/tests/test_quotation.py diff --git a/sale_quotation/README.rst b/sale_quotation/README.rst index b87feb7a5a..3e8882045f 100644 --- a/sale_quotation/README.rst +++ b/sale_quotation/README.rst @@ -24,27 +24,8 @@ Sale Quotation This module is used in the context of e-commerce project (like shopinvader) - -It allows to specify whether your product requires a quotation. -This can be set per "product.template" or per "product.variant" - - -For example, you have a product where the supplier price fluctuates a lot, -you can not give a public price but your customer should be able to request a quotation. - -.. image:: https://raw.githubusercontent.com/shopinvader/odoo-shopinvader/16.0/sale_quotation/static/description/product.png - :width: 400px - :alt: Widget in action - - - -On your shopinvader website when a customer has added a product that requires a quotation, -instead of validating the cart it will "request a quotation". - - - -On Odoo Backoffice the menu quotation has been improved and a new state (quotation_state) has been added. -So you can easily process and follow the quotation request from your external system. +On Odoo Backoffice this module imporves the menu quotation by adding a new state (quotation_state). +Then, you can easily process and follow the quotation request from your external system. .. image:: https://raw.githubusercontent.com/shopinvader/odoo-shopinvader/16.0/sale_quotation/static/description/quotation.png :width: 400px @@ -82,6 +63,7 @@ Contributors * Matthieu Saison * Iván Todorovich * Simone Orsi +* Nicolas DELBOVIER Maintainers ~~~~~~~~~~~ diff --git a/sale_quotation/__manifest__.py b/sale_quotation/__manifest__.py index a98348c2c1..2cb1baa444 100644 --- a/sale_quotation/__manifest__.py +++ b/sale_quotation/__manifest__.py @@ -4,15 +4,17 @@ { "name": "Sale Quotation", "summary": "Sale Quotation", - "version": "16.0.0.2.0", + "version": "16.0.0.3.0", "category": "e-commerce", "development_status": "Production/Stable", "website": "https://github.com/shopinvader/odoo-shopinvader", "author": "Akretion", "license": "AGPL-3", - "depends": ["sale_cart"], + "depends": [ + "sale_cart", + "shopinvader_product_order_mode_on_request", + ], "data": [ - "views/product_view.xml", "views/sale_view.xml", ], "installable": True, diff --git a/sale_quotation/i18n/sale_quotation.pot b/sale_quotation/i18n/sale_quotation.pot index 94c88c9105..aad07b6837 100644 --- a/sale_quotation/i18n/sale_quotation.pot +++ b/sale_quotation/i18n/sale_quotation.pot @@ -1,120 +1,15 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * sale_quotation # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" +"Project-Id-Version: Odoo Server 16.0+e\n" "Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-16 13:39+0000\n" +"PO-Revision-Date: 2025-05-16 13:39+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__sale_order__quotation_state__accepted -#: model_terms:ir.ui.view,arch_db:sale_quotation.sale_order_view_search_inherit_quotation -msgid "Accepted" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__product_template__shop_only_quotation__all_variant -msgid "All Variant" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__sale_order__quotation_state__cancel -#: model_terms:ir.ui.view,arch_db:sale_quotation.sale_order_view_search_inherit_quotation -msgid "Cancel" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__sale_order__quotation_state__customer_request -msgid "Customer Request" -msgstr "" - -#. module: sale_quotation -#: model_terms:ir.ui.view,arch_db:sale_quotation.sale_order_view_search_inherit_quotation -msgid "Customer request" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__sale_order__quotation_state__draft -#: model_terms:ir.ui.view,arch_db:sale_quotation.sale_order_view_search_inherit_quotation -msgid "Draft" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__product_template__shop_only_quotation__manually_on_variant -msgid "Manually on Variant" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__product_template__shop_only_quotation__never -msgid "Never" -msgstr "" - -#. module: sale_quotation -#: model_terms:ir.ui.view,arch_db:sale_quotation.product_template_form_view -msgid "Only for Quotation" -msgstr "" - -#. module: sale_quotation -#. odoo-python -#: code:addons/sale_quotation/models/sale_order.py:0 -#, python-format -msgid "" -"Only orders of cart typology in draft state can be converted to quotation" -msgstr "" - -#. module: sale_quotation -#. odoo-python -#: code:addons/sale_quotation/models/sale_order.py:0 -#, python-format -msgid "Only quotation with the state 'waiting_acceptation' can be Confirmed." -msgstr "" - -#. module: sale_quotation -#: model:ir.model,name:sale_quotation.model_product_template -msgid "Product" -msgstr "" - -#. module: sale_quotation -#: model:ir.model,name:sale_quotation.model_product_product -msgid "Product Variant" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields,field_description:sale_quotation.field_sale_order__quotation_state -msgid "Quotation State" -msgstr "" - -#. module: sale_quotation -#: model:ir.model,name:sale_quotation.model_sale_order -msgid "Sales Order" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields,field_description:sale_quotation.field_sale_order__shop_only_quotation -msgid "Shop Only Quotation" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields,field_description:sale_quotation.field_product_product__shop_only_quotation -#: model:ir.model.fields,field_description:sale_quotation.field_product_template__shop_only_quotation -msgid "Shopinvader: Only for Quotation" -msgstr "" - -#. module: sale_quotation -#: model_terms:ir.ui.view,arch_db:sale_quotation.sale_order_view_search_inherit_quotation -#: model_terms:ir.ui.view,arch_db:sale_quotation.view_quotation_tree -msgid "State" -msgstr "" - -#. module: sale_quotation -#: model:ir.model.fields.selection,name:sale_quotation.selection__sale_order__quotation_state__waiting_acceptation -#: model_terms:ir.ui.view,arch_db:sale_quotation.sale_order_view_search_inherit_quotation -msgid "Waiting Acceptation" -msgstr "" diff --git a/sale_quotation/migrations/16.0.0.3.0/pre-migrate.py b/sale_quotation/migrations/16.0.0.3.0/pre-migrate.py new file mode 100644 index 0000000000..f5fe5a242b --- /dev/null +++ b/sale_quotation/migrations/16.0.0.3.0/pre-migrate.py @@ -0,0 +1,47 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def migrate(cr, version): + """ + For each product and for each template where 'shop_only_quotation' is True, + set 'shop_order_mode' to 'quotation_only'. + """ + openupgrade.logged_query( + cr, + """ + UPDATE product_template + SET shop_order_mode = 'quotation_only' + WHERE shop_only_quotation = 'all_variant'; + """, + ) + openupgrade.logged_query( + cr, + """ + UPDATE product_template + SET shop_order_mode = 'direct_sale_only' + WHERE shop_only_quotation = 'never'; + """, + ) + openupgrade.logged_query( + cr, + """ + UPDATE product_template + SET is_shop_order_mode_unabled_on_variant = TRUE + WHERE shop_only_quotation = 'manually_on_variant'; + """, + ) + + openupgrade.logged_query( + cr, + """ + UPDATE product_product + SET shop_order_mode = 'quotation_only' + FROM product_template AS pt + WHERE product_product.product_tmpl_id = pt.id + AND pt.is_shop_order_mode_unabled_on_variant = TRUE + AND product_product.shop_only_quotation = TRUE; + """, + ) diff --git a/sale_quotation/models/__init__.py b/sale_quotation/models/__init__.py index de7a683548..6aacb75313 100644 --- a/sale_quotation/models/__init__.py +++ b/sale_quotation/models/__init__.py @@ -1,3 +1 @@ -from . import product_product -from . import product_template from . import sale_order diff --git a/sale_quotation/models/product_product.py b/sale_quotation/models/product_product.py deleted file mode 100644 index 87bf509dbc..0000000000 --- a/sale_quotation/models/product_product.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2021 Camptocamp (https://www.camptocamp.com). -# @author Iván Todorovich -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class ProductProduct(models.Model): - _inherit = "product.product" - - shop_only_quotation = fields.Boolean( - string="Shopinvader: Only for Quotation", - ) diff --git a/sale_quotation/models/product_template.py b/sale_quotation/models/product_template.py deleted file mode 100644 index 1f58f4e52b..0000000000 --- a/sale_quotation/models/product_template.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2017-2018 Akretion (http://www.akretion.com). -# Copyright 2021 Camptocamp (https://www.camptocamp.com). -# @author Benoît GUILLOT -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import api, fields, models - - -class ProductTemplate(models.Model): - _inherit = "product.template" - - shop_only_quotation = fields.Selection( - selection=[ - ("all_variant", "All Variant"), - ("manually_on_variant", "Manually on Variant"), - ("never", "Never"), - ], - string="Shopinvader: Only for Quotation", - compute="_compute_shop_only_quotation", - inverse="_inverse_shop_only_quotation", - store=True, - ) - - @api.depends("product_variant_ids.shop_only_quotation") - def _compute_shop_only_quotation(self): - # True only if true for all its variants - for rec in self: - if not rec.product_variant_ids or not any( - rec.product_variant_ids.mapped("shop_only_quotation") - ): - rec.shop_only_quotation = "never" - elif all(rec.product_variant_ids.mapped("shop_only_quotation")): - rec.shop_only_quotation = "all_variant" - elif any(rec.product_variant_ids.mapped("shop_only_quotation")): - rec.shop_only_quotation = "manually_on_variant" - - def _inverse_shop_only_quotation(self): - # Sets the value on all its variants - for rec in self: - if rec.shop_only_quotation == "all_variant": - rec.product_variant_ids.shop_only_quotation = True - elif rec.shop_only_quotation == "never": - rec.product_variant_ids.shop_only_quotation = False - - def _create_variant_ids(self): - # Make sure new variants have the same value than the template. - res = super()._create_variant_ids() - for rec in self: - if rec.shop_only_quotation == "all_variant": - rec.product_variant_ids.shop_only_quotation = True - return res diff --git a/sale_quotation/models/sale_order.py b/sale_quotation/models/sale_order.py index aae40d577a..02302497f6 100644 --- a/sale_quotation/models/sale_order.py +++ b/sale_quotation/models/sale_order.py @@ -1,5 +1,6 @@ # Copyright 2017-2018 Akretion (http://www.akretion.com). # Copyright 2021 Camptocamp (http://www.camptocamp.com) +# Copyright 2025 ACSONE SA/NV # @author Benoît GUILLOT # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -24,7 +25,6 @@ class SaleOrder(models.Model): readonly=False, copy=False, ) - shop_only_quotation = fields.Boolean(compute="_compute_shop_only_quotation") @api.depends("state") def _compute_quotation_state(self): @@ -53,12 +53,6 @@ def action_request_quotation(self): self.write({"quotation_state": "customer_request", "typology": "sale"}) return True - def _compute_shop_only_quotation(self): - for record in self: - record.shop_only_quotation = any( - record.order_line.product_id.mapped("shop_only_quotation") - ) - def action_confirm_quotation(self): if any(rec.quotation_state != "waiting_acceptation" for rec in self): raise UserError( diff --git a/sale_quotation/readme/CONTRIBUTORS.rst b/sale_quotation/readme/CONTRIBUTORS.rst index 5116fef324..0b22e8639c 100644 --- a/sale_quotation/readme/CONTRIBUTORS.rst +++ b/sale_quotation/readme/CONTRIBUTORS.rst @@ -2,4 +2,5 @@ * Benoît GUILLOT * Matthieu Saison * Iván Todorovich -* Simone Orsi \ No newline at end of file +* Simone Orsi +* Nicolas DELBOVIER \ No newline at end of file diff --git a/sale_quotation/readme/DESCRIPTION.rst b/sale_quotation/readme/DESCRIPTION.rst index 2c68999acc..6cd8f3500a 100644 --- a/sale_quotation/readme/DESCRIPTION.rst +++ b/sale_quotation/readme/DESCRIPTION.rst @@ -1,26 +1,7 @@ This module is used in the context of e-commerce project (like shopinvader) - -It allows to specify whether your product requires a quotation. -This can be set per "product.template" or per "product.variant" - - -For example, you have a product where the supplier price fluctuates a lot, -you can not give a public price but your customer should be able to request a quotation. - -.. image:: ../static/description/product.png - :width: 400px - :alt: Widget in action - - - -On your shopinvader website when a customer has added a product that requires a quotation, -instead of validating the cart it will "request a quotation". - - - -On Odoo Backoffice the menu quotation has been improved and a new state (quotation_state) has been added. -So you can easily process and follow the quotation request from your external system. +On Odoo Backoffice this module imporves the menu quotation by adding a new state (quotation_state). +Then, you can easily process and follow the quotation request from your external system. .. image:: ../static/description/quotation.png :width: 400px diff --git a/sale_quotation/static/description/index.html b/sale_quotation/static/description/index.html index dc42117149..bc77cc85c2 100644 --- a/sale_quotation/static/description/index.html +++ b/sale_quotation/static/description/index.html @@ -371,15 +371,8 @@

Sale Quotation

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 shopinvader/odoo-shopinvader

This module is used in the context of e-commerce project (like shopinvader)

-

It allows to specify whether your product requires a quotation. -This can be set per “product.template” or per “product.variant”

-

For example, you have a product where the supplier price fluctuates a lot, -you can not give a public price but your customer should be able to request a quotation.

-Widget in action -

On your shopinvader website when a customer has added a product that requires a quotation, -instead of validating the cart it will “request a quotation”.

-

On Odoo Backoffice the menu quotation has been improved and a new state (quotation_state) has been added. -So you can easily process and follow the quotation request from your external system.

+

On Odoo Backoffice this module imporves the menu quotation by adding a new state (quotation_state). +Then, you can easily process and follow the quotation request from your external system.

Widget in action

Table of contents

@@ -417,6 +410,7 @@

Contributors

  • Matthieu Saison <matthieu.saison@akretion.com>
  • Iván Todorovich <ivan.todorovich@gmail.com>
  • Simone Orsi <simone.orsi@camptocamp.com>
  • +
  • Nicolas DELBOVIER <nicolas.delbovier@acsone.eu>
  • diff --git a/sale_quotation/static/description/product.png b/sale_quotation/static/description/product.png deleted file mode 100644 index 9f287fdf19ae229e0a502b12f56e6a36f3f49070..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156727 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⪚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%0AdcNVd{8fXf9 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

    W!Sr#3L?67;Kni87sL%~1b0-Tl;=ff~Mpjj;) zeXr`XFO>A%61@)>rq#P6lgYDR0nKOOR%Yj_7=9CrT%~7$Jhl@-(Egxw2|GeMDKO&h zbQQOmeZ^~7o>6w2R8$m(O)Xymu zAO#JlpaT~845kor-B%3KwPP%b5cTg=C1zqW1Bdgom(3?8r!ILVbc``b0K35w5<%hR z?@6n(%a^}VNHR54Gghp=`~`Y7o&i;QmjbONGo|k)X2>(uKTTo0is^P=ii@7DCyr-H z3g)Q+keQ6O#eQ}01OV1Qe2A*5?7!<4ce93pR@D%Vk**764*@%pYE=uiJ14aB&wnAM zXf|V7MTG=F6_W;4_l0C+=dDJD<88~5KD4PVdaRm%YXVCs&{KmS9GjnlX|$g3xPh?fcn5T8ZV zn=o^SO@K2t5=f{lvOgHA4b&|VE>5aG;u-PrZ8qGq6#e;e(ORW`G#&vg7EiHOO_cB0 z&+ou|%~wkar|@^46#0dPD0~i&jP=~d@*Fess)yX+Z@iYZx6QeGVD=MdDO3)dgL5& z;EsG}ZPraeXUb&e8tooRQSJ{wNxz3C*M3iaHD2rTjb+YHg)ONEul!s*3?k~tC+(B# zGL?NcQMerZ3RN3o?^`1sMza;4c1l80Pb}OB_5R~O4XYoc&=F}tmM7-I_94z4`82y= zFqryri$LnvSe1G~Yf-1e$tPp(v29AKE`;IRz9y^5i@vYl4DwI8oXGJT^64!GXvI{u zzrMa!#Y<@X{Og_n^tfKCuIV!hkPWA!w+kM^k2~-$dehI#ipsmq`WZ3IkaH$j{4nOb zsj%Asrv&^(Jh2=n8!3QCn#z8TnMhi!O4b*@`#}Rcu)AB&<@>9W4yh&jU+tyl5d(m` zX0}&5C=|fNqqnG9>(ba7!$}yZ{<@yk_=S!4eqEx^G7J8I71nULEIA-BppNE!uP4_WW^< zSQsrnHux|N6PW`$vWTHrbqZ4Tzi5WDP{mW45R;hz%awKln1uNUXCo+0Oj}f~zgIH7 z@~1R-Ahf|Fbhc`Y1y(9D1N1}Ocw2FiUcKlWj%mAkvXH!i%YX3zhdOUuOhFf}_b z<#hV?BGNz67eK~i+K-&z_0*$!UOPsJ|H2jqhlhJ-zrf$UfeNG~C;S0H4J-Ci0L_@+E~s5zkMmeX9d|;Nl>Lzq0t=yq zmtJWZsjuvaye@kv>{li7>go^fTa^bw15*}oCbt|Q^4GsimpEs06u>}2fcHG9maf3Z zASH~)KLu&p{q#(I@fFwdkgTu{Xa3SVmX_bGD}<)q&lZh*F)H*Q75m*1Ou2jA42W~< z#)hT;$F&C1=(i0H>-8sp=<^v9_zncH{5uN(8*8dl1tcFaP5-CT=Gxa?Mm5dcR10r4Nr7{Vl zOg;b;(m#@VDd-0&rlyy0jXwFoL3$}h^DQknMR~-rd9IYyZBY23u+`_vP&OGs;V|I{ zZoHVUem93v91;3iko}OUF|;~=hIV9}7z;RG*#BQ(-@SX^gsB{w>5`g9>jhGw;n1#F z^Cu66<77GS5$((Q8$;>1n~c?x3NWK|s`4QZK?x&h)@&;^lXh$u@o8lB{%>Ya(}^QU zD7iy*p&@nyMP&5S81MzwTqdyJs$7JbnDq_N^fBS1DgYgE+Vr;Mlh^YdYGw8n>y;ZpVF6U{WXO>c?ya&-pydA8 z?GztvRi5%c@x0T7nZxx*UT$z?+Q(X!96P2AIcB0q09Mlsss%8(zC9}-AS5urKSNu- zIQk&Kz~Zd#AwXC@3<)WR8fr&wY)8GxN`(6plNG$oNCf^%nSbxEOh($*v54Alu_-AD z2XBGv*RXnVmMW+TF@RnVU;p`O-SH0o65$Io#1#`zUw@Fqb*lzh$KU(Rqx*RzRVYd0 zz-N43wZ^hsj**zbdL%!w6HTxzcMVr6m|7dgJuETIUt!*IAl^9yUJvRF`N%Un@sc30 zW+D98a&d8yGOZyG79=y6?rv9JRekZT84cgvk_ej6NYbUM0 zpZqlu{jFO4@uQe|*2{_wVvUOAc*L|fy`2vSKjtG7@hSzKNs9q%%mjQW4`Lj*?kW_A zQ!&+*Y0Adca4daJ>Cs&&A6s9MloTQOtgb$ zixAD|CAR8EtgPya-RvFx#X%hUl=}@Nq@?B|V`E;pHXQ_gIip*dS5%Z+Czx@{#>Q4B zCp<`JAtU%vQr){bKusm|KifmTVQF26X#7-=@ z=H%#HCO?vq&Xrnl6*8bWTy~VT^)A)B$vkKdjFD&oMJj!eN~STPiim|$j7_^{Qn4-Uz|&$b+}I%K`UDsZb~h zXi(rM>cfBG>%*~0_dKVw{;QNI`4%L6@!p%e#2Yi=_>hzy3VPG<`Z2Fea)MxLi^8&L z3Oaj&oWONVg*rG^sZipHt#+idfsXZzAz-+lB`8lCs*B@oG8tHWRd0tJva zXwIC@-jzQNsqR!>N3nqs1xW#6J1+pa5tWc~2)7;vckIXmM~N8QW`V<@6MEqI1IpYk zoFkC!MzuyCXlrT$ARkJE{*So~Nokiv z!#HT3+f?Y!D029ip-cLO6wd96ze&xKR{;X$yD20UGmjZft7P8{Rsgr{;Pm-GJUqOu z8C+9W-GGdvl#`RyP11zv3}OrppOAiREm@ZSNt{8wdytL?*zxrne2B7~Muyv|=v8N# z&nhn%cFepHf<$V!JT>jM;!2SZfgXDTgZeqYQc{x(M|L5Cy6P`iOqk}}sAVuPfYb7K z^+Bh2Tt;s&$4G3UUjKeZJBsJgPW1*&!m9iBkoPYfa^F!OOuB*sNI;T7&ZRN2t|`P> zptqy|CVqE^HkuZ&_;cTPWCvEZTA@*bYlO^7A~p0cT;l1aHDC9K#G9tQ7mBo%R4^fU zVINLWMOq5%z0xbxctngLSxC8DldUb!Z*re%R&D%oj*A9K1749Ewe|Hd?4S$mIP5ei zVxCz~ecQYyHa9*_OcCHDNr!pQx+N6(tSQn|yUcFR_fK$g!eO!HsMZvm78)Q`weDkg z1M5H`nG@UEXG#9c=nx`jQZYe_bCKR%(jf%140Rza3qW}ww>x`V6fsaga z=6ysmgoJhVJw-tQc6-HEX0rWOhcCG8y5}2btNh#W-~Z1-kDkk9KSKI8Lzvr zW1ly~&feXv$llbW;;*GAS9EaS6tKP|hS)C>$!4Ve^^4p2wbA8*xoaRFbU+Coep5F! z$qlgoX^la++&_})e#T0StFLNz!f=hG+Sy^d0O;co3i=$#l=;1Q3j5lDnOykI6ck#% zk!MggFpT49(3Gm&KO1C|VWm?pTp#!-5AMWUi;X?&?6hKvLqf*peEBoTxD|zWy)7-w z9`m+TNanp4RLqIzpdk{43O0D>fVlx-BQ_xcx*@MYEjgeCxVoAIv0^k3U;$9Gll<96 z{a<*RFMUWzMaJkZuC6ax0)Al*W>|INYPwc3wFN~yT;v0X1k`dH3DEw<7G= zlIMkqGmQ>wedW2x*yg$#5SN(io7;q|BQ=O>oDF4IgAcdgv&slYmXd(9KN5qdNL+X7rbWWr z==d4Lt61^WxYQhUXK02!;Fwqch1WLdAG}Kof3$7dy$64#Q|_$)^d>1Sw#xoY0=(H+ zvEylPZ=bo7KgDXo4M+L-4Fz4Eeh!{UP1v6)ZHUGtK~B_t0D5`uDj<+~*OR%=$sgtc ztnMhP-b!bX4x4qS(<~POow!>+S9rTY1`EYWxZBr;j=iFSA*PkZXsC*|Beg0 zaJ5K+f~o&;)175s=^3gcZko*a-7W2$ZKzIMAZYRDxG*3sTlMS(UA-XM^1^aVy4DiQ_4C^x{U>XKPPG$?d3@X;CbPJnZ|pM_)@;DM9w>lE58n`5 z-?6&;fh$FJ-XcRDk1v~~HIll|qTqVgv+A-KoIWPWnqAG+k9q z8ZH@JU^FwB+s@;MPQ&-gE+5bSHo}-UDL5xuq%tDH&%~maZ{WVb*H`dGhB_P51y&6^n7gVbsadB5s5c4zbb}$2_n)(w4wu}DXQ1dVp=M7$$LLTYO z_G~h9g>ZLhvR0kP7|dZqhmi_?@NSK*SXKJGWWWyN@Ptv6>-`yjgC3^|XOjmtb z%P(GMmJfZ~aPPMTh&st1z3y~cEx_Nln8mrbm;YfqxOdN|;(Nx3!w~IxPaa$HV&1~{ zs`KT;pD2UvB9cee6Aa+a)Vk}iwi@&$pwqbkKSAU5?b|U zTPX+x`e)K`?5e~LI};|HBTx&MY0MTnK#n`U3lp@kwsaPibC?6Q?L7RQwF>H2 z5)n`)fC({uS#n4MG<(#>fX5Un8zS*9gE_93wi7bKe_k372cX2i%i${M7fkh)gKe7e z(6yzfP+p@58@y95K*$ z*Dm`z;BIByRv9!c{k=n#t-7Txx9WmFzzty`7ob&fG5z{^++ak2sZvh&ytOhg=e->E zPrIJOm0#FZ5hCxuT6hWXi9&C_ZoQRzL?xwI7Qk2pGS@d4B*_aBCuRdnn1Wf&yR81;I@vogdFo2|!xnYYBKOq>5s zaz47R7~*V*5ibIV8X+s^taZXv{8sD zDrP)U_9@skhBrynhs!r{ zJvaz#4ZF=f^!WZp{k7EzRD(HCfJ+};s)Oix~FvOG$$fy{LVb}Mk%q!|98HP68BCd1PMlFk}!Oap) zJ-^$Ty^~XwvGJJEjwKFLcc+f~RAJkWX@}NCm-{mIghC%bsv#z3s@q8K*=}REgU-KZ zZTa{GXg;4w0WdXhWL^>^@cZnZ-NtvFmZf4`)oO%k(JRr$RGWI0*AEgCygdK9Ae6fjf*(sxW(16UxBjRBE-=1&j1hY#e)9aO5{4 z-c=#wa{`l()%80zw*7}t!TIqH`NM=!PgJ*jatLYZtJwITo@Dh3@of19TP#oD`?}oR z!ShZD2W)*>4zf+SQfjLiF;C80Gh*T&5br;r(hW4cOUq&;Rg42xbsY*O6&`_~Rl}uz zw`&)Rt1iuE6OLrPYhI#1TFT-a`-9ZB#PMDu2o-m%|Nm;XFwZ;b1`&RpVJXSJ`Sw9;Gj#d#Es_Qh|0Y>GlD zNDreBj*wm=H~YO9VbLt3gCLM`83(oKskh{eC6XZa=>6sX6t9u59CX>gy`g`nuZC z`RG3+3yPR)m+6_8^Z7~}cuw)9?3(lwK9B5|xA-QkzTO#4!(KSsYrhW+lOLitDs!Jn zZ=NmbiWK{f=5_r^Z0tIw;^5b8q|w-@axBj^+ibm%b_j{QIdjU|EJSk%h@Q<(L=WOHrBj4?r4a$jC*e2WATC1d~_WW4m zlS&Wav(ptmUYI#I^1LOSyF-NDhI#);N<$%BDt+5wbH`kQ7fZxUf3M;$_uHbB&5AMA z9TN1?{eA2YcZ7>&ojIpQ^fyXZ^6amhJYyehuZ!Mf35_G0EIHMP)Tc8pmUjduIT(nl zCl1N^w+t;(nv!<*~3L?dF?~K|vgBJL0wVYS|qncX@O2htMi3|4Sv+`$5k?+UxD5ZDn-yXZ{9M&|I#W{bmn3EqJh1S@99$aLuHH2 z+meuR#B=?SjA848@AKzuVamM+xX0S;g6+I82F`5 z`n+(z!_qpgUA;%%nqz&m>>(=Zs9~lrFOd(Cru8}NA%7`Qk{rSS``CMj8EbW`ix{CP zE^|`AM80+1zfQ`0=gjQpY;mj6-i8~HX#7@^#?r>icZ^LBxjFLWZ674!`T418@;WFcku%J;p?}h=w8LuaM@Wz~PUDYu zGQGF4TEa~!t<3_oEvSh(GDnAV0t|xpFOT})i>ADnsKlrbgQhJn31Z*0p~ngJA#7sk zL0c}dvNu=N1PMcYrhgzHA6tU}x>Fg(RKDUH-o_5C_B1RhgS$ck0LbTWL;kqsHP$y;bpUQ9 zw8^BqE)ZEh_6U)kq}ox$;NbUR{Pk%$qPb5+wqgmRXj1e~#f7v-Zg#qAR`eF5o!K4` zm0B&zkyN0UJTIFvH%xv0*0=ssWlH4M$9TLc4F3X_i8g$c^EP*-p`)g?-E9p2CIl#xJqfAj>kaUH?~&J zt`$r>rJhHL6`+)ry~PVW6-tUMx?yGyND6JpMKKLow@wae&j5`4oT->2bhD<($asud zaH7S*iuX2GJNoim3Ae#+8~c3rQnxZs*hXeKAp0r}OK(bNN9SXZcpi7ViS4Z?EA_k4 zPV|GZ2P-}&vuVe=$@9L#C1d?-386P`ybc&Y6|N!`H%$C=sGsp&u6IFOo1h`w?^7MT z8IjPwdUd`EgW1an)0ey4x!xY~ZdXJ}*#td3-YPfNO@a?7dzATlU)WEncr!bQ=IW@L z9&;LGl4?PAi{s?4rgpy{7LqSWoc!wXM}#^4oepKA?qNCyI%*w;Clq zrCQ;dtoFLBr&;Adp{W5!Ob(^5~h{wBH}@A|_(-1%wqFc&%=$xQC#j^To+7Tz2= z)Vp|1;SW^GJ!{QE^&9!^e9Lt8n0Z{3>@&P8$W>XiI4KX_@x45G6#Rrf=i0GC|D+fG z)VIBxDs57tcoUUEt-}1)wr@(RRX#fw?^3|Z>+|uQ2Y@a2^yEUpfAhv`Vd+p=T znRaoxiv5`ki?sGSjVwJjI_azx+@bko>XvjEfVy4h+i^h8n1SZneO6VBu02*EPsJ_? z3so}41th=)*gcFz`AC3tx?2|qc_gPrC2~Qy{jTu3az*SgK+ahByZ%t?%G-k&fmFo~ z7&-Q=pawrMMBVZ3)V5)C#bmF*d3ilr|14xC&UYFlThLS`MJRat0O=vR2kK{(xj7bt z2^UARv|JZw8KQoydroD(ceygBHy5b<(2zE!QtzNEc)WU-p7e~9TEuHEMks-(6#c3gD0U*LdN8GC#1l^5nbqt$-X2daZD z?)(ICisYP0-Vj9>dwct4sPqJ9KzftcmvTm*9AAu69#ARnu%2qraEw%~LwYYX7^)(Z zagqmb@{*O#OZ{$c&A2M91~yZlP7aZ^rjc0w_zgeNZF){%x{fsJ704=mMS( zdcJ;<<+GmWjFVvPjL9go7?Sco!i+YRDjyhs;o|zI=$$7jVqP|MG1e1xxce<|;$%iZ zv2F~Z+uhsK#@4A=+{;0vOZjY}1H{mDl;4hZgCi0+SmX40x;*dT9bm8l1?xv3*8${1 zOC4J=i&iZMK;fZWnTUlfvCG}(LnI~xnja{!%)`aG@AvaX(#y}ifg^+kw|mIoB)iYs zu)aw6aqK7W;fD#D$k1)0O14tA-N0`gE3hE9ZaJFm z!0_@n-oARVH0H&@n$l-5(-P205Q=yg5CXGyI6XxUzM5c{`HcHxxwcG$GLL zHF(kqX5W5vO_g~y(U7_8b+%CzK=(X~F{H69=doYczbzkHE?i<*>Y#97?2Eb~T@|5I zrzjAKoQJZsj$OsX%#rtPx8HaT1#Eb?apj-sh<(Pijq!t}sa;m&HcE57He6662J!HP z*{@DkwCsyZ-$$a^%2~|T*C$P-$^)OnTo>g0n>T$*d^BBmigkHCylT> zwC1DRRO=2|2Csz#;LfAs8$-QA1fi3zXNYNCuugPoRMY-wm`vN%Ei@ z+7aQsJ?-0RI}6rd_Vq9pZO-d9kNwhT;JNZ!z9GNfe0wjQmbSLy^z8Kb<`n)`-9Hw2 z&--^RUGEMFXWv|Yl)ljS( z*D|)3uHMfe77X?;gmP#r$TCCTm1(GnF_{t8mN^}GD$>YJW0_Rg!zfbfIh!V?;s8XrGuJ` ztK_57s#@*NeiGwM8k0P0e<;ySiczK@1NvRp29wwBj$ud{X6|-*nG?TTd#>I2V#XQ{ z;_7|0c{pshxymj@V?eTAi=B7xunvh5{>(?KVICL)-&Z(dqKGc9sm=zQwqu#GCR05&5|qjGiM*oq3`4LfEIG<$^6ozd(-SG)kI370{_M%C-KW&dQS9;lnhCY z^=fUl77&9FF8E8S3(XOTp|*K?aw7Re`188Be^|;A06&t;a_TLnig~v1b^u*bRwju4;gR7*d|2%!gQQeuEoP8)qbeemtEp;+oWyWt<> zHMd1**6A>2Bj?`aXMn)oEVmAT&2BfroE}w0=|b4;t@4FaBgKHryFz7wUoM$CVJR`6%DJMU^K<#@Vq%X>VXj|7_{cTuJ3-P-JN> zSIZoY5*X3g^5kBV6@uKc&Fo@|H#4K{(`CUnG7%??Y_prpjiFKR+I%d8f|u4jeaG#+ zSJG6;zNKp73$m*n zRXQ0H_3m{2YhB&Jth%_|4Td-~Ci&ne=5DZ(o~cNDgNHj8gDyqcc>@VQK% z(Mf3c4zXFo`PM!8@O*Uy)JWB*=XrBy za4d3Cx#w86(Ape9n6t&#CG%A5!}usF(VGU_rXLzC#PYxGE(_htRNvA_8P!{)dallilAj3l3Add()aHyS5H z36R&5L=C7k?6}5xqQg>k$pmW!LfF)<^4o>srRW+wjm=UNIS&fcwl|t$$zDH8o$!}A zp2Igg{JYzE?z;d0T{c-dY7Z50oDtr# zUvppQ+25A(O-8r9306?@`_5XN>f91~kIC&oU`|IXo*1w*-*-*!3c113iao<&!nSUP zB5y{<3Ym#tFs+y?0SOh<)H<${XhPzGKo9>hyR4k%ZSC|wuAZr?pnrqw$30!e?5ED? z*EzZ)Df_hOrICVn)$~JbIgRN-_eDA6ZljouysPQj-|Gj%6 zznmH0+ui$BW=`dokUrrwGUAX=?Q*!TH;jfBzBEKry{r~z= zE=~mQ92uu@v#O75{u;lvka4iHfLL*WzfWxc?AJX}4;=hMUTA~wP2EKa%btc-NDc0D z3JjE~dT4Ewgyz5U5hqRl>8?7sI~kEOGD=CkB|Z6qfK0v&lOl*N`8N^r8T|m6G?Xjg zc_?RsN+2OM3HgVb|K49?^#4s({eQeIKkh7#MK^NZqTl{?=Ye_>>3p%`eJrI4u0pMN zhfwk{*_aaZaZ79PF;VjMCMUmt>3!TrM=(za1=8esyv2#Rh;pf#{nk^lhPvI9dRH+# ziZ~j$GvyOP=2^ie1LrS)t zcvAEb2y2%Avhnxf%(oaxW%%0}@C~{nxyk#v@oT!ZFi!hxxBW(x7m{Yuw!3eLQxm4N z*l+pr#%cHao~`2mm5Tx>ZtV618ybjD;0aXUMx<0BHdYT@@0wo}Qt zlc>qN-`qQ^oH-)walBp8)w-2bvQ-Fs+4fG<4h;`kQCSqc8osXpTa31Y=PbUZrFX&s z?(_H4*xck0*&}OOzFl5dwpjg+!Tpkw{Sx_W;ke&V1(STS?C%!x1}=`%be*2s8Tp=d zKtk9?^ctLQP{9$^jUfE7=|&>E^@{8661D!)(k)F5DiM}awoI3CgADV}FO+lu?WpN` z$$=N<;q~Q`eE<$gd##sZ(pML|$ClEh58SRDV*TM=m1FR_wX~7ypDHp$D9@mpg|3;i zbjtZZD)-_wlHKiwu4{B$&IaSlQiEdZQY>43Iw-j|dZ6EV@vbi#S9}~IdB>gpUP{Aw zZg564FRme~X;48VhRwDFYr9|oVUBA6XY@r@Hy1QF7wAPgwf((-y|0)dcs7$r?8QcC z7$YUW?}%_PW$`S@jyYpbrbyaSRT{?6iZ`&e2>Q86snVJjS?}33x8`Fq9J0)uw|(F1 z-N`H41v6xC#aF1e&I2V-Y?OgQ?DH&u_qyJXhsFth8~#i2aWW1_u*r;!&X>G2DcFsI$1pMYKg%jGsZLF=Hw?>*(0qutxT9nDGn*%=MrxkOP zntM3pTaxe`qkVqMR@Y|V7|{!=b0<5W(PCWC)fok2o%mtPwXn8j%@9UKYh`-0 zvIE!cSfFAVtVLh8e>tyG0|N@P%zOtEtlzQ|yT55&dHeCKWNe-~c=mt2*k1yV1owtu ze~yT?oqi%acx$!)17AWNXbN8UtHeo!UFF)&(h7Nl3MKMvcNV>_`j14t*a44vlICt; z2R?f4$|j!qv1$U}A0b{_DXKJJbeaag9th9ci+!f!u9Th`r~B4-LT8xq<^?bt(Od85 z9<-`BGR#$JG5z>{&}bT&g{tRemZJ`>$_S{KDWoD$=l!bF{hNra?2!by;Ka&zsmU=K z?+w)Gp>@NCX+#u!uMkH!UgFP+n7rh&j1-{I?w4_ieiOP;bW+$J!!-VvEwRzYxi%lD z#tY_7UasKu&6^2d=(%4-wMQ`)SniPPu*fQ{au+GIzgk;g)0l92A;rh`K)K_7@8g3H z^mIUCg07^yY9X|;uP0G%h2!y}dv}q85lV+Ax{n>=IIa!^#>_SLh`CO3h@dx4w(#0^ zMyO22026Tp$XXjeB4K5x=}TK4YuQ4wmF7Ke<_ejW(_OpMAAN+OgkgyWuYf=&)t$gN z8Ypyg??Ci@Z6p-x@>0;o#ycXNx6u8il1h_?x*jVu4_z)sIc|6`**ccnEh+6Yd3uB` z8V=Xz71*~>*R7LX9-gWj`oXkPhY7erhH_)#^e(R*^6+H944a3GH{)SoMXFQ072ehay$tgM7MQ)pqWI8PmJux~fNHL#QIb3prIBv%V8FS8 zwt><}X_0zp9+SmyB6=5(Cp3y7aE%hFexYW+t)q*j9GgJdksm7+Bj3i!kR^{}RAXBY zDhcO!LkUAmN)p1sW8*dUva1zjcQ4^l{9l+7Rs@@mhKq-q0<{<>CaR9-eI+b&bu#+d z*lbC_C9y(Lhh7awPF%C8mG><_Za8$T4086X#VX%~aKWP7uB}qU?i!N^_0w=o7|Q~=8qr+u z-J~AGLjB%>;?|hhMHPA6D}@#xZrD=c!Ox>0Q*%#rpc($$=-Bhy6EgvgL|K;qL)=?N zwbgauqO`n}QVJAzXmO`_@wQm;BEj9A;!av9Zl$=py9EdkN`c}o!Gl`}4#7Ej-}ip! ze&ddDf1EqUId_je@*~M!d+ojEnrp5(pZUy=)7wf6OiK{8Z=5HzKW?eX?|RAKs%yBK zDf+XoD4kgGT*KLqY?5>(%lPsbE~)79;t2C83DZ9d>x0vLeqIi@O`q;>F*-OXG8c|p zT}Z1fQb7ozarvw&wib`m)o&OuEk)AU6YWmFCN|goQ+a1nVmh-%MBz)UkdnZxj@Rmk z)8$h(m@z2ttDoR-XST>*N}2Knd3nVTf3v0Qp*<@E=1Y&++|%h&Lrm=van>e0rwPl? z_JDbWxnC@IR*f0N=6l!+{DNY~!zGY0rMWU&D>dZeSE#ea=e@Bwn{*j0s^ml@9$a_O?gYta#nQd);hvpz;#sm=KpBABEWWUB+_Y*MXV5 zP;^Fm(O8~PlW?-KrtfbgcJ^b~SryUr1PsD*8M*JWTad+M-HS)BCg(yoV^S~}%@L3UIsSPC?s`=hr z>{}HMXm75px%2P8dwkdAE+9tO5YXnKx_lLLYvI%_;#W3vwkA8;H5v~$cj_C=%$TmW z?I$%5(SDtJgDLDP-BT*>cpq@DB%o;*Swju>_P?eX{bEDQ982+;lfY1_S&*da0&o=+ zecVw;8Mx*Fv^y$N3F44ERIn;40YX*u=pbyjuETpMHcpvf$-}~-+4;YMI9z)V5zX%D zp3VrX9sns(VuoFNXszLL?}8R1qg6MAy1Yu{*?vU>&H`A@E1J}C1eyJ}Er?FGcPNvT)<$rh*aQ3KTNo}By_jbap96L&&kx{w{ZW_t&Ahnz! zudBO&&|p^K;M7PH=#|NMW2VVJGMs*SJ`F8A?V${d&D1r@6ZA`O!yU-qce0m&k7wsV zt!!_uFAgi|RilMltYPbeM#j;NiR~E7gn4uUaId!5EnA;v+sR2y86yJ@GP}1I5~J3% zD`!*tFNwY4aHU@M$dFs(<>&afo22c@ z?NHo7%%0%&A)9MuylPu^)=P4_{Z+1PUTb98uBf4}Uj1NU032^;*XzdakF>K#XlYT@ zZh|W4KsbHLwv<{*x+ne0Yj=L-7#6pEAR1TSdHXmr%r1HNuo?pQ5$BA~VQe*J0CBc? zofG1#wQ2n!HcyeE?vz-P+&vVt)`C{ce_p+}sbI2#mNziYcI@1}?J)`o7ioMke!HJg zMG10$PIhD@n!O+YaWEo0e(t-c;|?<&K{^GI%EKNQxf&Pxtm2hg#`~n<6MJqsX1t3p z+u_sgZFpzZ0ftfUgY#J>joRHi>H#{A>)m$q*U$K8ZoP_vF`M(5F844hN*0X!0t-Gx z$0#ul1&(f0)qzI8ZKqsj#dOR=hc{prArno_XIL#ckTYrN%Jia?;JCy49TrqS$S^%l zJlni1&eGzj&sIhxDUR|Lq&Lae-&`N|!Q1d|ug{(PQ~TtGegwW0dg#2wUJL*smM+@o1Z%q0j&P~KyFp$;tC923#m=P{ zpt(lu6n1o}88SMF`^%!2yhjML0et+rZ7%U=O94)VayN%VZcuL0b60Sip=u?GEVNVJ z`ElXV%SD6z-p*sN>93*`ZA5atuaS}Cf<2G)^dEKpfT5G{MPcgnCFOBi+ozVE3JN$o zC>K{o)TThTmk!~PTEgJN1AK6;_fgc=+i zFa12Mtxdc#(C)r!pc&C$oKQh5r`oxUS8+H!ShLl^i(UVHIYYxPQ_6bun9t|9LKRiU z4z(TH2lyGZ=uTG}kq~=T^)PCNg=-p!G=C`zunO-`IVta~X6V%h3#oCIH&lhxFSa#X zA3K@SDqjd%M5Hg0YgXQGoF`QTa0c_kt_Gz0Mci*TsV}ytZtzZg-lh}r!hH=`Iy=1d z)0diht#fWE+@v?>r)4hMuAw06lhpa4+$U|n8>mFbj!ze=e({fS#jDCKY&vsTDh93foS{^_~7jFz93uSu=6$v|p1~8b4&e`J$|% z4M#sYHMLLj!PAUV$dB04$x~3ZXU7QaA0~8;?LjcKbxP4{V7t3+`)NL6z)}_alchMMrwF}0I^)9#W{u1_^ zFzFOVy3t>fb6+{-qucfF%bo8yTwG2BU!6Kx5J~hgpD+ql6Ti6ee1e<#vHHglu|j|w zGy0(7XaW|4N8{U#NR@k!^WClAk-p_>EJ`}anc@x^Eyu<53Mf%Yv1UI8T^|4bm?3;D zQOt<0th1t7Gw@8wEaJVi|Ni_H$1Foj8qMw8%4j;dZt07p`h9E1_a96e9bY!3QRrsf zCe4knX!YzQ5ltf@m5_!g@af<@amG3UHgU}fWVsRojIxcARi>mpu>S1pGW2jHIp0oS ztZs8P+kuil-od#U`(jl+;j;t}>_B1s?6Nh<(!QSRo3XmsBHlN`)zNNtdzXzoVQhjb z&ob-zxgSraV#|xNa4Ra40WU6^n$|rBmC3DX9cl_R`?p@SezZ8U>e1RMvO0Etu%Js^ zY0>*dDJ*#A?|o+_9>-3ozCI{iLQ*Y@EZ1LN7XOu6UtKJIM!r$jr_ycAmAH)hH(V5S ztkFmCZv^Go)l_V6sCRb{UhB>*s?GG{5JNBjsfkysoVVuL%YfhcF~|ew}w=(%5~(NmI%dNqM13(sQ8GeBYn6d9cp*L0Lao z`^N;fj8L?+atuD(mg~3DYHZEC#=>Z8-9+jt2f8bkAa~SSbV@_|+V0MFhwvnZ%d`Q{ z8Je6<*W728^|=s6U(8FgeZItv8#yY`qz7?n8r{L_!d(}U5I9*bClb2YWC5>+$%r$D z5*tMiYo4fAn77S1ehezwQC(`!JX%p;hNIL`Chc`SwD1*-dN8R88p4u`WPVhHC2sef0>6&ptCOE4_VeS@StDSPc`kv z2T4V>dk-#t%2%!)HvT$n{MU-m=zS~8&3OIuFyjTEazO{UQLTXSo6%~;J{2dkc%90; z5_7Y17oaVm__Cjv0^B#7sHK^pqo{v39i%wj6P>HcVyC*osO^;{?9H^H5@?B2K?eYw zTAY3dRkPNV;}g}fKc+XH=7Hh?&ry=~TRS-!t1>xsmLA4`A!ej;sS{JTzp(JqSY7R5 z*gCp|S`-H;Y^kA($zyPm*ir-fM4(| z08QsQC3#b(38kTin+-*7nIa0v{9}pp4sj}Mn}9zv&@Xumh3R! zbo^7VxF;|(MImd4z3(k)f;s7^EYY+19wQdb3yS^E8T8n_F813wEoDawmhNtp=)`P_ z%V&$NnV~ZuW}1$RfJ>TK&kvw|!=Dq%B0Jlyr1mwu`g}9q1I9Xm+K<_mCu=((M+sz~ z*!8pzCsl8aTbNnXQu?`_&shn&*ZP!CUu3A`GB#+tLaa=|v9iM-zCep2Siu4$xqg$z zVU36^mR35gCun_dAeGJi3ZGR2bfj0;{HIm1VS)y%9#40+8F68qGd&AZk9a}vx(jOv zyA2M*XBXI5xf_ty21o^8sq2!fKEopkE4y_eE(5<9P^7Kof#erTn-uL+sEV44jnz&4 zO5tO(BLLsRapnGz8Z>TH8P^nQwbamy7c0n(w2>&J=anKCK2=tNQBImMYL zBrc}kS6yY0!Us$_m0kGqBwXt{0*vQGHbz{bFG=v)N>{tN?+B=s2F%f=@@SQ*3!xOG!~7mAdTghN0n8JUmvT%cGyF>fF3JlFAES zR)QKT1gZN+@WU|XXT6wA1vsd|fztDY$ljGO+MwThs}S%-rCSf=T&y5nSSt8R zt=m;S?=5R{qQHSbsl0}HXt+0*%wW$N#|sK$f%961m@{ zL)N9$$~0@2I!*Ilb8}YVli>gWY|&*vI9cc$^;z86z^++;dS-okLn-_mk`&$fNyr<* zT47Okxu%Tf-_D!-e%=9-%m`|Ltvq9UyfZS8Mvw|0OE1FifN*i4YjpC1D<7jwF>F~k z!w0UyG+MpT$i1edlpBLY*EpC5)9z4J8VWcW;KdE$lo{qEvYDB4Z~w3jPkNXdB^v`C zh;^(rA*cm*b$(xee{XNi^KXQxU94Z;SWvd&IyFL^j}&1bY=4)Q`1!vKLFkLvBr<0%d+{0yS*Qb%q>yw=e#F7cy@dhE$7!1lyVB7U3@pZ{ zd(a2vv61EUc~_HOD1xJtH}2dIqU*JV+_vbKa1lHRP{hE{>O_@VY^E43(* z+qRZxA5*avNv!Wg$tCqX!@INywuZs;MWn~LXVY8De3eHxrK+Nc!u-k;44yq}`Bb`ZcV4U$pVg$9l|=fdccpS$sfs3>X^33F z(cITAVA>eov-~ldpLnY*rG_@La=KxfE9zbA#F&ludqcuDazrjj@HViGJbOs@;sTsA zT3akjLa;TGCFiL%muBk{Ehf~QCsjf&PAR2qxpV?l;p>uJD39iC$gTg-C9EMw`2B_T ztO2K>0M7*ei6uq~{R@n;hStLhm7(u<-J;vn=F9%W!#E6dGJwOXMYR;RndRVgomxGN z3n%J=f-2^1=d5TkCz_=}DR_w9SxlyV6XN6D=!3`ht{IciFBK^c&0gyhRey+iGY!<) zIr_@S$>LW%t(c1hKRlVyS#U0LCmc9_{Lwr)gUVd zm1VH;qt98qk7S|H1_+py)T1(1UF{Ham? z(UBM)K5M#Ws<>otD>{uHtf5i%qb{bDSvYvBV^58e~(sU6hC^omE zJuqPWY-le`U_WcVFPBu|l|9#RnW!{SR%Q>^0sEwuHAwdedr(d=TaI+yGjE6pH29@a z)<9iPp%hw_LR6wxV*_@?=!;*UOv#>~&H%CG=s%$;`#WdgEi0$d!#8Byi1GRsz2)JYx#|B_xVu*7xTk<>jS5;M8 zQ!gfJ7MU=DeQ_NZ-1iVp1%b%1P=tuGdgh7`!^1DIUAD%=_(VOjfwyM4t;ZwjL!ar@ zfzh`9n4GLpiyoad1BEOo76f=IUs_+z`#6bE#AGkWQ0e|PUSz$qLhVjF8lf|#vRtAt zdMNg)?%;Ehy3EM01>WQPZ-}RwHQb9=$ecbZN3cMJ{oV^@;L-bgRB~Z5r~U(_7>`J` zREeeojE1s~N50sk$Buwgick*wB9uXcAZHwlem*PA8F78z zcjT>lOc-Wre8IoydpO*Ry%@{-PT8@7gk@8*LVOXot)|IXEzrJqS}M))*)WYwbScr_O{{iu3X2cD*N(CyO|(c|Slku!=F5_E zsys`)JrH}gA&**vR)$FPyU=o*_aALc8!E|FnT-^e^|6xAifv zuGU>u4`I>POpuP$RL_Lv@$TyOcKF}Tpc<0-2a)jAt7OA$M>MdSd;i}R&PXJ>^Y7pP zIllh+Z=?dh=YJy={uhGM|90%}7AgLVsR3<#?!Q41|BqYcH@Eu_?CIJtcsHAr{0r+w zL-XEgM+lcy6|quat-P5t|F_3zXhtF0;`}&g|2Ln-|4PLC|4Zlo559;0t+!J7&h}Ni z{I&3QM}fTBbrLVJ?d|(Vm_8Y0p8DUTHO}+5n)eoul`d+Ph{_BYpdJ_#L{59SqulLerzKG=e zrA>GN5Kkze*U&Ak8>wY=bS~E z<~l)L-!tR=Hg~w_P8O*KhtAAZ59cUp0_erHi-{C*Dzn5sid^jOnICiEwh|@I5YLEJ z2cU%I5GX`Ka}A5-7e<*E1If;!6t2>PlZn_^m7O;kdzsfNde?r&j-j8j2~UPnBkavo zxpf1RddAS4hMjdvcO6bk_&6C@{qW$VR%O0N zHA(W!g7*!4yn=1&h1S&LprcZwl~AN_awIztH^;B?3f4^O6Qh48#oW>7=9kA2h}`#R z%TK`e;naV;q?hx!nC*aheza7$DhGOopq$PVZ_k{Toedt*2zeH8P!e~Y7k2KzpoNfg zuQQiV^ElIrfB*5rd5&@5c(Vq(u`{hA0g07G;RX!dgM`% zc)bH1rJffUp6c$SFEvC)R8+6%Lp5nGl=(=7M45&9YLD_MuQt2@J;Sf?M4^{3BQP?k zTq6g=Z?5&?mh*%BU?H`e#4fBVAUCI`pdwED>`=3Uy!FOl+(y@&v8=5wbF#M%b_ zq~jGdqb6l`B7>tNV8wD}w3>7fHYr;EqJC}!2E3Yq`8i|b=4FN9?k!e?Bv>8x9=Et2 zpf;9hk68XLAL0#O%Z|+&)d-fYH#!8}RoVehNg}&RaS^k#u*IQ?Wy#w6T3hylvwcHs zO%@2ma&*o}IUvB`UeAO4aVoo~{Pw=3u^{U2zHICvNXSTL-G+bnpO%cQyrLAMT!AV| z``a2UQ8ThS9i>V7NaxfRf8(mouKrf#$Jwq!AD2C|)II7ro>Mkha{kPoSB2xY(s;4h zi6-y?5f*TRycTI*cRWh%aG4=z)(Vt#nXT58S$hzszl~0N=+27qfduhK zHSKzDIxR;f>v~U4DnI~Ewp2MhCw$Z=Io3}lf=e|&0&ubI&O8G3!r;-^J|)2Hv%W5> zf?0G-Bwpgu5@dG>Se<142ZhD_h5gHW()!SPXplXeY+ag;PJ-PP4kE;gP7X;Zi2}Fh z>Xg@vy$C6>!pm-dBU7F44BzaBwFYbh-Nxh?xt7J)rNiEoL+Tus=0ddDbvd!U@DP#b z>^#>vMoBqiqUXKiah`?j(hJGBDtf0u!yk)K!d|3n*k|I*2cXj$Q)~W~(9iJT_|aYP zXgF3Zqww5qJ)B5rG>d&r82*C&VoNIQlOOjjFuh>r9bPshasR?X0bpf)+ zmTu3MN1*Ryu>*Kq&(170YO-}vc}VIPAYiCCuVxZ)1b;8xp1z2~2bLJ4i;d!p%CTeP zVvk1cHe(lske}T*S^o%AW@~c^52+jT<=6nvR0j z_a?VBrjU-ktW*zhdQOw}nmqwant{MYTEGQU@F1d6jjz$|VrFB|O~B*jyFsVV&ObUE z0``B5#clJ6ctRc~GvO)CV}6fTd*z%O?F{F*C|%StsQ5U1dtJH;wIB8~k|mJQ)k0xK z;yn!!{WcglSFVSzNQKbx@zr)np1G|&?kq*v7X)RJKwPzqWvpt0ktrSjcgr^pG>?ygUaQN^IVw#S)25(HCoOIbd(d72@Riw%j< za}5z=_kc%T_&3F7<&*ZsBrO9&5uiroM?UX`VoBN7QZ<=_$apDWN6SeG;6A9NrCvr( zyRu!YOYd9{Wa}g<6`NSb{> z)u63lrR*x>LXzp~w9CVeX4OoEtW}FXR*liDAx&2+&X0!g@)vSOWt_}YYRDbC)B%$> zW%QjQo_1Z68d=-Tv%`y>tIcOcnP+prS^q0n9lp93A@!m`V z>8F^HG&a}FSpRwXF#(VErF=v<;c}bcdX)3wa*HF--9K5W&>k@XhJJlAGHh3(30Rry zuraro%vuVnFQ@n*Gd5VT{A(@vDUOV^w7?#IhPIQan|Og{9-TnrbDFc>a1%~hm-{0% z;~e)4kgM#}f~{J;IV{dNb;70V=Nm36z#oKB;~U-+6{iojML{!fU2yhbBTX#(yAg_5 zX^idQ2se0Fft&yzK0f}*UKq*PB1wiWi$=IW4E0lAT-wQbwcu7DG4XvTcid9+P^C$u zl6Zy}rEqO)jNUS-{r?MWjOr9Mw6t_sH(yZbnz&?RaO+uEd>g60;GUSp`R*Ij*`~8Y zKsQ8~TJ83>l+FV&#|4u$7IPKx+I=-82f>uxM+QQLP9kV5&o&@dWZ6H zx7r2ib9xYZ{>+jY__(J-lZMm-U1xxvIgB8Zq;4NvU6v)~1o7@Xo#ZXTvT}VEEMXOk)cv&1va;yS`|Eme1K-ygTEGPrS7=yKsUaLJY36@W~7c z{HRJ9%N_mBLq>&oM^!c%D>LA1`$opL%(m(9rIh z--;+R*GNE)kn;r#;JtyV0?^%Y;E{w|_tDDvkBxT(oAtjBD(N_L(ClNc^7GgGATO`m z8<@??(=3Cl|M^<4bm0e+7B9J<-HE*0ZxypV?p2 zE!k}}C#<(n+Wu+8`Ia{AmEBwEX^DP^(P&8_hbSir)M`Fw4y_@9N-1l<*iw%lzN2%U*m}|4z(Q;}X==knLaW0# zNa_@9Fp=?3u6)gevYqkNlB&4#L5_C5GkdVV9iH826Kr}6Ou;KXou^#8c2Jhl?A%6j zlQ%|R*3N4SyXh9b$%3TwOz3C5vD>4&*-YT{i)HenunV(9G4-!@h9oUCk?|DthQ~Fi zJYH`xE7ChVUYW`ut@!*C2X0A6jT-5Bo0M2zb=OCuSkmH<*5B8E(`Em)b$uNmG6JlX zG*nj~On^Lw4Rw~Kwtm?eAU4i2xK5C|b%71j%t+T%bJ17WZkyy)zVBm2$ALtjYUW4^4aO(;R3g6Q!7tPg-{gdmgO2`;XMMYE0^R+Q7<`e^dlY zo!UcQ+s#ZCpqP4@JOs@n!Y580w(~q${Wm0yCLu`XoI@)1<<%KKd}6Z|d<*Uhjy@`8P(X+ddPMV}B|VOQ50M3>f%#ozL_zsK9Nw{Odr zn3I%U_PU`D7Ow_SGySAxqy=@J%Z#lik-`v86BbJopWCx~muwmC{m?4%yZi~dZ<~XT zuiMV3xd;|%g>DpX);(Mi=gbW|u}7h)37}gRi%Ml)$s5a*&%@&iD>t>i<=V0jdN2J_ zLsg7((-4x`Md{@L_i*N5v0f@}S}gB{j>5FQUrG30)iSAYf!$xTH>VpkW-V8?a^5Ca zjZ-XjWYx58pS@hRI(MkL*!A?R&NO&ON06qkSNl=@1CMI)LGa@1w_uQHSWl1qp`MR#XBP$9f=kqe{_VyKB9{BaidbWT) zsQTI`qU&yNBnP7UTw0Yv`y^2k<#zg|k-KwV@8ZcR#hKDmL}S>rrZuHDYNv_cc6gMq z)iWO^*t{No_R_Jq(^PJ|+Op!7gM8y=SIJ>?a5UZgl<;>PW1RaJZ-y!&AAX$al@@Mu zWMs@e39iSFSIdS!BWDE0%}W`CnLq|_r_AhV9lUi0gwJUf4vy0B7p*wU$}JFJ52Wv| zms_5mRu^Gi4{7!dY~?S;0-9HwtaJh}6XSvShP^uZbXsM+D#Q4ML{|)Uk`AdXJ}p(_ zxHUf4(@hHvxtJ5n-lC@!RV8P?Fv6s@<+92ZBH?#sM)}o$p1(i=TyPe%-K;8V3_HCL zUTYN>h70H9y!pXWh;-0PamU{JTiW9C^1;u_F(_L^N}7!Ti^|sNT`Z4g=KNTPz$^EU) zzx{P@HA3Z%GBlG0c4{oJdrEirOV7$823)_n!SDAlgR6wqnJlXfT_u^fdN|^9hlf~K z2NDHdy!3n}15=0|cza_K6zAlKeE&u}yoZjiyt!&YTU;n;O&nBj?b67w&6n7}7b5YIJ`wX3Q zcJ>3fZouZ^NLd1f>gs#Dt_gyaH!vl8>+AQl=`%>md&J}upR_c_7XZhA`L)&@#45sp z=J1ZM{04VK)}L)Dt(O00b8GTPZCH@HtdwE`KHK1+Pkx)9noO&biGY9rxfwsdfUZPy z`#NmYIHuA-g`g~yEC!V@c)XOwsoAAiqh2$Sb15liX+9m4?FbT1d+xQa@nXxY?L$ti z=SKLahOO_a`f|hXNG{BC1mT&8mvi;!i;+M-O)Kh~W}U#jxviWV_qja_y0Ss##ARnj z*@}_xtjc4f!ftk+$XZG2s*6%FQRu^&RFt1clY{Mpm)XK0tDzG(y}@U%nD)?cQoFf} zmDGO8Q7O*SdbWi0EgMAKPgHU#OMB_6$+%+qlEjWt-i6$+m87^ZJ4_ zeW>wCb24v8oh`D&`mMU0H6NGQ)?HASR7G+n^^U~d)Du7EO37w#o3jf+ToOG8OHnH#%yjx6yzJ%&me^}dHRv)tFWjSnj(ixC>>GNZx`_O4-slEQOj z3PhH|Uge9N!Z(&^;M=i~DG(OLyOEiL9ow^fl~C(OS_^=jKHDVj-SJeH*zFTo;7YHN zdS8YOnVtv*0l+|?B3qtmIen(762HL7s%%U)1%+)o|BQl?=;)T7sF9NcHb496bNg7? zC95Bp*$xsE*g6&U8I1I^5Q*74FBY~WWan)TbB@%y6QC5In-t0EDUYwI8}wel{(UWR zLI9tHoW+zwe`Tl8C|F21t6}#R9-1`eE7X)kv;qmduqyrtoo$xsGf_lkt_U3RIrlDo zQ+AkEAgJzJ`cyIAc=8-U=Id1y7{6R=`FQK7PAZA5_Tl4Fm8c~FwxPW}LHJz!dK0`P(_;f7n_Iq#hY(w@ZaeB_k$CaNHN4!t=pB?~@|*EQ zpJ`{lZ`~^4$?{j4Tl#Jf-Qcr7Uown_)fq;*?JkWNcJSM6MTRR~O9Ao!`bLIMOUoz7 ze6!o+C-g~un@TcJitFOs%WhQ$ler0(;&kALl^wB*OW6y$(Vl6|!cXC?x+fopT25QB zB6=$wOV&CTo`KuiTCQ6&%4W1LK3*rGudxO}YT_8>dR}$^%>B#~9L>SaEtIQUAx z?Va$-;hF{=a$W5A+*9ST=KRvX8qdi*3Ev`34Hr-oX7(ddw%C#fs=BjX5>$^ znq?*KsV~R%i6$PCr0`k$)bvlf>eK!3{bsCI%*O?SiV3rQ>t*VhePBX)v{Yx+yTlqo zk(LAvL;PaiqwN?fBQvx7F#;5>>N-OHI_W8XnfG75^7c-d;T%Na;opB^myD$-iaixC ze2H&Ox>?f)lW;pdz6<`H8lVYAVot0UQc|G=xsqd?)b z5%M-6JkN${z?x;q-U*a&B$S&1&dSvZ^8cgi7o{X63OXN8Dv_lXrqb9D_8f&~4NEg5 zQr){;od&I(ZO4>$=qB)5BKnLSW+*#uDg@cILH91oy)(1#gR4fP#Ot=c)XyLLNM5|P z4EcGSBbh|M1L4cHLhB29_AKjhz8uCorW%Ki?p%6zbQi@xmx222lMI2x7^<&L%ko3H7Z%;J8G@HG>c8+uCbqqND5|TsV+WD^y#yw z<5JI@KZx%{#OvLlzzsZ_S;T&aR#u)1RlDK4EoO*cYqY?Xnd-|w7lYps#xLUYS~#e(o@2*f zvuEWe+Xopto2_N9(_OA&4t$=4Ji-!f1*mh}0&-D7px`Tq6)SjMsy~}VnLtI2s$O53#ZycuQy0Vtf?X=})UQkG! z-qh%GA4ov+E1I7aQ1Wlxl7lF{sAJ}+0R|(bd#Ur7qS{!6p77Fm7Q2*jcO* z@tbIc34Ta^^sM@eSlm1*YT}lAJDFXY;%3%~y5t-_GpnM6N(3THCAYKr(F%DXC7o$` zv^UiXfO{SpddZI0pTHkyN}afnhj0Fnfl>uCj4#FxrQp>AGfEx4D&#_FZ%~+nU)WvV zld{Tb?M?^b#$($<0v!I}=p3F7ZOYna8~r_5aiXBnk5e3U2#p68>`m>f;V-D)uj^;qPzS5k?o@>Pp4`$)QL4 z{}C4Ozcn2Ge-P~OcjUbp$N1;NUUn)X%~Du_Se3MyF)J&*G+*mKqp(W++gsiH)>A=% z*xaxIZ}HhMa^@2x(01}#ossQ7kO(v*#%Sgc(tMUNc1xV|>9myVVm1`V4G$9?M6>Sx zW1v&a$M-V-qP0*GQ}G=YrO)K`fG56v-0lDhbNl^nxVn=v_C(?wNWg8n76~X`$h0wq zwcL882IxH~D6nupII1cY7GFTWfy2-Kj`#n4tR{U_xse{sz;5$h5XMv$RXf|4pg%Ft z^wm{!a95Hrer&jkj+z)?WE`;Owm#)PY6!XO(9pwZZ2>Qewu-jg_&#ff^WIJQWN>fw zQ=ZyE@|Bo?0LDaG5vm8<rK@5;>0wOGvCTT)pH)YgOcLv2Nyp>0^MvJ7R33E+X~5i3aMI2Y zY_$|N7#?zjy!W>Mtq(3|&+$3#%j+(#>0kt|F#lgYLTmR=9lzUl2@MR8yB0_2M#svf zsL|Bea35zio+2R&FXP3S*Hd^1p*`6~^>^;b>#FJOEkh$aEZq$m+RG$2x|aTG4m*^E{iEpO(4nA_e(34=vzDy0L;t)00MWEOLl4oiP)%&W5bntc)Q)MpLk}gNX~zHdF*Q~e*OxLzw!|~ty*xD_+M*lwAXUO4sc}BND(|r5;BCx}e^TV) z;+2TnqDEkn0_SQva#G^_5^w`bxWp6H0b};GTyOAM&&F!9MC`_j?kqekDk5bow6bnE=DJ6#V9t1Pfty1=Z@HK3I_WtdQ3PEmurWv3PhV7p*~~<{Q12xTJKDx7$63ggnA{ReC4D%R|xE zk$jG5S7)OKT>2)Y$t#|F3L25GSx62d1t=7iqSJdAy!KKQTW?)k6Wh)e%?67|mGgNn zSFqVW)#c@WO#{Uy$^6KD-8ozdUOcd>ZzNTH$ z(6dA5{9!tp8?zZ#$xu+Sq~z@wym3*;LQ>dY>GQtM#UQq2-_)4SA4cJ?!ndD6Sl>@J zWPRRVw%4rO)h!*So}b6%a8Yu61Ko`!uiQTjG&u*jSx*wKbzst*f@zI!-)VC{?|=u- zJPHj4W8_lxHJ~A&Sp@M9t8$OR3({i6?e`n{4gNZ{mK{HQVMt|YKgrgDJ{vcUKJrkA z^x#)HLFhHlg(&RdZCg_5Hn8z##^sPhyY2Sck7haZItO`K+#xw)`KzJHGVuJAgn9>6 z^rjq{94i0h>juijxy&5<*Z%4t@d>?``Bppx=zkS!82qmCqdf&VjtkK;DuFY z7>Bp7=|oPaM9cQ)g&+K0Pto3A$lPk**D@E>f|OSYh(w(RCewHZE6jaBghy507YE@f zl0cKsvwkR7A>~<57?6?^UTO~C8%ya(C%@Rl94qPnnm!a;Y23ufS2Ov36C%|&+5!9! z)cgv~+wi8`*(-;8c19>eI4=PfU1DrV!MgkCjfQ6LahVRtz}*J$yAh22cpjJh@N|8; za^&iyZ&_~49+){H)FY@opfh zjQ44~)7YSp;HI`f+QCP;u^*-lDID1(Z1iZ2Tj%AEP6HFO(}Lt9RPkU81|=i+$Ajl7 zhn|k2k81%@5`Uq+dZ}A?+Em7Mb6>-KU3d~Jw9+D!ih-6pTj0FnhQa|#)6*Cup$uol zm~(c#(8rd;FGjk2%x5(GzL)EaWo%ezXaY$zcQ=b!D1HiAqsrsYs)4($ZS*cgZ2!(p z)C6^U_D9~CMvWmhD&ekiB$w5BweKoaHH+Y8UanUTH@k_6(UloV>ztQwYD*YEb~qXD zPTj}C#AV2@^AzoMj6u8qK?<{1B8>`Ef};A+M`a<^XEho_nm*-GxVM1b{S&JGeEC5Z zLEKqiy`Hz0fcc(#4xDn{Mj6QT<}~fFf#)>!*4X!*md%PA!_@ovH(u3b8>3Ir;KC;l z!okhJ&p*-vCq*28zdvEQZ4H)#zngWQm5S;vr0(4B4>cc*j;Kb_ba|WPI9&&q-wa2a z#a>WI^5}Wn&v0&J@$#Bo^xjQjgxZ>K{xuu|e>lp?sA!iRHk25e`4YGoO8cnW+i|wA zwE8f`850}h-ak0TULO-n`~>Ywk)*r^xwKG1BO* zWasHO7S=CGC9xLCzQv>ZR8>R^=Yq7{p1(G}30gvKe}~C&mh;#~(HYuGna)D!XqIk1 z{VcLLd%QO{jDPEW{=KzfN!l~u?gmu2 zISCIZ1>bHmTSC_&IxP^Y9ES=`0* z@V91{IXk#!Prka~;6F|ETPDB>Tk+e=;vf)rT%rF^DHs6X#(P|*^W8{`e=+H~-2e>m z(G>JwIc;T&sH)3fnI{+J|BvQ=zRd8F{f??qiRsO_Q_~y^8l{Mb?^Vf>!Mhgtxp97# zAD)2vNw{r7JNarlWxCI61?RED-1Oj*6l`?I5#xtwF2xZw-;XNqf@@|YKor4tlce5Oyx+@+`Rpd3We`b%o7F`qZO4OP#su* zaAw(Jo3C|Q_i-oWs-MjI=?{XP>C;u_TTo8UiFak`BcC)gF2Q!Tc)$dSsPyw+fX(b` z>+a&-L9Ce^oqO{qq)q2Z*35oCbNAfQ}sVmxLquA-Kw z(4UGF-q+Q6Cj;Ff0W>B9i>D)4QmgISMtVdnPrq6aBd6IHy{Kxac}5B#jfF{AX>QQ` zh{F~tj{iB&1DOo|I^NlPIrbS1P0(aJ^BpF{Y79h==|$y)R>I?@bZ&u0Mk>UJ-dF|( z`5C`fbX4ZU>yg=WmGKW_F~}s({gX~1-Z7u}JW&Ay-Cx;Kz$@#BxJUftKGcyCT6w>J zFO;L7Oj2a=1dXhn<8 zWw_LS{p$gpnKK%;X?xPVu4%iVRzdWY;)M#auBBAMhNWl4h*i^k)^B=rGAVia`}f!o zf|ND-?hs{b6@z+oG-D68wxAd);o+FCRma1+39rp8g4nGa796zNq`cihOQ!u!!X8Hv>U671{p)$^6 z)bvwrUy+*l|3TJUM@89n|HBupf~0f_(v5T@El4O*Qqs~nbSWSpB_Q414Bai=J>(4C z-OW3C-_P^>zU%#G)|zY0oa>yk&)NCed*{p0-TMBSLvg5}`_IWzM#f5{RRR9F6auTO ztqGB99@8JqIRD#YOfuygbVJXHjW9PcGuW_CvqVyx*FncI8!|AH&Y5*5R+m2eIF#72 zX5vRP8vA&!5g68n8X^B)mvbS?URe$k-&nWzW+!@Za#N`9$<5dLlAQSt6p-Kh^rmj9^R&-H;oBT6FD7& zj;4zg3c+y6MY^`p4e$_S2=ipO3W+sq=(>?Adb;>FU`Zu#7;Zt*QhJ-JgoTS=a~0_j zJ#0!5B2F4|e9GTnqxVU?e>?Kq#7mX}O*A#}Oy;FSz+Ixg8_}72xVH)>n~oEm@RbHU$VY?`g3XY!~fV8MriayRFK2n3OD%PvT*kasq^7C zoRn90$da*-pa&@ZbgHYz#np2Iy{d}*ran51PiK&I92W7zQ6f@@5&aF@`-#L3HRkR; z42)wOZIZ%-?KG=fh2C>|N#32v);trjW|2!j6Fkv6ohjq=c)he)`?QO*SK1D+-kW1( z^PUdGxARE(wrzZB zQ~VZ0;C3pHc+0iBZ#MXdHEc-O^|{W^aHVj6@HDA_Zwuy&Ab)?p+H(Q63D1p2LNPfP zBGJu<0KcXbx%sY?L$IKS2x}0Pze+zAHkZj0r28JTul7+wIMa>?jta899#wkOr?I#I zwtMHG&+CnF6WVREp2s{wpA*e*uVm!^dt01VIh1Y&qG({vFAeW+Hv`@(5MUgz z>12}Ko?Pntm);#F`kAp4d{t-*>O!aV_I#hLpr16yAK~u3U)#)aDq-qM6Usp zlRfCB)X#>xxVs_3e$-CS1}NrNzxn+&s`gK#T8Wd<*-z|;P zI?}n~082AmxEl>C*{|t|M>lThD0F5Y!tH20CB5)aMp|%3P^GZctp=D%V_-tXF#fVO z0oR47p1A>v`?@lf9(w=E8JzzaBi{{@oy7N+=xftFDsFS^)>^rSA4%PtytjLE;@n$b zId#R};d>?bdCiP#wCLRQPx%-$5%}oV)j`Tj?L+i#4BsE(NzV08m_w3q8@t#dlAZO} zxPkQV3$+`>Zqp(Ro#*}av!RzOlqZB(n#fT<3zR*%&x1uDWnM_eHDBLXh^jsTqT2c* z;O(8tMWJ691)S=B8Qq()rLDL#V?3;ene<#7FryAzeHnd`JlI=Q?m;7bx3HoKH~u~g z=dq;LJx=L@Md$hL-&OT%^(G}Uvm%^s(AVLHJnO$0W>QBSSq>85#6krT3T zszK_r*(eX^i%M=~q#CUkTCOLQ%`4>7A;fnZZ1462zNq7J-5$)SQhMEwn8|n31vU%> zO{r)wE4G-qGboU9wLZLGeccOF#IT++b0(Oleb2t`J`W z`WlR!>V&$J)kopS3IsHFBM<~@p{q+2N&;r&sZI34xy9U;JMqjlpA;JEj|})LQBCJ* z>j=!)1If)(RMD}l3A|L@hbQuJQUqQjyI|MHvIFsrX&#}WkJ(30ewYFP?LD39eQZUbLaXKolZ{YZA%|j@8*lqR7xc+G9AAx zPcP~7^F^;WqsczPX!^whO@}?gT=;V9Y(tG>5V%2O81t3X0|M~#ce9QL(EOOKh6I*n7uK}^DdWsLtu*V}5 zzUn29{PB3MB2>C}i+128meo#o8uHd->w5FxNwlH$J=UypYvWxB-cz6ZgE392Sk(Wj zC^pt64HkTa0>Do;axoXm(bLT$eLcKz`gm zsf(xl?pT`Z02FWk}d-R@A zIW_*~XZ$~d(zj8I|1-e7JITN>thV(`%5Rp&A@LXXBWx^{LX+X|IOj{Q|BCxjupFDS zN`a(aD#@dN9B6mB7*Z`9`}EJCHgEV-|9#sTGp>7J=@}~U1^*+MrGRg?OzzLLoR~ZQ zeSd?ZrlkX~SG~o2hH6*LkZE@xGsA0xj%1HmCCcKLeBQ6;oVR-~?#*`YJwYg+{(}WD z73CHQYL2PsTW{uaJjgTA(f2X>O#!V~9}9YvcDEi{$D+*tg^YEp@N(E(5apfJyh}fr@@aNh$$HO^rD`mi)>b&KU zbkVH2{Fw6TT&a>KNg;WrPQA_us&-Y{D;UsPC8>Ql>4Qtv%Zb6o2|$( z__#n@kVsayGD+r3U|Mpd2lEF}m9e5?52oa_q)%`J#*a@AUgRjiT;k;-`T$)R5if&< z=H1NpY3i)XxJX_K_}->(G`%b+y)52inip=V6td5pfde+qStD8^1 z?A9Nq!Xfw@`Pu$QL0e_MdO9{fhzn^HCzb}YMKUl@01tSR z9q|Nsm!}SGzN>WVOP;8pW_#y?xlQyb1nB5gk=snX-I-@L$_YY2d zFadTkN4selmd|m-2z|+Y#o)ZhDQT%q`1RNw4`m;DtKlY8L`slVS1fT z*2^~>i*bn%-(D-L3qP21f-s3=abMaLv02yf2Z1&F#PyT8)@1jW7*(Ay9XmDuG7uX@ zqJKG$kDVvTga=ECjA+GY6+PG?&S7N7t$is_^!_1|g8*R2$rgGhm#mt0v$RfQ0BK*h zgx=A?_OQ>}2#X$Z^IpbQR4WfXZf2y|dI|b0h{?zcV&aB--rOpi^?%vTFdL^>99Ur(0i2KGq6RLhY7X#R&352&Us*au-l5 zs(ftRT{7X$!7VjH6J$i$jJCHyoPW9HV#87Nv%l>2CM)X~^lsAxzr8?MKbauH&`j`K0Ja%{!d5Ofr&MvgB=JU&eyoG&QQCYIG77 z1~SPnsh({^0yy+4ACLE>G%Ozy*)Mqo^F(h~5Pb-MtD8{CP1cv8a*7W5Q(t&K`3Z{7 z1uqW_r&?xonvA|=CwPYj^*)2c$w=_9>_7+tsNE?(&KJs(IagO-|1dmlg8xV%AG^>% zxEBeeuBb(e=5?qDd;ESGx|#BLqr4yT_^@)+4WV4TD%H5lG<&!nJbP)c-!mTDO%RS- zpO^P)yQQX9=Rnex7Ov1`T_&sIkgy8d z=1{#VZ10+7Y_4##o#$apSuc~lA_B0UMOrwOlkONM2zY8grca$}b;!;bVOVaH_C@Xx) zqfn=?jn-eR(VW1QWDSHGiJxSeJ)U+xp{llIe^HCDy0_7RJNL;>sRsd23D4@}F}M2l zgk`pd!7vrBfskv-udm zvguD*P;mEw2d$%yc0|k$vR9Q**@Nla_D2#sY<9zPI9;zPiAnJNC5TXur zzoKH40&1d8@hRR$3*vEsBzCT{0Y*S`(3C=ajeuwUE1!Pi=E%wSObOPAA*@OH2T?bf)b{Z;N?~ zTMLPaZxCgiS8h(Y*z)dLQn3}VWD$!odX?EO*^vVnyY6tzwi}tjw1w^U%Q8zUy!IOB zQ-ih62Aj3xe7qkja|#-N-$|c$#qG=!j}$12@k=-uF`9asig-P4G>Q%0?mPr1O|~Pj zNmwd?0#-9YyA;n%Y+GIisY+P|ZS@9Zf!UfKkFaeS^*t)enKR##y0kt_r=s>9wLY$v zzQk`jnyHBf8{b{7gW=sARW!GvTG?hEj<9B#50$Q|UUn)E$0sorepKGSaf%e0wDvS! z>7N_5t8Sc8_gK!3HTo6#-hwRR?^zAQe9*yG&cV?k)@TWu)S2D|mS^ zt08wOHd!h)eM96(b%b5*tp@}nL`^Gf4ikQvCqv4VQ6$EvLn-4(^_#LfuQx)&q7azR zCK5a^k2w_M4nq0dwAoT|vTVO>bJ2h27)8r_vrf#zKc_i)jIEDOd>DE?6m{DTP)%`Md zDCuHtJEpz-l_64g=da`C-A2-x(Z)3Gd{YF?M>n5V_75|umwidvg{~{fCwFsG>}-n9 zU8>b}dk5G{^Prp=@D10>(X}``y13~zhs6VAO=L)bt&l^5lSf#=@Uvky8^={d*#XYZ(7)9B z`!Pz1jCxy&g(E0W6YPf=E5n$>%A>zNRjb1C;{Tc8!%Ge&cdsB0QR=Hc{XYEqcX^}x=&RX$7fv5s%bgpS=d$p2QEs^yXFY7XxVd-@Z^&vn|BSEcvfGjr z-S%WQZNkduQS9&#a@=a7tm<+0I}WM?>h65ZrB#|QQntRnXa%>N^q;afUw88*Ox-rxzuK|c2+C6GLPxC!I?k5?eSsZx9hp_4!U8pU&@r1TpFsm)z9uHTUu*9 zQoxMNHW@7lVtmV!3v-{#xFjw%T3%!-&N(3MW?kUle;*o8X9chQTAd8%i!-&Hn)Rzc z*L&5|Qx`?rx7|0Te~|wXQ!ydA*x1@stBn4%wRnBEpiaLq@-8X{^3l5><|g|J=INKg z+8~d&iuYnq+!sPd<62*Pnc_H^+PQ2>CWa5LT1milM3iR^o?i z-MhqT#y){C?ei*&>ab1`J=*((>h?N1(W6?|&$etz^7GttpLUWn;r*IzDlG>JU3+@P zFOwSSH0*blGPoW>=wT{-vurteab@paSd)09By0jcwS@n;?IP)8kAD@dt|7bFef1bB zM+{NX#quu5c!rwa!r*0FV{TP^6fyghFImKuTU8KbHPW$CqyXX#{0Z1zBaHMDapgYImcf6`d9Ob`+4SKoY>u!J*D>MU*+ z_cH$l!D`QvAGfrmee9j3@Z(~P^9Js-IHGG#Ei}y1Ap$G|WO{_UQ5znp9`{wUYMLm7 zyYrLXjU&KQU)~k__`*h>J;%G?J3Ou{Jb6~s%ibG!uQsp;*&R18Qkv*+rN0S2mc;z4 z*fxY2B%j@+Q4KbRC+p519Pjz3%dynJJWQs|Y-hmLw|UUUml>DFAc-e{iltY322w(v zyoEMvaRd9smG4B{nkku>u8tmL+|!2csP%w77Hsjk9G6+imc?ZCvs{!cl~~CTg;yj2|CHltr+|A3UgSGv+^M21sj#; zY1d*S7rQ!d4bePH6OF^g@5@ess229gTmPH!-?y(v;6W7aLFcboE?*XYRgD>(gjW=^ zO~FGwD>p8~HAMTEHgA6CWh*|5#+$Q)OZvs0P#^cMCmN`L@5F)E?I{@~A%c}sMj`wF z=2DSeF*}HET>6#KWszqg(0oAm9(t48DNB*G)B^x{kYvGM-VNfa;9A^Z%e7(?uE74M zz9q|sO72SDk)@?m?@+?~m&Q(GX*@VQb?ZxYm-0?pcVa0Sjy-?(k>9>nu!KjOq60um z@t1L02GmeZ=atc~3)G5|r0n+3732BMJh#?^yJ})rl0#R`*v)s&Ov4dJ@}us_fUCsc zhdxdpc~^tca8sTDNBBapN2cFK!?432vDg4q$Nu6fqxf6JoH`f2KwzkTjca`4TMhLd z#|!dDcLOv44iWE)#uKrI6zb3AGxD8utBT#h7y;^Vt+PqpNNT7~jK`6kOiS)`Z?Mj! zJ>Gb(EHm8p)0UowcapE38bXbpWjlIT|ERn=X$46L_d37O6I>6z?Phoi9F@?wy1U4( z9+@aBpPL~7dvP`1M_pR50H1m;nMq|X+Slxd9M%bzd9u7bkw1kljRtsI>mWY_CM1_FPFUi zPgS+N6=!?+3f=@*^wZlLW-~UQ-yxR$hH;_-eA!urSA||_#li>Nw|$M#FS1)Zu~aO@ zlLoj^p;3>E%-4Yb^W>AgljY$-@ke?0OI(*h{Jrbf^Y_D>ha}VQyJ|YBZA+Vp1Rv|K zmqm8&8XBA~OdVv0NECTu=!@_%fFss=G4Dp{Re{qCl&8Z)eE;~(;3|_B&QoGEA?7wl z^Hz_GaXc-OVUx*UYc1+$*``Ao?CRttjwHf{cO1&O%aa+aq>!KmiWCwS#JQh7G>xMK zoX+D<>PJ8MY<$`HN_puj!s8P3Yla)G<&KjOl~!5tIM)9A4r*|Jf^Ty}aR>DW5SX@S zyPtVKs!UuN)t4ls5^Iy^dl-34_KT_*)BTazmYrk|DJv(7Uz1LZr*;S7L#YT@G(ucJ zRrQzoOVV%%e|}(RHm0gW9OfZY8OH|%;HWy$0S{j=WU<5OJ7(>6MR@W|h2GBj=tq5m ztGU-DT1$Bsgowas&516oSkcy=95*d$i7j8i>uM%kq$T4r+>T-4Y$(=K>ukoHF_l`| zF-&jUu}jKyPTkvhD)G!=hZtW-m{mGks744P-bIZl9@Zrwd#a5VMk+L|2KNW(elra>MhwbsK#MI!Mk@4$Na+?l5!9vqs49MzeXi zg=KZAVUa?4k(k;3=Z~)>Ec?3e{R+{ghl(xU#aD^m57=T7Aw|i;3Sw1_ym*cs%@Fot zOk#NL^ZTO_0Bz7l1G{tV5i~Nm;P?Pj$Fi|w)kpvVWkOlk9|xA zEgju}l(1lY3ke8I+H6*O%>qSl$T+x7JM8#!@3vwmxaC;Qe0M+%f_9{vFye_dZP5Bq zsKz%i62elFGFZQw4LrmuY~$1v7+OP2gJ6SfC8&r(9~RH;6)%J!@j^5;dw=fSa@1nC z&GCxd2<`0?4#Nm>DqEI?v#uMfXlhF$B+w|OC-@i*>@$Uav z9h#_(;w=(5`V<+_*SWET*(%W`w^M1|b2C+?s+pf2_7a^^P*eVku2?k-ZJTc6$`{E; z+t%V%)YV?!lAWu$5f^hQs;8@_?jhRj6v9-sA6$QyFQw zcPpJV8kS?jBGSVMgTs4M&dNkOxfZ zlyYJpc3u~}!9*r1*Z%n%AE*89tXRThZ6^3tvYf*5@^AX-7=?T4Kul6GsmYIaYgfT( z&bP>{p;5R*>5+cTC&o8@3Kj8h@nrZbHhxU>gdai~j=hYP(m%J6Qs&ot!gfGtOvpP_ zoJOlL#TT8^s?~JV)bLb=W)GmWP{#}hIW#3M+Ylfss7*vEvlu9HE7y?n4E;1rGT?}n zvQXXKci9&V#qg~bJe>z$nv?Puo#xaP8k2f)RjBAtd4kxRp?J zu}db`1&P_7L-u}nKm^Ln;rel2{(B=lNn~Iws_RNRFAP$YC&ta*)$>*ScDqBj)7{_~X&0NGK+<72$x3s<|x9VlHdR ziU|%vicwF#bWus>2~G7kgimSZ=0>TkPTfcf-y6@r^yq-Jq#H+lzN{?eml`-sXd-=Q-!K z1_Lf4kEZntUth)O1l2x-^7*~0S5}E0E1pRXYe(3eDw3_$gL<-E&(AI>?w!S+%P7?) z4xW1`1V|p-ou=S8AOe1577DHGY|{=nekd0Y3N4#q28^@K&Z-DfBOZ3%p+&%#`2ut? zoK2$YWCWt4yXHf$pkI73ULJ2Wr16qHP*$r=K$F&;qcgpt8ll-Ki(^}nZ=o2Y*oBO9 z3%EkJ!%vw|eZO<6Jl;{-6yxh-4d!1=G=+tMf5z#8uHDOM>PT(ZH~K=I1>UPz+z0KP}%>C}3RvwhiI zQgdO%{`+A+@=im;+W38d1>o5*OSP~MPZE+t-T3B>C#F&|>qJtbOG1uEh(8Asn(GJu z!yCXcxP^T23=B_vbGEDul2(`Me(Gh|Kio7pZO}RNK;m}iG%2eKUtJ@D=KQcQR%5*7 z$=>}Fv9)36=fz!g&aZD#$1W8?Ev}&KyhM)1@t}0ZGcoG=NS{ z?l{v4`bmRDd;3>BaKT2lH^L&G(e+Mo=<7z?lR;$2@b6$hsdujOj5*zofe{}CI|rML zvR!(t78*W;X)O^hjUd`ht>Y79FtveC-+4-F1t{(jZgqXrbrh~d{MG}FT$*N{QPv)) zD(O@x+2K4m{&YUC6>KO5x*QhpRDh0oM(>vO8F@BNd;OFndy@0wiAuDRwyZ9anwO25 z6d8>nLbVqBzV3{erCOoGYnO#gJp(>73PcqcNkqwBRUl2DR5RPO)m~02`%%>>&^s`p zP~qkJyC_Z)26eFjImT^Khs0GDg4Oi3eI{4MZk*&i1LgzFdmq$((j##kU3^9Urn_dkt1Z)WCmR2BS}H#kyI*6i({G83RBC?=8C6?h+w{@?6JNa$V6Yo z#q+l%WYn{6^NHW;jZS~mjcVTPDcLMJkj%VuhoyMJCt&D_u{dMDOLOC3=CH1*N*GZWh-gkkOA|6l?1-;U@Z22Dy+WpopWpa2$4 z@g5nAJiMaWmmg3dvpF!S16LzyH>BeyM=pECwA2n47aA+>dw-?yR*m24+p+zgaVX|X zC+u@#&hZy<+BtrKs4li9?R?XG$Dtuf=tw&73@hC&7`e!DA+sfaMr#B!67b_HK_OJA zmTd71T9CyVeNgvmY~bq*XkE>DJKeWbJWwsKV2>nCE(x^m-t8f^S<_WjP&(Y-;9lqa z{7rFDbs2@84Th!y+pkY+lDNMGd(cjJ$j6@fPysVV15WCZOzA@J`gkV!j*26aGL3dB zrrOzilTgyFX{GU<8@+ZtMrR8I2mHkm+G)lE&2bvm{Me4wt0+aFF+;!O!tX zIC!|wn((HmaiG4i;ya%EZNqC|a%w6eW9VG*MbmRPdbTPzx-bk7;M=>6o;Q(#R^t`p z!s2vCUikqj8`-MighY>^L>+gjFhpU#s~yLnnSd+%W<`ShP{1Z$eiBrG8#YBnGa84I z9%k|hZ#7F@w>o2x2N!I4h+~K}r{i1@d7SdX-VL7kqN>Xbdgko>a{3g-q?xH!T6=oX zmHsN^>FD&26ZLM7I(@U^Z(#O`L`}*VdB9WAJUI1z-go9whAC%Vt>+-66PN8{kJonh zh@*7Y38}~Ed*Q+37Zv03JFj2goTz`O%4i8DL~anoDYG14+8+-ZV5ZS~Lmh*w;34Fz z7Ux|jm0D*U-%{8+X_KKNxsef#3i%ZvnL$P$`YYj((C6($2= zJ9_@%ypml-R$=!b{ObE0YQKBE3jv^^uhvtkms)DvCIi(2&5Ry0ARXHt#5{e^tmtt ziLxo-$paTIcxGU~q4A7#Bt^QYV9CVZLIWf8FC2yn9=~u(!coXZF4R6(f2Lv6*HOz; zJrgv(fAUhqW550mb%l$2CUVrYBsbt#Y420kPu>{w&`?_lomi+a3usyO>6{TM(`T2%>{i@FKpO! zIo}lJ7Ju(wMbhnE*U^D`x-FsWVw%Md@Jvmbe1wn|P>D5`>5{#`eF?N^6>SseWDc#9 zh#ZrY;vxVz9&XHUR5fg?C6(NtK21x46M!o6Dk8Dfa3+5u$$O@LGw-Y|FBe%tF=3c# zkLb~alV~nDO9I+Bu$n}g@^yxQHabZ((cMWKU z_VC=0CXT^oq3pL|gr^F{6+zgPu{GS>m&H0|Tij|NQ;R=v02hfnpY&pvARv6zoZPQ< zvNuAwOJ_1j_yx(O3-O6XrNtFO$hrcE_USiph6MLZ*F~s-!f24@#wR;)G)4xHfRebA zkmnMhczHm_BmvH_Rj<$S=uC{m!#izkYNyvJdMl&`m5)J!)ZYcOGH7i#%PsQa1B~vG zfwtNU{ecCZzhW&s`eO036{{jDQ-srgDP7CDgdWOa)QdG4-`&XtT0nBk$J)^Wjw+tE3` zn(4Gqx(3Z?>D0&B9D_xDmRYAyCAxxA>+MJ1O`B8BNxdGE`pVf_y_a_Y9Uh3oau#j!iBQM=5-}#zVs_pLZOE@89*S7FBAzUzL5aLMXBSjV@qrE@sVRMiwGA zYijrP?cJnme^ePBMgXV)0tJsvWKFpLB-Q9Vs&|cuZ@iqmj5^&im4Qd?7erN~+Givl zZ#SPLl87e5+Wi&Qq-phs&kY;nrzLaG@a&I|ULXK@tVLBJ_YKsDo&mYzY!_HN*A-lu zOrN#V;dOg7sxli2zq%Y9Icz6$X)DAaSjeIR`PDm}8;WlG(6?De$xn`Cn!lQnr5IxX z*l3a1`C}aoZVf`rH=#ODlF`CKPtEwVGMXX!#8i^$i^c~FK|OlM z-1pJpDPe+&wDjlbXM1w4L*-$&>@!UtWUyj!=b&1{y2no(P25=33h#ybQ;v_0ab}CE z2rDl)x!vg$gZAoeg(6+C$cd@w>HNN;&1Ft(5!+U$ApuXaKj83ibJtv^&JC6!F3pFg zDA1}nf)w$3MHah|0SwevCdtWUwhYG`&|tK!cr0sM+r#A3VbTz1a)v7Rt~~!AzR!?Z zGEn_8PF1wSO2dts@0vA%GVNkAj%+TEs%8m|fl3NJdBnEf><@b30_4q zu6dyd-4yA=g$}bog8t`-NMnBvVBb2nRKarGC54y2hfZ1uw?!?_oC^B&ABpg7nCTU=c{1FEZNvbtSpX4z&yz85q6%xDEAtgNkW1noqS||p0yN&B_1_b!Kmft$mSjEv6 z1eDOyN2C_U^bDoou)b>B=8ka=Tsq)+W{pphqDxZXV3yBldpj-hRwX?yE4}1EN2X<# zk!c|gVP$1^zm1R#=823tfU$1o;K0OxbU<{krcug+e%H{Qc>*#?tU1sP71FC{VS?*< z?~&0O+Q#-2=X2gN)f619jXBc&#Qp9pT}Nb+bTAk^PO#Y3#AQMkSEqFudWwe#yl2qW zyv-RTr`M(Hx~`ML0u2x2yUU_W3G)fTOj(bGb|l(-J6|;Uf`k3Sjs$6^EH$^RR~mE zzjD{-XhJ8wmn_tC{VcsQS+Zdup@;!8+vPW@Tdw(4|0kv6#o&Riv)1@H4JCqaj??80 zSCinV22~ZeZolyC9`H_FOwK{41;vZ{);8g%aqlHlx{DJ9c*ccP z)}8hSGs4hj`1wjV3}sBqpX(6E0swz~-IJ@1JtX4PRaw-FAoWGL6$IaNOicSh0msj~ z2p;~GIU72r7hTR%lL3q^EC9nd$zOK-W)r-#K|o~7*<`uuP*YnV6#!sDMJe7L7c}4M z1iCqZ0^prK-Lc_fx6M_$+dXxr)n|XMX67@>#I(xn9=vgHe3Ce|b2{rLCi#2+p!uDo zsfqyuX53Cu-2|)I?A(I4Lr|_W!0I=Of4Nb{oCP3>~~9h=^36NB;3O5j4kC zDEEQ!CMAv8?!DmgwE(^U!zv~5h6Iqj79P6Y(Px-P<)ZXshm5Q5Qh%Uoe-59o&S6Pr zoLNMJhX}yF3PcGWPFZYipW>FYU=nkrR`@M7oEVu+gB0fpE>pMjofw-4;}hSDD25=Gt({+v?xO)1Sca>c%hmemb0F_UHBD zP{reckbLTMD6Rrprdc`u+`5|9j`FUEwc0c2jcz*YVLC z2eEM4M@)MuM&KIVWjcXRCTvoMQ@+6b6__W{RA3T|9icpGgDS}nRAowb~s8MpZ1Zj79W zcqVGjGxios?G(IFOPy(udXD#4-%MwH`WrYw4E17 z@X%bCbo*LQ|4HB#N@NQc;W&lznU{XP3(8SRLxs#UJLiFJN8I`%pWZDGndcEscsN^Z zW@;`1kS|Pg$f9o+nagms?8*ok3nJ;2(_U-=z@%}_-0o?FjvU{q1pQ@O#>8Pf|Ht8@ zt~sVgevUFnN9w>)#*hsz56lhndH2L0DZ+>=XgI+88Gq^3p(JHv+gS#?3oJ?=pQsHx zD$8Ynh&oIzDTni5NKA{@&^Y}r7Z(cuY$J)8yIhAE((RXw zFP3J)`XO$ z**1qUNDgoBM>GAjW>h?st$yu;k4xNwZhwr{iV@xmL=$OzuVn&jGE5|GU}<95C|nW=YCCeORr z)>~KM@frKpoS6qsG=Vv1Z_V2hnRk|zD{L@0jGQDZJ)?eoby7dCyD%&_*3MUtaZ+RK=rjImmL1sMyN*@OOVbL0EhLnP{VDkc@ZVv=fAc zZGp?_h+JRps+=o!ux4L0s`Zq4n*QSf8pV-DQ3KM|fB-x=QE1QA1KAhxwz7RbLjN&E z;Kw(~JT3>(te9qmj7oK*wL6)C`=!h@cz{P=}y7N*a-qJex3mhI9_idxT2H7#w zUavkZ2p1k1PJbR<+MqT8tzKPq_vZjgcXO%Nh8v-$`kS$bx1~BSup_z2mjab(e;h7e z#Upp+-(e96!3)bIaExL)3k04);~w&r(c1hY(!`3k8y8k8y5Am0N@R>Oi zxg&{_qe94gtX}Qcc)R!hL&623FM(>_CyH>=rx~Q7_k$Wf4-HhO5Bj?8cC^EVi>I@3 zvfJ@!2Wixs`ep&G7b>a`X5f8?cehhW6B4$AAJ=@KpKY9~?$-%M*kB$dj8Ic1NB+~!S`(Eq5cqs@bT_m-v|2-`1-y>xVqs} zo=Og8>m7@4O)R0hW`MBcV#2p*5^(m^<+}$;6Fm);E6ezSpBkS(XKm)9z&=xLS5JJl z;h$8uw{s!(54a9S`D{BKLm{TdE~m%Kh?~{SyllfDVqq6-o_8^Myn+g2w2Te!`uR_W zPsStpARMUJ$}(3trWGI5B6P2dTH3sG6`7)@zJf<4V)y{grIUWr2U5MVjLWO%2P1!n zHN5OPMD@sq{dsNMJ!GS7{yDq_dR3BcXfL0Xr~@=*;`IxtX1m7Zha{$FjupFmo@qy0 zlWHOKp%@+flz#z`8IQEdE`KnW|H2`$8>V&l-7YcIIsf-aVv8jn#A!EIZ&rmr*QrCdl##WV&;6A2!+YHQeGcI!Q~t-vKRUJY+MH5U zh5m<;pq$Bq?ZgT@7OlG zY{J*e`7CweQ$qyoc*$TVV-5SW^o2X`c)RIYEIPNgLk{XJAM9Yv0%)Tb6<+g>11!JQ zMStQ8+CAB^@p3b2XWQ=e8ZcbpfK%qr`f3b(o@bzI2wC@tKCUvSZMux^K16dfNv%okpSz_s-)%-<@z?=XyH__F(3y`54}NJ;H59jp3hX}4PU#xZI#2*SKP~E& zx5ilGXqBTV0)m9Bq!x7g(MrImvBS|f&-9j*z4>_TBI=E@bSm%a1;KJ+ z!SS|U9YQHR#Xcm0#bd3x(x&(sd{ee^zrj7Uiv3zXH=o}-&Rm)IeQidP%h%Pgw!6tL z@C08P1WX!^@{PCkJv$^lNWv~1m^97#>PdlMw+oVqpm{@nBbgR8$D{VC$faRCV&UDN z8Ro+fT0tWN5zV^B)Kt;l_v0Zh$|~|iOM77^vPzEI85;X!ZQnzCeQfw6BIpx#E9cwC zGK6%d0&8q$v{VegRG!|yI5r}Hm0SJL8ND@gIqlm|vl?V8G`;{uFSfY(xLiS|<+V+<`D~@m02@el! zRy3y)Ay6-;s_a`aVuLR$RS8~T`>9RuyI~L5T53t{k)L7rVEsLu8t;za>U8SHUBqM~WTnW8-Q72u-F@h0C8F%+OA#t_>6s&J z^tTn^?oI+!2DU-1_X@mx76*y4Y7(k_E-)+4w?@~ymPL5TG#sln^nQ(XEwr;mt+!SY zbRUS@F-DCRZ+2j!g_uNqKGKLq0!NGA33thTZU-v(+2QG8o>%9DV*G4!aIf0;s^yuQ zAzKk!w~Lb7=%!LS(Q2+`;@8Vqx)^q0?kO>m1+T98bpKq<9T6z$i9_Myueh?SnvZ>L z;C$H0BLzj(cB~2tDvz@< z#9hX_T#G(TsNo_?{eSGeRa9J2*DY8;u;A_zG`PD4cMb0D?oddAI|P@)U4y$@aEIXT z?hak~?*04rz5T!Rdp}lnY%)q7RIxq_*hPuQ4uLAJ;_~gav zIPc|L{xe!@TU7du-WMnZ4ziiCl`L?W*JThXF`#=@`WutrqqNa|TH8<*24n-~>6jf{ z>T`;P29*~?cPhKpi|@rQ@z*W}BTU6SsjR$1otXUF*~){K!xa)eR5vS!Annghw*0U( zDDN8%7BFZzs-{(v>#HsAmr+6rH{_eNYmG;#l*chR*`4w|P^qj@UguA5250Fh&GhIi z1mHDw4Q8em=OVXeu}tTV^%=@G_zTe;q~Nv5#U|2*_-y_;c{SExY1-bTur}zbX+k#g z(6>IexP$j%Gd^#mRh6U-Y%~vKsO!tIqux4lM$4`ABB@@JwGeW;*abiWbS1Bb9L_Dr z+h2}&)_QVYUYi{JJ$ao6R7Zaen07-epzyv-Z*0#n4n84b5H-oV_4WkG3dFEJiyDRx*z0G_=mV3x>hDmX05#rely94 z#=%VxblBQ8nmPY`R9>DHoO&k?iwycY-IThf|Kqs+smUDaVL%xV{1n%AqTD2q!<2~F z{&1Njw?DjhHpRn?Kdz+Mq>Mc2`t%ss`(Gxok#x3NZQ*!)N&#`(Onc&5iUe_{g8P3MmJQp(9f<2M7Y)q_~f`rpzILJih34^KiglrEPDrN8@w5XMQF z`NaV9oEecn;{w)foiKh#t*Dh$J|)V1X_n^~E{-Rptthg`Zq z9-3CX7Q|NVSZ<^J*IPr#pht-y*3B84=uBq%FAQ2VX5-6wr`vi2Kr(+e@=ORv{2QSc z)9iI0&HZLPBV!k7_w#eR9;aA&w8-F~)=cEjB9H(DdGSJd+!S0lX(<^#+NO_(400q$ zGZklb?|_5XTs<>ni7}|0{j!}V0=XJ$!zk7ZNqf z-q~Layf*W8p_VZ-UPNBl%&5!_Tgus==~gYY+|2GsrIgXN3KU!jegS5T4o*T~7~&2ybSAi(gIl~@z>JdpiaVIQ-8!_Q{y z68F7oruQMsYuIw4OS8FWSUM^_wDy%qnznzC}{6NyqbY&@zl}=HBR$l}V@EA)- zY0=4lJCNlxIj$VWKls+Bu-rRnF<{_)WyY{vTRuZ~$JNx2d>PHXSW(@;(o@@6?b1W1 zf;VBlh=0rzRyTLNQu6TCYXfxe75^FUgl2Kki}X|V)&JOH2THCO*SH)F?w!EzgO~lC ze#OPP5*N1-#C#6da9;pXpBs(@$0{qErW{F9j5ai0Y($F$>dk=^B!JA`^j>Gs+P5!C zWK{Kaqar;i0B%}E4b6_w;z79Q>dG%ZeMRJ;ktY-e5b76FLm{o2$VbH3&lCWKj-sp^ zJRTzzZ@Gs~DLb=_rd{{a6#jZwR{(dd+I zD*z&e@xv1Y8@94n1OTJDhnnFWhCuVEsaZ$IDd-3aV{0xQegHpUS=+lThSr=aWNIQm zNj}qd`IlB21R|inSXX;8CS8D_Q*Z~b<*~=C^cygzD8HCUPAYLZgBdv&wc8jhB!S%I zBpzM-RkDOo{pXP0ca3fiqLs%rui?jel(Fv-Z~-eDp6$2IeLWC{|7;T$`KT!9gMLsI zE2tj)fav^i&RFKvs7#du!yO*CO`Do9*tV{X4&_R_AT)fct zvUUD+pYOnLdwS~S1iMJ&c?AEqtHpL8HyVY~o=L#!R5>K*YvCUX?8$fWv1c7Ab9kH2 ze{Q(&dwlUhp%`SDr(Ovaxa}P&c;6_TU3fi{r1&TwP%scowJhuOOW2VN>hx{HJJKwd$u8<|10oo%V=hE);RhHIbJ0*2BRl z{dMP;ricrP7&}Bh=X`GY&E~Qn+N8I8*}Q^rweD&K-NI2NFN)IG-vEuH$wLK@vwxlE za^Ia;pX#>*QSxLvk*i(38yrdPI=S^Qw}_i5Qb_v?`jXue5t*q)WML(3PtF1v)F5Fo zu8`C^Ug>5A&9`{Acxf6={`1csV}i}XA%k3jN)If63>DsMZqHR2u&njVz&2V&2{m*U z9o1_RW+gT89>|@K6}@mI%eu0g7)+T-~62L5@8J%OIPD6 zoHN?qO}4vPS)-BwO7x2Da#VtO0|1wWf+E&++B|@qWEaWU2ANsv=xY<*Bp#k5cJGPn zim&hxfGXwk8Zhks8N5@Xm$g$;bn%8EEvn`l<4}`UGXS2)H}}~bp^BOu7O5l%fO^q# zyDz!ZwD1r)mEM)X@uhnm8Nt+O2mvlW&9cWMZh~Fio1j+1vZw|(&biFBkNGBOqL~5; zEPX4|pedvPwU`MwiyjyI(S4N{fd2vXr$^Rq4!n%!&Z=*sfk<#CB!`m#Pn!ML`-IHZ zN|JX!uZU*BgSQR-hV^a;BQ0zIKQXe+qePF!#ludg8ywQzst+QpfC-2 ztsr}XUgt+c<1v0~5DUgZG_v_>Vk2n9C8xe};}`eNR_1Kgp9(7{ z{gjPpXaK3kv&FyG^9>*en34}&MoNb{=8`z~-?D+UocU9*bKtTf5kmG>cN4F z@`w}4R&T8LBK_kl_z^VBWp*y*07;6=;y(avz3IM>o!lKz&;iOOALpSV3GL`0yRr_V$%emgzg%~Hntw+$$J ztY}#va_nG0!Qo2c*8p8_JqBuNH5hb2RZVS0aLEBR>_gOW!%02T92Wy^0APx-qca@F zY}hB%Sjk2;G8nw8Z=M&EhCHikBTv4*ZVo`o{J0eRQJY$!rllGZd1(v<9<2TJs@$}Z z)_TP>H0l4kyW{8o-gg`&g$8k{X{|_7iULYT<8dB&%t6XPS`k`ue_5tv6&4qT-;YlMNj^G7f{X*e3iugTuBjNYaUS11TLUL)z++&(S}-vSj! zM(W=^(4d#6XZFn@1EG9WTNOGwlRg#G416k?&#}qz!$_Rzq()1V0}&E*Z1$u%0hw~e z$gpmPg^8|vQsBWCAV95<_`vUw67T7Q(KLQ;OUtXYDWxg+WrkW3PJX}Yo{jG&N^qF2 z=#QuHN{A*YfB+k9EYr&7`1(^avA|APrhxt$kuQ2YaQhl|hB|At*6$#z+GSpGLfzo- zz@=z`GbSAIs1mhGtGJ;y;wv5rxcY#3tevtyzGYXt;9DkUG1>cS08`RXIW7VWeDw)5 zDOQ*jF0``thZJV;_QgO-rGGH*p3SNDxFY+GMh{R@)gizthoqFdfT>z&3z}Z@cRk{` z%Y1-yyGVB90@2SozMF&wvvup~YMdh{q$|V5b7lhnQDLu&|at zSa2I+$=q2shNUY|1XHsG5-Tp`mWC1ju#A&Ovh^R+(lC8D<4euuxCqlIyGrzF>MPg=6v~m$o z;C1WT5hl!|fGi-hs?si>yp3dKw~B4f`>&MOCr;sidoE(D-N&^qcjDfiE&fr4=~&cx zT)QKhTJ>HKa?xZCkFZ^;FbbmgVOx2^HZ&k^Q}}4$Uz`F46@m$aTS%QG3dpsjmP#Cf zQI9Cr>_{#`93-P7MP^0^fkJh|>OsX>;pK zd!)uB(vv#jbBigtM9+OI$egrwwdey;{W>Cih&&s_CA*Yk8F+M5qD-iOFm_g58G>6i z%9ofqnb~4<7+qaPvd<(Og5PB5_4MATiCF1TxXYIo1V(-=M9a>i$L|FMtmITagqe+?!T?&w%xQ1$_wVLI zXKmdcY&R!h@{`~{BkYSl*jW(ivnEA>CmDEYJ%At$YJ5Q#U5blet;Ub~^r@AZ{!&Ug z*}L0uiLJcHCvxr)FW{+Ay#vjsyv45P{f1DS=x8LYmgS5Lz7mJ;$5CpLGe8;uLCfR# z$NK>n!i)&$`5Hfg+kSbi+V=H!`a2820_zVt`Pwr^%hF3sqK>E}lh-|{VU!#f!0A4o zQN7MWvpiJ9z*FuK^eKW+bCL_43?AjisQb(nX$~bvzD?-0^*iek*Cb;UmfnmOM#SUX z%(C^7FUg{j^#SgccoRm0dLqa}b{oSqr9XXcT}5x!I3Ju;z|A{6q?HWX5HQGg$0QQ8 z$EJ!=xcvgK{cf%e&~V#TvU_-oJvD0yj*enwW5De?`Ujp?;es(+F1^qcYV^;RFH5I{ z8@Ra*E#uU_3L#R}R^&!BwOLhki=~Sm!kT3=QUPJ1(NXC34A0IW3pwK;F3of|ZTsDb z;H$MhvH@B(I70r$mf#SDPQJbmpEs3lg39~QzF?hBj9BI0c31^xSzW@tnN6o zo?eX;NX>th8PF^_HuO?oe81m^vbByY&V9A?OG1E5TR(tG$k{|) z(5eOJ?ywx?h6|unQj>YfQRp~XPjW%*Y>R>B#i7M32{HPaKLyw<%q@d^B_0F-MfXN* z>S!H`gf#U~s94$DPqu@5yR{3s%Z5eJh)~r7DpsEofcdBa^c{CWTLsu&p1XfrdMzQr z;WFQAY5)m%J03AeOC;s-wH^v>-^VOWY{;;-p+6Q!nCTcFTKuq4#gf*4+ATxljzt1L zF_)-|u;AE^&dZt~A0x4u*Hars{I_o&&}IU6-xgG%Iwes#E2d73*oWgD9)}T)7Zny{4tQ)+bDk4%O>JXn{OVh@G-D?xMc|GWgIr8Mysk)m7x;2S zvcWd4h=h^a5Zd)twMuutKI{wPjm_1*Fx;Dtcuz5@r#m2->`({nWO<%=Bmmpla2N>O zuM=WF?zKnfK4rOYG@5YEGn7-gKX5;hJLn}l?FQcY`}VPdodWu5Dt)v?iZ}RZG2EXUP5rB&$+~7WTIgJ-?8B)dpJX)ZFamxOl$^{ysmfQnBcr8}54p zlY&D}nQI5S@Cn5OPpra0Ml7_sE%(5uBQ`UvW$X-6dV8@7-Q|qaD{V#WqtmT~h1K*! zK4F~n_z^P7vxb9-EFL{8R~X7k0s_Nh)4i*jo<^4!lm$4|B1I(wTrFXW>n*f`amV+u zv|lqf4V8@H^o)L<&8fQUtfC!oJ2+_D_7(K=!n%Ww{xEP|H?K7_j7t@K`;^bj0Abtb zp9x6o`6(4?nA0z$<+<2!U~99p_0dgqHC9g|n zkb*xFdQU=1lw_@(-=n0Sv`!~A?mdmqTVM?5Wf{zd2K4^gpV`Jw@$MnIk$Ain(9y&! zz2sN~3Axl3R4mXEvDnr2E^L9#m*8;R6N&y*p%uzy%kWoxgpJPu=Gn^0GiU2Tq*z6sBws|}Zmv&}}nfzzHXKy;UU|+(>Fec>8%y%(0 z-K)y$A-23KNvxN+%QjW;5&@M>w2*dNd=o>=xB9|laFNYn(#*6K97QOgJEh`HZ}ITH znoo%&#^OGo|G*T5e(z3|Y39>4VG{{z;wQh}My7zMD`MUtmCoQ1pbUbIa(F%Tlpbv% zUuy9>;C)rW7JNSuC-*@Rr5Kschis#bL?-5SZ~Yd>{IG6uE``*u_3-Z+EzdyaHBtaT zfyTom&O~9sM$!vq*E^}P2e<$91Z-j8+wvRR88m>QNTGqBkN~39-3dgw;n$ff#(R;g zw)4||&Ys3?Zm;^~)X--;!hj*Hrv&w1!^*`Udw(*NHLUR3Cq#l5K6~*GHn9mCG(@~k z=~Ww*zUq&iirxQzvydHEA}SNQYTuX72bu`zVzEFTLu3Ai#3BF7N~P5ZTvblR4sfVp z?wc+yJqd12mzHBI;^MhNtvCK!Mxwq!&a#GeiUamX@mZMCwzE+D`&FMB|Dnd8DR+%&+Ji-IWs%eNH_kk#<{mrHeh0?ozwo z1|yq*@%kE=Tu??W&0;J8Uh@B}JGfwj`C$ZB%K{zZznY{TSM>v&eENng(pPBIKerw^ z+PAIUK~?A4OG?USEV)Eut@gcL)444Z5!c1zuE_*q2xXahJE>2wJF@t}j^+1qAw$Hyqs@kdYT!C`$;|*`V1v*-zblyztJ3EP} z1jXqN+$ptP?Z%AjaM^8ioG5*(Ne)-|^`cvOHyx9A=l|sKMOrTUp4gaZVn*Jf(`Sms z)>an)HK$$kv=-Yq4_l0cvYqfT-ne<}yzb>=60G=Ka%u1z&VJ3qp|dTQ(e?FD9D&o6 z;9^|U`S_&bRI_p!1Oe2de*KmKiD`zK)%5$=_$nL`WybpR(0NrVixb4+Qp&Fst$86k zqUg{Ky|&JNK1wwz(t!WDZbB6uv-H^Tfd5l5@TRQcb{=t^E`Yri6%2^_0{9^D5U6|jwUZQY(bJY-vU;OB$% zo~;8_$uwY*o^tt-@H-NiR3)wffRf7m!qM@|rH{2AA?fS~CqzGJgD+DMjV&|ofw0kG zzkJpJp#mB2f6s|tCeoPjF^1jJmMap64$uIO3;td6S}WR>gncH%<7;qhgVxc(#Y*D( z(Z4fKD*mk0Q7WFa(~5Vsm%Uptl*@vN&`)AC9W~@XQCoK(?~j^ZFgE^DJlzx>Dt=z% z*eP-Ow}sxEFW(D_SnZ$*)cb`)$*)CAoe#S z?qdaPmI^ps&ZrNGjR9{zil9d0GFs78z*xz^pYIx!jK%vSs~y3~G&HZ}%D1EC$Jv9G zi*qjPYsyAjI;RyoZ8YAkSlVm?q(>C{zFGg~G)JFN)fdDS}O-VRgc zlye%nNq3@(`S@%;ijH^uFAc|c^6W}(ZiBL|)^0EpI;F_%_^|2!Zsl(+J_k%_%IPs; zqxhl!n#nGAMNqa#v*XC2keS#<0*`GjNi)bERuJnyi9KDI?ZPcUISB~~)}d|RIr~pp z67nEW_;*@Qz-Af4-@1?bp1;P&^-lcRQ;$w{_C(dF7_sIG;3B6i&+oT zD;q6TANyOq@4lkquVotyF?$dIi*f;9 zinbiN*}q2l`hG04=lURGk?vW2kjJHzvYB3aQiP~6)tCXZ7Ui?w&h~%L(@m*^7RJ&P zEBxO-0nn(bfzE|9Iab^J=S2Q~U>g$*-n}0iV{j$`>HiG@M+vA}{t5DlxRX##8Q9+s zR%pJ6FCBSTPA!%-a4pDr%v`suZ3cWr$}GD+QUJoxv=8N!x*(X$e?bHQ8nAue7m6CA zB>h2q%N*b3_08cg(w5(^LT<<}fYx4w2moOQa74?Myy~%NS6U_UuGApMQG@y>_;Zg7 zG0*NP%oO%BuPqo@zIUhAG(uTE?e#NLb4L067XgRTvKNE|y;BoqeJ>AK8jESRgo@}T zS--8-%s(LrpScxZBNNh9+MhaK6-E@4j_y;yxL2q1rcV-0^60CWS)Tm%0dV{Mlo>?+ z&BHNNi-M_ktX+}$mD&9^N_^J1EtK7*D8knVfi7yi@(ddm4k}5y&OR**li9G-$EpSk z?`F}IrhRppq1xYFq~Y~xWg8rGS9Lw^;H$!Nnq`5Fyn{5K9HPy^VdbiqnSUFZA`1am zp7pjWkS5shzX;Q??&~*ooMrdN{Dzl5tJLRdn8Gj)4Cf0ot3)jpiZ}hp)P{I4j*I=b8bJk#4G%JIc1G4;#cc_8j z_Kd>Ov;YNavx;tlL6Av0i^n&U%cfi)&BOG;TzNM?axN+*1t*uWDdQ&&1lV}D@o#t8 z0R=RM^{kmcIR7YN^3rK`Ml>xMV@Zt3<-vR|Mn?!@O4sfsrZ37na5rt^{?(PP@9jAWSVwDpMKnp2(s>0W+l+0vy@$+Ve5CM{l;9b^Z`rlmG z9W0gXvak(GogKI!6R*2U-L2^4X?o~6ozp~9*xeHr1}*b?*1qpRmljgJ)ZK;N*?OuR zH{UWliN4>HQ?<|rLEyGuPL)irc(N9@`&j1wV}sBKX+`j6*e&JX!_Y$_72tc!JRHG8 z;Yv1YX%lBD*wwhM{ska!Q3SJ&j@NA!NpsGUx zOzq6;m>HjQy}i0vfX&$3^(>V6{9ozOL=|g$PIN71gl@Mazc%|fcu7;|kuf=c>#wV- zzkNelpJOP{oSHE0wYwVnPYDt-Sxn?|>YNXU~bkE0i@@AQ23@%%y4zg-J zKSVP+ue~1E<%D2LTFNe`mKK_6Fa|$)I~A3+ul|hIfx`5NzHg&2Jux^1DAf6TScQg8 zLgj#U1i|6o#qXm|chhZL!C=D|3;!Gjq#EmgDh^A->s2rE0zl9Obpq>LLA4!qZrbNB zU|zg)@%SGH^J!yR#(-7K2A-4f(b5DBk$}%aq`80A%odI2m9Yi66Pv*36W?QGY5x76 zqRIO#4;Umx5MrgG00Baki)2`v>-*)@;LKr#v!miAATp^41H15V66!nbES=_#n$ zKD->N%Bsj93;M|jbX3EjZ^~~vD@qs87UNgs#RPp+=F#!+Jat;^^Z#*0ohBC#vvaMzy+&IC;Tzuj}X52K3Kz?FQIGK%0_Z&_#VPa=zYavSR zXG~DU7#fM?$)aI`RsS@%U~4z^_X%Uq6B;aYgL9c7h{VZ8Yt|yxpV#{!Z15(hmQjHL z+Y8B8pBe8a)KZyB;RRq8N%@Cl=i-x)16Z zFNVnZSdKqR34&u-ID+?hUnplsN{;$Nn4F`&V325nuqeN5mX@xJ!q2ZgmCG8aPCii< zQx82LL*5;-{})ldF37d$YP;icK)ItK#H>m~2C(I;&Bj{4wN! zmzQMf3aT%a=~Z$`35F7av0QBVbyF4Kly4g7u>$h0jK7iYIIC)5;Rob6MO{^ISkp#j z8PR%-XCWD6Uq$J@Zr09`3MzpoGmxn$WX|N9W6DOT6gcgME5-mYfNj-QCMyN|kK<1% z?Z@wheZFvjBo8bc66e1>IWfm;2KF?8@vi0lvnhW)qzC zIx6xwSU`AZ(XpF&60c-YHk_`vyud*Mx08$nA__o4YK1);#5OlO?f0f*|6v?>lj+BY zMNJ!sm!roEW(YDX4%7C=-SF$F(6gv;;z_J!1FM(KrjFLO%=W7V^GEJmF zS8;rlB!nvshi3j{_S{poIf}XX%qMEG-e#^1mMy6C+ND2S)0V9ihrpti>(7b>4o1u1 zq49L%S^arn%^Jk-n3V;mP^6;uW|c9%9}a1_Ic!NPq$U#go$%D2J-Wu>_~6%a*)skv z!3IkffXx8`<+^Pd5j%6ao+9F1hr?Z!HS^n4@5;k0KcedLJNH-Pw*jAzc$4=GysfyX7hGwd3hey`XZ~N$E4JY@w^m(Iq5%8O?YDLqJXaccK;L zSuCH?>$&bPtPSaB4(+$fdaEWb=aZj12J?sVu?F4Ej?3YV@3kE?>dhu?9)PA=PlE>P zw78BbZ!cFuBvPKnJF0kr>oDPs`*ylbJ;0M@N+Ax2drK~%x7()ohpx8~`Yt+sY`ed2 zFU1RO8epF7)$sbJx;LNa_j>|ECyIRA-iVCF?7rXiQIi)I5)m>Axz6o=M$-Uua>SYe zu)U+XwsaKUNcO$<9{G7Zp_=g4;H_9_9J$SYg08Xd!|%M3{JKrw-`f~9CKcQ+k4aa{ zin03w2VE;d34RnS7#wVimyme^{%K{WyQMph&?yn2g7$orN!JAh5$$Tg5p!G1iX^;d z$l2_I`kxkNp@&xbhnpiP^JQkXdbTnU?_Hm2$}WGjGALH0}C>;2q_XyqUcM7EXud)4$^luwb>6 zlk2xki$hfNEdHi3=~Z0XdY~}N7zif%3cvQ7PNlnMZ@iGpNCBZ<@f1bd(teUC%?^_x z;qchqqIwX!5tQE{GnsAFS%ztnnddC^K`eSZdl;4b@23}ns~AZ|B~Wn8WI{p^ z8s5$Z@j(6bu{NKp%qvZOj)Z1Y{meh>OEZ=w^nWJGar)^AsEX`{?lBvlEUL89R|TLq z@)s9c2l^O0cxWu>xyzg+74*1ye>ZVM#>Y7(s(=yve~O8;ya)Yg#6G~~)9d^6Q{ezi zta5(S4cUA=J(!Xye+a{PHuvJY`=|N(`Uclb>sP{nRBIipT<&p8{~=OoZKJ18YevCm zSPoSsUERbSUk3NCBoET`E#!|qKm1X*rHau9i!1M9$JyKSK%@Bf{zkcfN%JrsePia6 zo*4fukL%sOyO}iEkj`x#iaP_ZnfB^t{v>* zvT+ycll>;qbkm@yF%Gn!yPKtM+^_cC-aCbv;M!Xkdb^s|a_Xd4ve4P9$%|<&Auy{p zPNCQ553T)WLxW-`bUXO=L61H8dIpOUjrpDk%|SHD@KBO$q0hS2z2@z{5>ol1Fo(+r zo*ZCO5wN|@G%Ftp(K}0)7C{>QiveqKuT2x89| zShNNfz#igU95x=iVx!Y*NWlfjR+qX&?x=N4>bi5xaX9~~XNHIBPIwOcwqjKze1-*k zV;v-IGZ^xFb6U*`w7r#|7hJ&9Wo^HM`$6gt!#{X_O8OHvv!$bShKm@ceB)mLPG&VDzB6kfSaSCA+=m;Dy- zU!sqVCdSw+T{E0T<;R{#=tRh^a7Z&2t@7F;M4b36MG*5P2&1dXx=mk4(RYaMEj9P^ z#k9?FCZyJPMfVd{O9Jj;DKo&29_tyCM<>eNC)h_}Gn)(yQ=sp3jdyjB*{~=ON^!-N z_hvwmpqOEtftZT)PqwT2ciiTw(xZqhA+O_dH-&F<9>ib&-;xa(#L9WB%FIHq+3X2N zVR-V}1o2q?8lZ74lPh8QY17v#pYldI6tFWIeck-=5&Nhz~pW#a;kQdyst z<4K6MtY}60$<#4#uFn&W^?XhpGg_I{dO#&|Nz1|%;Cc)Z%umm-Gt*>;zeEd~b= zcP78f)ppcxza6kGpal1$p!f;hsCKxwpEGmyL#vX`3HDpJ=Wq{Q_A&>D+noRF9|T37 zJce;QLPerO(Eyt%ywmnSUk3ub{!CpIVuL#4pB2B~uKL`{l($x2`g)z9GPx%vS^W=5 zjaa75qK%x~qh$+fHO%x!q7sYCc>61v%!&25`%d3zil5*MG4u}Z$k&L6S!lqo6Tt77;t8= zEbblHNS4jVfnN@0lZ+4p*j`F&V(pzwJAs$^^J`jR#e0v>59@25zcp7BD0`Hx$_@NZ zsvggG7Hd+5ko@rgf5YYtPCtD2#fjXj>`(X%PSz+qw?-X4?9~+?#o8f5uKdf7r&>B!`0_-f~MW+V&b%! z#i30(i>CIM^%S3oGJ^5UZZ6HD2zkBxIa%4fe{QdL=O68N(;kwv>4d#gk5>ri{;!={ zW#^dFdJS)2^GK}BsyVLum2j|^L6U#_+bi4Tc@wo@gWFA@GPt*!c_rD3h_zTF~VQTsmMk&eLDpz28f8X1X+t>Kit9aMqWrO?p*_ zETRz}?o(ZqLFkvaY*R#boyLG)kb_`qGMnDM#CI3 z3lD6x{-XAw|r$EoeCB>6!;3|lDg_1IkN8ju}bxP zE3!5O9^P@G(H#dKu9aD0Zq2i?K3FU&;-0F0Am_xm4=Y|Dpj+G$=}iNCmW0S0m^(5z zOE}kI7a75bC|pN>7sjwSv$MnACfMD`_{Qy)4nr%i`ho$locJaTw}3(TL>Atp`R619&sVyL^R|rVKT>1mP?l~tWNpu7`w}j;-0Q6G zGqX7?+)o`)doWefL$UN<2q;i?h^*}GoiZ}66&hfp^|6?U$*4GNUdBaXi#sn(F~U*yxdf*1x=F^94Gba>S&ZFo!I1LPyKr+1&aJFW^WS$1_x!?nG6nG4XV=DdGo4R zkUx}8$wO+3hI!<(-C6rDIFS`>%F>zOcGjGcr}W%b**RY38U0 zR{eOESGrv{>NcKYpWXCK^Y|!o8E!36Ggy;)W5x1ARB|!#<%5?#BTARnaq~vNsEWG@ z&#L_=$P5&%_%XM}}n}LMDa;DaMB8yzy}^GwXBga2N?RcRhnC|xWz z=LQMZAmFd!UbZVFlh^g5u(Na+#ilf2yUoEu!NiO}8+&^}#cfDt-r0*yF++mE7FrY= zF^>uq3*GZjfuWGwr7#qp*9VDynhUvn@BMco26pf0<};s%xXq{+{f~q9*6z~DkE5)b zZvtn2^CRiw)$eCJcOQ>RZ^!vG#*lO7)=g*|mr7d$Z*Aa{jDd>3AS7(@H=ARS!mn>g z5rpNP#5FLfYYFBMQo+Ve;;VNsff?26K{tSAi4<8q_^*b~hOyeDqK^JkfBnK zP#2ROtrI8KnaCXu4xZRJ>{Fiw++PMx@)GhlPU=7j$j+A+c+=G@Y{ zJr%kEjn5T&yC$;^&qrL2ENdIMKKVlOxCMcjO=);k>)EO?-Y}16Cuix&&1bsB<6?m` zB=+nID_*~rb7I;%bjv^Hy_xTy zWMa50IC?GCcW%VsD(W)e}r?kA@iGy%~j_b zvBr9Ne*N6s*O$o;J@`Kwu234$)E_#a;{UV}d+~^XS4UNg4y+y4yxMYW-f_a9-;3Vl zgs%gMlh@tTR_9BdbJpzj--M0-#!VjJmR-Xe0Z!Fn1K_|y>Xfz<(-eba2J#MgPu>+? zez~vt4 z(ul|Zj-jvlu%PE;47l{`qhjMv|9GJv1V;PT~zpR?kDd6V~Uc)8jwE1;Cg&d;YN^PqI&_5^y8 zRt}qyo^lF>x8odcY~lkDpPatcf0u?X*)M?%6T(4=m{HKZwpZx zoU@+XSMYo;S7Faj5u_mk!t$^`tgWu9g?N4+P0j4KY<>E=*R9%gbu>=^0Z83!ne=xx zK;7%sYj}UBJb?gcF4axXT`V ze}8EbQ{9*|Du(Pr8_o84N|sv7dJtMR&|CStVDF~V$TVZ0QA6HO|K@=y5;}?$~7DAtD7XTk!pCC)$YfsbzDZ>?ikEr(V&Yr-j8Qhf5M+t ztajPIW#wZ#RuV>OgbvqfU&(i#L6&?<6@<6n44>FDVwV|vBd=$%PcN_j0 zj}B@7pAzRRt7J4NS_)pE(f=dGKwCA>7tcl(MB~|;ciU}5o*$$W*{OSgmw>y4lxz-4 zCTKA>0(PBqq79pnEk&sMbi(7}P#;$?(tfqxOC=5Yvc}eS#^18tTcJ(LAFm|5GHHJB zI0#ocZYOx;G8jaYfKE}Yc=i~#n3#k}SE#6`ItrgQ-u%Xvu)9I^-S4F0VpZDbA zkA2VZamQy(zYf?u%@6?URZcK@!ORSnp<>>GF4`--Fii@G-xKCw! zowxm+4Bip~J_~T*L5gUe4^OU8@Eqt>=^GQZfJ9}F#HXr|1ZsS~=%nJw@2O(&H|mUG z7-#|iozf65IUwKsp-44Th3IJX)MHwjo}rfEQO84!YaG>r=I;mn1YDiD z5|)-yd<=ll$qS(#Z{OM80n{)vfIZBW&Eap84s>fvrN zkydAslD5cZaV&d5rAwpQfYM|RXR^JY>+y72Q|X`5RJW)aFMGopaodWF%$aLt!e=vH zr!jdNj`o6KHG3MC$Wl|c*z9_?`=0Pwn7$1KiNho-N}wLd ze;+qKI#!C2x|GVfBqORNUF<$I)j3$t&UH2tjXyZEpeqcQOG&k#t}C)#gB#zbpr`M= zoy5nQC#uSHEIY}dtwxcz6jd-=9K0_lZq#68=@FYg2I~e{7jm`8aN7!)&$K;Cu&qg2 z-=+tI@C0Jv_*}l)he8#;NKRUpe89@>qbZ=B951O(*CQBdhbUn1~U|||h z(z!oA{CjAc>^IS>EADPiV|HryZc1@a$9cCiq9gc(`p9#EA8s=8Na8yYlJe7g_{#o`TU`YXrCT@^4yxxcKBZX_LK(Gnoq45_qos$XuSrD4t~eL$n|l_=-pAC2?LzmCG3h(zq2WmeFn*KICMW8id_!jw!@*?6?z-=5LT{l)vE z)giLfEc=eNNWUJfle_Iti)uBzK+p`|0Sb}!%TeHSw%~=WOhO~(bmFPG2(TvoLF%GN z=+f4G=FT~9k#e=tiYfONs!y+xqL|8{`p}-t>anf}hu7P+fJGjz(Au;oPFn=d1pjL5 ztB_sIiG&HC4*%7MFWIc{paJ z%V9jvIcdN}RGXK0ABzKP&n&k1Q=X+762r8MV#ZVJ*eG?vHBsyR(GDrvoHPxeK)Pcv z7n6aH;f&XmlB8Q})$V;-zGN~(#iFj@bW`zwSpt@1#;#cZUz&l((xF2zQa!TPOf2HMHd&pn;aU zI5Fjuru0{lzp!a2yJegUu@0dHo^c8Jr~;sh#06G1wPI(_hxHDq+pKlPKg{(XaU+3} zzf9b9u`Tg58&3qPBD2$Y<>53E?{bp8(1CDrWRi&6$k{EJBrYHHEqG56tCw2> z9TJz_-sl9_gs_N^&QiNL?2S`7ktW|Reeo9s13o`eaof$qS>K%TJp`4;2{Rpq4AzaF1+;3%C>IDi4iSAP?4U=y@*HHZ2KM>D+E_*09DOja13!`zma_#4xywZESv6*oY6yN)gqqyXQHBpvd5Pj!RS zqsJ4-v~7nQ7vorJ8r0en#cW^huBP+hz5vDd-$eAO``2@hwkTx9yq~vUN@4RI%B+sk zSL#Uw`KM+Nr^A%7h9>%W=g-?5S-fL~S9&RJ9TzLun$op~S7FM4@4wH`i>zIe=P7b)~WwdxEO z!kNYnVsUD@kM^O@lB)GDrZ&Zgvf&;)5q+(W!cx;t`NTT;p}{noMEf0s=$iD5MsurbJ>5+a$@ zY7(TQi=Dk~vUbI~pyRkp7~$fb_L*QJFd<&AHWa1 zn-`S;`tq<$5&Fu!PL=lS5`Jo9UtHD*ep$PRQH$H~rVTEb_mZjLQ`tHT|4P;P&! zU2nMdZu7)Juc*u}?n&l}S=HDaOS#9tMHlNk{qmQM>ARGTS7%;(CVArD?%%;Zk-Fm2 zX;IWsxmO8G^;YC2qUibzg4d`$ee{o}aUL-D0u+dYrw63>B>Q zHO~&SzUgl<=(Q7@APV0^C z6SKR*pRE=uHVNyD4}I8^OgR9B`v^pR=OU@>nv*?4k!-0g)4fQZ`HFnbq_K*jo-n1+$~KVU=E@-!})ZiJS5bAyq2 zq5`)ttax5@A|I|HnJ>7{Z3c#Q9Zpny#1Ry)KDHbzwr?rj@9%;dl(2XePCeoE>T9F9 z<}e0LIwuLtTr>Ppgb3vWsvP+oRcKBvky38>CAQ7#zIbt}*TZYQ@rY?ZanIhi#alPu zw8pb9A$4TKD%fdflM|S>q2E;~p{F=g^1aO(sl3w0%6Lq6kY)BN(y9(mX1aRX0%i#1vm3L$O?G zpL6_{iv8A!!~;8NgJ)&;FFr8|%vFe8{`T`grAwI7Vc!k{$79*QqdFwTivEFWHhRib z`>r8h-%jHNcx4K)``PW#Fzb{#@4hSVnol_4Jh`<27N%Df!J|QtOS@b=eL^-O^kbET zx{dYb7JV>F{lnS3S@C-}5}{o6;_U(B=KjoTnhZ9JW0HlD{?Kn&w4vCbDv_{v2Dokg8ff2P7;cdHLPvCre|v+7YPYb z=Aa+lb*m=k+=ejD>xaJIzHbvL7U~8)1z!xABM%U%SAyA8SgnRLlh{Z^)TRF@-v#s_ zsI(;+S3)*TFw%YR+C}`kHLyN8Xik^NcXYeVcyD@}rd_nub@?$Djh=mr)-<6fw$920 zNlKIDCg>WHXZ(D|-}nP6A4}L5FlXks*Od_~IQVzP-Q?{vOAhUbM1Hf8(dk@SRZGK9 zX$?F+Vf$F8kcMF5sK?paESc{d4VB71C-E**=N4s2MDG<17QJ{aC!_Z^jGFFY<3ln} zvk{6O?(nPa2Ce&m-Rm2n$HVy>y$Ae!v-y(M#QYo6TF`4Z@5q_f<8Uf*Gm_AGp#&RY z5Yl7*sU)V6P8s}4M?00eC+36+^wbfT`~J(-#1D&PFq7fmKL z?eCzDnBbgPmVWjXOuDH_!uo`N)O5)oUGa+4-Hd@-D@_^fswJi=BAjyho~QZ>^2T!Q zQEbuv6QpwMhgYVj9s>ijJU*W6EtCS*x!b0A0zNV;$GlEdno_7&;l z%08tu0mM~NEC$)5;oFbl+UrqC+n#cLy}F5VKmgqP(d<+akDB)`XsvwJ|2A%Q3t0O|kC9{T03bd6? zDu#z1#r@91bXy#5B0ku0X4jg?K$gTO-`=aHfn7vP$Fk=8#SO6~{jXEbtKp4Jnd`n6 z@%F4FiK+3~DwTD-KclWm6$+afJDLM3BQUGq`*Q7fXP_3IQEbyv^H}QffeWd_4c-^f zNxqD#xb#9DKqCyBEj#wUu_y%01!~$hFd?c& zn`UG5@7$WmHa6*;D#60`;%rs#gw1<&wMQVnTqYQXh>Mk|LD=zlTQZ%%q z!JZt6u^81CH|1IWsB(T5=HXUur9R9c*K<#Gvq&*QQtN}_V$?k zS5GetIcHrB z04D1R^e-W&v;zUZ;gzJQdV}{<_2YJLP410uk!p=2ZZm1{vZqv1G{&cCk=t7tEhk+8 zN_y!w7rTRLLdqd&qj`&uLdxX@JzaML>=0xTlY38&HN^zZ^J)YY@A2O$ z$sB>KPbeD-N)Ai2DEtfx;l>K>okv1KCY;&^=Rdm5SHBS&Ok$ddINc*S@u%sf{inEY zs~LDKuA^S>zy~IFAYu6R{u5_u*|mAxG$Q3G)6Y^wvx|?;B>w5O8)+!>9}>{ekh7F| zc3^jFMmgHAHcY$WOy<535)JzpNGo!wuXX&17chN#78|g>FjM%V)3S&6za+o%GZ6%* zI%^JXZ{*N;gwH9=Y~%lY@2y(AR2% zfF9@HA}22=pesLSqcpeu0$wt=;jGP4{b3hd=TWtn*#RYl@qZLT;ky)c?^~wYJ`G z&^}Tmn3OZ(R#X>WrX{e|bBueh9-CK#-siiN50`oo7aA!Lxa@=$z)OzGe2lh45iUM* zaw9%i3uES8H1l05-9(oNbIW`&IE&Ai-H+q>g*|HrHYBb4? zN|S4gS8^$tk9sNS*~^jfwvo<6)L~OB*uMY4qhDz8u6~ze`!Tyq!&wUAy;a0s z)IR?*)`?H2$Z~5fPpt<(-F%F2?nf8jynE37#dcjbQ4AMW=o3p<1FZ5l%g-|Dg-x!0 z4(47!{DhJ`E-YNAbE&7Ms?X}K>a_APslvOGHpArGkvAioh(e~l;K)Pii=dHS5x<49 z{mQK?5yxpR`y5;z19fkEp;9k!fof1eJz}Tacgfo=qa}qd*QKPC*+lE0Ilt)ABGRtU z(c1lGIp}^q^K7?1bRt~1Day;g7jZm`37RNM@4Mg;Yl43=tfx9|U7hZm$V*dV&3@Gk zEYSAj%%zZ8#0$E$zio1SQSJDHDHOMpnIP)8TpCX)^TT7yu_veVkGI1#=CCg;1?Wu) zpXmPMarzvj%~ra}WXq%3c5>V5G-r0OO?uZ%j|v|&U9@$jQG{zwM|Ll6UwGA4f52Qdtov#{sUIa@H1_z(rJD84%5br} z{Z(~pWa;t&^5UAIzGd5*>*~IRODAvI^-a(~@g>%$*5>9No72Ju45Nr`2GbIeG&+pu z#{?D;a4x@|h3}ko9C;@8%MCPVuHLOECB;I^P)oMrocH2jvou@Q$T5e@8bVsrjsBq~ zk1$7KEVPbdNg(wMupJBbdT#xUONMzaBG*m$#O#b3KP}HZY>zS+8MKlZKas`iVyL~N z&fOHfn+Lkr{9qGxs@-^^Tk4Wa<&5jel^%vaYN0i=yUsxDx^5riRjb+LX6&SasHLZf z<~%s#e)eoUZyUW=9G~csqK)N4LoHYm22KW^a2jrwM}_SmNSLn1wZe;R>+3(yNGE2SH#b{?5n2CR@j>gBlMo!M2&o;92(_pj{-s?mnQ5wQ&ByeliUGY!@5 zGW`73Brh!->P_4!9A3_f^lQN0XC&lPAlDo2M}{LKPHk@@uH}2Iz$QLCx^flQcU@s- zlqNnt=_OCOgF=g1Qi+rdi$+e!a4nfem@21?VKU{YuqTzHn%T1$0?K)Wxh~k)K{lt~ z(nq%fvs%u{vxB9&%jfQ)i8z9f8j~%a5KKzre48#jS1~;i?xBh z%OxuUKPAOzzIai95w*8@GFHfBOR_ZJxgDGxM%3Sgq5JM{o7IcwkvnaXk$HOuy9Egj z_g9l%o)o%OofyoYvb}}sD*5MjTJa2&ScR(`3}q8BapY~Ov<)sRmb_Lz_>1HK$+B!e zr2)ikU`sKvFQpW`J{GiQk$jbz#eFuFo+UU~bKF27Q|?qL461S7x^543AEZU%TJNVm| zAja~1skuBiaV1w_Jk_ji8!4OJb!&go_49-{65`w<6XRv0g`8CpeNuX5j~i+d-x1Bk zbznt;;NWIa^Ov-?xZH4tFgL^%g;=VKv7QcjX_@)%g5_!W(sASdz5Aks#k~EJ8nRUn zhnDPS+XtI{tor2TLNdc`@8Y&!akcw2xH=ytpvPp={mAUnz~^JoS3@sI+<-S%TLP+f zcjwEYKX~IM4>mcvw!iZJG>1p*>mBaZ^)&3%;Um=HU#fgH`)j&g_@%rr-hLC2K~{8Z z9FsHPU!t5F611aV&fUJ8Y`^zdn>(&&Q6s>BFSzF(u@EL4W=I zcZ=S;p2k5l*cQNP(I7QL`EIX-BdS(-k>^f49P>3e)a0VcT>fg;Yo|_wN$o!;&{W(2qyIlgZ)O48<39&z|NEkU zMe+ZEAkxtk|5Rqr+mqYfot*eYJ@+-?x7C^92VrSF{J_e{XaUn$>wlVJiM6F9tDxlB z;Rt3njoQ_P3#zc$n-+93e!mg;y5=!ZQH$BXt+qsVfK*(c^Zasd;A;S%tA{2s*xtY@ z$}*42DA?}PG0Iv_FM}fDTJx_49YWxu!?8A#j0vTyBbq=ZD?I=8O}v~sHKeFh634!VG^MR0hQ(H z4_`Lox6M1lsqFW5w@nM#MtMIhRpG?I+MQjss)YZUD9s>g0{pSSV<$~w{F1{8Q`^>< z{W7bFq^olZX9p4+g4Z~hC2UE~@Y>Cl6W>|$CfR-Xu-Wlw-kde>T?^}+mppBi{-C790qt^%xGwD^cI?2<(cE_ED66!tsNYcXp{#1 z>G>e&0&#liN*7H)-5#Q?i zd(g)^r&3f9Xqh*93aPviMlT!nm-1?ZBr+v3-&dPOoN^QV8AmFwZF<2EA@_|wd@SYf zb!qaugCoUTfm{{~lWLp0-g{Asj_!0pxSt>;zCw9|uY<}Pojs7s?Uce9hMjV(K26~% zT-M)1UPxnt(yN>YZUc$NkG2T32LIO0^NXEA3ifV{1+)7JIMQ%2wAU1Y)ko{j4_@!B zH+Hqw0Qwk+qTYCKOp>n}i-kbKmTUSemHKuBw#}L833Fb0pOnC%LA;g+O>F=cNB-kg zh`L*;{>9ZOBzZF~()LXh&bC5Wm34zmx~YJ~#`FtUH;@!&=7d*SQ$y-r-RS7^&9@V$RY@KVcO}|o>=ut%3E{G#CaNB$ z4(FMRFtV1%z^$5i??w7xh=%&u+w9xZG16v#o#vaJT%jH(9s_yUgEK8foCKAe3qj$o ziE_zGB2}V$Z}QEpyjkbVO)()tX4|=|eOD8k7%k=dt&;r(Xf-tn6itP+-ex8=)hMGh z^^k$q$8jizeHz4EZ1ULXZ!2S4eCzFi{v+Qm+gb|=%xI9I&>DhvUKMqdS6;g}ZtF1H zESk|2LDA^+^FwtaX3fnUhPD!AWUJ4in^L7rqO?55Jl~#l=&6u-Bv>%f<;>17em`e2 z;|m`}FhqN)-GO!sAl!K43@QdAuQNo0C&r0Pdj|4NB0&v)G}T|RQ{adYFu(oZXdd3s zBu+^XXnAJlns5pVg{F)8-WF*V$K{lsTs`|W1md11x(m=B$$blLi)hUU|Dax%(H$yR zAGfZT@+Y~beQW%5K~f*vLgFQsHFhZO?4g(#Nz=9Qm(a}OD8BS~GSpT5G}oRxigPe> z6%zBzCQR12C-|A&<#Xy+BNLoiH-q!$QFJ+uDS1j|m&VffP*S4$SJ(e2S0Jva%(ye9 zPP4*a^T^|fUEGEg7Rq`jC_o91mR`?QL5XgsCnrvWR3tKiWCKO9T9G96RHaHEw^k1g z{MmQUGpYeg%iC|y_MG1_o9@|z!=XvcOpS%77Hu0tL^NhLur=<|mOGn4dwr3*y=>B+ zeK#M{GHJY7C=H72-S=-#Z46pmlZzyq58qs`?K3DFG|5}*2{s87PJLWf5Ifs8 z=Yve(ug;u)q~BMYMZ+BM%=znmA^dv0mym!6VD4J+;9qMZK{`&N+d^;w@gB8v0k z_~Jz`zmG>;{Z{OACRgkA#M>kfPwB!MrE%wziVy0#hOb@?w~(}(Pkw2rA%=*aj@!*p zfZ|W5m8})qko+nLO}9d}{gpvVe|gq%mlG_1_L<>@r4`F6U?Gs%CWoF?m*H-P9Arn? zR$XL-67ssJW|5_RIYBpj`}y;^?AZ7(slDSwgr0C~zZr+S%#q%5-^8Db(8D3(I}2da zgT7AymMHV7b&UuQmy?r=%P;*N`xnpy0@R*2_+8I$!MC{CBT|>$2O|#xo7sn-hI3O_rhocC>cwxwuXOi~7i&pKGH=gCX9VO#NM<<)qz&Y%o- z)_+Fo^8haJn5(uqgh9b~#@D=&VY)t>ci^Nn@%0jmtU05fv(5mJOBGu3D%Oilp~dOW zX0;!Wp50qk7&fK7wJgxS8DemyBez5L(&7#N<^6IP-(rFK73$-Do@}I*&+IqYBgG}( z)5Jq}FC}*`0#{Bq$rniBrIyT>H?^h^H?ztn^7h5*ruHuZ&_y1RJu!1Pl$(Roj>Gs{ z9@WR`^|rqD@O8JDX)hsRE7m!ulOh93Z;l*#&zpcu(lL`u@O_KAzb)Hgl(hd+nflee z!+5#64=pzp8AR=AGzhW&#nnSfTKWeYK>-SqNuGO1HddLc8vF9hv|KNT$7arbZ)HSf z`(EVEFXgIh*A}D6JF3d|y;O0bR+|1K~Y>(E?-iI3-96Wu;rbjKBu-xyTx+~%Pv z@hDaz`7rPP{ly*Ly_aI$&}L7G;mOsj(n$5h7E0ry(*rH*Fhgz#fZcmMzkIeZ>Iyj!LZcgUjc$(@S z-j%%UjE|3RU$Fd2ebcAJX7`Lj+z4ro^wJr88RC1p`0{PAgY>p?{uF+u^~dj4;y}f2 zx$)&8^iWg!{F)Y1-u0nm)`YJ}K@TUK>gp_801ns0O%QD3%slUHJxDUXQ2sYQ(CBx&>O#sSqH4btq2dIW*5>8oT`FfyjFju*zS=1eG>EDX&*+z3K&fwL)~6HaBI!S zH&=8KQP;U+Pa=0}EA&kNuU^fpxh*$~IhVMm;3Qn?*a45u_!kKdL1SE8srHidBz8PN7hO#iTZZZ;H$e0*2Ep|kS4iDZ5dd-^UZ^jDq7`jDm7FKSb zF+rk5gu8=G1%fSI=pjS~!tuKKcr&HdkGm>adpIKM2DRyW48A`R4tA@+4HIlsGfThmE8j4NB}Bly)Vb8rV*3MPGgsa%6f(pygULqGoUU$Ce{eOn&BAO-h^fG>u8#q zKFlpBWo^vxF@`KDELAu&)SCx87BxQui)>t``TH3h*IH=(=!MwU{e!fl%lsQ&Y<2); zD>Kt{v2AI_p%j>T3wrZac-=1eGv@1T^C;A&k*5e9uM^H8myrgBkZUL|8;eLa81sqo zi>TSVikt-P@p_zH9vg|c>L6+#)80d$1dANoH26L8vz$P-%rP zKT=*nA^hvt@9${274!4*)CdT1aX;AGE04SQ_y|L)o;`a8w32aU!)l>gEzTcZGC)SE z$;&tepy@1>gq;F}taX0f4f2grw>uvt-QeDH<*uhBk(oM;pIB;cQ|afvCki|o`MQFtO2wvNR`6;{)xTGkhcOc@!bY5;QB(;X@o;X?rs{oqQEK= z2Kj7U`Qay8_NQ!_w#zs&hDca1cIsd0qE$YHb)gkqB7-5Q&U3WQtQoSfzGA(dMd7b zcE?jXTslL8Gpxdi34nd)to=ely^))pUV`EhfzqT0|4Ci0tEH0^|+9YYXYzINg zAZ6e%roBPr?c@5v@neQE{Sj&7)@|=MgG=36oqbHxO-~%|;KLbsrsVv(@Tb`N{M%2? z)BY4(w<+cE^qMINNN#J>@U(;4-MF=N&_%N0Szo9)jJSfihO*LOZlFy}KN9H(zds@L zjUbbO5j#1YE)!`-JVnmWE$N4qz4HBTEwJBzYxD}XTS)TX`7Lxs8(;fwx~af3_^bQi zo*3nK!i>g=t?8dCni#eHhn5sg=5#~+Npp>k_BQ9wMU@C7$Ac;kxTplmRVsutNkJt_ zq(MIXje!DPMCtQ2TN=L2qDUI3Ueag)hgoa>(`I4w@msOGt)y0kQpV7%mtia7A`-L& zRNbZN7Mb)4YnfpbAbjm1jYO5$l%+FI|Xj1h)llqPq7-Q)Ck%v?>HWj`~1mp?B}z$;mx|oelDW39cM}>0M|RL1TM& zv!{)%!U{=isng?O8c>%467biSJiD1j7vn3pUYFIyeWu;U*9Ls%+1f%$dC*5}I$YUN z^{R?l9*kDA)Uy%V#N*I(%w&3+zO?-t(4| z^`h!~#r5d63nsH-`&-X2D>TaoTcZ8i(>O;pi`PM!&+MhxN2lUP^i}PQ)~nyLiPeA4 zrt_KuM!wu+$G~`YdcJw$H}yw)+F%_NqA)ArDx7nd+ce19_bx$EyZ9!N5u}{D#=-&~ zijub^B`%I(w*2y0B}GuM$fveuO?ELzrq_!bq+|FKPhx*Zj?pu>W(%HD1LO>c%Ch7L zXS%q!TDGa_-tgb&O34I2moua)-GV~^^sB6(s3>OOh|t9r&d{ zug*xo3hC|Zo1UJAe0-fN^t7ZHrG1s`@8)&et4vC97KJAf3SqWkRTv5U4M(kj`j!^B zy@$mQzgf5K&ASLQMdu=KRuegCoi4pecYpkc3!tp-L+Yg3cDEMgtd(Ze({1i(! ziJL1~h@SF#E8Pz*%>nl|I(*F1eqgPDFF|lsr;!glV|Lmg2TzQ)^gDCa>2htK4JxS+K&g~hOc$JlLJueH3}ICV&{RHcfIt^~AaAAw zZi{^3>0?TIo;oEr3Pm&y;ARCD5zO_V8=tw(ybe#Zh~kl3huXH_FUT4ogmOe*T*0&%uP zKa}k(Y;&>Y!pgqq;2RU)9w(1f26fQuBVPWylPipNkPLQY#q%xDTrG>iq=^lPF9^^) z>^W2Nu%0|oP%t#5H5L~a*URp$C;1ni4B*Xjp)vExwybvU+ui6Af&r{K`peAq{HJ^C zx%0QPIYrJWBl0yr;WJp~Qk$f$uCPe8&oL5gzAe=*b#rT!;UmdwFA6Xhx}Q+w4wzQ;iH&>*t zH!#REhCavq+s)05TXDJ3Ip64C$mluJv+q9*2DI97gxg=OXVUn8*}S|;1|HWnH#u&^m}Nc#XZY4 z-P$q>b_0rEPp=qb|Gw{NJToUh!KFr=b^mJF#HBC&V$Io)jTN z*BAw0Ofn6xB4 zeZyDlmV~a~qH|-FJ<2QBZ$0+zaLUWqtWS>@Sqgalzi4i50(nssM=Ivl$^+58epmBX z$S)Kb?PR!kpxOJ_h{<+m!@gW{p!f;b82dr9erMFW8FAY|T zqw_HNH2u0xqTX&XMlRP@SDHGSUr2~WljUFU!{!@VLFoL6!PoD&S&YKS&Hml_s!OEb zN2hL%u6kDM4LO{==>`lbg^51sBcP&{o;=0WUXE~mHv4g1K!eLvDbcBx@g#Zj=QSD0 zKN&eR6z^Rkss=-?B5{LHHXT{=*hE~onkyuzCU~7AUcPo+nt6jBjGNWqypqq;{5*EN z`V6wqh>gCzDqjyf*QBb#a7@**opHat?PAvLn&EUg%{qQG&lxnaB-CWobBwWWPc&6b z+!!RKn8I0^T058hdG>QPsh@u1*+ImeAP+u~`9-I>Inn0-EdZ--t?NB!(NpuuNv2~Z z@Nb}nh>|%?g4^2KDl6eEC8G`_V2)veP*FEw{gOE&KDd;r&Dn211MVs{nJM{I z06=x(-WkTJ=9%rT<{P3ofK+*~n8L=+MLik4*HUh0{bCr;KT@OX5!oB$6{$$-Wl)78 zgv_5IPZg;0v&>D8a}2eU3}h|GgZjr%cNRN1-_+!~CVq~-ONsXJIE*_R0HMwx@AMw5 zvf%qAaacMB>;JIc-(!whYWHdmr%@gs-ph&4?zMeajGyf3;XS=9FNoYOi3_;{XLyl{ zaM{Hb50SO7!9N=yHtrK{uZ4kmWTo+c1&cYYgrbPIFJQ6xHA;vZc~K`KUSajaij@O_ zV8GWbJq6?gA)+@F@T+s!z>Iy1mn*l^V!Q7hMhZU34M|}A_KKEXRUY+zZeAjHl^`hd zI*SG}r5~r35a_>DRgwLm>jhc5nAKccP=;3bx&EwVOtb&Rr9xweS|4qZ*KxAHrr$wlT8A0TdNk2bLvHeQgby?&y!^4h~o87n@&IChSu@^QTI zy66rP6X>scoVnJ8d~|c$0?iPdt%ua=H4#AA$1m~IBp%O2=OLp@{pF?rHyZRlAW3auu8vx;xb4QVG1LEh zYe4^Tt|-UTy}4S1w?T9JbglPxzmoXW-gOHgvK7}KmVz7{6~J7Sm)8p@7(G2bU0srl z1Y28Mr^3g8>1=OrZ)vFm*v=Xn8{6A`jI%B~FO`2YZ3aIjf^~0y&P4oGXPDy7E>fKX z<_J#DkGRCg9UPcfM|L#si$xLpLZ{_onH3>hVDCtyG4IQ65$)-mgY;Q0w=b=^^m+-} z#CKh*(yJn($t1p>j%$}uUSQ{%sW`KoZOyYe@o{sP$y`j%9ZvJXww&C=$f09z$JW4j zUXYjAx}`vP*N%YXGo^dPT2XYSzutVR^=NG_j?*i1aviaufX@71u1J(f*Iuq;+=wpu`KPHfNBuk8r#(VxeCMKrytfeY~LHb`7k#O{}wC^NTVr*ah@bt0>G3xVHmwv!8H_!Yl{_GcpsuFP9dV8yPSf}&yk zsl(SO-^q*+*m6sHnqr~sQ*z(*u2yVIAh&vruZlM67UTXIgcJ+^rAAuOZ$XAF(FdF|1(o!=O-YZ zKU?{Sy@^6QJG*kXGp__w`=ypfuZumtkD%Air{}IUm{b!c*ijiRneg>3Wf11sZ?yLJ zg)ONTDQt|49<{E3jz34j(>D2MqWpbwC7s81t|nOMHP7QU{z&htpe)8uZ$6ltza2kM z7jh?g!Aua!V?EU(Aasfn0TcYswN`7D8~W}~y(-k0yUQ&pVU_qAA5Tct-QRC9TV+~T zSLf8|^B>Jwr|>^&z^{9DG?&zNa{>JsK7M<9i?}*gCz$-$7}Ih0ho14P$QxdpnGVM^ zK#dq)?DP0PcoP2vedn{181pCoS1)>|C!VR_|(kkWfi5lew}1|7>UTx ze(*n0LH^sD4gDFX_JJ4svJM<6z*L2{LXL;~W^Zq)faH%V7Eu4GmIr?F=6CfiG>m(S z#du}Szg_shZutKvMdUwPbBEri*sURf?@bK#RRNlC2RDPoF3vZErN3lVj`45nw6IER zs#;=4xm{FLZPPY*_7MBW7j4sdUEk z`k{n1P^X81!=ecA7me{-69!z!AF;1;Q0whUSkm+<-rHf?XkDglyFj*eVozVuw6+Dh z{RuC(*Z)mN$RWb*F@*h)=mhSGe5u1I;>+M`yj z1v(PN_}h=Kdl0(m=GYGAhH#|5l^M`7Kw!nA6h?oE& zXtWkK{<2@sg+|wr2sXG~t%)ckB9v3)sZ8agADplw7T;8b(rz0?BXhmX^T*u(?0guxm-|@#F zJ{ev=z4E<*_fE+5w#;v*!VE&iDBX{A?;`2Y$)#PBy+u>&;cN($_Gy<-BuBy$Sg`!C zUoWBVa@`)s6vDIcwnW(B$m(u4sQVhf-3PZsK6DtxZR_YWN$T`Ukj`yK*x1LPGH{wy z!Z{aX+7lxTSEdYkk!y(@5WQG`t27r9J&H9NI&x>ZR7qClz!q-UFFTo6Y+*J`u5 z!Vq;+5;dtK5<$2nYyxZqy@}bysnN-)0rNRdVyd8i=k+On-WuzI)MoeQXMv*nWWL#_ zqh3C;Yr!Z5;u(>#dC2%r=yLl!LoHe?njuQ;H#@kw%@%KMdUSou-{`@Q zaNQGeHC1qvS8p7XVadZ5CZadGUq)biJCFCNc5R`ewU7$UCu20r;d>XWTciykfLh$q z{P16UbA2Y<+1&X(^$8irh;%I+D_pu={3mrNiwIF7D?`vpd_9ap{vD)fs>027d%Z-@ zAGKEZkaB1NuAn3fSU0pau~A=*$6+8=E05A6-A+wnyat(|omvMb{#TBepSM3}ltQtD$+dF>-< zOlYDA>kmbs`Gpk=TlV|3lrx$HHJ5G?^J>(;i5-LK#>O9xch`;RSY^X*T=%*&o?EBd z1^+UHd=eLz4%Ym0&rV#En-fY!z)=c-oK_Y?ZhrH>R>=D{x3_?3%&J;Go7ZvyV>Q)$ zxyp<2TcRav--ffl{4f0b-~lA#Vug~Ql{w5%h+JFnV})pLL$pU_jER0{m!dAo*Nz!% zo%1t~v$yP;ePssE)jRgxZ;a-vKE04A6LuW!$GLB~pI=;jiV z8x0Q*eN)oOfl@lol-j${o<=rId#%2CtLc*rjL;?1e>KYy>Q9;;N#v3o%Jh7C0qqq)5YZidcT4c1?%3TCFmAlk(fN%eKo1KHBs@3V7W&}R&{ z86#ulcg`6w$l6(E_U#3qP-8^wQ+#jvaC(mXGHprhJ^V>PonD07SZeuaHl^21v2K8Xm_QeX{R^o$VVXLaTimD_Hk(JFQx8BO04zY z59K6Y@h8W5N^j*6g5L=iKbfWB7`Y&cg2=P6vBiwAnkEznG7l76FAE9Nokh}F56>!< z8+dn?g@qsMeo4}pIdwYIq(We5UE0M9TTgI2fr1lL-N#2%;m=OSWhE5l1-v&~E^9Wp zo9REc2VmLrGndEYE#|$R;hOS4eIJ!j$@xmK&UU#Bk}6cHIrtGZ^Ma6wD4UjBKKUZ! z*<;iCBr5i&5km$yJ?45&<(yi!KbP1IJ?-c0^w#fjgl|4vot){l`5vZBzV`VF29bHai>x@vETFYddc+V5EYk zJ{*vE_lO~o^>JxK#k=Oo*Y9G~?p<0Bjo<2CEVN8hpnMjY5~s33Ui<5)7KBsA$zZ3U z^}U$EKGy0=dnS8sn^uNncIr%>&lz;kS^HztWvslPuIF(6_BcL8bF;~|9s2$8mwVNO zt}v~PDl9tcvTx;HR@-1)e!!=G08-6TDYZ3`CLfOAJb(rqiqzK zJ?|+u)dh<3zYPge2*Z__IeiB8-28^)p7N#gz92-fY0>9ZsNoBVYV&kRBW}DvA2z9m zvbhNN#UwGtVepKoh_$9=zk-26UtL?3i0kMT7VP~OT&w$Gdw;o#C;7eZ+*d80F!4sS zWjqn&M?|s!yRxoN(|Kq8RQScQLXz7My?zP~u?7gpt|<9ls)E%9L#tfOynXWZ%T{0g zk4Ex>4mm#PMMXuH3`+4B^?#i@y+3Z# zjrFsfoEQocu`B}FAOyXNC#@=V5B)|t{ixTw3ndhHvh`}=)s(HcaS2C@o&^*6o(PLp zEf4s{G6VghG7V|*dy*CXRFb+)0wS9O*#R5{bul;PjXIyEadUTE^YoAvP8 z-pIU)_Y+)S4GvYIJ|ZVXA)V!`oapM6(7hgMR^oh?Z+;8jZ@T%SSq$1CMA_!l3v7B= zwOQHlU*0^Rs>0>-t!&x=QXtodka^wZub+W-8gH7-6gw9N$1V?r(~PDzI?C`tlmuE} z5c8-zjs^E25=eS*G`|}%=O;Hhi?2nMR;2bQ(|V$@bvo@!e!F+Y)n!v#UHC!Lw{}H~ zv3^M<5#)L-x^nH;^RakZiq`iN)2~7J-ZPCF-5g{Rg zbdaqRBu=orNOdq(Vd(BGDBnB2G%sk1z+UuU^I z5N%k=v~bKCXk+2sHi zFNIK**fbYyul#Fo7ysS;LuR8AnxnT_hWi4*$qSk4T>#m|^ zIaiSxeynFjJ6BANUJbILJE!OPBLcU3k z5+<|?Ik_0Umw5l8OZpyw~_{>&kyRD4$Mf@V7N6-C{oJ0Y!GswqI*ROAy zL2P7VbI`*;QSbUZww*-A?|jL?uchNLno4@ILZ?j|zm7hu0^iNqZsCLrh~1qkNF(#k zdGl$aUz^uyUPH_;+te;P;bLDM=Pqf}R9@7BTOOPIbOPympS*9?^U~>D{SIUetR-xgpWa-?YPS7Qw3sP)ORjW|u^5%|$pVOPAYf4PMvuqfCa2cY zi~flBii#Lm0XE|ghOt2HeLGuo#_){6nxa-v^!&P5xy}`}z4l6S5z!9974g0;ktTJ! zZ#(c%N?(Ol2i{A>_~)+%PLtoTYje@__75=5+Gvqrpsggi2>K@L2z__B z>^S18Vui#|%T}?P(Eyxg#SdjCQ&U1_|=YN7bwM+jv0FIlpypSR&Ho=gFQ?KobN za9g|qh#O>SqZZsG5j<8C@0663l$CLwK6MG2792S6_;urF#VuTgP&3HzFz?H!Mn*=z z0#M=su>I*HNjJATs2K}odozi4!k^a+Eb`e|vf(8nBEkoHy+dt9MFsGv)wB9H;I2Jg z^*>D#S#Xz$I8Cdms2J(%3*mi$wvyuG2eq4yb>3SEELOII z@e+|Ptflt(I=&4MivW6T5zX!I-c}!0R5V#CcQmY&X%t<46;2B3I-S2IKh7&SFatd4 zul_uqe@a5Vu`$XGDs;E}NS>!SS(L*{&IcKiDkVBU(?@I5qvZw=p$fnrzn%A!>Bx!m zkhh9UU;|^#FD1^eJ^!94nr>0g!0YnnJ=d^<(R0gB72=y?h5P4K=cd$}uZ|slbE>Yq z;&D$byPIr+Z))MdAkABk&R-eoAs4@Pv{&E+@f@GiSdWF{LBWsn^8rsv0r+?Dk;-BG zCK$q0Cu>-nDs|hRa97O`Ws~ge0O`C*6}`Pl7ec>(^)useg}uDJwIXMeg(ld5h9O@6 zFKTLlDKhFBI7Re%O&AH~9_jTX*bzjqQ zNlBX~U2^AaJ5x-b%5qR6RnF5VuC70iMZz62(Oy0CdlKe%WlLIfPg7!7zXo7sLq04th>h)s~gzZ-I$hv%fHt3zXaW&4tVDLdCqQ^|p zdZx3{v`RBS%@=35n)BC38J2#HkGNrLM^VEnV3OA||N9O4@x`GoJ~NTLW!EGtGAF!Y zZ6a30O07rBaym7;_mF8{bp|@xViX+{#rWx~X;;;`j>DpBB=nEHhD9{DV) z0_Xo7^#8qhEIMkRdt^Ws4|vjGqC@#WIj z=v407EeWT|)v_FsemNt-q>k?5%on39nXexRA;8|Bktv0t!iyH=#I zPkU|1FnHyrR=e6Qu7HMsbgI5Vk&&=%>=q=8XUTo{N0Shbj3%SkQ?1yUqbuA(?nJnI zlcX;pr$k3Rfzy~uT{JOHKJY9!2M8ljV4RawJ_f$tR}NsnoE#H3%6EB-uQJB3bn#+NF$XzXm9^xNX1Ct(W`tjV!y_``ejIiGI zlhi}kU>nj@c($gzPO$H-;;?aAUCyA9PVoXkLvo?>WMf05i#&d2qB>qxB-kE4(_3w< zGO5Y-MDK3-G*%xXnkXx?VT0sH`1!A=Mea1i{C1Gob^%1|rj>Nf-WRMRULJtcQ}I=8 z)s%FcDHdj^QtcOAYuBrhpz&PV}w`erFd{(^+&5xcdlME69}W97(r zh6gie>y~xLWO@(PU;x2#49aQ&zI8p=f4N~GgQsNb3@2-DH(rVrV9Mv7=4VQN-$>WA z)Eg!qBv)TRdM7QOsV1s<)6gPN4QGCr856kmO^Aj2aN|f_FBQ()^DD;RvLg|(<+DzI zI`=UV(@B&z5iuH2r3nt+E4Wm=KM?d?DWOw5k?eSt`&z%7`QTu8dJ|0V!__zfq9%0v zyG4sU!hfP5uQ4RBG9;cpo}SCuV*7BhHJ#v0LO8C);p7}rVrZ=H-8=+DvORum$)^L& zqo_|`p4%g&VU*O57uRY@pYAeQ)J|VxIsKKOXeF|otlIxa_UX1f7GKWfg3k8;x4LLO zDMGRoRigg^5IptzA-e-{O*>ui6CiT2{YuUNO%>#DqQr(1-TA5Vxx3ssc(3lh zigHy9^p6D#)k^-^3_0Uvx}i~~a1X9woTOOCJ`bwV<3 z2{gjcx4zc*GZI_sRj9YgXXUksKJ$tdqx_EBdR>H}36x~YV<{QxoaD!2F1MTfLfbll z0lCgFehT%p&Sty)Q_&t6dDlhGReJ?P*AB(h5onuZ`_gyx_hicd{e<T~STe^^}V zDAs4{aI zsu~p?y<`L+z(DX0nvI#+QS1)Q<@Xu|37i{)Wy3Z%?C>4ki&mD8=KMeE7HuFca0*mK z3dd_b%w@b|rMKe$Hf2w14Qfu(1{jDZV^+%5PG?Vd6^Hoz>fGv8+***HGLVq}I0%w-6?<}a^TZ0Pg?h`?{#M0djzwM*ssooU! zmbHbz6I0{&%X5w1C5~%lv8L657&Z?MjGidm?THm(bA)gF1(l`DJ}Qjv)?Lj(QD+T{ zoLIy)7SSrj56`G_?A)^Vu(Pg_n$T|Hn)O-dPI=N4Lw|KhVDN#@Qav;KELP-d(0;BY z<|M$~1YFraf8DHQ?#(bAA;#-V*pL@Il(;uIqY0Hw(S2Rl%Y9yG08y6r(u}dUZF5pT z^4FS(13)CZOvox_Gh}jxf`an-Z#O`)z|PgEs`*!o^5Ctz+8lGaA;)fn5!v>w+6U+! zY0klMbJKeFnMO}%yX+8g8=wpmXpOWL5mD`2qD{rYnGou;g>-!fiLrH@VH!vyA3o=r zhf(Gc6#8eYo#y1eV~0qTCmGv+y+Dd*m6PsmL>Kaqq!Jsc5;tu~ivD+AZ#! zThQ5NvWI8&+jh!--F7)Hs1r6m8B<%!Y6&dxn-Bk{N8N(&Ug$Rqr>`<(lROj$Jlx-d z?T7l5uAvCVo@Nu%kn@a}gH2{4`>dnWc81*v-MwVcc{6F&#K(E5*0MjMf9-^pDluGZ z&=u|foMv<4dk!Fy7ItHt&^pXyhnv=l3Gf*|WaGP^cfUvZ-}+8|tX}S2J#P;7DbH=s z!R=i&0_UulzC@|bUJRVAw(f(_&OSe7-FndA+*uX3>be zm6)bDF)2(AmVo@8(_lx%PkJbi^!3NWUxc&NWv$yb3@=tnH zv-7&Ue7R}%ll8NTWnX88mGxnB0kly$RQ^HeCY@S{JQQkbBk5n?W80>$C&WD#b;Ix# zDFmI|Ztr!aG%6#bwZv({Sm&YiR`{5eZ42NNQ~!2?>np1uFhE(gSo%EtEnbAQu)FD7 z1c{8`gI7KJ4u<1~t6R*g7k|A0XBhCXSY_i_&7d_-QZV`8u%Y5&@lg#CjugmXyS=`RMjVhCt3U%qOx;qG=?KBrq)53s-O4f7`tm?;BMrNXGGtu z5dpW(q|0efEJL>d^v-DuL?HpJq}`GTb^=e`KOFw< zdJAU^cXM5=_~76d8k!L3fQCV zxdySV;Z~5^OvPcvi`%BxLbuBT55qtc(DASvBhG!lB#`!o!m7OWU?WGT@dD+-dk4dQ zhfxxGAp*xf%<_A!5@jk^Tl~-a;ppS}maPHG31_X|YJN6Fb)hbeKtZI^Nl#G%CJ>J$ z76_^k3THS+)>i$3Re%0C56TTPYF%YMC;M!^%i{5lnpy&|D`!%&@!0^@F(r;OWaED_ z&R5)anm_j2RT~M~RZH>l@d*omx(LDi?*{mrylio8H)r2!bV5f*n@!{1jRGAMqP>OhyU-Mdc{MR?%ruWisLwCOY!|-KvjTzdcI6Q7ml*AA zoZ!f0JH?w8n%}{o*|6l6V1>9c2*@P8$bz8TnoPXx8y)I$m=>zYqk(0oz7;Le6yP^; zR4J+fn);W?LZb?#@h}*?Fop#y9zODREShpT7Okx;a7jJfSnJgshTo#ldhr`KcrZ9w z7(j~|$!DmN=GETN%B)m*h-+OjW2T75EHbXl(@)G>U(;9%c&{QM@UJhW#D8!X624 z+$madKXY7AQP-|H#UB?h)x|lRbtU6AH)2^5L}*lN+ss+~QXY%rMmu+scb;&)DDas5 z14wU5|BZ4f-1;m&gLwb)#j@4g4rc~XBI2Bv_09-I7&}*?(g~7ArGG#O(wO103gE>5 zA6wu8!LW+#OgZrOAT;j$D8wBEus#G$G0YDPD6(w%+Z%GL0j!_P?}}qPhTf&t=-tF zdch8i&2yn^KXvr>OhYZ4bF|Nwy;;K7S=Aut1-MijZktWnbr`n?%yTBKXHuZErn8}s zqkZ`A@y@Xl);ukf5BHjbEM`zE&1p;41OC{~e5(0JUkg*p`rTV}lNv&~syoHxwps(6I_X6#G!R<1-a((mHT1N9u8x65m<( z4rSk|N?>6kADkThEXZ#Pv9GvSQzc5O!wwct`2Cu*R4V0lo!EaNYN3`QPjd19$w-Xv zoOJU$DmSZfL=}Aot8_jAi9YjP~%8m-YGbM*^9cYjGo0V8^c9q03^xx~#f&W5tfruGMfoV$YNM@LHTujiLZGU9j5F%W)pCF9&;J2>g1n96 zyBIzOSYB!sXKSX@TZ2wQ6D4ratNnFr%Y_!x|k<^VW5t40AcX z!t_dKzbGo)q-fnexv@y!o#B@$a1zma9A;P)CRVss1Xb&;?{sf0EHiI=u^Az*T&h4N zTuF2d59fj(e&SNs7hn4zof8Gd-b#XON>ar~i;fJ!SgR@C zn08^tE*dxAIU1jP&(i0iib8$ex}W`e`tel*C4`XL9`;Gu`#8>R?gZ_`Bm4 zdaG$7zK&HcLDxZlcJ`W`K0<@F@OBoV&8b1myJ;YE^Y{|J0KlfirHzIc;P;U*Gj6jE zbwwp>R2i0RSZ35LUT)nKf*EK(G$GbeH8!yn8hFTo5}F`Q!r(_qW=&6MOj?iMIfuu_ zR}UNCFo0oA#;CWZn_D$OE@qJV*30Vq66I6Y)C~n);a|*N7;i=}>xLS)ocq{4OWR&t zaH))r$aQ*HB;>tT;vQmFOI&Vfx-m9|(w}@joggJY7M*acl32E81GScg0U5XsIBEKXHa01q(V_%uXrSbl-xzW`8$m0b3fD zn0gU?Qfly0Kv1!8(!>)T5WS4s5XO}JGK_cj*_?8jrerbPW?e~WI(>6ToL{c^G#M4R z9BGI-sTBhcDKeywAB>u6h@9E)3M(K}wZ9~&R(&fn$S`@x6=qj$BF@3d5vBvP&(3cNt~yFFwnV75QTo(B;${RGICeRh zvwF*8Xsd5V0}x<#?&)Eqbg}RgD~B;H-V#;nhGDa?$SLFo-2KG~H3YS1?v(7VDezMD z6_Yrn+~fTLUcg}a$*&6+rzdc;%UkZPKML-f>pxJSDzz()6Z9(9xos$EsGXW*o=Z*l z@%?QI5AS~JXZ8)_6RtC{_VA`5aJzrz#}yN&t@x?TlZdutvVXt`H%6Y{>2 zO|dKlo1lZhwELBYuD~Us{M!teUxI>8Tjo`5MYZ&^o09QeomOlI(IvD!)8)t z_&XJnp4`j(vje&U3SPK{`GMhurKudJKhSrKRV|gienMbrA=N9{xl}}J2uNp^MinnZ z8-L|Lbc>ce-#wH3tMt#&5|l|oB&E5O5kCb*3RL5N9 zWzX~eThh%G2ZdCVPgw`4Pka{u7r{P7MT478B)d`l9;A5pvbpYTDxBvrgn*jHATfHy z%x2A(oFtu1F`!!t`x}o!tOg8G$s(s#x^LCN;JsbivWc(B1)PA`S70o2fK5+RQuK{L zKiJyJ+{W97LFah}GrrUYVzb<+1NcJQv1Y zcsO7pitT9sQoHoO1VT%S4txXW#$Kb>BL7?@)p#`fFB$J3KMtC|Auu~nsdmpkA9`OCzxpm&=b0uH9Xc_IceN|Dd#iDU{E33F$ zKGzznpnp3c=^@4ZNx`R)N4=N^S?vBook*78d5Qka^%C{}Z~^EQY}OdVBL7vD0L-_n z+QCgmwVR8}QH=NGpa!kjX35Gams`;tE)qOkTtH2eJ}N4m4nkh9X31zG^fMgf0~|<8 zgx}7mm>3{2e);{g0+h-~_FdG^{}qvJ?9RLV-#bKKPabAZR_(<&{gQ9;S#8QS7^HJl z0JM&N(EJnntF@LVk+wzh*@V2Q-6l6S{Ma`ayDqH(cJKCYpF>YZ_2*~4s}VzhSezbn zs4ftSyx9I{H)=^%S>k%*hG0_Ms3 z;fA1>-G4NL$addmX4kT_L)t>7L{xwN(nR0BorpL@(S=TDr0F`;IYd=Q8*2H_-O36g zID>li8?_?-rJc8hDwmt;F}(!{35{rqJRxi`^93GFxD$&x^b_kGx+voyO>&_6+Q0+Y z8bJD+DC-b+4ooPfiPP#SXBxzsiiJ*{gnbhYu&wHr5o>fzk%I(^?T%9mK^A^KM17hm zv#3SxC19QydgF9O1bcKu&N*afVP9lH+T7c>6)Fbs<9+ zJJ4NcfDgwdDw+^>lVZL?OkFgi|NHmvXJ==$j7j^mWFdBbrvcm`hHOc!-Pg)OW@>x^ z7{4k-M+!wQ_A28n=CAUvo||P7s_0FO4ODxknyjMDdD|>ZQUk0o`PuSNU@cuvwH1R> zW|;i(;9!?pIp~47D`BzpuXK8o;j)fioo#rn6EJTsPd24MAw&dYkfse|(p1UnJMXR# z&{+qJfm=wWl8U37NkHHutN;j5>Rik3Y0QWjkXCVcA%hHA!h-7VYKRExhBw#DZ&%I* zjp*+O0nIwqy>jIW72u-}iIsl+`Xi_K)m?(}Cx)#w{RDYxvWLP1kfpLSU+OA2+pVW> z3kmE*iI6kzZC-<%x1TXq7Xn#jj%5?#s}mcoLa&VzVV`QfHp*sv_gDI$XZzFT=yMlm zTz758C)${pqFJYnkH!fF>bex-NXVR{LuaehTJ3~VtF!g)ry;Do4h%TR-n?ZJI;H?j;pvc zgA~IKC!g$xV!SB}JW|BAx|6mvrq1X8aPac}5~k%Hf5_hbz&T3owa-!``bH%4#bD#};lAJ= z4Srny2G_v6Ualr@Nt@nSJ0+4Xb-?Q@$jc>bK}42d`<7O}LzYL&cu2~%^&+V8;X+ld{r>sT08Cg zY4j@o%yvt3bjCnjTtK|Pgt|K~#Kje|0*`lYujkF8cREa~);}h_!~XDVC#6h4Ja)S5 zxu(Ola!GM6Ak@DHb~5nO*49c*4{T2wzv02OkAOx1+iB@9m2)aOF;snY|->bkIzQOY#!ye{)3y7%D*5@g*HCNmPA@;tJ>-4d07&^{9`^G@kK&G9Z#-MZ` zU}OSW?qw98hnX4_r4qa$``#1Y-e&YnkQYq#HHgx4t}WZxJIoe6;_XYHDh20vm5 zhj7Gfo%NOkw946}yR4f?mu4pAH=UmpNqW(F+f8}Q<~;qa#z@g3UipTG3ClzS{f^U% z$Q$u43cuu9#f$d?1Gxf+cno7S`GM#t-`8iv9GXTtKBXZWRcJSq18P_x4mbQ9|ED|tFQkjLiRELIJWBBu$N7L z{wu_{`lEl1U!yH%X4MmYJk-Uh;xw@8OW6l6a_Z#Hl0NBegVuTUq(sx&-AwHd;6vDx z*GM$S>EGzEvkw>JxN(Q0VM0OigBe(bmw|Nbemp8vV`e&Ssail69uX_J20|8ED-%}l zwwy}q_nYI09x`y&x^*pEsCSrC#lXhWP+vX429qyz zZ{vbu^d^+4^k5WFE2i<^7zT%!mHbJeMKhdzF!=e2DIMGOHL!pp*r|BT%uGk;xO_Bl z>}95^M|lq0sck>BZgrx6!r!r(btvFKR(pWy#&B*H3wX^rUbkJMR0PRwam5A2ZNlT& z5j24-VYSgijjB1qjl{c!>?TRMt+jqov%te@y)}K9NwiiTQ)qoh@aQ)GVSN&@ zQbq!I4&V&|6^WWQh$xygm`Sc%zuT@vi@TkPScyCjZkAGJRMpD64H8;fA2zGC8pu%f%0HaEgY`~aEMpcu{Cy3j!Iopgc(tg(3 zFFc9WsClKte7)_C_<^1#Rz9&DC_6L=_6NmpGNX8(YgbDQ8mDc}1}}SC@8Cwutg|D# zFs7O^4Nj2U+WqAI7L{P4Fq@3e##Dj&?F-6R%RiOLdTBR?ESKjytuWi{61L{jg{kR~ zqg|t^i7Alh31GQsD_Dhr`K`|ll?bXQ*D zDanrzx|80+RE2o1M5+2$K|EeB{SI&)rk%fQgWs6`n^e=_{tG&hKakcSRrqWLZlZ0O zlW$iR?6=+1i>nWgQq!{>QLc%>o5L;F(5GN5ffCFnh{p~+Q4pz_ZL?Arm_C`xGZis^ zNzKnc>;S0fo>~0kJgV937_>)lzB&O9h;X?j1>{F(L~H#i^C_evYv+6Q9f&NDdqo0C zv$wF6bEG8vqh;4KH7%k&`IXr23g&fvQ8Y zu`JsNa*`Qh1baXpQ{&}uoe3AoKzJVOtSQ!g_`~%*BS)A`F#}t^Ok$=T9nPqWw^A42 zG25*TWWfMQ7xA)x14#T|moJY{0hd0%R-k6Bvw+14)l0Oy>O&Ju1aAS{StBEVSah#k z>1E;!5Qk7qjtYXA%u&|N9(TAWV5DctjK8(O02;Wr29sz-kh$XVpJ$~q5@tu(V#avB z!vzE6%A7Le`6sfB`?14HqNLYMZ4|B7#Ejbhz|_4!qT9BH6Hq;pLQO?gh-T#cosW}$ zy4Pa8o0S0qX$`aJAG#d=h}Mkk162jg#D^(FdmI*CM@tvKVu;xPrMvGi-mr2CVR-=) zu5@GME3_$=Uq6S^8C5ty+V(*c$<*&NKn|#{kJPCy5=3VfI*w^8JG0Py$BB#!pY0z% zZRnzU`UgixSF}D%Sfk8{V-e6JnnedC^qhTDQBm>q^jyIJ5}G5A0paYc)YN?E6+uMY zuU{|oh8-T<_L0uwByY&C|KC*nd=?Q;e(|}9UUMoAecp33sdmQijbjQu+wQ<+E;ic# zLJ9w!qnaObYfpF&I8L3XnBUCbAD%lzeq~AT%#5c%04pT7X(OOo`gdLY{E*)pz@L(m zN)xc)TG-xxRSo>(hSac&gp8Ebf8i(_0UYHjK}2KY>e^aL=mrj_kpgrAr;3-KAF!rT zcJ|KZCV%gv-$}HuchF}^k>8?n>l+$YmX{Ml-5=aOC6i;7?mO~P{IJx#nD#Ij%4xyl+SK#E~ z0pw6_$9DE))-;YM{>_`AU^NWTC`N4rLm)!8)V#bNx&BG<^!xI%4WLA_wFN_M^mvar znHm3PD|=|I$XeZeF)XHzssVIB01CQdB<(SvB;oIWJ60*33gEE!*?EVAglK4dijG!$ z{ram#xyYajjCl0z_lwJFxROU)$tIJ@{dV2}jT{h1Gd5<%UZU`L1(43m%E|!x`GY&L z(phCyqK<^SO&S02<`X!Js@}g^@%WBJB2E6fxR6#shuYxBq+-ZR{@>yEucvMm`%o?Y zv7WFCs{6dd+H>tM$vuq4#HGWee>QNo^gb|x?l%gY#tuJ+X5W^KPQU+zR1O|{-4p$;VE|2GmhcK zb^6Ahl`rx*+*E6mZzUNy{#Ipp)AH#PcfD)PyPvMq1+Rv)GDho*jcXT2Au{oT9O@Y_ z0?sn3H>zSTA!wIL5^ly5pz{eg6`?1!kf5p@i;P?1S;ff2QeN`WFpBxu4wnBoAY3w< z#y9Egh?azzPr=r}!(3{X7<#{#De4>c89S`=>eZSAyBB8*8?TaIF<0j9TSnOIM=w!IFBMJc1`eQLZmIuh2T>FPsUDbyCjqu$ zayFd&B`Z~Xb&>tfBIKb2LZ$p?sN>O*yq<<>+8FmW?z}~+q`w7+D1Kl*W*$hO_7g%e z!A-wh7^L$GLSN1`ipv7-i=|Yx-`tmx-DIJydNqZ~Xi<&3vT0vTUGy2>S4W5XxMaQQ z?YuO0MTd@EO@LQeNT7N;eA{M!KdzAB`mzc0_37xrER82wU(9WWZ`#wHr)z%^Z@$JP zD(~&owy(z#_Br#N>Fjl^5Oq%aVAdRSnA`ecjAgP)mY!`)pZY@;R{DVxN87UCnL*e!f~GY`Ratd$Y#j+54uU z@=Ipr=4PptO;-*}-kXB$10}IT59UX-^iW|2URI=OKqs0bhNF^gGeAXQ^2z`=_jkKY zkxHs&nBh~S=l<%%eOHH$jgMVA-dCggZ6(wt>LVnaEGD+B1kCGn-7HLkI+4D;Ve|^W z{s2ZANkE}`xqyp1OUe)Jl0<_;if@fl6FlPqXDZ%r|7hc&>7IAmm@wY}Qd{WGUnYaI zud|&xiYSRtAcb1(RMon^U@E}RKf1(yE{hG|qtvjwc{5E-2&_JyF$;O;G)7DGP6h^x zz>_`FxNbxsHX|aLjsj-OK3TDo>Ao~T16gsdi&1l1{DMmRi)-}}%$)af0s_>(TS%{2 z&~q6DjLhtp(h0g+c7~a=WT)|B-QKmfAxaa+@$R2L0Vwkb0aRfekGoAj7~-V%e9zs8 zy9bP0U^}Pq8ktdl7n6OCPd8^Uj=$BW!V-pE@TADqG~kE zZRe1?1~T5YZ=Eo6!5Stv7k%`je3#-ASM2TAde5$ZvACRLa-~{lz@qw@4IRk6Jm`zhF6yfM-TW&(rCJWr|@;1j+OPNrwHJnZL!>|v2s=4`NHQ(LC5iKE5l z#zA|K_*xx-?0&>PYLOWv3?IGED>!P-W31-&XoP{d3N2@vsL9{6Q$$(h*@`(^8mtCH z`@O_INXRvgu0o+caV_8cS>rUn#tHP?clOEV^UTfbtU43s?n1v_s!XStj(*i|r?6KM z(2%NMvnf)3=^v9)U6FjVXkbPAn2@pF7!6GD!=~;Z_m&(l#0n$lw@>4k-U8D?!tA9y zCxB@#H#CQoT`&tYFpd>Kue4&vi1+$K2ER)uhn~9@|ju&!qrZC{uXCQz^MeQ>K#|{=L&1$6C&N8uE?y3dz)CP<}6MeozEbM1-xa?-!2ub$@V?C z)=uK$eWxNeHTSc`kLcc_Z|dtl#w{5aJGFfB^k4t_OAq>I+Y{2+eATnO4-IlUW7(R= z?{!e9F7F5s(z}rq-FyF-zD+B%Y!4%OSI|8hF+wcaQR;ZRSF{AEM@zY?E9%|OY&%|O9lXW#y+-J=KPivU7 zH=4c1pk+gH!6tEv>)UQFbtARIUq(C^dPbEZ5N+tr@r7QD5PD|z$(1?9_U}U*0Or-u z!@=wp$<58W19y|0YlGMJ=C=IC{Wh^F$YmPk_@cvPkTeGeO7c;vvn#OZB*5WZJv_AF zr8!^%VI)jv;`|m}&ew^QNwq$1FacZ-y&;)Pr zz1gU&gg~9kO|5%q7P`A^`zh!Z>7)7y8S+A9@{T|4({l}3FZ|-&x09MDwBJ!_^wL<+ zi?4-2o108&GJeTxW##=VFzYpYJ8u4l2|+z+1_c<3n#1Q#tij6_Z=MNZ!(ZWA?qUk- zf!(d1)x$AM?F}vGuDInmU7#p2%nP7abr@(!lJ z?Wd2)AZlx|$Yx9Ab>3pAK@q>P@_hWgTV7;!RPnUDOJ|2d4@xqxLlNh64mZ zZ=WA4$X!7Y>~1RL4aek_B}{ZG9|y}_p^R0!nY;Ki$+@Vbp<`tWB(lPP31})0$!pkA zca$)q80-$9DvDrU=?xjiNc!BA(K238R{j3U(RXqeaTPb)OlY^U*YHtvHpyqvDrkaRX)E&Lqz!7g|%> z=;Iu+aUyRLdbZ`V^D&Xo_$3pfpinF=jfR9;5{6MZZF!~OiJz~JuNn3zPq|1i(r)rd za&{7wL^T+_r+CATN`*i-U$jX33PS!y6adyMg48mymk_{ovc`;%k2YKqs5RebUD z$4@+s7IBKgDY1$l6zU#H1o`@$yv73y?X_XIBKK-xO#21u+Ws3HwnRY7n1Vv?8VN4< zmDluc(@|tvj!#ff(D}BC$Qa777hGpBLH8-n(Y3Txo@V9Dgd=dARuR9x{*oh;p(2almmf->D*ah);bv?=+@zXxnYb9CjOc*X|jo4}s; zH?Q>JIycz>2OX zEM#aC>0$YGx3l2WmASd83h>Tq@a;`uo7|)M`JY(*s~uLOQzi}MuMcFW@r&r8qmAb5 z^QpUKNsq8(WjcepCFlmeeTrV;B{4SU7P~O!Y~cj8V8X#R9i%y3&Su;cL@Z#BiwKM^ zT1|-Tg^|*xxAlbBfU`Bs?@S-9EyiI+yOgqJcS3yS0min1JAatJ{l>US0b(Rc&36;w z9JiJ8z+@ndm$bAbY)FkdFg~{*5`rbKqcWpGuBrv!!I?G zfFG$LpxTzcN8V4U1)%&lm~KL|0315nsibNT{p2$k4g=jj8o@z`uC5>Mzi(&&OB>=c?$G6EWgO$j%8tXwB8j+hQ%`Bb^}D=vp))%RKRO2G zqi`i9gBUZnNImY9Uc*3va6KxH=N)@#8`um1;3;q`1?`KJJ*}H%tixf<%evCapp|iD zeHyIpyRRmM$s1mqha=NHZSCO9p4I3TbSt`T<367cQwB?KRCl2u1;w8`D$k$k!RgYq z%k*T3d)pmLwIBBHXNN%fL}ty+8)~Ojm>Mbb$VK zvgo6EQ{h*WSCob4%S;hpTTw!pf&u5nWP^sS7oz>M*Mb%{7ZT2c4~a)cRF>AH?&FWw zqZ4mqvyOL=#;}mH2{p1~^@f}6{_Bn<6m3Y!~ S0r$U8QhA~AyiDQsyZ;LhkpT|? diff --git a/sale_quotation/tests/__init__.py b/sale_quotation/tests/__init__.py index 626580ed4e..43ae815229 100644 --- a/sale_quotation/tests/__init__.py +++ b/sale_quotation/tests/__init__.py @@ -1 +1 @@ -from . import test_product +from . import test_quotation diff --git a/sale_quotation/tests/test_product.py b/sale_quotation/tests/test_product.py deleted file mode 100644 index 39d15fab51..0000000000 --- a/sale_quotation/tests/test_product.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2021 Camptocamp (https://www.camptocamp.com). -# @author Iván Todorovich -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo.tests.common import SavepointCase - - -class TestProduct(SavepointCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - cls.attribute = cls.env.ref("product.product_attribute_2") - cls.value_1 = cls.env.ref("product.product_attribute_value_3") - cls.value_2 = cls.env.ref("product.product_attribute_value_4") - cls.product_template = cls.env["product.template"].create( - { - "name": "Test Product", - } - ) - - def test_simple_product(self): - # Back and forth flow, setting and reading on template and single variant - self.assertEqual(self.product_template.shop_only_quotation, "never") - self.product_template.shop_only_quotation = "all_variant" - self.assertTrue(self.product_template.product_variant_ids.shop_only_quotation) - self.product_template.product_variant_ids.shop_only_quotation = False - self.assertEqual(self.product_template.shop_only_quotation, "never") - - def test_multi_product(self): - # Configure product with multiple variants - self.product_template.attribute_line_ids = [ - ( - 0, - 0, - { - "attribute_id": self.attribute.id, - "value_ids": [(6, 0, [self.value_1.id, self.value_2.id])], - }, - ) - ] - self.assertEqual(len(self.product_template.product_variant_ids), 2) - template = self.product_template - product_1 = self.product_template.product_variant_ids[0] - product_2 = self.product_template.product_variant_ids[1] - # Set on the template should set all variants - template.shop_only_quotation = "all_variant" - self.assertTrue(product_1.shop_only_quotation) - self.assertTrue(product_2.shop_only_quotation) - # Set a variant to false should set the template to false - # It's only true if it's true for all variants - product_1.shop_only_quotation = False - self.assertEqual(template.shop_only_quotation, "manually_on_variant") - self.assertTrue(product_2.shop_only_quotation, "Other variant shouldn't change") - # Set false on template should set all variants to false - template.shop_only_quotation = "never" - self.assertFalse(product_1.shop_only_quotation) - self.assertFalse(product_2.shop_only_quotation) diff --git a/sale_quotation/views/product_view.xml b/sale_quotation/views/product_view.xml deleted file mode 100644 index 08ce8aaa26..0000000000 --- a/sale_quotation/views/product_view.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - product.template - - - - - - - - diff --git a/setup/shopinvader_product_order_mode/odoo/addons/shopinvader_product_order_mode b/setup/shopinvader_product_order_mode/odoo/addons/shopinvader_product_order_mode new file mode 120000 index 0000000000..304624b9e4 --- /dev/null +++ b/setup/shopinvader_product_order_mode/odoo/addons/shopinvader_product_order_mode @@ -0,0 +1 @@ +../../../../shopinvader_product_order_mode \ No newline at end of file diff --git a/setup/shopinvader_product_order_mode/setup.py b/setup/shopinvader_product_order_mode/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopinvader_product_order_mode/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/shopinvader_product_order_mode_on_request/odoo/addons/shopinvader_product_order_mode_on_request b/setup/shopinvader_product_order_mode_on_request/odoo/addons/shopinvader_product_order_mode_on_request new file mode 120000 index 0000000000..4b8a7d922b --- /dev/null +++ b/setup/shopinvader_product_order_mode_on_request/odoo/addons/shopinvader_product_order_mode_on_request @@ -0,0 +1 @@ +../../../../shopinvader_product_order_mode_on_request \ No newline at end of file diff --git a/setup/shopinvader_product_order_mode_on_request/setup.py b/setup/shopinvader_product_order_mode_on_request/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopinvader_product_order_mode_on_request/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopinvader_product_order_mode/README.rst b/shopinvader_product_order_mode/README.rst new file mode 100644 index 0000000000..2a68385836 --- /dev/null +++ b/shopinvader_product_order_mode/README.rst @@ -0,0 +1,60 @@ +============================== +Shopinvader Product Order Mode +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:276e322df2fe0bcec29a719db9b5ed5ce83469bd68da22df82a44616e3075471 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github + :target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/shopinvader_product_order_mode + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +This module is a base for generating generic shop order modes on products + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Nicolas DELBOVIER + +Maintainers +~~~~~~~~~~~ + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/shopinvader_product_order_mode/__init__.py b/shopinvader_product_order_mode/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/shopinvader_product_order_mode/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/shopinvader_product_order_mode/__manifest__.py b/shopinvader_product_order_mode/__manifest__.py new file mode 100644 index 0000000000..d7280981ad --- /dev/null +++ b/shopinvader_product_order_mode/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shopinvader Product Order Mode", + "summary": """Add a field for specifying the order mode of a product in a generic way.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV", + "website": "https://github.com/shopinvader/odoo-shopinvader", + "depends": [ + "product", + ], + "data": [ + "views/product_product.xml", + "views/product_template.xml", + ], + "demo": [], +} diff --git a/shopinvader_product_order_mode/i18n/shopinvader_product_order_mode.pot b/shopinvader_product_order_mode/i18n/shopinvader_product_order_mode.pot new file mode 100644 index 0000000000..451fe7dd8e --- /dev/null +++ b/shopinvader_product_order_mode/i18n/shopinvader_product_order_mode.pot @@ -0,0 +1,81 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shopinvader_product_order_mode +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-16 13:41+0000\n" +"PO-Revision-Date: 2025-05-16 13:41+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: shopinvader_product_order_mode +#: model:ir.model.fields,help:shopinvader_product_order_mode.field_product_template__shop_order_mode +msgid "Deault order mode for this template's products in Shopinvader." +msgstr "" + +#. module: shopinvader_product_order_mode +#. odoo-python +#: code:addons/shopinvader_product_order_mode/models/product_product.py:0 +#: code:addons/shopinvader_product_order_mode/models/product_template.py:0 +#: model:ir.model.fields.selection,name:shopinvader_product_order_mode.selection__product_product__shop_order_mode__direct_sale_only +#: model:ir.model.fields.selection,name:shopinvader_product_order_mode.selection__product_template__shop_order_mode__direct_sale_only +#, python-format +msgid "Direct Sale Only" +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model.fields,help:shopinvader_product_order_mode.field_product_product__is_shop_order_mode_unabled +#: model:ir.model.fields,help:shopinvader_product_order_mode.field_product_product__is_shop_order_mode_unabled_on_variant +#: model:ir.model.fields,help:shopinvader_product_order_mode.field_product_template__is_shop_order_mode_unabled_on_variant +msgid "" +"If True, unables product variants to have a different value for " +"'shop_order_mode' than this product template." +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model.fields,help:shopinvader_product_order_mode.field_product_product__shop_order_mode +msgid "Order mode for the product in Shopinvader." +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model,name:shopinvader_product_order_mode.model_product_template +msgid "Product" +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model,name:shopinvader_product_order_mode.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model.fields,field_description:shopinvader_product_order_mode.field_product_product__shop_order_mode +#: model:ir.model.fields,field_description:shopinvader_product_order_mode.field_product_template__shop_order_mode +msgid "Shopinvader Order Mode" +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model.fields,field_description:shopinvader_product_order_mode.field_product_product__is_shop_order_mode_unabled_on_variant +#: model:ir.model.fields,field_description:shopinvader_product_order_mode.field_product_template__is_shop_order_mode_unabled_on_variant +msgid "Shopinvader Order Mode Unabled on Variant" +msgstr "" + +#. module: shopinvader_product_order_mode +#: model:ir.model.fields,field_description:shopinvader_product_order_mode.field_product_product__is_shop_order_mode_unabled +msgid "Template Shop Order Mode Disabled" +msgstr "" + +#. module: shopinvader_product_order_mode +#. odoo-python +#: code:addons/shopinvader_product_order_mode/models/product_product.py:0 +#, python-format +msgid "" +"Unable to modify 'shop_order_mode' on the product '%(product_name)s' as it " +"is blocked by the template '%(template_name)s'." +msgstr "" diff --git a/shopinvader_product_order_mode/models/__init__.py b/shopinvader_product_order_mode/models/__init__.py new file mode 100644 index 0000000000..049669dd0f --- /dev/null +++ b/shopinvader_product_order_mode/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_template +from . import product_product diff --git a/shopinvader_product_order_mode/models/product_product.py b/shopinvader_product_order_mode/models/product_product.py new file mode 100644 index 0000000000..6a8d0eb16e --- /dev/null +++ b/shopinvader_product_order_mode/models/product_product.py @@ -0,0 +1,50 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProductProduct(models.Model): + _inherit = "product.product" + + shop_order_mode = fields.Selection( + selection=[("direct_sale_only", _("Direct Sale Only"))], + string="Shopinvader Order Mode", + store=True, + readonly=False, + compute="_compute_shop_order_mode", + inverse="_inverse_shop_order_mode", + help="Order mode for the product in Shopinvader.", + ) + is_shop_order_mode_unabled = fields.Boolean( + string="Template Shop Order Mode Disabled", + related="product_tmpl_id.is_shop_order_mode_unabled_on_variant", + store=True, + readonly=True, + ) + + @api.depends("product_tmpl_id.shop_order_mode") + def _compute_shop_order_mode(self): + for product in self: + if product.product_tmpl_id: + product.shop_order_mode = product.product_tmpl_id.shop_order_mode + else: + product.shop_order_mode = False + + def _inverse_shop_order_mode(self): + for product in self: + if not product.is_shop_order_mode_unabled and not self.env.context.get( + "skip_shop_order_mode_validation" + ): + raise ValidationError( + _( + "Unable to modify 'shop_order_mode' on the product " + "'%(product_name)s' as it is blocked by the template " + "'%(template_name)s'." + ) + % { + "product_name": product.name, + "template_name": product.product_tmpl_id.name, + } + ) diff --git a/shopinvader_product_order_mode/models/product_template.py b/shopinvader_product_order_mode/models/product_template.py new file mode 100644 index 0000000000..7e145ceb9b --- /dev/null +++ b/shopinvader_product_order_mode/models/product_template.py @@ -0,0 +1,39 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + shop_order_mode = fields.Selection( + selection=[("direct_sale_only", _("Direct Sale Only"))], + string="Shopinvader Order Mode", + default="direct_sale_only", + help="Deault order mode for this template's products in Shopinvader.", + ) + + is_shop_order_mode_unabled_on_variant = fields.Boolean( + string="Shopinvader Order Mode Unabled on Variant", + help="If True, unables product variants to have a different value for" + " 'shop_order_mode' than this product template.", + default=False, + ) + + # if product order mode goes from unabled to disabled on variants, put back all variants + # to the value of the template + @api.model + def write(self, vals): + res = super().write(vals) + if ( + "is_shop_order_mode_unabled_on_variant" in vals + and not vals["is_shop_order_mode_unabled_on_variant"] + ): + for template in self: + template.with_context( + skip_shop_order_mode_validation=True + ).product_variant_ids.write( + {"shop_order_mode": template.shop_order_mode} + ) + return res diff --git a/shopinvader_product_order_mode/readme/CONTRIBUTORS.rst b/shopinvader_product_order_mode/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..5341bc4906 --- /dev/null +++ b/shopinvader_product_order_mode/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Nicolas DELBOVIER \ No newline at end of file diff --git a/shopinvader_product_order_mode/readme/DESCRIPTION.rst b/shopinvader_product_order_mode/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..21de8412d5 --- /dev/null +++ b/shopinvader_product_order_mode/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module is a base for generating generic shop order modes on products \ No newline at end of file diff --git a/shopinvader_product_order_mode/static/description/index.html b/shopinvader_product_order_mode/static/description/index.html new file mode 100644 index 0000000000..49aaeb58e0 --- /dev/null +++ b/shopinvader_product_order_mode/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +Shopinvader Product Order Mode + + + +

    +

    Shopinvader Product Order Mode

    + + +

    Beta License: AGPL-3 shopinvader/odoo-shopinvader

    +

    This module is a base for generating generic shop order modes on products

    +

    Table of contents

    + +
    +

    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 to smash it by providing a detailed and welcomed +feedback.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • ACSONE SA/NV
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is part of the shopinvader/odoo-shopinvader project on GitHub.

    +

    You are welcome to contribute.

    +
    +
    +
    + + diff --git a/shopinvader_product_order_mode/tests/__init__.py b/shopinvader_product_order_mode/tests/__init__.py new file mode 100644 index 0000000000..0ce6d0db23 --- /dev/null +++ b/shopinvader_product_order_mode/tests/__init__.py @@ -0,0 +1 @@ +from . import test_shop_order_mode diff --git a/shopinvader_product_order_mode/tests/test_shop_order_mode.py b/shopinvader_product_order_mode/tests/test_shop_order_mode.py new file mode 100644 index 0000000000..f00b8934dc --- /dev/null +++ b/shopinvader_product_order_mode/tests/test_shop_order_mode.py @@ -0,0 +1,77 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import Command +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestShopOrderMode(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Define different attributes to allow to have mulitple products for the same template + cls.test_attribute = cls.env["product.attribute"].create( + {"name": "Test attribute"} + ) + cls.test_attribute_1 = cls.env["product.attribute.value"].create( + {"name": "Test attribute value 1", "attribute_id": cls.test_attribute.id} + ) + cls.test_attribute_2 = cls.env["product.attribute.value"].create( + {"name": "Test attribute value 2", "attribute_id": cls.test_attribute.id} + ) + + cls.template = cls.env["product.template"].create( + { + "name": "Test Product Template", + "attribute_line_ids": [ + Command.create( + { + "attribute_id": cls.test_attribute.id, + "value_ids": [ + Command.link(cls.test_attribute_1.id), + Command.link(cls.test_attribute_2.id), + ], + } + ) + ], + "shop_order_mode": "direct_sale_only", + "is_shop_order_mode_unabled_on_variant": False, + } + ) + + cls.product = cls.template.product_variant_ids[0] + cls.product_2 = cls.template.product_variant_ids[1] + + def test_default_order_mode_on_products(self): + """ + Test that products order mode is initiated to the order mode of the template. + """ + self.assertEqual(self.product.shop_order_mode, self.template.shop_order_mode) + + def test_product_shop_order_mode_constraint(self): + """ + Test that the shop_order_mode field on product.product is not editable + when is_shop_order_mode_unabled_on_variant is False on product.template. + """ + + with self.assertRaises(ValidationError): + self.product.shop_order_mode = None + + self.template.is_shop_order_mode_unabled_on_variant = True + + self.product.shop_order_mode = None + self.assertNotEqual(self.product.shop_order_mode, "direct_sale_only") + self.assertEqual(self.template.shop_order_mode, "direct_sale_only") + + def test_shop_order_mode_reset(self): + """ + Test that the `shop_order_mode` field on product.product is reset to the + value of the product.template when `is_shop_order_mode_unabled_on_variant` is True. + """ + self.template.is_shop_order_mode_unabled_on_variant = True + self.product.shop_order_mode = None + self.template.is_shop_order_mode_unabled_on_variant = False + self.assertEqual(self.product.shop_order_mode, self.template.shop_order_mode) diff --git a/shopinvader_product_order_mode/views/product_product.xml b/shopinvader_product_order_mode/views/product_product.xml new file mode 100644 index 0000000000..a77c331dd5 --- /dev/null +++ b/shopinvader_product_order_mode/views/product_product.xml @@ -0,0 +1,25 @@ + + + + + + product.product + + + + + + + + + + + + diff --git a/shopinvader_product_order_mode/views/product_template.xml b/shopinvader_product_order_mode/views/product_template.xml new file mode 100644 index 0000000000..e64acf2820 --- /dev/null +++ b/shopinvader_product_order_mode/views/product_template.xml @@ -0,0 +1,21 @@ + + + + + + product.template + + + + + + + + + + + diff --git a/shopinvader_product_order_mode_on_request/README.rst b/shopinvader_product_order_mode_on_request/README.rst new file mode 100644 index 0000000000..88abdc3ec6 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/README.rst @@ -0,0 +1,60 @@ +========================================= +Shopinvader Product Order Mode On Request +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:98b6bb5bcac769474c7ce34ae341ba3ede691e6cdeca6896af9e0b058cb95d85 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github + :target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/shopinvader_product_order_mode_on_request + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +This module adds 2 shop order modes for products: "Quotation Only" and "Direct Sale Or Quotation" + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Nicolas DELBOVIER + +Maintainers +~~~~~~~~~~~ + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/shopinvader_product_order_mode_on_request/__init__.py b/shopinvader_product_order_mode_on_request/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/shopinvader_product_order_mode_on_request/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/shopinvader_product_order_mode_on_request/__manifest__.py b/shopinvader_product_order_mode_on_request/__manifest__.py new file mode 100644 index 0000000000..53c452551e --- /dev/null +++ b/shopinvader_product_order_mode_on_request/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shopinvader Product Order Mode On Request", + "summary": """ Add new order modes on products.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV", + "website": "https://github.com/shopinvader/odoo-shopinvader", + "depends": [ + "shopinvader_product_order_mode", + "sale_cart", + ], + "data": [], + "demo": [], +} diff --git a/shopinvader_product_order_mode_on_request/i18n/shopinvader_product_order_mode_on_request.pot b/shopinvader_product_order_mode_on_request/i18n/shopinvader_product_order_mode_on_request.pot new file mode 100644 index 0000000000..88ca915f76 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/i18n/shopinvader_product_order_mode_on_request.pot @@ -0,0 +1,72 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shopinvader_product_order_mode_on_request +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-16 13:41+0000\n" +"PO-Revision-Date: 2025-05-16 13:41+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model.fields,help:shopinvader_product_order_mode_on_request.field_product_template__shop_order_mode +msgid "Deault order mode for this template's products in Shopinvader." +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#. odoo-python +#: code:addons/shopinvader_product_order_mode_on_request/models/product_product.py:0 +#: code:addons/shopinvader_product_order_mode_on_request/models/product_template.py:0 +#: model:ir.model.fields.selection,name:shopinvader_product_order_mode_on_request.selection__product_product__shop_order_mode__direct_sale_or_quotation +#: model:ir.model.fields.selection,name:shopinvader_product_order_mode_on_request.selection__product_template__shop_order_mode__direct_sale_or_quotation +#, python-format +msgid "Direct Sale or Quotation" +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model.fields,help:shopinvader_product_order_mode_on_request.field_product_product__shop_order_mode +msgid "Order mode for the product in Shopinvader." +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model,name:shopinvader_product_order_mode_on_request.model_product_template +msgid "Product" +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model,name:shopinvader_product_order_mode_on_request.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#. odoo-python +#: code:addons/shopinvader_product_order_mode_on_request/models/product_product.py:0 +#: code:addons/shopinvader_product_order_mode_on_request/models/product_template.py:0 +#: model:ir.model.fields.selection,name:shopinvader_product_order_mode_on_request.selection__product_product__shop_order_mode__quotation_only +#: model:ir.model.fields.selection,name:shopinvader_product_order_mode_on_request.selection__product_template__shop_order_mode__quotation_only +#, python-format +msgid "Quotation Only" +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model,name:shopinvader_product_order_mode_on_request.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model.fields,field_description:shopinvader_product_order_mode_on_request.field_sale_order__shop_only_quotation +msgid "Shop Only Quotation" +msgstr "" + +#. module: shopinvader_product_order_mode_on_request +#: model:ir.model.fields,field_description:shopinvader_product_order_mode_on_request.field_product_product__shop_order_mode +#: model:ir.model.fields,field_description:shopinvader_product_order_mode_on_request.field_product_template__shop_order_mode +msgid "Shopinvader Order Mode" +msgstr "" diff --git a/shopinvader_product_order_mode_on_request/models/__init__.py b/shopinvader_product_order_mode_on_request/models/__init__.py new file mode 100644 index 0000000000..de7a683548 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_product +from . import product_template +from . import sale_order diff --git a/shopinvader_product_order_mode_on_request/models/product_product.py b/shopinvader_product_order_mode_on_request/models/product_product.py new file mode 100644 index 0000000000..cc890a94ca --- /dev/null +++ b/shopinvader_product_order_mode_on_request/models/product_product.py @@ -0,0 +1,19 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + shop_order_mode = fields.Selection( + selection_add=[ + ("quotation_only", _("Quotation Only")), + ("direct_sale_or_quotation", _("Direct Sale or Quotation")), + ], + ondelete={ + "quotation_only": "set null", + "direct_sale_or_quotation": "set null", + }, + ) diff --git a/shopinvader_product_order_mode_on_request/models/product_template.py b/shopinvader_product_order_mode_on_request/models/product_template.py new file mode 100644 index 0000000000..e96d8ab084 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/models/product_template.py @@ -0,0 +1,19 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + shop_order_mode = fields.Selection( + selection_add=[ + ("quotation_only", _("Quotation Only")), + ("direct_sale_or_quotation", _("Direct Sale or Quotation")), + ], + ondelete={ + "quotation_only": "set null", + "direct_sale_or_quotation": "set null", + }, + ) diff --git a/shopinvader_product_order_mode_on_request/models/sale_order.py b/shopinvader_product_order_mode_on_request/models/sale_order.py new file mode 100644 index 0000000000..0ffc858c42 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/models/sale_order.py @@ -0,0 +1,21 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + shop_only_quotation = fields.Boolean( + compute="_compute_shop_only_quotation", help="" + ) + + @api.depends("order_line.product_id.shop_order_mode") + def _compute_shop_only_quotation(self): + for record in self: + record.shop_only_quotation = any( + record.order_line.product_id.filtered( + lambda p: p.shop_order_mode == "quotation_only" + ) + ) diff --git a/shopinvader_product_order_mode_on_request/readme/CONTRIBUTORS.rst b/shopinvader_product_order_mode_on_request/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..5341bc4906 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Nicolas DELBOVIER \ No newline at end of file diff --git a/shopinvader_product_order_mode_on_request/readme/DESCRIPTION.rst b/shopinvader_product_order_mode_on_request/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..0b4373ac01 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds 2 shop order modes for products: "Quotation Only" and "Direct Sale Or Quotation" \ No newline at end of file diff --git a/shopinvader_product_order_mode_on_request/static/description/index.html b/shopinvader_product_order_mode_on_request/static/description/index.html new file mode 100644 index 0000000000..60fbaefd81 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +Shopinvader Product Order Mode On Request + + + +
    +

    Shopinvader Product Order Mode On Request

    + + +

    Beta License: AGPL-3 shopinvader/odoo-shopinvader

    +

    This module adds 2 shop order modes for products: “Quotation Only” and “Direct Sale Or Quotation”

    +

    Table of contents

    + +
    +

    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 to smash it by providing a detailed and welcomed +feedback.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • ACSONE SA/NV
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is part of the shopinvader/odoo-shopinvader project on GitHub.

    +

    You are welcome to contribute.

    +
    +
    +
    + + diff --git a/shopinvader_product_order_mode_on_request/tests/__init__.py b/shopinvader_product_order_mode_on_request/tests/__init__.py new file mode 100644 index 0000000000..43ae815229 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/tests/__init__.py @@ -0,0 +1 @@ +from . import test_quotation diff --git a/shopinvader_product_order_mode_on_request/tests/test_quotation.py b/shopinvader_product_order_mode_on_request/tests/test_quotation.py new file mode 100644 index 0000000000..a3747d66d9 --- /dev/null +++ b/shopinvader_product_order_mode_on_request/tests/test_quotation.py @@ -0,0 +1,102 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests.common import TransactionCase + + +class TestQuotation(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # as the shop_order_mode is readonly by default except if unabled in the template + # we need to create the product template first and then get the product variant + cls.product_quotation_only = ( + cls.env["product.template"] + .create( + {"name": "Quotation only Product", "shop_order_mode": "quotation_only"} + ) + .product_variant_id + ) + + cls.product_direct_sale_only = ( + cls.env["product.template"] + .create( + { + "name": "Direct sale only Product", + "shop_order_mode": "direct_sale_only", + } + ) + .product_variant_id + ) + + cls.product_direct_sale_or_quotation = ( + cls.env["product.template"] + .create( + { + "name": "Direct Sale Or Quotation Product", + "shop_order_mode": "direct_sale_or_quotation", + } + ) + .product_variant_id + ) + + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Partner", + } + ) + + def test_sale_order_shop_only_quotation(self): + so = self.env["sale.order"].create( + { + "name": "Sale order that should be quotation only", + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product_quotation_only.id, + "product_uom_qty": 1, + } + ), + Command.create( + { + "product_id": self.product_direct_sale_only.id, + "product_uom_qty": 1, + } + ), + Command.create( + { + "product_id": self.product_direct_sale_or_quotation.id, + "product_uom_qty": 1, + } + ), + ], + } + ) + + self.assertTrue(so.shop_only_quotation) + + so_2 = self.env["sale.order"].create( + { + "name": "Sale order that should NOT be quotation only", + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product_direct_sale_only.id, + "product_uom_qty": 1, + } + ), + Command.create( + { + "product_id": self.product_direct_sale_or_quotation.id, + "product_uom_qty": 1, + } + ), + ], + } + ) + + self.assertFalse(so_2.shop_only_quotation) From 7db97c2db2cc3c9f5f416563893c29017c7eb8dc Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Fri, 16 May 2025 16:47:10 +0200 Subject: [PATCH 2/8] [ADD] sale_typology: extract the concept of "typology" in sale.order in a separate module --- sale_cart/README.rst | 3 +- sale_cart/__manifest__.py | 2 +- sale_cart/models/sale_order.py | 2 +- sale_cart/readme/CONTRIBUTORS.rst | 1 + sale_cart/readme/DESCRIPTION.rst | 2 +- sale_cart/static/description/index.html | 11 +- sale_typology/README.rst | 60 +++ sale_typology/__init__.py | 1 + sale_typology/__manifest__.py | 14 + sale_typology/models/__init__.py | 1 + sale_typology/models/sale_order.py | 11 + sale_typology/readme/CONTRIBUTORS.rst | 1 + sale_typology/readme/DESCRIPTION.rst | 1 + sale_typology/static/description/index.html | 416 ++++++++++++++++++ sale_typology/tests/__init__.py | 1 + sale_typology/tests/test_sale_typology.py | 30 ++ setup/sale_typology/odoo/addons/sale_typology | 1 + setup/sale_typology/setup.py | 6 + 18 files changed, 555 insertions(+), 9 deletions(-) create mode 100644 sale_typology/README.rst create mode 100644 sale_typology/__init__.py create mode 100644 sale_typology/__manifest__.py create mode 100644 sale_typology/models/__init__.py create mode 100644 sale_typology/models/sale_order.py create mode 100644 sale_typology/readme/CONTRIBUTORS.rst create mode 100644 sale_typology/readme/DESCRIPTION.rst create mode 100644 sale_typology/static/description/index.html create mode 100644 sale_typology/tests/__init__.py create mode 100644 sale_typology/tests/test_sale_typology.py create mode 120000 setup/sale_typology/odoo/addons/sale_typology create mode 100644 setup/sale_typology/setup.py diff --git a/sale_cart/README.rst b/sale_cart/README.rst index 32c9053ea0..2b4e5598d0 100644 --- a/sale_cart/README.rst +++ b/sale_cart/README.rst @@ -22,7 +22,7 @@ Sale Cart |badge1| |badge2| |badge3| -This addon add a typology on sale.order to distinguish the sale orders that are +This addon adds a "cart" typology on sale.order to distinguish the sale orders that are in the process of being written (cart) and those that are quotations ready to be confirmed. @@ -53,6 +53,7 @@ Contributors ~~~~~~~~~~~~ * Laurent Mignon +* Nicolas DELBOVIER Maintainers ~~~~~~~~~~~ diff --git a/sale_cart/__manifest__.py b/sale_cart/__manifest__.py index f2fa994b16..ebf3a30f3f 100644 --- a/sale_cart/__manifest__.py +++ b/sale_cart/__manifest__.py @@ -10,7 +10,7 @@ "license": "AGPL-3", "author": "ACSONE SA/NV", "website": "https://github.com/shopinvader/odoo-shopinvader", - "depends": ["sale", "sales_team"], + "depends": ["sale", "sales_team", "sale_typology"], "data": ["views/sale_order.xml"], "demo": [], "external_dependencies": {"python": ["openupgradelib"]}, diff --git a/sale_cart/models/sale_order.py b/sale_cart/models/sale_order.py index 982bbc8f16..4e08f755ca 100644 --- a/sale_cart/models/sale_order.py +++ b/sale_cart/models/sale_order.py @@ -8,7 +8,7 @@ class SaleOrder(models.Model): _inherit = "sale.order" - typology = fields.Selection([("sale", "Sale"), ("cart", "Cart")], default="sale") + typology = fields.Selection(selection_add=[("cart", "Cart")]) def _confirm_cart(self): self.ensure_one() diff --git a/sale_cart/readme/CONTRIBUTORS.rst b/sale_cart/readme/CONTRIBUTORS.rst index 172b2d223c..a455b581a9 100644 --- a/sale_cart/readme/CONTRIBUTORS.rst +++ b/sale_cart/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Laurent Mignon +* Nicolas DELBOVIER \ No newline at end of file diff --git a/sale_cart/readme/DESCRIPTION.rst b/sale_cart/readme/DESCRIPTION.rst index 63df8c890c..f342e93679 100644 --- a/sale_cart/readme/DESCRIPTION.rst +++ b/sale_cart/readme/DESCRIPTION.rst @@ -1,3 +1,3 @@ -This addon add a typology on sale.order to distinguish the sale orders that are +This addon adds a "cart" typology on sale.order to distinguish the sale orders that are in the process of being written (cart) and those that are quotations ready to be confirmed. diff --git a/sale_cart/static/description/index.html b/sale_cart/static/description/index.html index 2c96015922..1988a12509 100644 --- a/sale_cart/static/description/index.html +++ b/sale_cart/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -370,7 +370,7 @@

    Sale Cart

    !! source digest: sha256:669b6f8b598ec1aa67334391cf3183bed210a1af06ea0bce3d1f34fbc6c3ebf8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: AGPL-3 shopinvader/odoo-shopinvader

    -

    This addon add a typology on sale.order to distinguish the sale orders that are +

    This addon adds a “cart” typology on sale.order to distinguish the sale orders that are in the process of being written (cart) and those that are quotations ready to be confirmed.

    Table of contents

    @@ -405,6 +405,7 @@

    Authors

    Contributors

    diff --git a/sale_typology/README.rst b/sale_typology/README.rst new file mode 100644 index 0000000000..01c044a2c7 --- /dev/null +++ b/sale_typology/README.rst @@ -0,0 +1,60 @@ +============= +Sale Typology +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b322433186805aca5486f254a72af4c1f83b4fc5d91a8ca0c1a985c1c0e3f071 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github + :target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/sale_typology + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +This addon adds a typology on sale.order + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Nicolas DELBOVIER + +Maintainers +~~~~~~~~~~~ + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/sale_typology/__init__.py b/sale_typology/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/sale_typology/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_typology/__manifest__.py b/sale_typology/__manifest__.py new file mode 100644 index 0000000000..d7f40583ec --- /dev/null +++ b/sale_typology/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sale Typology", + "summary": """Adds a 'typology' field to sale orders to distinguish sales from carts.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV", + "website": "https://github.com/shopinvader/odoo-shopinvader", + "depends": ["sale"], + "data": [], + "demo": [], +} diff --git a/sale_typology/models/__init__.py b/sale_typology/models/__init__.py new file mode 100644 index 0000000000..6aacb75313 --- /dev/null +++ b/sale_typology/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/sale_typology/models/sale_order.py b/sale_typology/models/sale_order.py new file mode 100644 index 0000000000..f4ee724916 --- /dev/null +++ b/sale_typology/models/sale_order.py @@ -0,0 +1,11 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleOrder(models.Model): + + _inherit = "sale.order" + + typology = fields.Selection([("sale", "Sale")], default="sale") diff --git a/sale_typology/readme/CONTRIBUTORS.rst b/sale_typology/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..5341bc4906 --- /dev/null +++ b/sale_typology/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Nicolas DELBOVIER \ No newline at end of file diff --git a/sale_typology/readme/DESCRIPTION.rst b/sale_typology/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..a3dbee0ade --- /dev/null +++ b/sale_typology/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This addon adds a typology on sale.order \ No newline at end of file diff --git a/sale_typology/static/description/index.html b/sale_typology/static/description/index.html new file mode 100644 index 0000000000..fec9901a28 --- /dev/null +++ b/sale_typology/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +Sale Typology + + + +
    +

    Sale Typology

    + + +

    Beta License: AGPL-3 shopinvader/odoo-shopinvader

    +

    This addon adds a typology on sale.order

    +

    Table of contents

    + +
    +

    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 to smash it by providing a detailed and welcomed +feedback.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • ACSONE SA/NV
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is part of the shopinvader/odoo-shopinvader project on GitHub.

    +

    You are welcome to contribute.

    +
    +
    +
    + + diff --git a/sale_typology/tests/__init__.py b/sale_typology/tests/__init__.py new file mode 100644 index 0000000000..f357e9a2cf --- /dev/null +++ b/sale_typology/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_typology diff --git a/sale_typology/tests/test_sale_typology.py b/sale_typology/tests/test_sale_typology.py new file mode 100644 index 0000000000..5fb8d25675 --- /dev/null +++ b/sale_typology/tests/test_sale_typology.py @@ -0,0 +1,30 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests.common import TransactionCase + + +class TestSaleCart(TransactionCase): + @classmethod + def setUpClass(cls): + super(TestSaleCart, cls).setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.product = cls.env["product.product"].create( + { + "name": "product", + "uom_id": cls.env.ref("uom.product_uom_unit").id, + } + ) + cls.partner = cls.env["res.partner"].create({"name": "partner"}) + cls.sale_order = cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "order_line": [ + Command.create({"product_id": cls.product.id, "product_uom_qty": 1}) + ], + } + ) + + def test_typology_default(self): + self.assertEqual(self.sale_order.typology, "sale") diff --git a/setup/sale_typology/odoo/addons/sale_typology b/setup/sale_typology/odoo/addons/sale_typology new file mode 120000 index 0000000000..dfce1a0912 --- /dev/null +++ b/setup/sale_typology/odoo/addons/sale_typology @@ -0,0 +1 @@ +../../../../sale_typology \ No newline at end of file diff --git a/setup/sale_typology/setup.py b/setup/sale_typology/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/sale_typology/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 91daa447f5dc3c5932c4d874ac2edc6fdb344c9e Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 19 May 2025 10:50:58 +0200 Subject: [PATCH 3/8] [IMP] sale_quotation: use the "typology" field to delcare the sale order as a quotation --- sale_quotation/models/sale_order.py | 12 ++++++++++++ sale_quotation/tests/test_quotation.py | 8 ++++++++ sale_quotation/views/sale_view.xml | 16 ++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/sale_quotation/models/sale_order.py b/sale_quotation/models/sale_order.py index 02302497f6..72d6524a53 100644 --- a/sale_quotation/models/sale_order.py +++ b/sale_quotation/models/sale_order.py @@ -26,6 +26,8 @@ class SaleOrder(models.Model): copy=False, ) + typology = fields.Selection(selection_add=[("quote", "Quote")], default="quote") + @api.depends("state") def _compute_quotation_state(self): for record in self: @@ -62,3 +64,13 @@ def action_confirm_quotation(self): ) ) self.quotation_state = "accepted" + self.typology = "sale" + + def action_confirm(self): + if self.typology == "quote": + self.action_confirm_quotation() + return super().action_confirm() + + def action_draft(self): + self.typology = "quote" + return super().action_draft() diff --git a/sale_quotation/tests/test_quotation.py b/sale_quotation/tests/test_quotation.py index a4a7a62adf..fb237d958c 100644 --- a/sale_quotation/tests/test_quotation.py +++ b/sale_quotation/tests/test_quotation.py @@ -22,6 +22,7 @@ def setUpClass(cls): def test_create_quotation(self): self.assertEqual(self.so.quotation_state, "draft") + self.assertEqual(self.so.typology, "quote") def test_request_quotation(self): self.so.typology = "cart" @@ -42,8 +43,15 @@ def test_confirm_quotation(self): self.so.action_quotation_sent() self.so.action_confirm_quotation() self.assertEqual(self.so.quotation_state, "accepted") + self.assertEqual(self.so.typology, "sale") def test_not_allowed_confirm_quotation(self): with self.assertRaises(UserError) as cm: self.so.action_confirm_quotation() self.assertIn("Only quotation with the state", cm.exception.args[0]) + + def test_convert_to_draft_resets_typology(self): + self.so.action_quotation_sent() + self.so.action_confirm() + self.so.action_draft() + self.assertEqual(self.so.typology, "quote") diff --git a/sale_quotation/views/sale_view.xml b/sale_quotation/views/sale_view.xml index f8b771ea4b..41d140383b 100644 --- a/sale_quotation/views/sale_view.xml +++ b/sale_quotation/views/sale_view.xml @@ -70,6 +70,14 @@ domain="[('quotation_state', '=', 'cancel')]" /> + + + 1 @@ -90,4 +98,12 @@ + + [('typology', '=', 'quote')] + + + + [('typology', '=', 'quote')] + + From fb00500b91b6c84fd651334191d62006458a2db8 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 19 May 2025 11:40:38 +0200 Subject: [PATCH 4/8] [ADD] sale_cart_quotation: introduce module to decouple sale_quotation and sale_cart --- sale_cart_quotation/README.rst | 60 +++ sale_cart_quotation/__init__.py | 1 + sale_cart_quotation/__manifest__.py | 16 + sale_cart_quotation/models/__init__.py | 1 + sale_cart_quotation/models/sale_order.py | 20 + sale_cart_quotation/readme/CONTRIBUTORS.rst | 1 + sale_cart_quotation/readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 416 ++++++++++++++++++ sale_cart_quotation/tests/__init__.py | 1 + sale_cart_quotation/tests/test_quotation.py | 30 ++ sale_cart_quotation/views/sale_order.xml | 12 + sale_quotation/__manifest__.py | 1 - sale_quotation/models/sale_order.py | 11 - .../odoo/addons/sale_cart_quotation | 1 + setup/sale_cart_quotation/setup.py | 6 + 15 files changed, 566 insertions(+), 12 deletions(-) create mode 100644 sale_cart_quotation/README.rst create mode 100644 sale_cart_quotation/__init__.py create mode 100644 sale_cart_quotation/__manifest__.py create mode 100644 sale_cart_quotation/models/__init__.py create mode 100644 sale_cart_quotation/models/sale_order.py create mode 100644 sale_cart_quotation/readme/CONTRIBUTORS.rst create mode 100644 sale_cart_quotation/readme/DESCRIPTION.rst create mode 100644 sale_cart_quotation/static/description/index.html create mode 100644 sale_cart_quotation/tests/__init__.py create mode 100644 sale_cart_quotation/tests/test_quotation.py create mode 100644 sale_cart_quotation/views/sale_order.xml create mode 120000 setup/sale_cart_quotation/odoo/addons/sale_cart_quotation create mode 100644 setup/sale_cart_quotation/setup.py diff --git a/sale_cart_quotation/README.rst b/sale_cart_quotation/README.rst new file mode 100644 index 0000000000..fd6f19a665 --- /dev/null +++ b/sale_cart_quotation/README.rst @@ -0,0 +1,60 @@ +=================== +Sale Cart Quotation +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:092764e34a3532b13b87a0b07712f405be8de3df4ce5265a1c4f0640ea1f44f8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github + :target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/sale_cart_quotation + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +Glue module between sale_quotation and sale_cart + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Nicolas DELBOVIER + +Maintainers +~~~~~~~~~~~ + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/sale_cart_quotation/__init__.py b/sale_cart_quotation/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/sale_cart_quotation/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_cart_quotation/__manifest__.py b/sale_cart_quotation/__manifest__.py new file mode 100644 index 0000000000..b2a8636aaf --- /dev/null +++ b/sale_cart_quotation/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sale Cart Quotation", + "summary": """Glue module between sale_quotation and sale_cart""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV", + "website": "https://github.com/shopinvader/odoo-shopinvader", + "depends": ["sale_quotation", "sale_cart"], + "data": [ + "views/sale_order.xml", + ], + "demo": [], +} diff --git a/sale_cart_quotation/models/__init__.py b/sale_cart_quotation/models/__init__.py new file mode 100644 index 0000000000..6aacb75313 --- /dev/null +++ b/sale_cart_quotation/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/sale_cart_quotation/models/sale_order.py b/sale_cart_quotation/models/sale_order.py new file mode 100644 index 0000000000..6a37793c28 --- /dev/null +++ b/sale_cart_quotation/models/sale_order.py @@ -0,0 +1,20 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_request_quotation(self): + if any(rec.state != "draft" or rec.typology != "cart" for rec in self): + raise UserError( + _( + "Only orders of cart typology in draft state " + "can be converted to quotation" + ) + ) + self.write({"quotation_state": "customer_request", "typology": "sale"}) + return True diff --git a/sale_cart_quotation/readme/CONTRIBUTORS.rst b/sale_cart_quotation/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..5341bc4906 --- /dev/null +++ b/sale_cart_quotation/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Nicolas DELBOVIER \ No newline at end of file diff --git a/sale_cart_quotation/readme/DESCRIPTION.rst b/sale_cart_quotation/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..b8745a7bb0 --- /dev/null +++ b/sale_cart_quotation/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Glue module between sale_quotation and sale_cart \ No newline at end of file diff --git a/sale_cart_quotation/static/description/index.html b/sale_cart_quotation/static/description/index.html new file mode 100644 index 0000000000..779434cb6c --- /dev/null +++ b/sale_cart_quotation/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +Sale Cart Quotation + + + +
    +

    Sale Cart Quotation

    + + +

    Beta License: AGPL-3 shopinvader/odoo-shopinvader

    +

    Glue module between sale_quotation and sale_cart

    +

    Table of contents

    + +
    +

    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 to smash it by providing a detailed and welcomed +feedback.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • ACSONE SA/NV
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is part of the shopinvader/odoo-shopinvader project on GitHub.

    +

    You are welcome to contribute.

    +
    +
    +
    + + diff --git a/sale_cart_quotation/tests/__init__.py b/sale_cart_quotation/tests/__init__.py new file mode 100644 index 0000000000..43ae815229 --- /dev/null +++ b/sale_cart_quotation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_quotation diff --git a/sale_cart_quotation/tests/test_quotation.py b/sale_cart_quotation/tests/test_quotation.py new file mode 100644 index 0000000000..132b88e6f9 --- /dev/null +++ b/sale_cart_quotation/tests/test_quotation.py @@ -0,0 +1,30 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import SavepointCase + + +class TestQuotation(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + product = cls.env["product.product"].create({"name": "Test Product"}) + partner = cls.env["res.partner"].create({"name": "John"}) + cls.so = cls.env["sale.order"].create( + { + "partner_id": partner.id, + "order_line": [(0, 0, {"product_id": product.id})], + } + ) + + def test_request_quotation(self): + self.so.typology = "cart" + self.so.action_request_quotation() + self.assertEqual(self.so.quotation_state, "customer_request") + + def test_send_requested_quotation(self): + self.so.typology = "cart" + self.so.action_request_quotation() + self.so.action_quotation_sent() + self.assertEqual(self.so.quotation_state, "waiting_acceptation") diff --git a/sale_cart_quotation/views/sale_order.xml b/sale_cart_quotation/views/sale_order.xml new file mode 100644 index 0000000000..945ebf92cc --- /dev/null +++ b/sale_cart_quotation/views/sale_order.xml @@ -0,0 +1,12 @@ + + + + + [('typology', '=', 'quote')] + + + + [('typology', '=', 'quote')] + + diff --git a/sale_quotation/__manifest__.py b/sale_quotation/__manifest__.py index 2cb1baa444..c77168ab16 100644 --- a/sale_quotation/__manifest__.py +++ b/sale_quotation/__manifest__.py @@ -11,7 +11,6 @@ "author": "Akretion", "license": "AGPL-3", "depends": [ - "sale_cart", "shopinvader_product_order_mode_on_request", ], "data": [ diff --git a/sale_quotation/models/sale_order.py b/sale_quotation/models/sale_order.py index 72d6524a53..8b75f6cdb2 100644 --- a/sale_quotation/models/sale_order.py +++ b/sale_quotation/models/sale_order.py @@ -44,17 +44,6 @@ def _compute_quotation_state(self): elif record.state == "sale": record.quotation_state = "accepted" - def action_request_quotation(self): - if any(rec.state != "draft" or rec.typology != "cart" for rec in self): - raise UserError( - _( - "Only orders of cart typology in draft state " - "can be converted to quotation" - ) - ) - self.write({"quotation_state": "customer_request", "typology": "sale"}) - return True - def action_confirm_quotation(self): if any(rec.quotation_state != "waiting_acceptation" for rec in self): raise UserError( diff --git a/setup/sale_cart_quotation/odoo/addons/sale_cart_quotation b/setup/sale_cart_quotation/odoo/addons/sale_cart_quotation new file mode 120000 index 0000000000..9258838b8e --- /dev/null +++ b/setup/sale_cart_quotation/odoo/addons/sale_cart_quotation @@ -0,0 +1 @@ +../../../../sale_cart_quotation \ No newline at end of file diff --git a/setup/sale_cart_quotation/setup.py b/setup/sale_cart_quotation/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/sale_cart_quotation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 0580ae73c1828cd6805841141d6b30f00529d0d8 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Tue, 20 May 2025 14:57:33 +0200 Subject: [PATCH 5/8] [IMP] shopinvader_api_quotation: remove sale_cart dependency and enhance API - put shopinvader_sale_state as a dependency in order to remove sale_cart because we need the field sale.order.shopinvader_state This commit introduces significant enhancements to the REST API for quotation management, including: - **Create Method:** Added a new endpoint to create quotations. - **Line Management Methods:** Implemented dedicated endpoints for adding, deleting, or updating single or multiple lines within a quotation. - **Update Extension:** Extended the existing update method to support the addition of new lines --- shopinvader_api_quotation/__init__.py | 3 + shopinvader_api_quotation/__manifest__.py | 2 +- shopinvader_api_quotation/dependencies.py | 140 +++++++++ .../routers/quotation.py | 190 +++++++----- shopinvader_api_quotation/schemas/sale.py | 65 +++- .../tests/test_shopinvader_api_quotation.py | 283 ++++++++++++++++-- 6 files changed, 582 insertions(+), 101 deletions(-) create mode 100644 shopinvader_api_quotation/dependencies.py diff --git a/shopinvader_api_quotation/__init__.py b/shopinvader_api_quotation/__init__.py index 62a5d54f85..cecef1199b 100644 --- a/shopinvader_api_quotation/__init__.py +++ b/shopinvader_api_quotation/__init__.py @@ -1 +1,4 @@ from . import routers +from . import dependencies +from . import schemas +from . import models diff --git a/shopinvader_api_quotation/__manifest__.py b/shopinvader_api_quotation/__manifest__.py index d214e9859d..d9693232a0 100644 --- a/shopinvader_api_quotation/__manifest__.py +++ b/shopinvader_api_quotation/__manifest__.py @@ -13,7 +13,7 @@ "shopinvader_api_security_sale", "shopinvader_api_cart", "shopinvader_api_sale", - "sale_cart", + "shopinvader_sale_state", "sale_quotation", ], "data": [], diff --git a/shopinvader_api_quotation/dependencies.py b/shopinvader_api_quotation/dependencies.py new file mode 100644 index 0000000000..b27ce45f51 --- /dev/null +++ b/shopinvader_api_quotation/dependencies.py @@ -0,0 +1,140 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from typing import Annotated + +from fastapi import Depends + +from odoo import Command, models +from odoo.api import Environment +from odoo.exceptions import MissingError + +from odoo.addons.base.models.res_partner import Partner as ResPartner +from odoo.addons.fastapi.dependencies import ( + authenticated_partner, + authenticated_partner_env, +) +from odoo.addons.sale.models.sale_order import SaleOrder + +from .schemas.sale import ( + QuotationAddLineRequest, + QuotationCreateRequest, + QuotationDeleteLineRequest, + QuotationLines, + QuotationUpdateLineRequest, +) + + +class ShopinvaderApiQuotationRouterHelper(models.AbstractModel): + _name = "shopinvader_api_quotation.router.helper" + _description = "Shopinvader api quotations router helper" + _inherit = "shopinvader_api_sale.sales_router.helper" + + def _get_domain_adapter(self): + return [ + ("partner_id", "=", self.partner.id), + ("typology", "=", "quote"), + ] + + def _process_confirm_quotation(self, quotation, data): + """Process the quotation confirmation + Can be inherited if you expect specific params + for confirming a quotation""" + return quotation.action_confirm_quotation() + + def _confirm(self, quotation_id, data): + quotation = self._get(quotation_id) + self._process_confirm_quotation(quotation, data) + return quotation + + def _update(self, quotation_id, data): + quotation = self._get(quotation_id) + quotation.write(data.to_sale_order_vals()) + return quotation + + def _create(self, rqst: QuotationCreateRequest) -> SaleOrder: + vals = rqst.model_dump() + vals["partner_id"] = self.partner.id + + if "lines" in vals: + vals["order_line"] = [ + Command.create( + { + "product_id": line["product_id"], + "product_uom_qty": line["quantity"], + } + ) + # use "pop" so that "lines" does not stay in "vals" + # preventing an error in the "create" below + for line in vals.pop("lines") + ] + + quotation = self.env["sale.order"].create(vals) + + return quotation + + def _add_lines( + self, quotation_id: int, rqst: QuotationLines[QuotationAddLineRequest] + ) -> SaleOrder: + quotation = self._get(quotation_id) + for line in rqst.lines: + quotation.write( + { + "order_line": [ + Command.create( + { + "product_id": line.product_id, + "product_uom_qty": line.quantity, + } + ) + ] + } + ) + return quotation + + def _update_lines( + self, quotation_id: int, rqst: QuotationLines[QuotationUpdateLineRequest] + ) -> SaleOrder: + quotation = self._get(quotation_id) + existing_lines_ids = {line.id for line in quotation.order_line} + vals = {"order_line": []} + for update_line_rqst in rqst.lines: + if update_line_rqst.line_id not in existing_lines_ids: + raise MissingError( + f"Line with ID {update_line_rqst.line_id} " + f"not found in quotation {quotation_id}" + ) + vals["order_line"].append( + Command.update( + update_line_rqst.line_id, + { + "product_id": update_line_rqst.product_id, + "product_uom_qty": update_line_rqst.quantity, + "sequence": update_line_rqst.sequence, + }, + ) + ) + quotation.write(vals) + return quotation + + def _delete_lines( + self, quotation_id: int, rqst: QuotationLines[QuotationDeleteLineRequest] + ) -> SaleOrder: + quotation = self._get(quotation_id) + existing_lines_ids = {line.id for line in quotation.order_line} + vals = {"order_line": []} + for delete_line_rqst in rqst.lines: + if delete_line_rqst.line_id not in existing_lines_ids: + raise MissingError( + f"Line with ID {delete_line_rqst.line_id} " + f"not found in quotation {quotation_id}" + ) + vals["order_line"].append(Command.delete(delete_line_rqst.line_id)) + quotation.write(vals) + return quotation + + +def quotation_router_helper( + env: Annotated[Environment, Depends(authenticated_partner_env)], + partner: Annotated[ResPartner, Depends(authenticated_partner)], +) -> ShopinvaderApiQuotationRouterHelper: + return env["shopinvader_api_quotation.router.helper"].new({"partner": partner}) diff --git a/shopinvader_api_quotation/routers/quotation.py b/shopinvader_api_quotation/routers/quotation.py index 4cf3316655..bdfa538cbe 100644 --- a/shopinvader_api_quotation/routers/quotation.py +++ b/shopinvader_api_quotation/routers/quotation.py @@ -3,51 +3,58 @@ from fastapi import APIRouter, Depends from fastapi.responses import FileResponse, StreamingResponse -from odoo import models -from odoo.api import Environment from odoo.http import content_disposition -from odoo.addons.base.models.res_partner import Partner as ResPartner from odoo.addons.extendable_fastapi.schemas import PagedCollection -from odoo.addons.fastapi.dependencies import ( - authenticated_partner, - authenticated_partner_env, - paging, -) +from odoo.addons.fastapi.dependencies import paging from odoo.addons.fastapi.schemas import Paging from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale, SaleSearch -from ..schemas.sale import QuotationConfirmInput, QuotationUpdateInput +from ..dependencies import ShopinvaderApiQuotationRouterHelper, quotation_router_helper +from ..schemas.sale import ( + QuotationAddLineRequest, + QuotationConfirmInput, + QuotationCreateRequest, + QuotationDeleteLineRequest, + QuotationLines, + QuotationUpdateInput, + QuotationUpdateLineRequest, +) -# create a router quotation_router = APIRouter(tags=["quotations"]) +@quotation_router.post("/quotations/create", status_code=201) +def create_quotation( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + rqst: QuotationCreateRequest, +) -> Sale: + quotation = quotation_router_helper._create(rqst) + return Sale.from_sale_order(quotation) + + @quotation_router.get("/quotations/{quotation_id}") def get( - env: Annotated[Environment, Depends(authenticated_partner_env)], - partner: Annotated[ResPartner, Depends(authenticated_partner)], + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], quotation_id: int, ) -> Sale | None: - return Sale.from_sale_order( - env["shopinvader_api_quotation.quotations_router.helper"] - .new({"partner": partner}) - ._get(quotation_id) - ) + quotation = quotation_router_helper._get(quotation_id) + return Sale.from_sale_order(quotation) @quotation_router.post("/quotations/{quotation_id}/confirm", status_code=200) def confirm_quotation( - env: Annotated[Environment, Depends(authenticated_partner_env)], - partner: Annotated[ResPartner, Depends(authenticated_partner)], + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], quotation_id: int, data: QuotationConfirmInput | None = None, ) -> None: - order = ( - env["shopinvader_api_quotation.quotations_router.helper"] - .new({"partner": partner}) - ._confirm(quotation_id, data) - ) + order = quotation_router_helper._confirm(quotation_id, data) return Sale.from_sale_order(order) @@ -55,14 +62,11 @@ def confirm_quotation( def search_quotation( params: Annotated[SaleSearch, Depends()], paging: Annotated[Paging, Depends(paging)], - env: Annotated[Environment, Depends(authenticated_partner_env)], - partner: Annotated[ResPartner, Depends(authenticated_partner)], + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], ) -> PagedCollection[Sale]: - count, orders = ( - env["shopinvader_api_quotation.quotations_router.helper"] - .new({"partner": partner}) - ._search(paging, params) - ) + count, orders = quotation_router_helper._search(paging, params) return PagedCollection[Sale]( count=count, items=[Sale.from_sale_order(order) for order in orders], @@ -72,30 +76,24 @@ def search_quotation( @quotation_router.post("/quotations/{quotation_id}") def update_quotation( data: QuotationUpdateInput, - env: Annotated[Environment, Depends(authenticated_partner_env)], - partner: Annotated["ResPartner", Depends(authenticated_partner)], + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], quotation_id: int, ) -> Sale: - order = ( - env["shopinvader_api_quotation.quotations_router.helper"] - .new({"partner": partner}) - ._update(quotation_id, data) - ) + order = quotation_router_helper._update(quotation_id, data) return Sale.from_sale_order(order) @quotation_router.get("/quotations/{quotation_id}/download") def download( quotation_id: int, - env: Annotated[Environment, Depends(authenticated_partner_env)], - partner: Annotated[ResPartner, Depends(authenticated_partner)], + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], ) -> FileResponse: """Download document.""" - filename, pdf = ( - env["shopinvader_api_quotation.quotations_router.helper"] - .new({"partner": partner}) - ._get_pdf(quotation_id) - ) + filename, pdf = quotation_router_helper._get_pdf(quotation_id) if not filename.lower().endswith(".pdf"): filename += ".pdf" header = { @@ -110,29 +108,79 @@ def pseudo_stream(): ) -class ShopinvaderApiSaleSalesRouterHelper(models.AbstractModel): - _name = "shopinvader_api_quotation.quotations_router.helper" - _inherit = "shopinvader_api_sale.sales_router.helper" - _description = "Shopinvader api sale router helper" - - def _get_domain_adapter(self): - return [ - ("partner_id", "=", self.partner.id), - ("quotation_state", "in", ("customer_request", "waiting_acceptation")), - ] - - def _process_confirm_quotation(self, quotation, data): - """Process the quotation confirmation - Can be inherited if you expect specific params - for confirming a quotation""" - return quotation.action_confirm_quotation() - - def _confirm(self, quotation_id, data): - quotation = self._get(quotation_id) - self._process_confirm_quotation(quotation, data) - return quotation - - def _update(self, quotation_id, data): - quotation = self._get(quotation_id) - quotation.write(data.to_sale_order_vals()) - return quotation +@quotation_router.post("/quotations/{quotation_id}/add_line", status_code=201) +def add_line( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + quotation_id: int, + rqst: QuotationAddLineRequest, +) -> Sale: + quotation = quotation_router_helper._add_lines( + quotation_id, QuotationLines[QuotationAddLineRequest](lines=[rqst]) + ) + return Sale.from_sale_order(quotation) + + +@quotation_router.post("/quotations/{quotation_id}/add_lines", status_code=201) +def add_lines( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + quotation_id: int, + rqst: QuotationLines[QuotationAddLineRequest], +) -> Sale: + quotation = quotation_router_helper._add_lines(quotation_id, rqst) + return Sale.from_sale_order(quotation) + + +@quotation_router.put("/quotations/{quotation_id}/update_line", status_code=200) +def update_line( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + quotation_id: int, + rqst: QuotationUpdateLineRequest, +) -> Sale: + quotation = quotation_router_helper._update_lines( + quotation_id, QuotationLines[QuotationUpdateLineRequest](lines=[rqst]) + ) + return Sale.from_sale_order(quotation) + + +@quotation_router.put("/quotations/{quotation_id}/update_lines", status_code=200) +def update_lines( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + quotation_id: int, + rqst: QuotationLines[QuotationUpdateLineRequest], +) -> Sale: + quotation = quotation_router_helper._update_lines(quotation_id, rqst) + return Sale.from_sale_order(quotation) + + +@quotation_router.post("/quotations/{quotation_id}/delete_line", status_code=200) +def delete_line( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + quotation_id: int, + rqst: QuotationDeleteLineRequest, +) -> Sale: + quotation = quotation_router_helper._delete_lines( + quotation_id, QuotationLines[QuotationDeleteLineRequest](lines=[rqst]) + ) + return Sale.from_sale_order(quotation) + + +@quotation_router.post("/quotations/{quotation_id}/delete_lines", status_code=200) +def delete_lines( + quotation_router_helper: Annotated[ + ShopinvaderApiQuotationRouterHelper, Depends(quotation_router_helper) + ], + quotation_id: int, + rqst: QuotationLines[QuotationDeleteLineRequest], +) -> Sale: + quotation = quotation_router_helper._delete_lines(quotation_id, rqst) + return Sale.from_sale_order(quotation) diff --git a/shopinvader_api_quotation/schemas/sale.py b/shopinvader_api_quotation/schemas/sale.py index 8a63debb89..ce237d74cd 100644 --- a/shopinvader_api_quotation/schemas/sale.py +++ b/shopinvader_api_quotation/schemas/sale.py @@ -1,5 +1,9 @@ +from typing import Generic, TypeVar + from extendable_pydantic import StrictExtendableBaseModel +from odoo import Command + from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale @@ -13,12 +17,63 @@ def from_sale_order(cls, odoo_rec): return res -class QuotationUpdateInput(StrictExtendableBaseModel, extra="ignore"): +class QuotationConfirmInput(StrictExtendableBaseModel): + """Extend it if you need params for the confirmation""" + + +class QuotationLineCreateRequest(StrictExtendableBaseModel, extra="ignore"): + sequence: int | None = None + product_id: int + quantity: float + + +class QuotationCreateRequest(StrictExtendableBaseModel, extra="ignore"): + name: str + lines: list[QuotationLineCreateRequest] | None = None client_order_ref: str | None = None + typology: str - def to_sale_order_vals(self) -> dict: - return {"client_order_ref": self.client_order_ref} +class QuotationAddLineRequest(StrictExtendableBaseModel, extra="ignore"): + sequence: int | None = None + product_id: int + quantity: float -class QuotationConfirmInput(StrictExtendableBaseModel): - """Extend it if you need params for the confirmation""" + +class QuotationUpdateLineRequest(StrictExtendableBaseModel, extra="ignore"): + line_id: int + sequence: int | None = None + product_id: int + quantity: float + + +class QuotationDeleteLineRequest(StrictExtendableBaseModel, extra="ignore"): + line_id: int + + +T = TypeVar("T") + + +class QuotationLines(StrictExtendableBaseModel, Generic[T]): + lines: list[T] = [] + + +class QuotationUpdateInput(StrictExtendableBaseModel, extra="ignore"): + client_order_ref: str | None = None + lines: list[QuotationUpdateLineRequest] | None = None + + def to_sale_order_vals(self) -> dict: + return { + "client_order_ref": self.client_order_ref, + "order_line": [ + Command.update( + line.line_id, + { + "product_id": line.product_id, + "product_uom_qty": line.quantity, + "sequence": line.sequence, + }, + ) + for line in self.lines + ], + } diff --git a/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py b/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py index 1d3b390246..2b3c5e58e8 100644 --- a/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py +++ b/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py @@ -5,6 +5,7 @@ from fastapi import status from requests import Response +from odoo import Command from odoo.tests.common import tagged from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase @@ -24,7 +25,7 @@ def setUpClass(cls) -> None: { "name": "Test User Without Rights", "login": "user_no_rights", - "groups_id": [(6, 0, [])], + "groups_id": [Command.set([])], } ) user_with_rights = cls.env["res.users"].create( @@ -33,13 +34,13 @@ def setUpClass(cls) -> None: "login": "user_with_rights", "groups_id": [ ( - 6, - 0, - [ - cls.env.ref( - "shopinvader_api_security_sale.shopinvader_sale_user_group" - ).id, - ], + Command.set( + [ + cls.env.ref( + "shopinvader_api_security_sale.shopinvader_sale_user_group" + ).id, + ] + ) ) ], } @@ -65,11 +66,37 @@ def setUpClass(cls) -> None: } ) + cls.quotation = cls.env["sale.order"].create( + { + "partner_id": cls.default_fastapi_authenticated_partner.id, + "quotation_state": "waiting_acceptation", + "order_line": [ + Command.create( + { + "product_id": cls.product_1.id, + "product_uom_qty": 1.0, + } + ), + Command.create( + { + "product_id": cls.product_2.id, + "product_uom_qty": 2.0, + } + ), + ], + } + ) + + def test_default_typology(self): + self.assertEqual(self.quotation.typology, "quote") + def test_search_quotations(self): + # This one should not be returned as this is a "sale" and not a "quote" self.env["sale.order"].create( { "partner_id": self.default_fastapi_authenticated_partner.id, "quotation_state": "waiting_acceptation", + "typology": "sale", } ) with self._create_test_client() as test_client: @@ -78,16 +105,10 @@ def test_search_quotations(self): self.assertEqual(response.json()["count"], 1) def test_get_quotation(self): - sale = self.env["sale.order"].create( - { - "partner_id": self.default_fastapi_authenticated_partner.id, - "quotation_state": "waiting_acceptation", - } - ) with self._create_test_client() as test_client: - response: Response = test_client.get(f"/quotations/{sale.id}") + response: Response = test_client.get(f"/quotations/{self.quotation.id}") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["name"], sale.name) + self.assertEqual(response.json()["name"], self.quotation.name) def test_confirm_quotation(self): quotation = self.env["sale.order"].create( @@ -113,13 +134,18 @@ def test_request_quotation(self): self.assertEqual(response_json["typology"], "sale") def test_update_quotation(self): - data = {"client_order_ref": "PO_123123"} - quotation = self.env["sale.order"].create( - { - "partner_id": self.default_fastapi_authenticated_partner.id, - "quotation_state": "waiting_acceptation", - } - ) + quotation = self.quotation + data = { + "client_order_ref": "PO_123123", + "lines": [ + { + "line_id": quotation.order_line[0].id, + "sequence": 42, + "product_id": self.product_2.id, + "quantity": 123, + } + ], + } with self._create_test_client() as test_client: response: Response = test_client.post( f"/quotations/{quotation.id}", content=json.dumps(data) @@ -131,4 +157,213 @@ def test_update_quotation(self): ) response_json = response.json() self.assertEqual(response_json["id"], quotation.id) - self.assertEqual(response_json["client_order_ref"], "PO_123123") + self.assertEqual(quotation.client_order_ref, "PO_123123") + self.assertEqual(quotation.order_line[0].product_id, self.product_2) + self.assertEqual(quotation.order_line[0].product_uom_qty, 123) + self.assertEqual(quotation.order_line[0].sequence, 42) + + def test_create_quotation(self): + data = { + "name": "Test Quotation", + "typology": "quote", + "client_order_ref": "PO_12345", + "lines": [ + { + "product_id": self.product_1.id, + "quantity": 1.0, + }, + { + "product_id": self.product_2.id, + "quantity": 2.0, + }, + ], + } + + with self._create_test_client() as test_client: + response: Response = test_client.post( + "/quotations/create", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response_json = response.json() + self.assertIn("id", response_json) + self.assertEqual(response_json["name"], "Test Quotation") + self.assertEqual(response_json["typology"], "quote") + self.assertEqual(response_json["client_order_ref"], "PO_12345") + + created_quotation = self.env["sale.order"].browse(response_json["id"]) + self.assertTrue(created_quotation.exists()) + self.assertEqual(created_quotation.name, "Test Quotation") + self.assertEqual( + created_quotation.partner_id.id, + self.default_fastapi_authenticated_partner.id, + ) + self.assertEqual(len(created_quotation.order_line), 2) + self.assertEqual( + created_quotation.order_line[0].product_id.id, self.product_1.id + ) + self.assertEqual(created_quotation.order_line[0].product_uom_qty, 1.0) + self.assertEqual( + created_quotation.order_line[1].product_id.id, self.product_2.id + ) + self.assertEqual(created_quotation.order_line[1].product_uom_qty, 2.0) + + def test_add_quotation_line(self): + quotation = self.quotation + + data = { + "product_id": self.product_1.id, + "quantity": 3.0, + } + + with self._create_test_client() as test_client: + response: Response = test_client.post( + f"/quotations/{quotation.id}/add_line", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + response_json = response.json() + self.assertEqual(response_json["id"], quotation.id) + + self.assertEqual(len(quotation.order_line), 3) + self.assertEqual(quotation.order_line[-1].product_id.id, self.product_1.id) + self.assertEqual(quotation.order_line[-1].product_uom_qty, 3.0) + + def test_add_quotation_lines(self): + quotation = self.quotation + + data = { + "lines": [ + { + "product_id": self.product_1.id, + "quantity": 3.0, + }, + { + "product_id": self.product_2.id, + "quantity": 4.0, + }, + ] + } + + with self._create_test_client() as test_client: + response: Response = test_client.post( + f"/quotations/{quotation.id}/add_lines", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + response_json = response.json() + self.assertEqual(response_json["id"], quotation.id) + + self.assertEqual(len(quotation.order_line), 4) + self.assertEqual(quotation.order_line[-2].product_id.id, self.product_1.id) + self.assertEqual(quotation.order_line[-2].product_uom_qty, 3.0) + self.assertEqual(quotation.order_line[-1].product_id.id, self.product_2.id) + self.assertEqual(quotation.order_line[-1].product_uom_qty, 4.0) + + def test_update_quotation_line(self): + quotation = self.quotation + line_id = quotation.order_line[0].id + data = { + "line_id": line_id, + "quantity": 42.0, + "product_id": self.product_2.id, + "sequence": 42, + } + + with self._create_test_client() as test_client: + response: Response = test_client.put( + f"/quotations/{quotation.id}/update_line", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_json = response.json() + self.assertEqual(response_json["id"], quotation.id) + + self.assertEqual(len(quotation.order_line), 2) + self.assertEqual(quotation.order_line[0].id, line_id) + self.assertEqual(quotation.order_line[0].product_uom_qty, 42) + self.assertEqual(quotation.order_line[0].product_id.id, self.product_2.id) + self.assertEqual(quotation.order_line[0].sequence, 42) + + def test_update_quotation_lines(self): + quotation = self.quotation + data = { + "lines": [ + { + "line_id": quotation.order_line[0].id, + "quantity": 10, + "product_id": self.product_2.id, + "sequence": 10, + }, + { + "line_id": quotation.order_line[1].id, + "quantity": 20, + "product_id": self.product_1.id, + "sequence": 20, + }, + ] + } + + with self._create_test_client() as test_client: + response: Response = test_client.put( + f"/quotations/{quotation.id}/update_lines", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_json = response.json() + self.assertEqual(response_json["id"], quotation.id) + + self.assertEqual(len(quotation.order_line), 2) + + self.assertEqual(quotation.order_line[0].id, data["lines"][0]["line_id"]) + self.assertEqual(quotation.order_line[0].product_uom_qty, 10) + self.assertEqual(quotation.order_line[0].product_id.id, self.product_2.id) + self.assertEqual(quotation.order_line[0].sequence, 10) + + self.assertEqual(quotation.order_line[1].id, data["lines"][1]["line_id"]) + self.assertEqual(quotation.order_line[1].product_uom_qty, 20) + self.assertEqual(quotation.order_line[1].product_id.id, self.product_1.id) + self.assertEqual(quotation.order_line[1].sequence, 20) + + def test_delete_quotation_line(self): + quotation = self.quotation + line_id = quotation.order_line[0].id + data = { + "line_id": line_id, + } + + with self._create_test_client() as test_client: + response: Response = test_client.post( + f"/quotations/{quotation.id}/delete_line", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_json = response.json() + self.assertEqual(response_json["id"], quotation.id) + + self.assertEqual(len(quotation.order_line), 1) + + def test_delete_quotation_lines(self): + quotation = self.quotation + data = { + "lines": [ + { + "line_id": quotation.order_line[0].id, + }, + { + "line_id": quotation.order_line[1].id, + }, + ] + } + + with self._create_test_client() as test_client: + response: Response = test_client.post( + f"/quotations/{quotation.id}/delete_lines", content=json.dumps(data) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_json = response.json() + self.assertEqual(response_json["id"], quotation.id) + + self.assertEqual(len(quotation.order_line), 0) From 5392b3ddfb438004039602c0245a7b53c0410f86 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Wed, 21 May 2025 10:24:54 +0200 Subject: [PATCH 6/8] [ADD] shopinvader_api_cart_quotation: add a module to isolate the logic of transforming a cart to a quotation. --- .../addons/shopinvader_api_cart_quotation | 1 + setup/shopinvader_api_cart_quotation/setup.py | 6 + shopinvader_api_cart_quotation/README.rst | 60 +++ shopinvader_api_cart_quotation/__init__.py | 1 + .../__manifest__.py | 14 + .../i18n/shopinvader_api_cart_quotation.pot | 15 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../routers/__init__.py | 1 + .../routers/cart.py | 8 +- .../static/description/index.html | 416 ++++++++++++++++++ .../tests/__init__.py | 1 + .../test_shopinvader_api_cart_quotation.py | 59 +++ shopinvader_api_quotation/routers/__init__.py | 1 - .../tests/test_shopinvader_api_quotation.py | 13 +- 15 files changed, 581 insertions(+), 17 deletions(-) create mode 120000 setup/shopinvader_api_cart_quotation/odoo/addons/shopinvader_api_cart_quotation create mode 100644 setup/shopinvader_api_cart_quotation/setup.py create mode 100644 shopinvader_api_cart_quotation/README.rst create mode 100644 shopinvader_api_cart_quotation/__init__.py create mode 100644 shopinvader_api_cart_quotation/__manifest__.py create mode 100644 shopinvader_api_cart_quotation/i18n/shopinvader_api_cart_quotation.pot create mode 100644 shopinvader_api_cart_quotation/readme/CONTRIBUTORS.rst create mode 100644 shopinvader_api_cart_quotation/readme/DESCRIPTION.rst create mode 100644 shopinvader_api_cart_quotation/routers/__init__.py rename {shopinvader_api_quotation => shopinvader_api_cart_quotation}/routers/cart.py (83%) create mode 100644 shopinvader_api_cart_quotation/static/description/index.html create mode 100644 shopinvader_api_cart_quotation/tests/__init__.py create mode 100644 shopinvader_api_cart_quotation/tests/test_shopinvader_api_cart_quotation.py diff --git a/setup/shopinvader_api_cart_quotation/odoo/addons/shopinvader_api_cart_quotation b/setup/shopinvader_api_cart_quotation/odoo/addons/shopinvader_api_cart_quotation new file mode 120000 index 0000000000..b04403464c --- /dev/null +++ b/setup/shopinvader_api_cart_quotation/odoo/addons/shopinvader_api_cart_quotation @@ -0,0 +1 @@ +../../../../shopinvader_api_cart_quotation \ No newline at end of file diff --git a/setup/shopinvader_api_cart_quotation/setup.py b/setup/shopinvader_api_cart_quotation/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/shopinvader_api_cart_quotation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopinvader_api_cart_quotation/README.rst b/shopinvader_api_cart_quotation/README.rst new file mode 100644 index 0000000000..4c009f5c1e --- /dev/null +++ b/shopinvader_api_cart_quotation/README.rst @@ -0,0 +1,60 @@ +============================== +Shopinvader Api Cart Quotation +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:34a803016d1c4168229e05a2f73933ab0457957f9985ea347e670a1cc816825a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github + :target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/shopinvader_api_cart_quotation + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| + +This module adds a REST API for shopinvader to manage the transformation of carts into quotations. + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Nicolas DELBOVIER + +Maintainers +~~~~~~~~~~~ + +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. + +You are welcome to contribute. diff --git a/shopinvader_api_cart_quotation/__init__.py b/shopinvader_api_cart_quotation/__init__.py new file mode 100644 index 0000000000..62a5d54f85 --- /dev/null +++ b/shopinvader_api_cart_quotation/__init__.py @@ -0,0 +1 @@ +from . import routers diff --git a/shopinvader_api_cart_quotation/__manifest__.py b/shopinvader_api_cart_quotation/__manifest__.py new file mode 100644 index 0000000000..d7211cc3d2 --- /dev/null +++ b/shopinvader_api_cart_quotation/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shopinvader Api Cart Quotation", + "summary": """cart-to-quote management module""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV", + "website": "https://github.com/shopinvader/odoo-shopinvader", + "depends": ["shopinvader_api_quotation", "sale_cart_quotation"], + "data": [], + "demo": [], +} diff --git a/shopinvader_api_cart_quotation/i18n/shopinvader_api_cart_quotation.pot b/shopinvader_api_cart_quotation/i18n/shopinvader_api_cart_quotation.pot new file mode 100644 index 0000000000..80562d8f3d --- /dev/null +++ b/shopinvader_api_cart_quotation/i18n/shopinvader_api_cart_quotation.pot @@ -0,0 +1,15 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-21 06:38+0000\n" +"PO-Revision-Date: 2025-05-21 06:38+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/shopinvader_api_cart_quotation/readme/CONTRIBUTORS.rst b/shopinvader_api_cart_quotation/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..eaed2d826a --- /dev/null +++ b/shopinvader_api_cart_quotation/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Nicolas DELBOVIER diff --git a/shopinvader_api_cart_quotation/readme/DESCRIPTION.rst b/shopinvader_api_cart_quotation/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..2d0e6e6cb4 --- /dev/null +++ b/shopinvader_api_cart_quotation/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds a REST API for shopinvader to manage the transformation of carts into quotations. \ No newline at end of file diff --git a/shopinvader_api_cart_quotation/routers/__init__.py b/shopinvader_api_cart_quotation/routers/__init__.py new file mode 100644 index 0000000000..7a2ac32694 --- /dev/null +++ b/shopinvader_api_cart_quotation/routers/__init__.py @@ -0,0 +1 @@ +from .cart import cart_quotation_router diff --git a/shopinvader_api_quotation/routers/cart.py b/shopinvader_api_cart_quotation/routers/cart.py similarity index 83% rename from shopinvader_api_quotation/routers/cart.py rename to shopinvader_api_cart_quotation/routers/cart.py index 64f27e2514..39db676035 100644 --- a/shopinvader_api_quotation/routers/cart.py +++ b/shopinvader_api_cart_quotation/routers/cart.py @@ -12,12 +12,12 @@ ) from odoo.addons.shopinvader_schema_sale.schemas.sale import Sale -quotation_cart_router = APIRouter(tags=["carts"]) +cart_quotation_router = APIRouter(tags=["carts"]) -@quotation_cart_router.post("/{uuid}/request_quotation") -@quotation_cart_router.post("/current/request_quotation") -@quotation_cart_router.post("/request_quotation") +@cart_quotation_router.post("/{uuid}/request_quotation") +@cart_quotation_router.post("/current/request_quotation") +@cart_quotation_router.post("/request_quotation") def request_quotation( env: Annotated[api.Environment, Depends(authenticated_partner_env)], partner: Annotated[ResPartner, Depends(authenticated_partner)], diff --git a/shopinvader_api_cart_quotation/static/description/index.html b/shopinvader_api_cart_quotation/static/description/index.html new file mode 100644 index 0000000000..5449f93f0d --- /dev/null +++ b/shopinvader_api_cart_quotation/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +Shopinvader Api Cart Quotation + + + +
    +

    Shopinvader Api Cart Quotation

    + + +

    Beta License: AGPL-3 shopinvader/odoo-shopinvader

    +

    This module adds a REST API for shopinvader to manage the transformation of carts into quotations.

    +

    Table of contents

    + +
    +

    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 to smash it by providing a detailed and welcomed +feedback.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • ACSONE SA/NV
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is part of the shopinvader/odoo-shopinvader project on GitHub.

    +

    You are welcome to contribute.

    +
    +
    +
    + + diff --git a/shopinvader_api_cart_quotation/tests/__init__.py b/shopinvader_api_cart_quotation/tests/__init__.py new file mode 100644 index 0000000000..2e8fee4862 --- /dev/null +++ b/shopinvader_api_cart_quotation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_shopinvader_api_cart_quotation diff --git a/shopinvader_api_cart_quotation/tests/test_shopinvader_api_cart_quotation.py b/shopinvader_api_cart_quotation/tests/test_shopinvader_api_cart_quotation.py new file mode 100644 index 0000000000..789e4aa0a6 --- /dev/null +++ b/shopinvader_api_cart_quotation/tests/test_shopinvader_api_cart_quotation.py @@ -0,0 +1,59 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from fastapi import status +from requests import Response + +from odoo import Command +from odoo.tests.common import tagged + +from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase + +from ..routers import cart_quotation_router + + +@tagged("post_install", "-at_install") +class TestCartQuotation(FastAPITransactionCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + partner = cls.env["res.partner"].create({"name": "FastAPI Cart Demo"}) + + cls.user_no_rights = cls.env["res.users"].create( + { + "name": "Test User Without Rights", + "login": "user_no_rights", + "groups_id": [Command.set([])], + } + ) + user_with_rights = cls.env["res.users"].create( + { + "name": "Test User With Rights", + "login": "user_with_rights", + "groups_id": [ + ( + Command.set( + [ + cls.env.ref( + "shopinvader_api_security_sale.shopinvader_sale_user_group" + ).id, + ] + ) + ) + ], + } + ) + cls.default_fastapi_running_user = user_with_rights + cls.default_fastapi_authenticated_partner = partner.with_user(user_with_rights) + cls.default_fastapi_router = cart_quotation_router + + def test_request_quotation(self): + cart = self.env["sale.order"]._create_empty_cart( + self.default_fastapi_authenticated_partner.id + ) + with self._create_test_client() as test_client: + response: Response = test_client.post(f"/{cart.uuid}/request_quotation") + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_json = response.json() + self.assertEqual(response_json["uuid"], cart.uuid) + self.assertEqual(response_json["typology"], "sale") diff --git a/shopinvader_api_quotation/routers/__init__.py b/shopinvader_api_quotation/routers/__init__.py index 662b059b89..87ad64f4c0 100644 --- a/shopinvader_api_quotation/routers/__init__.py +++ b/shopinvader_api_quotation/routers/__init__.py @@ -1,2 +1 @@ from .quotation import quotation_router -from .cart import quotation_cart_router diff --git a/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py b/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py index 2b3c5e58e8..116075a08a 100644 --- a/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py +++ b/shopinvader_api_quotation/tests/test_shopinvader_api_quotation.py @@ -10,7 +10,7 @@ from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase -from ..routers import quotation_cart_router, quotation_router +from ..routers import quotation_router @tagged("post_install", "-at_install") @@ -122,17 +122,6 @@ def test_confirm_quotation(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["id"], quotation.id) - def test_request_quotation(self): - cart = self.env["sale.order"]._create_empty_cart( - self.default_fastapi_authenticated_partner.id - ) - with self._create_test_client(router=quotation_cart_router) as test_client: - response: Response = test_client.post(f"/{cart.uuid}/request_quotation") - self.assertEqual(response.status_code, status.HTTP_200_OK) - response_json = response.json() - self.assertEqual(response_json["uuid"], cart.uuid) - self.assertEqual(response_json["typology"], "sale") - def test_update_quotation(self): quotation = self.quotation data = { From f322469d383746427c5fe7d4169bbdb553b394c8 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 26 May 2025 17:08:02 +0200 Subject: [PATCH 7/8] [IMP] sale_cart_quotation: send a notification email to sales_team when client requests a quotation. --- sale_cart_quotation/__manifest__.py | 4 +- sale_cart_quotation/data/mail_templates.xml | 31 +++++ sale_cart_quotation/models/sale_order.py | 34 ++++- sale_cart_quotation/tests/__init__.py | 1 + sale_cart_quotation/tests/test_quotation.py | 4 +- .../tests/test_quotation_notifications.py | 116 ++++++++++++++++++ 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 sale_cart_quotation/data/mail_templates.xml create mode 100644 sale_cart_quotation/tests/test_quotation_notifications.py diff --git a/sale_cart_quotation/__manifest__.py b/sale_cart_quotation/__manifest__.py index b2a8636aaf..a8b8c2dd66 100644 --- a/sale_cart_quotation/__manifest__.py +++ b/sale_cart_quotation/__manifest__.py @@ -9,8 +9,6 @@ "author": "ACSONE SA/NV", "website": "https://github.com/shopinvader/odoo-shopinvader", "depends": ["sale_quotation", "sale_cart"], - "data": [ - "views/sale_order.xml", - ], + "data": ["views/sale_order.xml", "data/mail_templates.xml"], "demo": [], } diff --git a/sale_cart_quotation/data/mail_templates.xml b/sale_cart_quotation/data/mail_templates.xml new file mode 100644 index 0000000000..3b1363dc20 --- /dev/null +++ b/sale_cart_quotation/data/mail_templates.xml @@ -0,0 +1,31 @@ + + + Quotation Request + + {{ object.user_id.company_id.name }} Quotation Request (Ref {{ object.name }}) + {{ object.user_id.company_id.email_formatted }} + Used to notify the sales team when a client requests a quotation. + +
    +

    Dear Sales Team,

    +

    + A new quotation () has been requested by + the client . +

    + -- +

    + +

    +
    +
    + + {{ object.partner_id.lang }} + +
    +
    diff --git a/sale_cart_quotation/models/sale_order.py b/sale_cart_quotation/models/sale_order.py index 6a37793c28..c9261466d5 100644 --- a/sale_cart_quotation/models/sale_order.py +++ b/sale_cart_quotation/models/sale_order.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError class SaleOrder(models.Model): @@ -17,4 +17,36 @@ def action_request_quotation(self): ) ) self.write({"quotation_state": "customer_request", "typology": "sale"}) + self._send_request_notification() return True + + def _send_request_notification(self): + template = self.env.ref("sale_cart_quotation.mail_template_request_quotation") + for record in self: + recipients = self.env["res.partner"] + if record.user_id and record.user_id.partner_id: + recipients |= record.user_id.partner_id + + if record.team_id: + for member in record.team_id.member_ids: + recipients |= member.partner_id + + if not recipients: + raise ValidationError( + _( + "Unable to send quotation request email because no " + "valid recipients (salesperson or sales team members) " + "were found for quotation %s." % record.name + ) + ) + + # use the "mt_note" subtype to make sure only odoo users are notified + # (not the client) + record.with_context( + mail_notify_author=True, # author (ie user_id) is not notified by default + ).message_post_with_template( + template.id, + email_layout_xmlid="mail.mail_notification_layout", + subtype_id=self.env.ref("mail.mt_note").id, + partner_ids=recipients.ids, + ) diff --git a/sale_cart_quotation/tests/__init__.py b/sale_cart_quotation/tests/__init__.py index 43ae815229..62ca383172 100644 --- a/sale_cart_quotation/tests/__init__.py +++ b/sale_cart_quotation/tests/__init__.py @@ -1 +1,2 @@ from . import test_quotation +from . import test_quotation_notifications diff --git a/sale_cart_quotation/tests/test_quotation.py b/sale_cart_quotation/tests/test_quotation.py index 132b88e6f9..35556c4d98 100644 --- a/sale_cart_quotation/tests/test_quotation.py +++ b/sale_cart_quotation/tests/test_quotation.py @@ -1,10 +1,10 @@ # Copyright 2025 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo.tests.common import SavepointCase +from odoo.tests.common import TransactionCase -class TestQuotation(SavepointCase): +class TestQuotation(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() diff --git a/sale_cart_quotation/tests/test_quotation_notifications.py b/sale_cart_quotation/tests/test_quotation_notifications.py new file mode 100644 index 0000000000..d070d3dd8e --- /dev/null +++ b/sale_cart_quotation/tests/test_quotation_notifications.py @@ -0,0 +1,116 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.exceptions import ValidationError + +from odoo.addons.mail.tests.common import MailCommon + + +class TestQuotation(MailCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.product = cls.env["product.product"].create({"name": "Test Product"}) + + cls.salesperson_1 = cls.env["res.users"].create( + { + "name": "Salesperson One", + "login": "salesperson1", + "email": "salesperson1@example.com", + } + ) + cls.salesperson_2 = cls.env["res.users"].create( + { + "name": "Salesperson Two", + "login": "salesperson2", + "email": "salesperson2@example.com", + } + ) + cls.sales_team = cls.env["crm.team"].create( + { + "name": "Test Sales Team", + "member_ids": [ + Command.set([cls.salesperson_1.id, cls.salesperson_2.id]) + ], + } + ) + + cls.client_partner = cls.env["res.partner"].create( + { + "name": "Test Client", + "email": "client@example.com", + } + ) + + cls.so = cls.env["sale.order"].create( + { + "partner_id": cls.client_partner.id, + "user_id": cls.salesperson_1.id, + "team_id": cls.sales_team.id, + "state": "draft", + "typology": "cart", + "order_line": [ + Command.create( + { + "product_id": cls.product.id, + "product_uom_qty": 1, + "price_unit": 100, + } + ) + ], + } + ) + + def test_send_request_notification(self): + """Test if the email notification is sent to the correct recipients.""" + self.assertEqual(self.so.state, "draft") + self.assertEqual(self.so.typology, "cart") + + with self.mock_mail_gateway(): + self.so.action_request_quotation() + + expected_recipients = self.salesperson_1 | self.salesperson_2 + + self.assertMailMail( + recipients=expected_recipients.partner_id, + status="sent", + mail_message=self.so.message_ids, + email_values={ + "subject": f"{self.so.user_id.company_id.name} " + f"Quotation Request (Ref {self.so.name})", + "email_from": self.so.user_id.company_id.email_formatted, + }, + ) + + def test_send_request_notification_no_recipients(self): + """Test if UserError is raised when no recipients are found.""" + sale_order_no_recipients = self.env["sale.order"].create( + { + "partner_id": self.client_partner.id, + "state": "draft", + "typology": "cart", + "order_line": [ + Command.create( + { + "product_id": self.product.id, + "product_uom_qty": 1, + "price_unit": 100, + } + ) + ], + } + ) + sale_order_no_recipients.user_id = False + sale_order_no_recipients.team_id = False + + self.assertFalse(sale_order_no_recipients.user_id) + self.assertFalse(sale_order_no_recipients.team_id) + + with self.mock_mail_gateway(): + with self.assertRaises( + ValidationError, + msg="Should raise ValidationError if no recipients are found.", + ): + sale_order_no_recipients._send_request_notification() From 9c64dd646e03d05bf89541485605eb2a32faaf8a Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 26 May 2025 09:03:47 +0200 Subject: [PATCH 8/8] [IMP] sale_quotation: notify sales_team team when client accepts a quotation --- sale_quotation/README.rst | 2 +- sale_quotation/__manifest__.py | 4 +- sale_quotation/data/mail_templates.xml | 28 +++++ sale_quotation/models/sale_order.py | 46 ++++++- sale_quotation/readme/DESCRIPTION.rst | 2 +- sale_quotation/static/description/index.html | 2 +- sale_quotation/tests/__init__.py | 1 + .../tests/test_quotation_notifications.py | 114 ++++++++++++++++++ 8 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 sale_quotation/data/mail_templates.xml create mode 100644 sale_quotation/tests/test_quotation_notifications.py diff --git a/sale_quotation/README.rst b/sale_quotation/README.rst index 3e8882045f..89143d49f1 100644 --- a/sale_quotation/README.rst +++ b/sale_quotation/README.rst @@ -24,7 +24,7 @@ Sale Quotation This module is used in the context of e-commerce project (like shopinvader) -On Odoo Backoffice this module imporves the menu quotation by adding a new state (quotation_state). +On Odoo Backoffice this module improves the menu quotation by adding a new state (quotation_state). Then, you can easily process and follow the quotation request from your external system. .. image:: https://raw.githubusercontent.com/shopinvader/odoo-shopinvader/16.0/sale_quotation/static/description/quotation.png diff --git a/sale_quotation/__manifest__.py b/sale_quotation/__manifest__.py index c77168ab16..b088d5a3e3 100644 --- a/sale_quotation/__manifest__.py +++ b/sale_quotation/__manifest__.py @@ -13,8 +13,6 @@ "depends": [ "shopinvader_product_order_mode_on_request", ], - "data": [ - "views/sale_view.xml", - ], + "data": ["views/sale_view.xml", "data/mail_templates.xml"], "installable": True, } diff --git a/sale_quotation/data/mail_templates.xml b/sale_quotation/data/mail_templates.xml new file mode 100644 index 0000000000..06f1e3adb4 --- /dev/null +++ b/sale_quotation/data/mail_templates.xml @@ -0,0 +1,28 @@ + + + Quotation Accepted - Notify Sales Team + + {{ object.user_id.company_id.name }} Quotation Accepted (Ref {{ object.name }}) + {{ object.user_id.company_id.email_formatted }} + Used to notify the sales team when a client has accepted a quotation. + +
    +

    Dear Sales Team,

    +

    + The quotation has been accepted by + the client . +

    + --

    +
    +
    + + {{ object.partner_id.lang }} + +
    +
    diff --git a/sale_quotation/models/sale_order.py b/sale_quotation/models/sale_order.py index 8b75f6cdb2..a77422940c 100644 --- a/sale_quotation/models/sale_order.py +++ b/sale_quotation/models/sale_order.py @@ -6,7 +6,7 @@ from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError class SaleOrder(models.Model): @@ -44,6 +44,39 @@ def _compute_quotation_state(self): elif record.state == "sale": record.quotation_state = "accepted" + def _send_confirmation_email_to_sales_person_and_sales_team(self): + template = self.env.ref( + "sale_quotation.mail_template_quotation_accepted_for_sales_team" + ) + for record in self: + recipients = self.env["res.partner"] + if record.user_id and record.user_id.partner_id: + recipients |= record.user_id.partner_id + + if record.team_id: + for member in record.team_id.member_ids: + recipients |= member.partner_id + + if not recipients: + raise ValidationError( + _( + "Unable to send confirmation email because no valid " + "recipients (salesperson or sales team members) " + "were found for quotation %s." % record.name + ) + ) + + # use the "mt_note" subtype to make sure only odoo users are notified + # (not the client) + record.with_context( + mail_notify_author=True, # author (ie user_id) is not notified by default + ).message_post_with_template( + template.id, + email_layout_xmlid="mail.mail_notification_layout", + subtype_id=self.env.ref("mail.mt_note").id, + partner_ids=recipients.ids, + ) + def action_confirm_quotation(self): if any(rec.quotation_state != "waiting_acceptation" for rec in self): raise UserError( @@ -52,12 +85,15 @@ def action_confirm_quotation(self): "Confirmed." ) ) - self.quotation_state = "accepted" - self.typology = "sale" + for record in self: + record.quotation_state = "accepted" + record.typology = "sale" + record._send_confirmation_email_to_sales_person_and_sales_team() def action_confirm(self): - if self.typology == "quote": - self.action_confirm_quotation() + for record in self: + if record.typology == "quote": + record.action_confirm_quotation() return super().action_confirm() def action_draft(self): diff --git a/sale_quotation/readme/DESCRIPTION.rst b/sale_quotation/readme/DESCRIPTION.rst index 6cd8f3500a..676928afc9 100644 --- a/sale_quotation/readme/DESCRIPTION.rst +++ b/sale_quotation/readme/DESCRIPTION.rst @@ -1,6 +1,6 @@ This module is used in the context of e-commerce project (like shopinvader) -On Odoo Backoffice this module imporves the menu quotation by adding a new state (quotation_state). +On Odoo Backoffice this module improves the menu quotation by adding a new state (quotation_state). Then, you can easily process and follow the quotation request from your external system. .. image:: ../static/description/quotation.png diff --git a/sale_quotation/static/description/index.html b/sale_quotation/static/description/index.html index bc77cc85c2..7559bc05c9 100644 --- a/sale_quotation/static/description/index.html +++ b/sale_quotation/static/description/index.html @@ -371,7 +371,7 @@

    Sale Quotation

    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Production/Stable License: AGPL-3 shopinvader/odoo-shopinvader

    This module is used in the context of e-commerce project (like shopinvader)

    -

    On Odoo Backoffice this module imporves the menu quotation by adding a new state (quotation_state). +

    On Odoo Backoffice this module improves the menu quotation by adding a new state (quotation_state). Then, you can easily process and follow the quotation request from your external system.

    Widget in action

    Table of contents

    diff --git a/sale_quotation/tests/__init__.py b/sale_quotation/tests/__init__.py index 43ae815229..62ca383172 100644 --- a/sale_quotation/tests/__init__.py +++ b/sale_quotation/tests/__init__.py @@ -1 +1,2 @@ from . import test_quotation +from . import test_quotation_notifications diff --git a/sale_quotation/tests/test_quotation_notifications.py b/sale_quotation/tests/test_quotation_notifications.py new file mode 100644 index 0000000000..604fe9e2a3 --- /dev/null +++ b/sale_quotation/tests/test_quotation_notifications.py @@ -0,0 +1,114 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.exceptions import ValidationError + +from odoo.addons.mail.tests.common import MailCommon + + +class TestQuotationNotifications(MailCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.product = cls.env["product.product"].create({"name": "Test Product"}) + + cls.salesperson_1 = cls.env["res.users"].create( + { + "name": "Salesperson One", + "login": "salesperson1", + "email": "salesperson1@example.com", + } + ) + cls.salesperson_2 = cls.env["res.users"].create( + { + "name": "Salesperson Two", + "login": "salesperson2", + "email": "salesperson2@example.com", + } + ) + cls.sales_team = cls.env["crm.team"].create( + { + "name": "Test Sales Team", + "member_ids": [ + Command.set([cls.salesperson_1.id, cls.salesperson_2.id]) + ], + } + ) + + cls.client_partner = cls.env["res.partner"].create( + { + "name": "Test Client", + "email": "client@example.com", + } + ) + + cls.so = cls.env["sale.order"].create( + { + "partner_id": cls.client_partner.id, + "user_id": cls.salesperson_1.id, + "team_id": cls.sales_team.id, + "order_line": [ + Command.create( + { + "product_id": cls.product.id, + "product_uom_qty": 1, + "price_unit": 100, + } + ) + ], + "state": "sent", + "typology": "quote", + } + ) + + def test_send_confirmation_notification(self): + """Test if the email notification is sent to the correct recipients.""" + + with self.mock_mail_gateway(): + self.so.action_confirm() + + expected_recipients = self.salesperson_1 | self.salesperson_2 + + self.assertMailMail( + recipients=expected_recipients.partner_id, + status="sent", + mail_message=self.so.message_ids, + email_values={ + "subject": f"{self.so.user_id.company_id.name} " + f"Quotation Accepted (Ref {self.so.name})", + "email_from": self.so.user_id.company_id.email_formatted, + }, + ) + + def test_send_confirmation_notification_no_recipients(self): + """Test if UserError is raised when no recipients are found.""" + sale_order_no_recipients = self.env["sale.order"].create( + { + "partner_id": self.client_partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product.id, + "product_uom_qty": 1, + "price_unit": 100, + } + ) + ], + "state": "sent", + "typology": "quote", + } + ) + sale_order_no_recipients.user_id = False + sale_order_no_recipients.team_id = False + + self.assertFalse(sale_order_no_recipients.user_id) + self.assertFalse(sale_order_no_recipients.team_id) + + with self.mock_mail_gateway(): + with self.assertRaises( + ValidationError, + msg="Should raise ValidationError if no recipients are found.", + ): + sale_order_no_recipients.action_confirm()