From 2c2461a2d755973cb459914237ea049849986f7a Mon Sep 17 00:00:00 2001 From: Edilio Escalona Almira Date: Sun, 2 Aug 2026 16:13:11 -0500 Subject: [PATCH 1/5] [ADD] social_media_advertising_linkedin: Campaigns, campaign groups and ads for social media linkedin --- social_media_advertising_linkedin/README.rst | 351 ++++++ social_media_advertising_linkedin/__init__.py | 4 + .../__manifest__.py | 22 + .../data/social_stage_data.xml | 194 +++ .../i18n/es_ES.po | 1057 +++++++++++++++++ .../social_media_advertising_linkedin.pot | 933 +++++++++++++++ .../models/__init__.py | 9 + .../models/social_account.py | 757 ++++++++++++ .../models/social_campaign.py | 659 ++++++++++ .../models/social_campaign_group.py | 395 ++++++ .../models/social_media.py | 20 + .../models/social_post.py | 64 + .../models/social_post_account.py | 216 ++++ .../pyproject.toml | 3 + .../readme/CONFIGURE.md | 32 + .../readme/CONTRIBUTORS.md | 2 + .../readme/DESCRIPTION.md | 11 + .../readme/ROADMAP.md | 34 + .../readme/USAGE.md | 147 +++ .../social_advertising_linkedin_utils.py | 20 + .../static/description/icon.png | Bin 0 -> 4950 bytes .../static/description/index.html | 714 +++++++++++ .../static/img/readme/CAMPAIGN_ARCHIVE.png | Bin 0 -> 97676 bytes .../static/img/readme/CAMPAIGN_NOT_SYNCED.png | Bin 0 -> 129665 bytes .../tests/__init__.py | 8 + .../tests/test_common_advertising_linkedin.py | 90 ++ ...est_social_account_advertising_linkedin.py | 965 +++++++++++++++ .../test_social_campaign_group_linkedin.py | 145 +++ .../tests/test_social_campaign_linkedin.py | 482 ++++++++ .../test_social_post_advertising_linkedin.py | 350 ++++++ .../views/social_account_views.xml | 23 + .../views/social_campaign_group_views.xml | 133 +++ .../views/social_campaign_views.xml | 151 +++ 33 files changed, 7991 insertions(+) create mode 100644 social_media_advertising_linkedin/README.rst create mode 100644 social_media_advertising_linkedin/__init__.py create mode 100644 social_media_advertising_linkedin/__manifest__.py create mode 100644 social_media_advertising_linkedin/data/social_stage_data.xml create mode 100644 social_media_advertising_linkedin/i18n/es_ES.po create mode 100644 social_media_advertising_linkedin/i18n/social_media_advertising_linkedin.pot create mode 100644 social_media_advertising_linkedin/models/__init__.py create mode 100644 social_media_advertising_linkedin/models/social_account.py create mode 100644 social_media_advertising_linkedin/models/social_campaign.py create mode 100644 social_media_advertising_linkedin/models/social_campaign_group.py create mode 100644 social_media_advertising_linkedin/models/social_media.py create mode 100644 social_media_advertising_linkedin/models/social_post.py create mode 100644 social_media_advertising_linkedin/models/social_post_account.py create mode 100644 social_media_advertising_linkedin/pyproject.toml create mode 100644 social_media_advertising_linkedin/readme/CONFIGURE.md create mode 100644 social_media_advertising_linkedin/readme/CONTRIBUTORS.md create mode 100644 social_media_advertising_linkedin/readme/DESCRIPTION.md create mode 100644 social_media_advertising_linkedin/readme/ROADMAP.md create mode 100644 social_media_advertising_linkedin/readme/USAGE.md create mode 100644 social_media_advertising_linkedin/social_advertising_linkedin_utils.py create mode 100644 social_media_advertising_linkedin/static/description/icon.png create mode 100644 social_media_advertising_linkedin/static/description/index.html create mode 100644 social_media_advertising_linkedin/static/img/readme/CAMPAIGN_ARCHIVE.png create mode 100644 social_media_advertising_linkedin/static/img/readme/CAMPAIGN_NOT_SYNCED.png create mode 100644 social_media_advertising_linkedin/tests/__init__.py create mode 100644 social_media_advertising_linkedin/tests/test_common_advertising_linkedin.py create mode 100644 social_media_advertising_linkedin/tests/test_social_account_advertising_linkedin.py create mode 100644 social_media_advertising_linkedin/tests/test_social_campaign_group_linkedin.py create mode 100644 social_media_advertising_linkedin/tests/test_social_campaign_linkedin.py create mode 100644 social_media_advertising_linkedin/tests/test_social_post_advertising_linkedin.py create mode 100644 social_media_advertising_linkedin/views/social_account_views.xml create mode 100644 social_media_advertising_linkedin/views/social_campaign_group_views.xml create mode 100644 social_media_advertising_linkedin/views/social_campaign_views.xml diff --git a/social_media_advertising_linkedin/README.rst b/social_media_advertising_linkedin/README.rst new file mode 100644 index 0000000000..10e5225fab --- /dev/null +++ b/social_media_advertising_linkedin/README.rst @@ -0,0 +1,351 @@ +================================= +Social Media Advertising LinkedIn +================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:62b80ea53d651f4fbf4c8badfc7042c8646121807846b42c5ecdda8acaf57891 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/17.0/social_media_advertising_linkedin + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-17-0/social-17-0-social_media_advertising_linkedin + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module plugs LinkedIn Ads into *Social Media Advertising*. + +Main features: + +- Campaign group and campaign management synchronized with LinkedIn Ads: + create them in LinkedIn from Odoo (in draft state) or import the + existing ones, and link posts to campaigns as sponsored creatives. +- The LinkedIn statuses of campaigns, campaign groups and ads (Draft, + Active, Paused, Archived...) are declared as stages of *Social Media + Advertising*, so the badges and the status bars show the real LinkedIn + vocabulary. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +The LinkedIn application. +------------------------- + +- The campaign features (*Create in LinkedIn* and *Import campaigns*) + require an advertising account in the LinkedIn Campaign Manager and + the following products, which the *Social Media Linkedin* module does + not request on its own. Ask for them on the *Products* tab of your + LinkedIn Developer App, following the configuration steps of that + module: + + - Advertising API + - LinkedIn Ad Library + +- The member who authorizes the account must also have a role on that + advertising account (``ACCOUNT_BILLING_ADMIN``, ``ACCOUNT_MANAGER``, + ``CAMPAIGN_MANAGER``, ``CREATIVE_MANAGER`` or ``VIEWER``, the last one + read only). LinkedIn only answers the advertising accounts of the + authorized member, so without a role the import finds nothing. +- That requirement is unrelated to the formats LinkedIn accepts as an + ad: a post with several images cannot be sponsored with any product + nor with any advertising account, because LinkedIn does not turn a + multi-image post into an ad. See the ROADMAP section about it. + +Re-authorize the already associated accounts. +--------------------------------------------- + +- Installing this module adds the ``r_ads``, ``rw_ads`` and + ``r_ads_reporting`` OAuth scopes to the LinkedIn authorization. The + tokens already issued keep the scopes they were granted with — + refreshing a token does not renegotiate them — so every LinkedIn + account that was authorized **before** installing this module must be + re-authorized from the account wizard (*Update account* > *Update + token*) before it can call the Ads API. +- These scopes are only requested while this module is installed, + because LinkedIn rejects the authorization of an application that asks + for them without the *Advertising API* product enabled. + +Usage +===== + +Create a campaign in LinkedIn. +------------------------------ + +To link a post to a campaign, the campaign must already exist in +LinkedIn (only campaigns already synchronized with LinkedIn can be +selected on the post). Publishing a post does not create the campaign +anymore. + +- Go to *Social Media* > Advertising > Campaigns and open the campaign +- Fill in the campaign group, account, budget and currency fields +- Choose the *LinkedIn Ad Format*: *Standard update* for posts with text + or images, *Single video* for posts with a video. LinkedIn fixes the + format when the campaign is created and only accepts posts of that + format afterwards, so a post with a video needs its own video + campaign. +- The *LinkedIn Objective* is required by LinkedIn for the video format. + See the ROADMAP section about the objectives that are not offered + here. +- The *LinkedIn Political Intent* is the declaration LinkedIn requires + to create any campaign since the EU political advertising regulation. + It defaults to *Not political advertising*, which states that none of + your ads qualify as political advertising under the law of the + targeted countries, including EU law for ads targeted to the EU. + Change it to *Political advertising* when they do. +- Click on the *Create in LinkedIn* button +- The campaign group and the campaign are created in LinkedIn in + **DRAFT** status, so no budget is spent until you activate them from + the LinkedIn Campaign Manager. +- If the campaign cannot be created, the error is shown in a + notification and logged in the chatter instead of being raised: the + campaign group is already created on LinkedIn, which does not allow + deleting it, so its reference is kept to avoid importing it later as a + duplicate. Fix the reported problem and press *Create in LinkedIn* + again; the existing group is reused. +- A campaign group can also be created on LinkedIn on its own: open the + group and click its *Create in LinkedIn* button (it requires a + positive total budget and a currency). If you forget to do it, the + group is created automatically when the first campaign of the group is + published. +- When a post linked to a campaign is published, it is attached to the + campaign as a sponsored creative. If the post is published but the + creative cannot be linked, the post is kept and the error is logged in + the chatter. +- A post with more than one image cannot be sponsored: LinkedIn + publishes it as a multi-image post and does not accept that format as + an ad, so linking it to a campaign is refused before publishing. The + post form warns about it as soon as the campaign is selected, without + waiting for the *Post* button. + +Import campaigns from LinkedIn. +------------------------------- + +- Go to *Social Media* > Configuration > Accounts +- Select the account +- Click on the *Import campaigns* button +- The campaign groups, campaigns and their sponsored creatives are + imported from LinkedIn Ads. Every creative is matched with the + publication it promotes by its remote reference. +- A publication brought from the wall with *Update* has no post in Odoo, + so its campaign is taken from the creative and its badge appears on + the dashboard. Publications published from Odoo keep the campaign of + their post and are never overwritten by the import. +- The Posts API does not return the campaign of a post, so only this + import can resolve it. The order does not matter, but the publications + have to be already in Odoo: if they are brought **after** importing + the campaigns, press *Import campaigns* again. +- Campaign groups created on LinkedIn often carry no total budget, which + is optional there. Those groups set no limit in Odoo either: the rule + keeping the daily budgets of the campaigns within the total budget of + their group only applies when the group has one, and it is never + checked on the values brought by the import. The rule is enforced from + both sides: raising the daily budget of a campaign, moving it to + another group, and lowering the total budget of a group are all + refused when the sum goes over it. +- The real LinkedIn status of each campaign and campaign group (Draft, + Active, Paused, Archived...) is stored in its *Stage*, shown as a + status bar on the form and refreshed on every import. Campaigns + flagged as test campaigns on LinkedIn are marked as well. +- Campaigns and campaign groups deleted on LinkedIn (*Pending deletion*, + *Removed*) are hidden in the LinkedIn Campaign Manager, but the API + keeps returning them because of their performance data. They are + therefore kept in Odoo as history, and a note is logged in their + chatter when the deletion is detected. Keep in mind that deleting a + campaign group on LinkedIn also deletes all its campaigns and ads. + +Update a campaign in LinkedIn. +------------------------------ + +- When you modify in Odoo the editable fields of a campaign (name, unit + cost, daily budget, campaign group) or of a campaign group (name, + total budget) that already exists on LinkedIn, the record shows a + *Pending changes* ribbon, a warning banner explaining that the local + changes have not been pushed yet, and an *Update in LinkedIn* button. + A record that does not exist on LinkedIn yet carries the *Not synced* + ribbon instead. The tracked fields also log their changes in the + chatter. + + |CAMPAIGN_NOT_SYNCED| + +- Click *Update in LinkedIn* to push the local values to LinkedIn. To + move the campaign to another campaign group, the target group must + already exist on LinkedIn. The political advertising declaration is + pushed the same way, since LinkedIn allows updating it when the + targeting changes. + +- Synchronization priority is hybrid: the LinkedIn-only fields (stage, + test flag) are always refreshed by *Import campaigns*; for the + editable fields, your local pending changes are preserved by the + import — the LinkedIn values are logged in the chatter so you can + decide whether to keep your changes (push them with *Update in + LinkedIn*) or re-type the LinkedIn ones. + +- When the LinkedIn stage is *Archived*, *Canceled*, *Pending deletion* + or *Removed*, the campaign or campaign group cannot be modified in + Odoo (LinkedIn does not allow editing them either): the editable + fields become read-only, the *Update in LinkedIn* button is hidden and + a red banner explains the situation. The *Media* of a campaign that + exists on LinkedIn is read-only as well, so the lock cannot be escaped + by taking the campaign out of LinkedIn. The import keeps refreshing + their stage, so an archived record becomes editable again once it is + unarchived on LinkedIn. + +Archive a campaign or a campaign group in LinkedIn. +--------------------------------------------------- + +- LinkedIn does **not** allow deleting a campaign or a campaign group + through its API. Archiving is the only way to end them, and it is what + the *Archive in LinkedIn* button does. + +- Go to *Social Media* > Advertising, open the campaign or the campaign + group and click *Archive in LinkedIn*. The button is only shown when + the record already exists on LinkedIn and is not already archived, + canceled or deleted. + + |CAMPAIGN_ARCHIVE| + +- LinkedIn refuses to archive a campaign while its campaign group is + still in **Draft**: activate the group in the Campaign Manager first, + or archive the group instead, which archives its campaigns. Odoo + checks it before calling LinkedIn and explains it. + +- A campaign or a campaign group still in Draft keeps the start date + given when it was created, and LinkedIn rejects any later change + because that date already belongs to the past. Odoo therefore sends a + new run schedule when it updates or archives a record in Draft, which + moves its start date to the moment of the change. + +- The campaign stops running on LinkedIn and disappears from the + Campaign Manager active lists, but it is kept in Odoo with its + performance data. Its stage becomes *Archived*, which makes the record + read-only in Odoo, because LinkedIn no longer accepts changes on it. + +- **It cannot be reactivated from Odoo.** To unarchive it, use the + LinkedIn Campaign Manager and then run *Import campaigns* to refresh + the stage. + +- Archiving a campaign group archives its campaigns on LinkedIn as well, + except the ones still in Draft, which LinkedIn leaves untouched. Odoo + does not know about any of it until you run *Import campaigns*, so run + it afterwards to refresh them, and archive the draft campaigns one by + one if you want them archived too. + +- Deleting the campaign in Odoo does not do anything on LinkedIn: the + campaign keeps running there and the next *Import campaigns* brings it + back. Use *Archive in LinkedIn* to actually end it. + +.. |CAMPAIGN_NOT_SYNCED| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_advertising_linkedin/static/img/readme/CAMPAIGN_NOT_SYNCED.png +.. |CAMPAIGN_ARCHIVE| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_advertising_linkedin/static/img/readme/CAMPAIGN_ARCHIVE.png + +Known issues / Roadmap +====================== + +Campaign objectives +------------------- + +- LinkedIn defines seven campaign objectives: brand awareness, video + views, website visits, engagement, lead generation, website + conversions and job applicants. +- Only the first four are offered by this module. The other three need a + configuration that lives on LinkedIn and that the module does not + create nor check: lead generation needs a lead gen form, website + conversions need conversion tracking, and job applicants needs + LinkedIn Talent Solutions and is not compatible with the video format. + Selecting them without that configuration is refused by LinkedIn when + the campaign is created. +- Campaigns imported from LinkedIn keep their real objective; the ones + that are not offered here are simply not shown in the field. + +Sponsored posts with several images +----------------------------------- + +- A post with more than one image is published as a multi-image post, + and LinkedIn does not sponsor that format: *"API partners can only + create non-sponsored multiImage posts"*. Linking such a post to a + campaign is refused before publishing, so the post is not left online + without its ad, and the post form warns about it as soon as the + campaign is selected. + + https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/multiimage-post-api + +- This is a restriction of the content format, not of the access level + of the application nor of the advertising account: no LinkedIn plan + sponsors a multi-image post. + +- The format LinkedIn does offer for several sponsored images is + Carousel ads (``content.carousel`` with an ``adContext``, on a + campaign whose format is set to carousel when it is created). This + module does not implement it, so the case is not supported here even + though the platform allows it. + +- Publish the post with a single image if it has to be sponsored. + +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 +------- + +* BinhexTeam + +Contributors +------------ + +- [Binhex] (https://www.binhex.cloud): + + - Edilio Escalona Almira e.escalona@binhex.cloud + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-edescalona| image:: https://github.com/edescalona.png?size=40px + :target: https://github.com/edescalona + :alt: edescalona + +Current `maintainer `__: + +|maintainer-edescalona| + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/social_media_advertising_linkedin/__init__.py b/social_media_advertising_linkedin/__init__.py new file mode 100644 index 0000000000..6f3d3b53c1 --- /dev/null +++ b/social_media_advertising_linkedin/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/social_media_advertising_linkedin/__manifest__.py b/social_media_advertising_linkedin/__manifest__.py new file mode 100644 index 0000000000..016e43d655 --- /dev/null +++ b/social_media_advertising_linkedin/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Social Media Advertising LinkedIn", + "summary": "LinkedIn Ads campaigns, campaign groups and sponsored creatives.", + "version": "17.0.1.0.0", + "category": "Social Network", + "development_status": "Beta", + "license": "AGPL-3", + "author": "BinhexTeam,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "maintainers": ["edescalona"], + "depends": ["social_media_advertising", "social_media_linkedin"], + "data": [ + "data/social_stage_data.xml", + "views/social_campaign_group_views.xml", + "views/social_campaign_views.xml", + "views/social_account_views.xml", + ], + "exclude": ["social"], + "installable": True, +} diff --git a/social_media_advertising_linkedin/data/social_stage_data.xml b/social_media_advertising_linkedin/data/social_stage_data.xml new file mode 100644 index 0000000000..2b405c72f1 --- /dev/null +++ b/social_media_advertising_linkedin/data/social_stage_data.xml @@ -0,0 +1,194 @@ + + + + + Draft + DRAFT + + campaign + info + 10 + + + Active + ACTIVE + + campaign + success + 20 + + + Paused + PAUSED + + campaign + warning + 30 + + + Completed + COMPLETED + + campaign + secondary + 40 + + + + Archived + ARCHIVED + + campaign + secondary + 50 + + + + Canceled + CANCELED + + campaign + danger + 60 + + + + Pending deletion + PENDING_DELETION + + campaign + danger + 70 + + + + Removed + REMOVED + + campaign + danger + 80 + + + + Draft + DRAFT + + group + info + 10 + + + Active + ACTIVE + + group + success + 20 + + + Paused + PAUSED + + group + warning + 30 + + + Archived + ARCHIVED + + group + secondary + 40 + + + + Canceled + CANCELED + + group + danger + 50 + + + + Pending deletion + PENDING_DELETION + + group + danger + 60 + + + + Removed + REMOVED + + group + danger + 70 + + + + Draft + DRAFT + + ad + info + 10 + + + Active + ACTIVE + + ad + success + 20 + + + Paused + PAUSED + + ad + warning + 30 + + + Archived + ARCHIVED + + ad + secondary + 40 + + + + Canceled + CANCELED + + ad + danger + 50 + + + + Pending deletion + PENDING_DELETION + + ad + danger + 60 + + + + Removed + REMOVED + + ad + danger + 70 + + + diff --git a/social_media_advertising_linkedin/i18n/es_ES.po b/social_media_advertising_linkedin/i18n/es_ES.po new file mode 100644 index 0000000000..0d31a058ad --- /dev/null +++ b/social_media_advertising_linkedin/i18n/es_ES.po @@ -0,0 +1,1057 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * social_media_advertising_linkedin +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-31 18:02+0000\n" +"PO-Revision-Date: 2026-07-27 22:25+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"%(groups)s campaign group(s), %(campaigns)s campaign(s) and %(ads)s " +"sponsored post(s) imported from LinkedIn." +msgstr "" +"%(groups)s grupo(s) de campaña, %(campaigns)s campaña(s) y %(ads)s " +"publicación(es) patrocinada(s) importadas de LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "%(message_error)s %(error_response)s" +msgstr "%(message_error)s %(error_response)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "A LinkedIn campaign can only have one LinkedIn account." +msgstr "Una campaña de LinkedIn solo puede tener una cuenta de LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post.py:0 +#, python-format +msgid "" +"A post with several images is published as a multi-image post, and LinkedIn " +"does not sponsor that format, so this post cannot be linked to the campaign " +"%(campaign)s. Leave a single image or remove the campaign." +msgstr "" +"Una publicación con varias imágenes se publica como publicación de varias " +"imágenes, y LinkedIn no patrocina ese formato, por lo que esta publicación " +"no se puede vincular a la campaña %(campaign)s. Deje una sola imagen o " +"quite la campaña." + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_active +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_active +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_active +msgid "Active" +msgstr "Activa" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_format +msgid "" +"Ad format of the campaign on LinkedIn. A campaign only accepts posts of its " +"format, and LinkedIn does not allow changing it once the campaign is " +"created, so a post with a video needs a campaign of the 'Single video' " +"format." +msgstr "" +"Formato de anuncio de la campaña en LinkedIn. Una campaña solo acepta " +"publicaciones de su formato, y LinkedIn no permite cambiarlo una vez creada " +"la campaña, por lo que una publicación con vídeo necesita una campaña con " +"el formato 'Vídeo único'." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Archive in LinkedIn" +msgstr "Archivar en LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_archived +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_archived +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_archived +msgid "Archived" +msgstr "Archivada" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__unit_cost +msgid "" +"Bid amount per click, impression or other event depending on the pricing " +"model. LinkedIn only applies it with manual, target cost or cost cap " +"bidding; with automatic bidding it is ignored." +msgstr "" +"Importe de la puja por clic, impresión u otro evento según el modelo de " +"precios. LinkedIn solo la aplica con puja manual, target cost o cost cap; " +"con puja automática se ignora." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__brand_awareness +msgid "Brand awareness" +msgstr "Notoriedad de marca" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign archived on LinkedIn." +msgstr "Campaña archivada en LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign created on LinkedIn in draft status." +msgstr "Campaña creada en LinkedIn en estado borrador." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Campaign group archived on LinkedIn." +msgstr "Grupo de campañas archivado en LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Campaign group created on LinkedIn in draft status." +msgstr "Grupo de campañas creado en LinkedIn en estado borrador." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Campaign group updated on LinkedIn." +msgstr "Grupo de campañas actualizado en LinkedIn." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_account_view_form_inherit +msgid "" +"Campaign groups and campaigns of the advertising account will be imported " +"from LinkedIn. Continue?" +msgstr "" +"Se importarán desde LinkedIn los grupos de campañas y las campañas de la " +"cuenta publicitaria. ¿Continuar?" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign not created on LinkedIn" +msgstr "Campaña no creada en LinkedIn" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign updated on LinkedIn." +msgstr "Campaña actualizada en LinkedIn." + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_canceled +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_canceled +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_canceled +msgid "Canceled" +msgstr "Cancelada" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_completed +msgid "Completed" +msgstr "Completada" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Create in LinkedIn" +msgstr "Crear en LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_post_account__creative_urn +msgid "Creative Urn" +msgstr "URN de la creatividad" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__currency_id +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__currency_id +msgid "Currency" +msgstr "Moneda" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__daily_budget +msgid "Daily Budget" +msgstr "Presupuesto diario" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_political_intent +msgid "" +"Declaration required by LinkedIn to create a campaign: I confirm this is not" +" political advertising. None of my ads qualify as political advertising " +"under the law of the targeted countries, including EU law for ads targeted " +"to the EU. Change it to 'Political advertising' when the campaign does " +"qualify as such." +msgstr "" +"Declaración que exige LinkedIn para crear una campaña: confirmo que no se " +"trata de publicidad política. Ninguno de mis anuncios se considera " +"publicidad política según la legislación de los países a los que se " +"dirigen, incluida la normativa de la UE para los anuncios dirigidos a la " +"UE. Cámbielo a 'Publicidad política' cuando la campaña sí lo sea." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Do you want to update the values on LinkedIn?" +msgstr "¿Desea actualizar los valores en LinkedIn?" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_draft +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_draft +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_draft +msgid "Draft" +msgstr "Borrador" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__engagement +msgid "Engagement" +msgstr "Engagement" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Error archiving campaign group in Linkedin: %(error)s" +msgstr "Error al archivar el grupo de campañas en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Error archiving campaign in Linkedin: %(error)s" +msgstr "Error al archivar la campaña en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "Error creating campaign post in Linkedin: %(error)s" +msgstr "Error al crear el post de campaña en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Error creating group campaign in Linkedin: %(error)s" +msgstr "Error al crear el grupo de campañas en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "Error get advertising account in Linkedin: %(error)s" +msgstr "Error al obtener la cuenta publicitaria de LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "Error getting campaigns from Linkedin: %(error)s" +msgstr "Error al obtener las campañas de LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Error updating campaign group in Linkedin: %(error)s" +msgstr "Error al actualizar el grupo de campañas en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Error updating campaign in Linkedin: %(error)s" +msgstr "Error al actualizar la campaña en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "GET ADS: %(error)s" +msgstr "GET ADS: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "GET CAMPAIGNS STATISTICS: %(error)s" +msgstr "GET CAMPAIGNS STATISTICS: %(error)s" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_objective +msgid "" +"Goal of the campaign on LinkedIn, required for the 'Single video' format:\n" +"- Brand awareness: reach as many people as possible.\n" +"- Video views: get the video of the post watched.\n" +"- Website visits: send traffic to a page.\n" +"- Engagement: get likes, comments and follows.\n" +"LinkedIn has three more objectives (lead generation, website conversions and job applicants) that need a lead gen form, conversion tracking or LinkedIn Talent Solutions, which are configured outside Odoo and are therefore not offered here." +msgstr "" +"Objetivo de la campaña en LinkedIn, obligatorio para el formato 'Vídeo " +"único':\n" +"- Notoriedad de marca: llegar al mayor número de personas posible.\n" +"- Visualizaciones de vídeo: conseguir que se vea el vídeo de la " +"publicación.\n" +"- Visitas al sitio web: enviar tráfico a una página.\n" +"- Interacción: conseguir me gusta, comentarios y seguidores.\n" +"LinkedIn tiene tres objetivos más (generación de contactos, conversiones en " +"el sitio web y candidaturas de empleo) que necesitan un formulario de " +"generación de contactos, seguimiento de conversiones o LinkedIn Talent " +"Solutions, que se configuran fuera de Odoo y por eso no se ofrecen aquí." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_account_view_form_inherit +msgid "Import campaigns" +msgstr "Importar campañas" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"Import kept the local pending changes. LinkedIn values: name: %(name)s, " +"total budget: %(total_budget)s %(currency)s" +msgstr "" +"La importación conservó los cambios locales pendientes. Valores de LinkedIn: " +"nombre: %(name)s, presupuesto total: %(total_budget)s %(currency)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"Import kept the local pending changes. LinkedIn values: name: %(name)s, unit " +"cost: %(unit_cost)s, daily budget: %(daily_budget)s, campaign group: " +"%(group)s" +msgstr "" +"La importación conservó los cambios locales pendientes. Valores de LinkedIn: " +"nombre: %(name)s, coste unitario: %(unit_cost)s, presupuesto diario: " +"%(daily_budget)s, grupo de campañas: %(group)s" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "LinkedIn" +msgstr "LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_format +msgid "LinkedIn Ad Format" +msgstr "Formato de anuncio de LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_objective +msgid "LinkedIn Objective" +msgstr "Objetivo de LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_needs_update +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__linkedin_needs_update +msgid "LinkedIn Pending Changes" +msgstr "Cambios pendientes de LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_political_intent +msgid "LinkedIn Political Intent" +msgstr "Intención política de LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_is_test +msgid "LinkedIn Test Campaign" +msgstr "Campaña de prueba de LinkedIn" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"LinkedIn does not archive a campaign whose campaign group is still in draft " +"status. Activate the group %(group)s in Campaign Manager first, or archive " +"the group itself, which archives its campaigns." +msgstr "" +"LinkedIn no archiva una campaña cuyo grupo de campañas sigue en estado " +"borrador. Active primero el grupo %(group)s en Campaign Manager, o archive " +"el propio grupo, lo que archiva sus campañas." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"LinkedIn does not sponsor posts with several images: they are published as a" +" multi-image post, a format its API cannot turn into an ad, so this post " +"cannot be linked to the campaign %(campaign)s. Publish it with a single " +"image or without a campaign. LinkedIn offers the Carousel ad format for this" +" case, which this module does not support yet." +msgstr "" +"LinkedIn no patrocina publicaciones con varias imágenes: se publican como " +"publicación de varias imágenes, un formato que su API no puede convertir en " +"anuncio, por lo que esta publicación no se puede vincular a la campaña " +"%(campaign)s. Publíquela con una sola imagen o sin campaña. LinkedIn ofrece " +"el formato de anuncio Carrusel para este caso, que este módulo todavía no " +"admite." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_locked +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign_group__linkedin_locked +msgid "LinkedIn no longer accepts changes on this record." +msgstr "LinkedIn ya no acepta cambios en este registro." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "LinkedIn requires an objective for the video format." +msgstr "LinkedIn exige un objetivo para el formato de vídeo." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"LinkedIn requires declaring whether the campaign is political advertising." +msgstr "LinkedIn exige declarar si la campaña es publicidad política." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_locked +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__linkedin_locked +msgid "Linkedin Locked" +msgstr "Bloqueado en LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign_group__total_budget +msgid "" +"Maximum amount to spend across all the campaigns and creatives of this group " +"for its entire duration. It cannot be lower than the daily budgets of its " +"campaigns." +msgstr "" +"Importe máximo a gastar entre todas las campañas y creatividades de este " +"grupo durante toda su duración. No puede ser inferior a los presupuestos " +"diarios de sus campañas." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__daily_budget +msgid "" +"Maximum amount to spend on this campaign per day. The campaign group total " +"budget limits the overall spending." +msgstr "" +"Importe máximo a gastar en esta campaña por día. El presupuesto total del " +"grupo de campañas limita el gasto global." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__media_type +msgid "Media Type" +msgstr "Tipo de red social" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"No LinkedIn advertising account is available for the account %(account)s." +msgstr "" +"No hay ninguna cuenta publicitaria de LinkedIn disponible para la cuenta " +"%(account)s." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "No LinkedIn social account is available to archive the group." +msgstr "" +"No hay ninguna cuenta social de LinkedIn disponible para archivar el grupo." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "No LinkedIn social account is available to create the group." +msgstr "" +"No hay ninguna cuenta social de LinkedIn disponible para crear el grupo." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "No LinkedIn social account is available to update the group." +msgstr "" +"No hay ninguna cuenta social de LinkedIn disponible para actualizar el grupo." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_political_intent__not_declared +msgid "Not declared" +msgstr "Sin declarar" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_political_intent__not_political +msgid "Not political advertising" +msgstr "Publicidad no política" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Not synced" +msgstr "Sin sincronizar" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_paused +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_paused +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_paused +msgid "Paused" +msgstr "Pausada" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Pending changes" +msgstr "Cambios pendientes" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_pending_deletion +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_pending_deletion +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_pending_deletion +msgid "Pending deletion" +msgstr "Pendiente de eliminación" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_political_intent__political +msgid "Political advertising" +msgstr "Publicidad política" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_removed +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_removed +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_removed +msgid "Removed" +msgstr "Eliminada" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_format__single_video +msgid "Single video" +msgstr "Vídeo único" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_account +msgid "Social Account" +msgstr "Cuenta de red social" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_campaign +msgid "Social Campaign" +msgstr "Campaña social" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_campaign_group +msgid "Social Campaign Group" +msgstr "Grupo de campañas sociales" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_media +msgid "Social Media" +msgstr "Red social" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_post +msgid "Social Post" +msgstr "Publicación de red social" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_post_account +msgid "Social Post Account" +msgstr "Publicación de red social" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_format__standard_update +msgid "Standard update" +msgstr "Publicación estándar" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"Stops the campaign group on LinkedIn and takes it out of the Campaign " +"Manager lists. LinkedIn does not allow deleting a campaign group, archiving " +"is the only way to end it. LinkedIn applies the same status to the campaigns" +" of the group, except the ones still in draft, so run Import campaigns " +"afterwards to refresh them in Odoo." +msgstr "" +"Detiene el grupo de campañas en LinkedIn y lo saca de las listas de " +"Campaign Manager. LinkedIn no permite eliminar un grupo de campañas, " +"archivarlo es la única forma de terminarlo. LinkedIn aplica el mismo estado " +"a las campañas del grupo, salvo a las que siguen en borrador, así que " +"ejecute después Importar campañas para actualizarlas en Odoo." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"Stops the campaign on LinkedIn and takes it out of the Campaign Manager " +"lists. LinkedIn does not allow deleting a campaign, archiving is the only " +"way to end it, and it refuses to archive a campaign whose campaign group is " +"still in draft status. The campaign is kept in Odoo with its performance " +"data, and it can no longer be modified." +msgstr "" +"Detiene la campaña en LinkedIn y la saca de las listas de Campaign Manager. " +"LinkedIn no permite eliminar una campaña, archivarla es la única forma de " +"terminarla, y se niega a archivar una campaña cuyo grupo de campañas sigue " +"en estado borrador. La campaña se conserva en Odoo con sus datos de " +"rendimiento, y ya no se puede modificar." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The LinkedIn campaign group stage with code %(code)s is missing. Update the " +"social_media_advertising_linkedin module to restore the LinkedIn stages." +msgstr "" +"Falta la etapa de grupo de campañas de LinkedIn con el código %(code)s. " +"Actualice el módulo social_media_advertising_linkedin para restaurar las " +"etapas de LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The LinkedIn campaign stage with code %(code)s is missing. Update the " +"social_media_advertising_linkedin module to restore the LinkedIn stages." +msgstr "" +"Falta la etapa de campaña de LinkedIn con el código %(code)s. Actualice el " +"módulo social_media_advertising_linkedin para restaurar las etapas de " +"LinkedIn." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The LinkedIn status of this campaign does not allow\n" +" modifications." +msgstr "" +"El estado en LinkedIn de esta campaña no permite\n" +" modificaciones." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The LinkedIn status of this campaign group does not allow\n" +" modifications." +msgstr "" +"El estado en LinkedIn de este grupo de campañas no permite\n" +" modificaciones." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign %(campaign)s does not belong to a campaign group." +msgstr "La campaña %(campaign)s no pertenece a ningún grupo de campañas." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The campaign %(campaign)s has not been created on LinkedIn yet. Use the " +"'Create in LinkedIn' button on the campaign before posting." +msgstr "" +"La campaña %(campaign)s aún no se ha creado en LinkedIn. Use el botón 'Crear " +"en LinkedIn' de la campaña antes de publicar." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The campaign %(campaign)s uses the 'Single video' format, so it only accepts" +" posts containing a video." +msgstr "" +"La campaña %(campaign)s usa el formato 'Vídeo único', por lo que solo " +"acepta publicaciones que contengan un vídeo." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign %(names)s cannot be modified because of its LinkedIn status " +"(%(status)s)." +msgstr "" +"La campaña %(names)s no se puede modificar por su estado en LinkedIn " +"(%(status)s)." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign cannot be archived because of its LinkedIn status (%(status)s)." +msgstr "" +"La campaña no se puede archivar por su estado en LinkedIn (%(status)s)." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign cannot be updated because of its LinkedIn status (%(status)s)." +msgstr "" +"La campaña no se puede actualizar por su estado en LinkedIn (%(status)s)." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign could not be created on LinkedIn: %(error)s" +msgstr "No se pudo crear la campaña en LinkedIn: %(error)s" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The campaign could not be generated for the post, please try again later." +msgstr "" +"La campaña no se pudo generar para el post, inténtelo de nuevo más tarde." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign daily budget must be positive." +msgstr "El presupuesto diario de la campaña debe ser positivo." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign does not exist on LinkedIn yet." +msgstr "La campaña aún no existe en LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign group %(group)s does not exist on LinkedIn. Create or import it " +"before updating the campaign." +msgstr "" +"El grupo de campañas %(group)s no existe en LinkedIn. Créelo o impórtelo " +"antes de actualizar la campaña." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group %(group)s is not linked to any LinkedIn account and the " +"database holds several of them. Set the account on one of its campaigns to " +"choose the advertising account to use." +msgstr "" +"El grupo de campañas %(group)s no está vinculado a ninguna cuenta de " +"LinkedIn y la base de datos tiene varias. Indique la cuenta en una de sus " +"campañas para elegir la cuenta publicitaria que se debe usar." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group %(names)s cannot be modified because of its LinkedIn " +"status (%(status)s)." +msgstr "" +"El grupo de campañas %(names)s no se puede modificar por su estado en " +"LinkedIn (%(status)s)." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group already exists on LinkedIn." +msgstr "El grupo de campañas ya existe en LinkedIn." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The campaign group and the campaign will be created on LinkedIn in draft " +"status. Continue?" +msgstr "" +"El grupo de campañas y la campaña se crearán en LinkedIn en estado borrador. " +"¿Continuar?" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group cannot be archived because of its LinkedIn status " +"(%(status)s)." +msgstr "" +"El grupo de campañas no se puede archivar por su estado en LinkedIn " +"(%(status)s)." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group cannot be updated because of its LinkedIn status " +"(%(status)s)." +msgstr "" +"El grupo de campañas no se puede actualizar por su estado en LinkedIn " +"(%(status)s)." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group does not exist on LinkedIn yet." +msgstr "El grupo de campañas aún no existe en LinkedIn." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The campaign group has local changes that are not\n" +" synchronized with LinkedIn. Use the Update in\n" +" LinkedIn button to push them." +msgstr "" +"El grupo de campañas tiene cambios locales que no están\n" +" sincronizados con LinkedIn. Use el botón Actualizar " +"en\n" +" LinkedIn para enviarlos." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group must have a currency." +msgstr "El grupo de campañas debe tener una moneda." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group total budget must be positive." +msgstr "El presupuesto total del grupo de campañas debe ser positivo." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign_group__linkedin_needs_update +msgid "" +"The campaign group was modified in Odoo after being synchronized with " +"LinkedIn. Use the 'Update in LinkedIn' button to push the local values." +msgstr "" +"El grupo de campañas se modificó en Odoo después de sincronizarse con " +"LinkedIn. Use el botón 'Actualizar en LinkedIn' para enviar los valores " +"locales." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The campaign group will be archived on LinkedIn, and LinkedIn will archive " +"its campaigns too, except the ones still in draft status. It cannot be " +"reactivated from Odoo. Continue?" +msgstr "" +"El grupo de campañas se archivará en LinkedIn, y LinkedIn archivará también " +"sus campañas, salvo las que sigan en estado borrador. No se puede reactivar " +"desde Odoo. ¿Continuar?" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The campaign group will be created on LinkedIn in draft status. Continue?" +msgstr "" +"El grupo de campañas se creará en LinkedIn en estado borrador. ¿Continuar?" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The campaign has local changes that are not synchronized\n" +" with LinkedIn. Use the Update in LinkedIn button " +"to\n" +" push them." +msgstr "" +"La campaña tiene cambios locales que no están sincronizados\n" +" con LinkedIn. Use el botón Actualizar en LinkedIn " +"para\n" +" enviarlos." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign media must be LinkedIn." +msgstr "El medio de la campaña debe ser LinkedIn." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign must belong to a campaign group." +msgstr "La campaña debe pertenecer a un grupo de campañas." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign must have a social account and a currency." +msgstr "La campaña debe tener una cuenta social y una moneda." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign must have a social account." +msgstr "La campaña debe tener una cuenta social." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign unit cost must be positive." +msgstr "El coste unitario de la campaña debe ser positivo." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_needs_update +msgid "" +"The campaign was modified in Odoo after being synchronized with LinkedIn. " +"Use the 'Update in LinkedIn' button to push the local values." +msgstr "" +"La campaña se modificó en Odoo después de sincronizarse con LinkedIn. Use el " +"botón 'Actualizar en LinkedIn' para enviar los valores locales." + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The campaign will be archived on LinkedIn and will stop running. It cannot " +"be reactivated from Odoo. It stays in Odoo with its performance data. " +"Continue?" +msgstr "" +"La campaña se archivará en LinkedIn y dejará de ejecutarse. No se puede " +"reactivar desde Odoo. Se mantiene en Odoo con sus datos de rendimiento. " +"¿Continuar?" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaigns of the group %(group)s point to several LinkedIn accounts " +"(%(accounts)s). Leave a single one so the advertising account to use is " +"unambiguous." +msgstr "" +"Las campañas del grupo %(group)s apuntan a varias cuentas de LinkedIn " +"(%(accounts)s). Deje una sola para que la cuenta publicitaria que se debe " +"usar no sea ambigua." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The daily budgets of the campaigns of the group %(group)s add up to " +"%(daily_budgets)s, which exceeds its total budget of %(total_budget)s." +msgstr "" +"Los presupuestos diarios de las campañas del grupo %(group)s suman " +"%(daily_budgets)s, lo que supera su presupuesto total de %(total_budget)s." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The post contains a video, so it needs a campaign of the 'Single video' " +"format. The campaign %(campaign)s uses the 'Standard update' format and " +"LinkedIn does not allow changing it once the campaign is created." +msgstr "" +"La publicación contiene un vídeo, por lo que necesita una campaña con el " +"formato 'Vídeo único'. La campaña %(campaign)s usa el formato 'Publicación " +"estándar' y LinkedIn no permite cambiarlo una vez creada la campaña." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The post was published on LinkedIn but the campaign creative could not be " +"created. Check the Ads permissions of the account." +msgstr "" +"El post se publicó en LinkedIn pero la creatividad de la campaña no se pudo " +"crear. Revise los permisos de Ads de la cuenta." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"This campaign group was deleted on LinkedIn. It is kept in Odoo as history " +"because LinkedIn still returns it with its performance data." +msgstr "" +"Este grupo de campañas se eliminó en LinkedIn. Se conserva en Odoo como " +"histórico porque LinkedIn lo sigue devolviendo con sus datos de rendimiento." + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"This campaign was deleted on LinkedIn. It is kept in Odoo as history because " +"LinkedIn still returns it with its performance data." +msgstr "" +"Esta campaña se eliminó en LinkedIn. Se conserva en Odoo como histórico " +"porque LinkedIn la sigue devolviendo con sus datos de rendimiento." + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__total_budget +msgid "Total Budget" +msgstr "Presupuesto total" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__unit_cost +msgid "Unit Cost" +msgstr "Costo unitario" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Update in LinkedIn" +msgstr "Actualizar en LinkedIn" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__video_view +msgid "Video views" +msgstr "Visualizaciones de vídeo" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__website_visit +msgid "Website visits" +msgstr "Visitas al sitio web" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_is_test +msgid "Whether LinkedIn flags this campaign as a test campaign." +msgstr "Indica si LinkedIn marca esta campaña como campaña de prueba." diff --git a/social_media_advertising_linkedin/i18n/social_media_advertising_linkedin.pot b/social_media_advertising_linkedin/i18n/social_media_advertising_linkedin.pot new file mode 100644 index 0000000000..e29c1c64dd --- /dev/null +++ b/social_media_advertising_linkedin/i18n/social_media_advertising_linkedin.pot @@ -0,0 +1,933 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * social_media_advertising_linkedin +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-02 03:37+0000\n" +"PO-Revision-Date: 2026-08-02 03:37+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: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"%(groups)s campaign group(s), %(campaigns)s campaign(s) and %(ads)s " +"sponsored post(s) imported from LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "%(message_error)s %(error_response)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "A LinkedIn campaign can only have one LinkedIn account." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post.py:0 +#, python-format +msgid "" +"A post with several images is published as a multi-image post, and LinkedIn " +"does not sponsor that format, so this post cannot be linked to the campaign " +"%(campaign)s. Leave a single image or remove the campaign." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_active +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_active +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_active +msgid "Active" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_format +msgid "" +"Ad format of the campaign on LinkedIn. A campaign only accepts posts of its " +"format, and LinkedIn does not allow changing it once the campaign is " +"created, so a post with a video needs a campaign of the 'Single video' " +"format." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Archive in LinkedIn" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_archived +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_archived +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_archived +msgid "Archived" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__unit_cost +msgid "" +"Bid amount per click, impression or other event depending on the pricing " +"model. LinkedIn only applies it with manual, target cost or cost cap " +"bidding; with automatic bidding it is ignored." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__brand_awareness +msgid "Brand awareness" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign archived on LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign created on LinkedIn in draft status." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Campaign group archived on LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Campaign group created on LinkedIn in draft status." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Campaign group updated on LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_account_view_form_inherit +msgid "" +"Campaign groups and campaigns of the advertising account will be imported " +"from LinkedIn. Continue?" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign not created on LinkedIn" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Campaign updated on LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_canceled +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_canceled +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_canceled +msgid "Canceled" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_completed +msgid "Completed" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Create in LinkedIn" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_post_account__creative_urn +msgid "Creative Urn" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__currency_id +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__currency_id +msgid "Currency" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__daily_budget +msgid "Daily Budget" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_political_intent +msgid "" +"Declaration required by LinkedIn to create a campaign: I confirm this is not" +" political advertising. None of my ads qualify as political advertising " +"under the law of the targeted countries, including EU law for ads targeted " +"to the EU. Change it to 'Political advertising' when the campaign does " +"qualify as such." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Do you want to update the values on LinkedIn?" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_draft +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_draft +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_draft +msgid "Draft" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__engagement +msgid "Engagement" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Error archiving campaign group in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Error archiving campaign in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "Error creating campaign post in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Error creating group campaign in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "Error get advertising account in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "Error getting campaigns from Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "Error updating campaign group in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "Error updating campaign in Linkedin: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "GET ADS: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "GET CAMPAIGNS STATISTICS: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_objective +msgid "" +"Goal of the campaign on LinkedIn, required for the 'Single video' format:\n" +"- Brand awareness: reach as many people as possible.\n" +"- Video views: get the video of the post watched.\n" +"- Website visits: send traffic to a page.\n" +"- Engagement: get likes, comments and follows.\n" +"LinkedIn has three more objectives (lead generation, website conversions and job applicants) that need a lead gen form, conversion tracking or LinkedIn Talent Solutions, which are configured outside Odoo and are therefore not offered here." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_account_view_form_inherit +msgid "Import campaigns" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"Import kept the local pending changes. LinkedIn values: name: %(name)s, " +"total budget: %(total_budget)s %(currency)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"Import kept the local pending changes. LinkedIn values: name: %(name)s, unit" +" cost: %(unit_cost)s, daily budget: %(daily_budget)s, campaign group: " +"%(group)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "LinkedIn" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_format +msgid "LinkedIn Ad Format" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_objective +msgid "LinkedIn Objective" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_needs_update +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__linkedin_needs_update +msgid "LinkedIn Pending Changes" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_political_intent +msgid "LinkedIn Political Intent" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_is_test +msgid "LinkedIn Test Campaign" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"LinkedIn does not archive a campaign whose campaign group is still in draft " +"status. Activate the group %(group)s in Campaign Manager first, or archive " +"the group itself, which archives its campaigns." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"LinkedIn does not sponsor posts with several images: they are published as a" +" multi-image post, a format its API cannot turn into an ad, so this post " +"cannot be linked to the campaign %(campaign)s. Publish it with a single " +"image or without a campaign. LinkedIn offers the Carousel ad format for this" +" case, which this module does not support yet." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_locked +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign_group__linkedin_locked +msgid "LinkedIn no longer accepts changes on this record." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "LinkedIn requires an objective for the video format." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"LinkedIn requires declaring whether the campaign is political advertising." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__linkedin_locked +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__linkedin_locked +msgid "Linkedin Locked" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign_group__total_budget +msgid "" +"Maximum amount to spend across all the campaigns and creatives of this group" +" for its entire duration. It cannot be lower than the daily budgets of its " +"campaigns." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__daily_budget +msgid "" +"Maximum amount to spend on this campaign per day. The campaign group total " +"budget limits the overall spending." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__media_type +msgid "Media Type" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"No LinkedIn advertising account is available for the account %(account)s." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "No LinkedIn social account is available to archive the group." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "No LinkedIn social account is available to create the group." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "No LinkedIn social account is available to update the group." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_political_intent__not_declared +msgid "Not declared" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_political_intent__not_political +msgid "Not political advertising" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Not synced" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_paused +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_paused +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_paused +msgid "Paused" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Pending changes" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_pending_deletion +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_pending_deletion +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_pending_deletion +msgid "Pending deletion" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_political_intent__political +msgid "Political advertising" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_ad_removed +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_campaign_removed +#: model:social.stage,name:social_media_advertising_linkedin.social_stage_linkedin_group_removed +msgid "Removed" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_format__single_video +msgid "Single video" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_account +msgid "Social Account" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_campaign +msgid "Social Campaign" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_campaign_group +msgid "Social Campaign Group" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_media +msgid "Social Media" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_post +msgid "Social Post" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model,name:social_media_advertising_linkedin.model_social_post_account +msgid "Social Post Account" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_format__standard_update +msgid "Standard update" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"Stops the campaign group on LinkedIn and takes it out of the Campaign " +"Manager lists. LinkedIn does not allow deleting a campaign group, archiving " +"is the only way to end it. LinkedIn applies the same status to the campaigns" +" of the group, except the ones still in draft, so run Import campaigns " +"afterwards to refresh them in Odoo." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"Stops the campaign on LinkedIn and takes it out of the Campaign Manager " +"lists. LinkedIn does not allow deleting a campaign, archiving is the only " +"way to end it, and it refuses to archive a campaign whose campaign group is " +"still in draft status. The campaign is kept in Odoo with its performance " +"data, and it can no longer be modified." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The LinkedIn campaign group stage with code %(code)s is missing. Update the " +"social_media_advertising_linkedin module to restore the LinkedIn stages." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The LinkedIn campaign stage with code %(code)s is missing. Update the " +"social_media_advertising_linkedin module to restore the LinkedIn stages." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The LinkedIn status of this campaign does not allow\n" +" modifications." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The LinkedIn status of this campaign group does not allow\n" +" modifications." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign %(campaign)s does not belong to a campaign group." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The campaign %(campaign)s has not been created on LinkedIn yet. Use the " +"'Create in LinkedIn' button on the campaign before posting." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The campaign %(campaign)s uses the 'Single video' format, so it only accepts" +" posts containing a video." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign %(names)s cannot be modified because of its LinkedIn status " +"(%(status)s)." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign cannot be archived because of its LinkedIn status (%(status)s)." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign cannot be updated because of its LinkedIn status (%(status)s)." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign could not be created on LinkedIn: %(error)s" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The campaign could not be generated for the post, please try again later." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign daily budget must be positive." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign does not exist on LinkedIn yet." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "" +"The campaign group %(group)s does not exist on LinkedIn. Create or import it" +" before updating the campaign." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group %(group)s is not linked to any LinkedIn account and the " +"database holds several of them. Set the account on one of its campaigns to " +"choose the advertising account to use." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group %(names)s cannot be modified because of its LinkedIn " +"status (%(status)s)." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group already exists on LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The campaign group and the campaign will be created on LinkedIn in draft " +"status. Continue?" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group cannot be archived because of its LinkedIn status " +"(%(status)s)." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaign group cannot be updated because of its LinkedIn status " +"(%(status)s)." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group does not exist on LinkedIn yet." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The campaign group has local changes that are not\n" +" synchronized with LinkedIn. Use the Update in\n" +" LinkedIn button to push them." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group must have a currency." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "The campaign group total budget must be positive." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign_group__linkedin_needs_update +msgid "" +"The campaign group was modified in Odoo after being synchronized with " +"LinkedIn. Use the 'Update in LinkedIn' button to push the local values." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The campaign group will be archived on LinkedIn, and LinkedIn will archive " +"its campaigns too, except the ones still in draft status. It cannot be " +"reactivated from Odoo. Continue?" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +msgid "" +"The campaign group will be created on LinkedIn in draft status. Continue?" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The campaign has local changes that are not synchronized\n" +" with LinkedIn. Use the Update in LinkedIn button to\n" +" push them." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign media must be LinkedIn." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign must belong to a campaign group." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign must have a social account and a currency." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign must have a social account." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign.py:0 +#, python-format +msgid "The campaign unit cost must be positive." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_needs_update +msgid "" +"The campaign was modified in Odoo after being synchronized with LinkedIn. " +"Use the 'Update in LinkedIn' button to push the local values." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "" +"The campaign will be archived on LinkedIn and will stop running. It cannot " +"be reactivated from Odoo. It stays in Odoo with its performance data. " +"Continue?" +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The campaigns of the group %(group)s point to several LinkedIn accounts " +"(%(accounts)s). Leave a single one so the advertising account to use is " +"unambiguous." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_campaign_group.py:0 +#, python-format +msgid "" +"The daily budgets of the campaigns of the group %(group)s add up to " +"%(daily_budgets)s, which exceeds its total budget of %(total_budget)s." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The post contains a video, so it needs a campaign of the 'Single video' " +"format. The campaign %(campaign)s uses the 'Standard update' format and " +"LinkedIn does not allow changing it once the campaign is created." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_post_account.py:0 +#, python-format +msgid "" +"The post was published on LinkedIn but the campaign creative could not be " +"created. Check the Ads permissions of the account." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"This campaign group was deleted on LinkedIn. It is kept in Odoo as history " +"because LinkedIn still returns it with its performance data." +msgstr "" + +#. module: social_media_advertising_linkedin +#. odoo-python +#: code:addons/social_media_advertising_linkedin/models/social_account.py:0 +#, python-format +msgid "" +"This campaign was deleted on LinkedIn. It is kept in Odoo as history because" +" LinkedIn still returns it with its performance data." +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign_group__total_budget +msgid "Total Budget" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,field_description:social_media_advertising_linkedin.field_social_campaign__unit_cost +msgid "Unit Cost" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_group_view_form +#: model_terms:ir.ui.view,arch_db:social_media_advertising_linkedin.social_campaign_view_form +msgid "Update in LinkedIn" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__video_view +msgid "Video views" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields.selection,name:social_media_advertising_linkedin.selection__social_campaign__linkedin_objective__website_visit +msgid "Website visits" +msgstr "" + +#. module: social_media_advertising_linkedin +#: model:ir.model.fields,help:social_media_advertising_linkedin.field_social_campaign__linkedin_is_test +msgid "Whether LinkedIn flags this campaign as a test campaign." +msgstr "" diff --git a/social_media_advertising_linkedin/models/__init__.py b/social_media_advertising_linkedin/models/__init__.py new file mode 100644 index 0000000000..59dfa56a9a --- /dev/null +++ b/social_media_advertising_linkedin/models/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import social_media +from . import social_campaign +from . import social_campaign_group +from . import social_account +from . import social_post +from . import social_post_account diff --git a/social_media_advertising_linkedin/models/social_account.py b/social_media_advertising_linkedin/models/social_account.py new file mode 100644 index 0000000000..047e990dc4 --- /dev/null +++ b/social_media_advertising_linkedin/models/social_account.py @@ -0,0 +1,757 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import itertools +import logging +from urllib.parse import quote + +from odoo import Command, _, models +from odoo.exceptions import UserError +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT + +from odoo.addons.social_media_base.social_utils import ( + _generate_timestamps, + convert_to_date, +) + +from ..social_advertising_linkedin_utils import ( + _ENDPOINT_AD_ACCOUNT_USERS_LINKEDIN, + _ENDPOINT_AD_ACCOUNTS_LINKEDIN, + _ENDPOINT_AD_ANALYTICS_LINKEDIN, + _ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN, + _ENDPOINT_AD_CAMPAIGNS_LINKEDIN, + _FIELDS_CAMPAIGN_LINKEDIN, + _FIELDS_STATISTIC_LINKEDIN, + _PAGE_SIZE_LINKEDIN, + _URL_CAMPAIGN_MANAGER_LINKEDIN, +) +from .social_campaign import LINKEDIN_DELETED_CODES + +_logger = logging.getLogger(__name__) + + +class SocialAccount(models.Model): + """LinkedIn Ads side of a social media account.""" + + _inherit = "social.account" + + def _get_linkedin_advertising_account(self): + """Return the advertising account URN matching the account environment. + + The URN is stored in ``advertising_account_id`` the first time it is + resolved: every campaign and campaign group endpoint is scoped by + advertising account, so without it each of them would need this + request first. + + :return: The advertising account URN or False if none is found. + :rtype: str | bool + """ + advertising_account_id = self.advertising_account_id + if advertising_account_id: + return advertising_account_id + is_test = self.enviroment == "test" + for account_urn in self._fetch_linkedin_ad_account_urns(): + if ( + bool(self._fetch_linkedin_ad_account(account_urn).get("test")) + is is_test + ): + self.sudo().write({"advertising_account_id": account_urn}) + return account_urn + return False + + def _fetch_linkedin_ad_account_urns(self): + """Return the advertising accounts the authorized member can reach. + + The ``search`` finder of ``adAccounts`` answers only to the partners + of the Marketing API (``partnerApiAdAccounts``), so the accounts are + discovered through the roles of the member instead, which every + application with the Advertising API product may read. + + :return: The advertising account URNs. + :rtype: list + """ + urns = [] + start = 0 + while True: + response = self._request_linkedin( + endpoint=_ENDPOINT_AD_ACCOUNT_USERS_LINKEDIN, + headers=self.media_id._get_linkedin_headers(self.sudo().access_token), + params_fields=["q", "start", "count"], + params_values={ + "q": "authenticatedUser", + "start": start, + "count": _PAGE_SIZE_LINKEDIN, + }, + return_json=False, + ) + if response.status_code != 200: + raise UserError( + _( + "Error get advertising account in Linkedin: %(error)s", + error=self._linkedin_error_message(response), + ) + ) + data = response.json() + elements = data.get("elements", []) + urns += [ + element["account"] for element in elements if element.get("account") + ] + start += _PAGE_SIZE_LINKEDIN + if not elements or start >= data.get("paging", {}).get("total", 0): + break + return urns + + def _fetch_linkedin_ad_account(self, account_urn): + """Return the advertising account of a URN. + + Its ``test`` flag is what tells the test accounts from the production + ones, and the role of the member does not carry it. + + :rtype: dict + """ + response = self._request_linkedin( + endpoint=(f"{_ENDPOINT_AD_ACCOUNTS_LINKEDIN}/{account_urn.split(':')[-1]}"), + headers=self.media_id._get_linkedin_headers(self.sudo().access_token), + return_json=False, + ) + if response.status_code != 200: + raise UserError( + _( + "Error get advertising account in Linkedin: %(error)s", + error=self._linkedin_error_message(response), + ) + ) + return response.json() + + def _linkedin_is_test(self): + """Return the ``test`` flag of the finders, as LinkedIn spells it. + + LinkedIn only returns the entities matching the requested flag, so it + must follow the environment of the account: a production account must + never see the test entities of the application. + + :rtype: str + """ + return "true" if self.enviroment == "test" else "false" + + def _get_linkedin_ad_account_id(self): + """Return the identifier of the advertising account, without its URN. + + :return: The identifier or False when the account has no advertising + account. + :rtype: str | bool + """ + advertising_account = self._get_linkedin_advertising_account() + return advertising_account.split(":")[-1] if advertising_account else False + + def _require_linkedin_ad_account_id(self): + """Return the identifier of the advertising account, or raise. + + Every campaign and campaign group endpoint is scoped by advertising + account, so an account without one cannot write anything on LinkedIn. + + :rtype: str + """ + ad_account_id = self._get_linkedin_ad_account_id() + if not ad_account_id: + raise UserError( + _( + "No LinkedIn advertising account is available for the " + "account %(account)s.", + account=self.display_name, + ) + ) + return ad_account_id + + def _fetch_linkedin_creatives(self, campaign_urns=None): + """Fetch the creatives of the advertising account. + + The Creatives API replaces ``adCreativesV2``: it answers the status + set by the advertiser and accepts both share and ugcPost references, + and it paginates with a cursor instead of an index. + + :param campaign_urns: Restrict the search to these campaign URNs. + :return: The creative elements, filtered by the environment of the + account. + :rtype: list + """ + ad_account_id = self._get_linkedin_ad_account_id() + if not ad_account_id: + return [] + is_test = self.enviroment == "test" + elements = [] + page_token = None + while True: + params_fields = ["q", "sortOrder", "pageSize"] + params_values = { + "q": "criteria", + "sortOrder": "ASCENDING", + "pageSize": _PAGE_SIZE_LINKEDIN, + } + if campaign_urns: + params_fields.append("campaigns") + params_values["campaigns"] = list(campaign_urns) + if page_token: + params_fields.append("pageToken") + params_values["pageToken"] = page_token + response = self._request_linkedin( + endpoint=f"/adAccounts/{ad_account_id}/creatives", + headers=self.media_id._get_linkedin_headers(self.sudo().access_token), + params_fields=params_fields, + params_values=params_values, + return_json=False, + format_quote=True, + ) + if response.status_code != 200: + raise UserError( + _( + "GET ADS: %(error)s", + error=self._linkedin_error_message(response), + ) + ) + data = response.json() + page_elements = data.get("elements", []) + elements += [ + element + for element in page_elements + if bool(element.get("isTest", False)) == is_test + ] + page_token = data.get("metadata", {}).get("nextPageToken") + if not page_elements or not page_token: + break + return elements + + def _fetch_linkedin_ad_entities(self, endpoint, search=None, fields=None): + """Fetch every element of an Ads search finder, following pagination. + + The search finders paginate with a cursor since the version 202401: + the answer carries the token of the next page in its metadata. + + :param endpoint: The Ads API endpoint (e.g. the campaigns of an + advertising account). + :param search: The Rest.li search criteria, without the ``search=`` + prefix (e.g. ``"(test:false)"``). + :param fields: The fields to project, comma separated. + :return: The list of elements. + :rtype: list + """ + elements = [] + page_token = None + while True: + params_fields = ["q", "pageSize"] + params_values = {"q": "search", "pageSize": _PAGE_SIZE_LINKEDIN} + params_values_char_ignore = None + if search: + params_fields.append("search") + params_values["search"] = search + params_values_char_ignore = {"search": [{"all": ":"}]} + if fields: + params_fields.append("fields") + params_values["fields"] = fields + if page_token: + params_fields.append("pageToken") + params_values["pageToken"] = page_token + response = self._request_linkedin( + endpoint=endpoint, + headers=self.media_id._get_linkedin_headers(self.sudo().access_token), + params_fields=params_fields, + params_values=params_values, + params_values_char_ignore=params_values_char_ignore, + return_json=False, + format_quote=True, + ) + if response.status_code != 200: + raise UserError( + _( + "Error getting campaigns from Linkedin: %(error)s", + error=self._linkedin_error_message(response), + ) + ) + data = response.json() + page_elements = data.get("elements", []) + elements += page_elements + page_token = data.get("metadata", {}).get("nextPageToken") + if not page_elements or not page_token: + break + return elements + + def _upsert_linkedin_campaigns(self, groups, campaigns): + """Create or update the campaign groups and campaigns from Ads elements. + + Records are searched and written with ``sudo()``: the record rule + scopes campaigns by ``user_id``, so without it an importer would not + see the records owned by other users and would create duplicates + with the same URN. + + :param groups: adCampaignGroups elements. + :param campaigns: adCampaigns elements. + :return: The number of created groups and campaigns. + :rtype: dict + """ + SocialGroup = ( + self.env["social.campaign.group"] + .sudo() + .with_context(active_test=False, skip_linkedin_budget_check=True) + ) + SocialCampaign = ( + self.env["social.campaign"] + .sudo() + .with_context(skip_linkedin_budget_check=True, active_test=False) + ) + Currency = self.env["res.currency"] + counts = {"groups": 0, "campaigns": 0} + groups_by_urn = {} + stages = self.env["social.stage"].search( + [("media_id.media_type", "=", "linkedin")] + ) + stage_by_scope = {(stage.applies_to, stage.code): stage.id for stage in stages} + campaign_selections = { + key: (field_name, dict(SocialCampaign._fields[field_name].selection)) + for key, field_name in ( + ("format", "linkedin_format"), + ("objectiveType", "linkedin_objective"), + ("politicalIntent", "linkedin_political_intent"), + ) + } + for element in groups: + urn = f"urn:li:sponsoredCampaignGroup:{element['id']}" + total_budget = element.get("totalBudget", {}) + status = element.get("status") or "" + vals = { + "name": element.get("name", ""), + "remote_ref": urn, + "total_budget": float(total_budget.get("amount", 0) or 0), + "stage_id": stage_by_scope.get(("group", status), False), + } + currency = Currency.search( + [("name", "=", total_budget.get("currencyCode"))], limit=1 + ) + if currency: + vals["currency_id"] = currency.id + group = SocialGroup.search([("remote_ref", "=", urn)], limit=1) + if group: + if group.linkedin_needs_update: + group.message_post( + body=_( + "Import kept the local pending changes. " + "LinkedIn values: name: %(name)s, " + "total budget: %(total_budget)s %(currency)s", + name=vals["name"], + total_budget=vals["total_budget"], + currency=total_budget.get("currencyCode", ""), + ) + ) + for field in ("name", "total_budget", "currency_id"): + vals.pop(field, None) + was_deleted = group.stage_id.code in LINKEDIN_DELETED_CODES + group.with_context(skip_linkedin_needs_update=True).write(vals) + if not was_deleted and group.stage_id.code in LINKEDIN_DELETED_CODES: + group.message_post( + body=_( + "This campaign group was deleted on LinkedIn. It " + "is kept in Odoo as history because LinkedIn " + "still returns it with its performance data." + ) + ) + else: + group = SocialGroup.create(vals) + counts["groups"] += 1 + groups_by_urn[urn] = group + for element in campaigns: + urn = f"urn:li:sponsoredCampaign:{element['id']}" + group_urn = element.get("campaignGroup", "") + group = groups_by_urn.get(group_urn) or SocialGroup.search( + [("remote_ref", "=", group_urn)], limit=1 + ) + status = element.get("status") or "" + vals = { + "name": element.get("name", ""), + "remote_ref": urn, + "unit_cost": float(element.get("unitCost", {}).get("amount", 0) or 0), + "daily_budget": float( + element.get("dailyBudget", {}).get("amount", 0) or 0 + ), + "media_id": self.media_id.id, + "account_ids": [Command.link(self.id)], + "stage_id": stage_by_scope.get(("campaign", status), False), + "linkedin_is_test": element.get("test", False), + } + vals.update( + { + field_name: element[key] + for key, (field_name, values) in campaign_selections.items() + if element.get(key) in values + } + ) + if group: + vals["campaign_group_id"] = group.id + campaign = SocialCampaign.search([("remote_ref", "=", urn)], limit=1) + if campaign: + if campaign.name == vals["name"]: + vals.pop("name") + if campaign.linkedin_needs_update: + campaign.message_post( + body=_( + "Import kept the local pending changes. " + "LinkedIn values: name: %(name)s, " + "unit cost: %(unit_cost)s, " + "daily budget: %(daily_budget)s, " + "campaign group: %(group)s", + name=element.get("name", ""), + unit_cost=vals["unit_cost"], + daily_budget=vals["daily_budget"], + group=group.name if group else "", + ) + ) + for field in ( + "name", + "unit_cost", + "daily_budget", + "campaign_group_id", + ): + vals.pop(field, None) + was_deleted = campaign.stage_id.code in LINKEDIN_DELETED_CODES + campaign.with_context(skip_linkedin_needs_update=True).write(vals) + if not was_deleted and campaign.stage_id.code in LINKEDIN_DELETED_CODES: + campaign.message_post( + body=_( + "This campaign was deleted on LinkedIn. It is " + "kept in Odoo as history because LinkedIn still " + "returns it with its performance data." + ) + ) + else: + SocialCampaign.create(vals) + counts["campaigns"] += 1 + return counts + + def _link_linkedin_creatives(self, creatives): + """Link the creatives with the publications they promote. + + The creative is the only source relating a post with its campaign: + the Posts API does not return it, so a publication imported from the + wall gets its campaign here. Publications published from Odoo take it + from their parent post instead and are never overwritten. + + :param creatives: Creatives API elements. + :return: The number of newly linked posts. + :rtype: int + """ + PostAccount = self.env["social.post.account"].sudo() + campaigns_by_urn = { + campaign.remote_ref: campaign + for campaign in self.env["social.campaign"] + .sudo() + .search( + [ + ( + "remote_ref", + "in", + [ + element["campaign"] + for element in creatives + if element.get("campaign") + ], + ) + ] + ) + } + linked = 0 + for element in creatives: + reference = element.get("content", {}).get("reference") + if not reference: + continue + post_account = PostAccount.search( + [ + ("remote_ref", "=", reference), + ("account_id", "=", self.id), + ], + limit=1, + ) + if not post_account: + continue + values = {} + creative_urn = str(element["id"]) + if post_account.creative_urn != creative_urn: + values["creative_urn"] = creative_urn + campaign = campaigns_by_urn.get(element.get("campaign")) + if ( + campaign + and not post_account.post_id + and post_account.social_campaign_id != campaign + ): + values["social_campaign_id"] = campaign.id + if values: + post_account.write(values) + linked += 1 + return linked + + def action_import_campaigns(self): + res = super().action_import_campaigns() + if self.media_id.media_type != "linkedin": + return res + try: + advertising_account_id = self._get_linkedin_advertising_account() + if not advertising_account_id: + return { + "success": False, + "message": _( + "No LinkedIn advertising account is available for " + "the account %(account)s.", + account=self.display_name, + ), + "groups": 0, + "campaigns": 0, + "ads": 0, + } + ad_account_id = advertising_account_id.split(":")[-1] + groups = self._fetch_linkedin_ad_entities( + _ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN % ad_account_id + ) + campaigns = self._fetch_linkedin_ad_entities( + _ENDPOINT_AD_CAMPAIGNS_LINKEDIN % ad_account_id + ) + campaign_urns = { + f"urn:li:sponsoredCampaign:{element['id']}" for element in campaigns + } + creatives = self._fetch_linkedin_creatives(campaign_urns=campaign_urns) + except UserError as error: + return { + "success": False, + "message": str(error), + "groups": 0, + "campaigns": 0, + "ads": 0, + } + counts = self._upsert_linkedin_campaigns(groups, campaigns) + counts["ads"] = self._link_linkedin_creatives(creatives) + return { + "success": True, + "message": _( + "%(groups)s campaign group(s), %(campaigns)s campaign(s) " + "and %(ads)s sponsored post(s) imported from LinkedIn.", + **counts, + ), + "groups": counts["groups"], + "campaigns": counts["campaigns"], + "ads": counts["ads"], + } + + def _get_campaigns(self, start_date=None, end_date=None, campaign_ids=None): + """Return the campaigns of the advertising account in a date range. + + The search finder has no date criteria, so the run schedule is + compared here: a campaign is kept when its schedule overlaps the + requested range, and a campaign without schedule is never dropped. + + :param campaign_ids: Restrict the answer to these campaign URNs. + :rtype: list + """ + ad_account_id = self._get_linkedin_ad_account_id() + if not ad_account_id: + return [] + campaigns = self._fetch_linkedin_ad_entities( + _ENDPOINT_AD_CAMPAIGNS_LINKEDIN % ad_account_id, + search=f"(test:{self._linkedin_is_test()})", + fields=_FIELDS_CAMPAIGN_LINKEDIN, + ) + if campaign_ids: + wanted = set(campaign_ids) + campaigns = [ + campaign + for campaign in campaigns + if f"urn:li:sponsoredCampaign:{campaign.get('id')}" in wanted + ] + start_time, end_time = _generate_timestamps(start_date, end_date) + return [ + campaign + for campaign in campaigns + if self._linkedin_campaign_in_range(campaign, start_time, end_time) + ] + + def _linkedin_campaign_in_range(self, campaign, start_time, end_time): + """Tell whether the run schedule of a campaign overlaps a range. + + :param campaign: An adCampaigns element. + :param start_time: Start of the range, in milliseconds. + :param end_time: End of the range, in milliseconds. + :rtype: bool + """ + run_schedule = campaign.get("runSchedule") or {} + schedule_start = run_schedule.get("start") + schedule_end = run_schedule.get("end") + if schedule_start and schedule_start > end_time: + return False + if schedule_end and schedule_end < start_time: + return False + return True + + def _get_statistics(self, ads_ids=None, start_date=None, end_date=None): + start_date, end_date = self._get_default_filter_date(start_date, end_date) + start_date = ( + start_date.strftime(DEFAULT_SERVER_DATE_FORMAT).split("-") + if not isinstance(start_date, str) + else start_date + ) + parse_start_date = ( + f"(year:{start_date[0]},month:{int(start_date[1])}," + f"day:{int(start_date[2])})" + ) + end_date = ( + end_date.strftime(DEFAULT_SERVER_DATE_FORMAT).split("-") + if not isinstance(end_date, str) + else end_date + ) + parse_end_date = ( + f"(year:{end_date[0]},month:{int(end_date[1])},day:{int(end_date[2])})" + ) + date_statistics_range = f"(start:{parse_start_date},end:{parse_end_date})" + + params_fields = [ + "q", + "pivots", + "timeGranularity", + "dateRange", + "fields", + ] + params_values = { + "q": "statistics", + "pivots": ["CAMPAIGN"], + "timeGranularity": "ALL", + "dateRange": date_statistics_range, + "fields": _FIELDS_STATISTIC_LINKEDIN, + } + if ads_ids: + params_fields.append("creatives") + params_values.update( + { + "pivots": ["CREATIVE"], + "creatives": list(ads_ids), + } + ) + response = self._request_linkedin( + endpoint=_ENDPOINT_AD_ANALYTICS_LINKEDIN, + headers=self.media_id._get_linkedin_headers(self.sudo().access_token), + params_fields=params_fields, + params_values=params_values, + params_values_char_ignore={"dateRange": [{"all": ":"}]}, + return_json=False, + format_quote=True, + ) + + if response.status_code == 200: + statistics = response.json().get("elements", []) + else: + raise UserError( + _( + "GET CAMPAIGNS STATISTICS: %(error)s", + error=self._linkedin_error_message(response), + ) + ) + return statistics + + def _get_statistics_ads(self, ads_ids, start_date, end_date): + return self._get_statistics( + ads_ids=ads_ids, start_date=start_date, end_date=end_date + ) + + def _load_ads(self, start_date=None, end_date=None): + start_date, end_date = self._get_default_filter_date(start_date, end_date) + ads = self._fetch_linkedin_creatives() + + ads_parse = [] + if ads: + ads_ids = list(map(lambda x: x["id"], ads)) + ads_statistics = self._get_statistics_ads( + ads_ids, start_date=None, end_date=None + ) + + campaign_ids = list(map(lambda x: x["campaign"], ads)) + ads_campaigns = self._get_campaigns( + start_date, end_date, campaign_ids=campaign_ids + ) + + references = { + ad["id"]: ad.get("content", {}).get("reference", False) for ad in ads + } + post_ids = [reference for reference in references.values() if reference] + ads_ugc_posts = {} + if post_ids: + ads_ugc_posts = { + ugc_post["id"]: ugc_post + for ugc_post in self._get_posts( + params_fields=["ids"], params_values={"ids": post_ids} + ) + } + stage_by_code = { + stage.code: stage + for stage in self.env["social.stage"].search( + [ + ("media_id.media_type", "=", "linkedin"), + ("applies_to", "=", "ad"), + ] + ) + } + for ad in ads: + statistic = list( + filter( + lambda x: ad["id"] in x["pivotValues"], + ads_statistics, + ) + ) + campaign = list( + filter( + lambda x: int(ad["campaign"].split(":")[-1]) == x["id"], + ads_campaigns, + ) + ) + post = {} + ugc_post = ads_ugc_posts.get(references.get(ad["id"], False), False) + if ugc_post: + post = { + "id": ugc_post["id"], + "name": ugc_post.get("commentary", ""), + } + campaign_data = campaign[0] if campaign else {} + account_id = campaign_data.get("account", "").split(":")[-1] + stage = stage_by_code.get(ad.get("intendedStatus", "")) + ad.update( + { + "media_type": self.media_type, + "statistic": statistic[0] if len(statistic) > 0 else {}, + "campaign": campaign_data, + "created": convert_to_date( + miliseconds=ad["createdAt"], + expire_date=False, + format_date="%d/%m/%Y", + ), + "status": stage.name if stage else ad.get("intendedStatus", ""), + "status_level": stage.level if stage else "secondary", + "status_detail": ", ".join(ad.get("servingHoldReasons", [])), + "post": post, + "url": f"{_URL_CAMPAIGN_MANAGER_LINKEDIN}{account_id}/" + f"creatives?creativeIds=" + f"{quote(str([ad['id'].split(':')[-1]]))}", + } + ) + ads_parse.append(ad) + return ads_parse + + def _load_ads_accounts(self): + res = super()._load_ads_accounts() + ads = list(res.get("ads", [])) + accounts = self.search([("media_type", "=", "linkedin")]).filtered( + lambda account: account.sudo().access_token + ) + for account in accounts: + try: + ads_linkedin = account._load_ads() + except UserError: + _logger.exception( + "Could not load LinkedIn ads for account %s", + account.display_name, + ) + continue + ads = list(itertools.chain(ads, ads_linkedin)) + res["ads"] = ads + return res diff --git a/social_media_advertising_linkedin/models/social_campaign.py b/social_media_advertising_linkedin/models/social_campaign.py new file mode 100644 index 0000000000..2f750ece6b --- /dev/null +++ b/social_media_advertising_linkedin/models/social_campaign.py @@ -0,0 +1,659 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + +from odoo.addons.social_media_base.social_utils import _generate_timestamps + +from ..social_advertising_linkedin_utils import ( + _ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN, + _ENDPOINT_AD_CAMPAIGNS_LINKEDIN, +) + +LINKEDIN_LOCKED_CODES = ("ARCHIVED", "CANCELED", "PENDING_DELETION", "REMOVED") +LINKEDIN_DELETED_CODES = ("PENDING_DELETION", "REMOVED") +LINKEDIN_START_MARGIN = 60 + + +class SocialCampaign(models.Model): + """Synchronization of the campaign with a LinkedIn Ads campaign.""" + + _inherit = "social.campaign" + + linkedin_is_test = fields.Boolean( + string="LinkedIn Test Campaign", + readonly=True, + copy=False, + help="Whether LinkedIn flags this campaign as a test campaign.", + ) + linkedin_format = fields.Selection( + [ + ("STANDARD_UPDATE", "Standard update"), + ("SINGLE_VIDEO", "Single video"), + ], + string="LinkedIn Ad Format", + default="STANDARD_UPDATE", + copy=False, + tracking=True, + help="Ad format of the campaign on LinkedIn. A campaign only accepts " + "posts of its format, and LinkedIn does not allow changing it once " + "the campaign is created, so a post with a video needs a campaign of " + "the 'Single video' format.", + ) + linkedin_objective = fields.Selection( + [ + ("BRAND_AWARENESS", "Brand awareness"), + ("VIDEO_VIEW", "Video views"), + ("WEBSITE_VISIT", "Website visits"), + ("ENGAGEMENT", "Engagement"), + ], + string="LinkedIn Objective", + copy=False, + tracking=True, + help="Goal of the campaign on LinkedIn, required for the 'Single " + "video' format:\n" + "- Brand awareness: reach as many people as possible.\n" + "- Video views: get the video of the post watched.\n" + "- Website visits: send traffic to a page.\n" + "- Engagement: get likes, comments and follows.\n" + "LinkedIn has three more objectives (lead generation, website " + "conversions and job applicants) that need a lead gen form, " + "conversion tracking or LinkedIn Talent Solutions, which are " + "configured outside Odoo and are therefore not offered here.", + ) + linkedin_political_intent = fields.Selection( + [ + ("NOT_POLITICAL", "Not political advertising"), + ("POLITICAL", "Political advertising"), + ("NOT_DECLARED", "Not declared"), + ], + string="LinkedIn Political Intent", + default="NOT_POLITICAL", + copy=False, + tracking=True, + help="Declaration required by LinkedIn to create a campaign: I " + "confirm this is not political advertising. None of my ads qualify " + "as political advertising under the law of the targeted countries, " + "including EU law for ads targeted to the EU. Change it to " + "'Political advertising' when the campaign does qualify as such.", + ) + linkedin_needs_update = fields.Boolean( + string="LinkedIn Pending Changes", + readonly=True, + copy=False, + help="The campaign was modified in Odoo after being synchronized " + "with LinkedIn. Use the 'Update in LinkedIn' button to push the " + "local values.", + ) + unit_cost = fields.Float( + help="Bid amount per click, impression or other event depending on " + "the pricing model. LinkedIn only applies it with manual, target " + "cost or cost cap bidding; with automatic bidding it is ignored.", + tracking=True, + ) + daily_budget = fields.Float( + help="Maximum amount to spend on this campaign per day. The " + "campaign group total budget limits the overall spending.", + tracking=True, + ) + currency_id = fields.Many2one( + "res.currency", related="campaign_group_id.currency_id" + ) + linkedin_locked = fields.Boolean( + compute="_compute_linkedin_locked", + store=True, + help="LinkedIn no longer accepts changes on this record.", + ) + + @api.depends("stage_id", "stage_id.code", "media_id", "media_id.media_type") + def _compute_linkedin_locked(self): + for campaign in self: + campaign.linkedin_locked = ( + campaign.media_id.media_type == "linkedin" + and campaign.stage_id.code in LINKEDIN_LOCKED_CODES + ) + + def _linkedin_account(self): + """Return the LinkedIn account used to call the Ads API. + + LinkedIn uses a single advertising account per campaign, so of the + ``account_ids`` of the base model only the first LinkedIn one is + relevant; :meth:`_check_linkedin_single_account` guarantees there is + at most one. + + :return: the account, empty when the campaign has none. + :rtype: recordset + """ + self.ensure_one() + return self.account_ids.filtered( + lambda account: account.media_id.media_type == "linkedin" + )[:1] + + @api.constrains("account_ids", "media_id") + def _check_linkedin_single_account(self): + """Restrict LinkedIn campaigns to a single LinkedIn account. + + The restriction lives in the connector on purpose: the base module + keeps ``account_ids`` open because another social media could allow + several accounts per campaign. + """ + for campaign in self: + if campaign.media_id.media_type != "linkedin": + continue + linkedin_accounts = campaign.account_ids.filtered( + lambda account: account.media_id.media_type == "linkedin" + ) + if len(linkedin_accounts) > 1: + raise ValidationError( + _("A LinkedIn campaign can only have one LinkedIn account.") + ) + + def _linkedin_runschedule_values(self): + """Return a fresh run schedule when LinkedIn would reject the stored one. + + LinkedIn validates the whole campaign on a partial update, and a + campaign left in DRAFT keeps the start date given on creation, + which is in the past as soon as some time goes by + (``DATE_TOO_EARLY``). Sending a new schedule is the only way to + update or archive it. A campaign already running keeps its own + dates, which LinkedIn accepts. + + :rtype: dict + """ + self.ensure_one() + if self.stage_id.code != "DRAFT": + return {} + start, end = _generate_timestamps() + return { + "runSchedule": { + "start": start + LINKEDIN_START_MARGIN * 1000, + "end": end, + } + } + + def _linkedin_sync_fields(self): + """Fields pushed to LinkedIn, flagged as pending when changed here. + + ``media_id`` is guarded as well although it is not pushed: changing + it is what decides whether the campaign is a LinkedIn one at all, so + without it the lock of an archived campaign is escaped by taking it + out of LinkedIn. + """ + return ( + "name", + "unit_cost", + "daily_budget", + "campaign_group_id", + "linkedin_political_intent", + "media_id", + ) + + def write(self, vals): + sync_change = not self.env.context.get("skip_linkedin_needs_update") and any( + field in vals for field in self._linkedin_sync_fields() + ) + if sync_change: + locked = self.filtered("linkedin_locked") + if locked: + raise UserError( + _( + "The campaign %(names)s cannot be modified because of " + "its LinkedIn status (%(status)s).", + names=", ".join(locked.mapped("display_name")), + status=", ".join(locked.mapped("stage_id.name")), + ) + ) + res = super().write(vals) + if sync_change: + to_flag = self.filtered( + lambda c: c.remote_ref + and c.media_type == "linkedin" + and not c.linkedin_needs_update + ) + if to_flag: + super(SocialCampaign, to_flag).write({"linkedin_needs_update": True}) + return res + + def _available_campaign(self): + media_type = super()._available_campaign() + media_type.append("linkedin") + return media_type + + def _linkedin_publish_campaign_group(self, account, advertising_account_id): + """Ensure the campaign group of this campaign exists on LinkedIn. + + An existing URN is verified, a missing one is created in DRAFT so it + does not spend budget until it is activated on Campaign Manager. + + :param account: The social.account used to call the LinkedIn API. + :param advertising_account_id: The advertising account URN. + :return: The campaign group LinkedIn URN or False. + :rtype: str | bool + """ + self.ensure_one() + if not self.campaign_group_id: + raise UserError( + _( + "The campaign %(campaign)s does not belong to a campaign " "group.", + campaign=self.display_name, + ) + ) + group_campaign = False + if advertising_account_id: + message_error = "Error creating group campaign in Linkedin:" + if self.campaign_group_id.remote_ref: + ad_account_id = advertising_account_id.split(":")[-1] + group_campaign = account._request_linkedin( + endpoint=( + f"{_ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN % ad_account_id}/" + f"{self.campaign_group_id.remote_ref.split(':')[-1]}" + ), + headers=account.media_id._get_linkedin_headers( + account.sudo().access_token + ), + return_json=False, + ) + if group_campaign and group_campaign.status_code == 200: + return self.campaign_group_id.remote_ref + elif not group_campaign or group_campaign.status_code == 404: + group_campaign = self.campaign_group_id._linkedin_create_group( + account, advertising_account_id + ) + else: + raise UserError( + _( + "%(message_error)s %(error_response)s", + message_error=message_error, + error_response=self.env[ + "social.account" + ]._linkedin_error_message(group_campaign), + ) + ) + return group_campaign + + def _linkedin_verify_campaign(self, account): + """Check that the stored campaign URN still exists on LinkedIn. + + :param account: The social.account used to call the LinkedIn API. + :return: The campaign URN if it exists, False otherwise. + :rtype: str | bool + """ + self.ensure_one() + if not self.remote_ref: + return False + ad_account_id = account._require_linkedin_ad_account_id() + campaign = account._request_linkedin( + endpoint=( + f"{_ENDPOINT_AD_CAMPAIGNS_LINKEDIN % ad_account_id}/" + f"{self.remote_ref.split(':')[-1]}" + ), + headers=account.media_id._get_linkedin_headers(account.sudo().access_token), + return_json=False, + ) + if campaign.status_code == 200: + return self.remote_ref + elif campaign.status_code == 404: + return False + raise UserError( + _( + "%(message_error)s %(error_response)s", + message_error="Error creating campaign in Linkedin:", + error_response=self.env["social.account"]._linkedin_error_message( + campaign + ), + ) + ) + + def _linkedin_create_campaign( + self, account, advertising_account_id, campaign_group_linkedin_urn + ): + """Create this campaign on LinkedIn under the given group. + + It is created in DRAFT so it does not spend budget until it is + activated on Campaign Manager. + + :param account: The social.account used to call the LinkedIn API. + :param advertising_account_id: The advertising account URN. + :param campaign_group_linkedin_urn: The campaign group URN. + :return: The campaign LinkedIn URN or False. + :rtype: str | bool + """ + self.ensure_one() + campaign = False + if campaign_group_linkedin_urn: + message_error = "Error creating campaign in Linkedin:" + stage = self.env["social.stage"]._get_stage("linkedin", "campaign", "DRAFT") + if not stage: + raise UserError( + _( + "The LinkedIn campaign stage with code %(code)s is " + "missing. Update the social_media_advertising_linkedin " + "module to restore the LinkedIn stages.", + code="DRAFT", + ) + ) + start, end = _generate_timestamps() + response = account._request_linkedin( + method="POST", + endpoint=( + _ENDPOINT_AD_CAMPAIGNS_LINKEDIN + % advertising_account_id.split(":")[-1] + ), + headers=account.media_id._get_linkedin_headers( + account.sudo().access_token + ), + json_data={ + "account": advertising_account_id, + "campaignGroup": campaign_group_linkedin_urn, + "name": f"{self.name}", + "type": "SPONSORED_UPDATES", + "format": self.linkedin_format or "STANDARD_UPDATE", + **( + {"objectiveType": self.linkedin_objective} + if self.linkedin_objective + else {} + ), + "politicalIntent": self.linkedin_political_intent, + "offsiteDeliveryEnabled": False, + "runSchedule": { + "start": start, + "end": end, + }, + "locale": { + "country": self.env.user.country_id.code or "US", + "language": self.env.user.lang.split("_")[0], + }, + "unitCost": { + "amount": f"{self.unit_cost}", + "currencyCode": self.currency_id.name, + }, + "dailyBudget": { + "amount": f"{self.daily_budget}", + "currencyCode": self.currency_id.name, + }, + "status": "DRAFT", + }, + return_json=False, + ) + if response.status_code == 201: + campaign = ( + "urn:li:sponsoredCampaign:" + f"{response.headers.get('Location').split('/')[-1]}" + ) + self.write({"remote_ref": campaign, "stage_id": stage.id}) + else: + raise UserError( + _( + "%(message_error)s %(error_response)s", + message_error=message_error, + error_response=self.env[ + "social.account" + ]._linkedin_error_message(response), + ) + ) + return campaign + + def _linkedin_publish_campaign( + self, account, advertising_account_id, campaign_group_linkedin_urn + ): + """Verify the stored campaign URN and create the campaign when missing. + + :param account: The social.account used to call the LinkedIn API. + :param advertising_account_id: The advertising account URN. + :param campaign_group_linkedin_urn: The campaign group URN. + :return: The campaign LinkedIn URN or False. + :rtype: str | bool + """ + self.ensure_one() + campaign = self._linkedin_verify_campaign(account) + if not campaign: + campaign = self._linkedin_create_campaign( + account, advertising_account_id, campaign_group_linkedin_urn + ) + return campaign + + def _validate_publish_linkedin(self): + """Check that this campaign is ready to be created on LinkedIn.""" + self.ensure_one() + errors = [] + if self.media_id.media_type != "linkedin": + errors.append(_("The campaign media must be LinkedIn.")) + if not self.campaign_group_id: + errors.append(_("The campaign must belong to a campaign group.")) + if not self._linkedin_account(): + errors.append(_("The campaign must have a social account.")) + if not self.currency_id: + errors.append(_("The campaign group must have a currency.")) + if self.campaign_group_id and self.campaign_group_id.total_budget <= 0: + errors.append(_("The campaign group total budget must be positive.")) + if self.unit_cost <= 0: + errors.append(_("The campaign unit cost must be positive.")) + if self.daily_budget <= 0: + errors.append(_("The campaign daily budget must be positive.")) + if self.linkedin_format == "SINGLE_VIDEO" and not self.linkedin_objective: + errors.append(_("LinkedIn requires an objective for the video format.")) + if not self.linkedin_political_intent: + errors.append( + _( + "LinkedIn requires declaring whether the campaign is " + "political advertising." + ) + ) + if errors: + raise UserError("\n".join(errors)) + + def action_publish_linkedin(self): + """Create the campaign group and the campaign on LinkedIn in DRAFT. + + The campaign can then be selected on posts and activated from + Campaign Manager when desired. + """ + self.ensure_one() + self._validate_publish_linkedin() + account = self._linkedin_account() + advertising_account_id = account._get_linkedin_advertising_account() + if not advertising_account_id: + raise UserError( + _( + "No LinkedIn advertising account is available for the " + "account %(account)s.", + account=account.display_name, + ) + ) + group_urn = self._linkedin_publish_campaign_group( + account, advertising_account_id + ) + try: + self._linkedin_publish_campaign(account, advertising_account_id, group_urn) + except UserError as error: + return self._register_publish_linkedin_failure(error) + self.message_post(body=_("Campaign created on LinkedIn in draft status.")) + return True + + def _register_publish_linkedin_failure(self, error): + """Report a campaign that could not be created on LinkedIn. + + :param error: The exception raised while creating the campaign. + :return: A client action showing the error to the user. + :rtype: dict + """ + self.ensure_one() + message = str(error) + self.message_post( + body=_( + "The campaign could not be created on LinkedIn: %(error)s", + error=message, + ) + ) + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Campaign not created on LinkedIn"), + "message": message, + "type": "danger", + "sticky": True, + "next": {"type": "ir.actions.act_window_close"}, + }, + } + + def action_update_linkedin(self): + """Push the local name, unit cost and daily budget to LinkedIn.""" + self.ensure_one() + if not self.remote_ref: + raise UserError(_("The campaign does not exist on LinkedIn yet.")) + if self.linkedin_locked: + raise UserError( + _( + "The campaign cannot be updated because of its LinkedIn " + "status (%(status)s).", + status=self.stage_id.name, + ) + ) + account = self._linkedin_account() + if not account or not self.currency_id: + raise UserError( + _("The campaign must have a social account and a currency.") + ) + if self.campaign_group_id and not self.campaign_group_id.remote_ref: + raise UserError( + _( + "The campaign group %(group)s does not exist on LinkedIn. " + "Create or import it before updating the campaign.", + group=self.campaign_group_id.display_name, + ) + ) + values = { + "name": f"{self.name}", + "unitCost": { + "amount": f"{self.unit_cost}", + "currencyCode": self.currency_id.name, + }, + "dailyBudget": { + "amount": f"{self.daily_budget}", + "currencyCode": self.currency_id.name, + }, + } + if self.campaign_group_id.remote_ref: + values["campaignGroup"] = self.campaign_group_id.remote_ref + if self.linkedin_political_intent: + values["politicalIntent"] = self.linkedin_political_intent + values.update(self._linkedin_runschedule_values()) + ad_account_id = account._require_linkedin_ad_account_id() + response = account._request_linkedin( + method="POST", + endpoint=( + f"{_ENDPOINT_AD_CAMPAIGNS_LINKEDIN % ad_account_id}/" + f"{self.remote_ref.split(':')[-1]}" + ), + headers=account.media_id._get_linkedin_headers( + account.sudo().access_token, + x_restli_method="PARTIAL_UPDATE", + ), + json_data={"patch": {"$set": values}}, + return_json=False, + ) + if response.status_code in (200, 204): + self.with_context(skip_linkedin_needs_update=True).write( + {"linkedin_needs_update": False} + ) + self.message_post(body=_("Campaign updated on LinkedIn.")) + else: + raise UserError( + _( + "Error updating campaign in Linkedin: %(error)s", + error=self.env["social.account"]._linkedin_error_message(response), + ) + ) + return True + + def action_archive_linkedin(self): + """Archive the campaign on LinkedIn. + + The Ads API does not allow deleting a campaign, so the campaign is + kept in Odoo with its performance data and becomes read only. + """ + self.ensure_one() + if not self.remote_ref: + raise UserError(_("The campaign does not exist on LinkedIn yet.")) + if self.linkedin_locked: + raise UserError( + _( + "The campaign cannot be archived because of its LinkedIn " + "status (%(status)s).", + status=self.stage_id.name, + ) + ) + if self.campaign_group_id.stage_id.code == "DRAFT": + raise UserError( + _( + "LinkedIn does not archive a campaign whose campaign " + "group is still in draft status. Activate the group " + "%(group)s in Campaign Manager first, or archive the " + "group itself, which archives its campaigns.", + group=self.campaign_group_id.display_name, + ) + ) + account = self._linkedin_account() + if not account: + raise UserError(_("The campaign must have a social account.")) + stage = self.env["social.stage"]._get_stage("linkedin", "campaign", "ARCHIVED") + if not stage: + raise UserError( + _( + "The LinkedIn campaign stage with code %(code)s is " + "missing. Update the social_media_advertising_linkedin " + "module to restore the LinkedIn stages.", + code="ARCHIVED", + ) + ) + ad_account_id = account._require_linkedin_ad_account_id() + response = account._request_linkedin( + method="POST", + endpoint=( + f"{_ENDPOINT_AD_CAMPAIGNS_LINKEDIN % ad_account_id}/" + f"{self.remote_ref.split(':')[-1]}" + ), + headers=account.media_id._get_linkedin_headers( + account.sudo().access_token, + x_restli_method="PARTIAL_UPDATE", + ), + json_data={ + "patch": { + "$set": { + "status": "ARCHIVED", + **self._linkedin_runschedule_values(), + } + } + }, + return_json=False, + ) + if response.status_code not in (200, 204): + raise UserError( + _( + "Error archiving campaign in Linkedin: %(error)s", + error=self.env["social.account"]._linkedin_error_message(response), + ) + ) + self.with_context(skip_linkedin_needs_update=True).write( + {"stage_id": stage.id, "linkedin_needs_update": False} + ) + self.message_post(body=_("Campaign archived on LinkedIn.")) + return True + + @api.constrains("daily_budget", "campaign_group_id") + def _check_daily_budget(self): + """Keep the daily budgets of a group within its total budget. + + This is a rule of Odoo, not of LinkedIn: a group without total + budget sets no limit there, and the import must never be stopped by + it, since the values it brings are the ones LinkedIn already + accepted. + + ``campaign_group_id`` is watched as well: moving a campaign into + another group is one more way of going over its total budget. + """ + if self.env.context.get("skip_linkedin_budget_check"): + return + self.campaign_group_id._check_linkedin_total_budget() diff --git a/social_media_advertising_linkedin/models/social_campaign_group.py b/social_media_advertising_linkedin/models/social_campaign_group.py new file mode 100644 index 0000000000..1411412a6a --- /dev/null +++ b/social_media_advertising_linkedin/models/social_campaign_group.py @@ -0,0 +1,395 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + +from odoo.addons.social_media_base.social_utils import _generate_timestamps + +from ..social_advertising_linkedin_utils import ( + _ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN, +) +from .social_campaign import LINKEDIN_LOCKED_CODES, LINKEDIN_START_MARGIN + + +class SocialCampaignGroup(models.Model): + """Synchronization of the group with a LinkedIn Ads campaign group.""" + + _inherit = "social.campaign.group" + + media_type = fields.Selection(related="media_id.media_type") + currency_id = fields.Many2one("res.currency") + total_budget = fields.Float( + tracking=True, + help="Maximum amount to spend across all the campaigns and " + "creatives of this group for its entire duration. It cannot be " + "lower than the daily budgets of its campaigns.", + ) + linkedin_needs_update = fields.Boolean( + string="LinkedIn Pending Changes", + readonly=True, + copy=False, + help="The campaign group was modified in Odoo after being " + "synchronized with LinkedIn. Use the 'Update in LinkedIn' button " + "to push the local values.", + ) + linkedin_locked = fields.Boolean( + compute="_compute_linkedin_locked", + store=True, + help="LinkedIn no longer accepts changes on this record.", + ) + + @api.depends("stage_id", "stage_id.code", "media_id", "media_id.media_type") + def _compute_linkedin_locked(self): + for group in self: + group.linkedin_locked = ( + group.media_id.media_type == "linkedin" + and group.stage_id.code in LINKEDIN_LOCKED_CODES + ) + + def _linkedin_sync_fields(self): + """Fields pushed to LinkedIn, flagged as pending when changed here.""" + return ("name", "total_budget") + + def write(self, vals): + sync_change = not self.env.context.get("skip_linkedin_needs_update") and any( + field in vals for field in self._linkedin_sync_fields() + ) + if sync_change: + locked = self.filtered("linkedin_locked") + if locked: + raise UserError( + _( + "The campaign group %(names)s cannot be modified " + "because of its LinkedIn status (%(status)s).", + names=", ".join(locked.mapped("display_name")), + status=", ".join(locked.mapped("stage_id.name")), + ) + ) + res = super().write(vals) + if sync_change: + to_flag = self.filtered( + lambda g: g.remote_ref + and g.media_id.media_type == "linkedin" + and not g.linkedin_needs_update + ) + if to_flag: + super(SocialCampaignGroup, to_flag).write( + {"linkedin_needs_update": True} + ) + return res + + @api.constrains("total_budget") + def _check_total_budget(self): + """Lowering the total budget must not leave the campaigns over it.""" + if self.env.context.get("skip_linkedin_budget_check"): + return + self._check_linkedin_total_budget() + + def _check_linkedin_total_budget(self): + """Raise when the daily budgets of a group exceed its total budget. + + Shared by both sides of the rule: the campaign checks it when its + daily budget or its group changes, the group when its total budget + does. + """ + for group in self: + if not group.total_budget: + continue + daily_budgets = sum(group.campaign_ids.mapped("daily_budget")) + if group.total_budget < daily_budgets: + raise ValidationError( + _( + "The daily budgets of the campaigns of the group " + "%(group)s add up to %(daily_budgets)s, which exceeds " + "its total budget of %(total_budget)s.", + group=group.display_name, + daily_budgets=daily_budgets, + total_budget=group.total_budget, + ) + ) + + def _get_linkedin_account(self): + """Return the social account used to call the LinkedIn API. + + The account decides which advertising account the group is written + to, so it is never guessed: it comes from the campaigns of the group, + and the database is only used as a fallback when it holds exactly one + LinkedIn account. Anything else is ambiguous and raises, because + picking the wrong one would create, update or archive the group in the + advertising account of another advertiser. + + :return: the account, possibly empty when none exists at all. + :rtype: recordset + :raises UserError: when several accounts could be used. + """ + self.ensure_one() + accounts = self.campaign_ids.account_ids.filtered( + lambda account: account.media_id.media_type == "linkedin" + ) + if not accounts: + accounts = self.env["social.account"].search( + [("media_id.media_type", "=", "linkedin")], order="id" + ) + if len(accounts) > 1: + raise UserError( + _( + "The campaign group %(group)s is not linked to any " + "LinkedIn account and the database holds several of " + "them. Set the account on one of its campaigns to " + "choose the advertising account to use.", + group=self.display_name, + ) + ) + elif len(accounts) > 1: + raise UserError( + _( + "The campaigns of the group %(group)s point to several " + "LinkedIn accounts (%(accounts)s). Leave a single one so " + "the advertising account to use is unambiguous.", + group=self.display_name, + accounts=", ".join(accounts.mapped("display_name")), + ) + ) + return accounts + + def _linkedin_create_group(self, account, advertising_account_id): + """Create this campaign group on LinkedIn in DRAFT status. + + DRAFT keeps it from spending budget until it is activated on + Campaign Manager. + + :param account: The social.account used to call the LinkedIn API. + :param advertising_account_id: The advertising account URN. + :return: The campaign group LinkedIn URN. + :rtype: str + """ + self.ensure_one() + stage = self.env["social.stage"]._get_stage("linkedin", "group", "DRAFT") + if not stage: + raise UserError( + _( + "The LinkedIn campaign group stage with code %(code)s is " + "missing. Update the social_media_advertising_linkedin " + "module to restore the LinkedIn stages.", + code="DRAFT", + ) + ) + start, end = _generate_timestamps() + response = account._request_linkedin( + method="POST", + endpoint=( + _ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN + % advertising_account_id.split(":")[-1] + ), + headers=account.media_id._get_linkedin_headers(account.sudo().access_token), + json_data={ + "account": advertising_account_id, + "name": self.name, + "runSchedule": { + "start": start, + "end": end, + }, + "status": "DRAFT", + "totalBudget": { + "amount": f"{self.total_budget}", + "currencyCode": self.currency_id.name, + }, + }, + return_json=False, + ) + if response.status_code != 201: + raise UserError( + _( + "Error creating group campaign in Linkedin: %(error)s", + error=self.env["social.account"]._linkedin_error_message(response), + ) + ) + urn = ( + "urn:li:sponsoredCampaignGroup:" + f"{response.headers.get('Location').split('/')[-1]}" + ) + self.with_context(skip_linkedin_needs_update=True).write( + {"remote_ref": urn, "stage_id": stage.id} + ) + return urn + + def _linkedin_runschedule_values(self): + """Return a fresh run schedule when LinkedIn would reject the stored one. + + LinkedIn validates the whole group on a partial update, and a group + left in DRAFT keeps the start date given on creation, which is in + the past as soon as some time goes by (``DATE_TOO_EARLY``). A group + already running keeps its own dates, which LinkedIn accepts. + + :rtype: dict + """ + self.ensure_one() + if self.stage_id.code != "DRAFT": + return {} + start, end = _generate_timestamps() + return { + "runSchedule": { + "start": start + LINKEDIN_START_MARGIN * 1000, + "end": end, + } + } + + def action_publish_linkedin(self): + """Create the campaign group on LinkedIn in DRAFT status. + + The group is also created automatically when the first campaign of + the group is published with the 'Create in LinkedIn' button. + """ + self.ensure_one() + if self.remote_ref: + raise UserError(_("The campaign group already exists on LinkedIn.")) + errors = [] + if not self.currency_id: + errors.append(_("The campaign group must have a currency.")) + if self.total_budget <= 0: + errors.append(_("The campaign group total budget must be positive.")) + if errors: + raise UserError("\n".join(errors)) + account = self._get_linkedin_account() + if not account: + raise UserError( + _("No LinkedIn social account is available to create the group.") + ) + advertising_account_id = account._get_linkedin_advertising_account() + if not advertising_account_id: + raise UserError( + _( + "No LinkedIn advertising account is available for the " + "account %(account)s.", + account=account.display_name, + ) + ) + self._linkedin_create_group(account, advertising_account_id) + self.message_post(body=_("Campaign group created on LinkedIn in draft status.")) + return True + + def action_update_linkedin(self): + """Push the local name and total budget to LinkedIn.""" + self.ensure_one() + if not self.remote_ref: + raise UserError(_("The campaign group does not exist on LinkedIn yet.")) + if self.linkedin_locked: + raise UserError( + _( + "The campaign group cannot be updated because of its " + "LinkedIn status (%(status)s).", + status=self.stage_id.name, + ) + ) + if not self.currency_id: + raise UserError(_("The campaign group must have a currency.")) + account = self._get_linkedin_account() + if not account: + raise UserError( + _("No LinkedIn social account is available to update the group.") + ) + ad_account_id = account._require_linkedin_ad_account_id() + response = account._request_linkedin( + method="POST", + endpoint=( + f"{_ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN % ad_account_id}/" + f"{self.remote_ref.split(':')[-1]}" + ), + headers=account.media_id._get_linkedin_headers( + account.sudo().access_token, + x_restli_method="PARTIAL_UPDATE", + ), + json_data={ + "patch": { + "$set": { + "name": f"{self.name}", + "totalBudget": { + "amount": f"{self.total_budget}", + "currencyCode": self.currency_id.name, + }, + **self._linkedin_runschedule_values(), + } + } + }, + return_json=False, + ) + if response.status_code in (200, 204): + self.with_context(skip_linkedin_needs_update=True).write( + {"linkedin_needs_update": False} + ) + self.message_post(body=_("Campaign group updated on LinkedIn.")) + else: + raise UserError( + _( + "Error updating campaign group in Linkedin: %(error)s", + error=self.env["social.account"]._linkedin_error_message(response), + ) + ) + return True + + def action_archive_linkedin(self): + """Archive the campaign group on LinkedIn. + + The Ads API does not allow deleting a group. LinkedIn applies the + same status to its campaigns, so run an import afterwards. + """ + self.ensure_one() + if not self.remote_ref: + raise UserError(_("The campaign group does not exist on LinkedIn yet.")) + if self.linkedin_locked: + raise UserError( + _( + "The campaign group cannot be archived because of its " + "LinkedIn status (%(status)s).", + status=self.stage_id.name, + ) + ) + account = self._get_linkedin_account() + if not account: + raise UserError( + _("No LinkedIn social account is available to archive the group.") + ) + stage = self.env["social.stage"]._get_stage("linkedin", "group", "ARCHIVED") + if not stage: + raise UserError( + _( + "The LinkedIn campaign group stage with code %(code)s is " + "missing. Update the social_media_advertising_linkedin " + "module to restore the LinkedIn stages.", + code="ARCHIVED", + ) + ) + ad_account_id = account._require_linkedin_ad_account_id() + response = account._request_linkedin( + method="POST", + endpoint=( + f"{_ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN % ad_account_id}/" + f"{self.remote_ref.split(':')[-1]}" + ), + headers=account.media_id._get_linkedin_headers( + account.sudo().access_token, + x_restli_method="PARTIAL_UPDATE", + ), + json_data={ + "patch": { + "$set": { + "status": "ARCHIVED", + **self._linkedin_runschedule_values(), + } + } + }, + return_json=False, + ) + if response.status_code not in (200, 204): + raise UserError( + _( + "Error archiving campaign group in Linkedin: %(error)s", + error=self.env["social.account"]._linkedin_error_message(response), + ) + ) + self.with_context(skip_linkedin_needs_update=True).write( + {"stage_id": stage.id, "linkedin_needs_update": False} + ) + self.message_post(body=_("Campaign group archived on LinkedIn.")) + return True diff --git a/social_media_advertising_linkedin/models/social_media.py b/social_media_advertising_linkedin/models/social_media.py new file mode 100644 index 0000000000..d79e9a4da5 --- /dev/null +++ b/social_media_advertising_linkedin/models/social_media.py @@ -0,0 +1,20 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from ..social_advertising_linkedin_utils import _SCOPE_ADS_LINKEDIN + + +class SocialMedia(models.Model): + _inherit = "social.media" + + def _get_linkedin_scopes(self): + """Append the Advertising API scopes to the LinkedIn authorization. + + The tokens already issued keep the scopes they were granted with, so + every account authorized before installing this module has to be + re-authorized from the account wizard to be able to call the Ads API. + """ + scopes = super()._get_linkedin_scopes() + return scopes + [s for s in _SCOPE_ADS_LINKEDIN if s not in scopes] diff --git a/social_media_advertising_linkedin/models/social_post.py b/social_media_advertising_linkedin/models/social_post.py new file mode 100644 index 0000000000..7ca4195fcf --- /dev/null +++ b/social_media_advertising_linkedin/models/social_post.py @@ -0,0 +1,64 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, models +from odoo.osv import expression + + +class SocialPost(models.Model): + """LinkedIn Ads constraints on the campaigns a post can promote.""" + + _inherit = "social.post" + + def _get_allow_social_campaign_domain(self): + """Only offer the LinkedIn campaigns that already exist on LinkedIn. + + A sponsored creative can only be attached to a campaign carrying a + remote reference, so campaigns not created on LinkedIn yet are kept + out of the selection. + + :rtype: list + """ + domain = super()._get_allow_social_campaign_domain() + return expression.AND( + [ + domain, + [ + "|", + ("media_id.media_type", "!=", "linkedin"), + ("remote_ref", "!=", False), + ], + ] + ) + + @api.depends( + "social_campaign_id", + "social_campaign_id.media_id", + "social_campaign_id.campaign_group_id", + ) + def _compute_message_info(self): + """Only declares the advertising dependencies of the computation.""" + return super()._compute_message_info() + + def _linkedin_message_info(self): + """Add the warning about sponsoring a multi-image post. + + :rtype: list + """ + messages = super()._linkedin_message_info() + if ( + len(self.image_ids) > 1 + and not self.video_ids + and self.social_campaign_id.media_id.media_type == "linkedin" + and self.social_campaign_id.campaign_group_id + ): + messages.append( + _( + "A post with several images is published as a multi-image " + "post, and LinkedIn does not sponsor that format, so this " + "post cannot be linked to the campaign %(campaign)s. Leave " + "a single image or remove the campaign.", + campaign=self.social_campaign_id.display_name, + ) + ) + return messages diff --git a/social_media_advertising_linkedin/models/social_post_account.py b/social_media_advertising_linkedin/models/social_post_account.py new file mode 100644 index 0000000000..b93f136076 --- /dev/null +++ b/social_media_advertising_linkedin/models/social_post_account.py @@ -0,0 +1,216 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +import psycopg2 + +from odoo import _, fields, models +from odoo.exceptions import UserError +from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY + +_logger = logging.getLogger(__name__) + + +class SocialPostAccount(models.Model): + """Sponsored creative of a publication on a LinkedIn account.""" + + _inherit = "social.post.account" + + creative_urn = fields.Char() + + def _linkedin_advertising_accounts(self): + return self.account_id._get_linkedin_advertising_account() + + def _requires_campaign_post(self): + """Return whether publishing must create a LinkedIn sponsored creative. + + The campaign is read from ``social_campaign_id`` and not from the + parent post, so publications imported from LinkedIn — which have no + parent post — resolve it the same way. + + :rtype: bool + """ + self.ensure_one() + return bool( + self.media_id.media_type == "linkedin" + and self.social_campaign_id + and self.social_campaign_id.campaign_group_id + and self.social_campaign_id.media_id.media_type == "linkedin" + ) + + def _check_linkedin_campaign_format(self): + """Check that the post matches the ad format of its campaign. + + LinkedIn only accepts creatives of the format chosen when the campaign + was created, so publishing a video in a standard campaign would leave + the post online without its ad. A post carrying several images is + published as a multi-image post, which LinkedIn does not sponsor at + all. + + :raise UserError: When the post and the campaign formats differ. + """ + self.ensure_one() + campaign = self.social_campaign_id + has_video = bool(self.post_id.video_ids) + is_video_campaign = campaign.linkedin_format == "SINGLE_VIDEO" + if not has_video and len(self.post_id.image_ids) > 1: + raise UserError( + _( + "LinkedIn does not sponsor posts with several images: they " + "are published as a multi-image post, a format its API " + "cannot turn into an ad, so this post cannot be linked to " + "the campaign %(campaign)s. Publish it with a single image " + "or without a campaign. LinkedIn offers the Carousel ad " + "format for this case, which this module does not support " + "yet.", + campaign=campaign.display_name, + ) + ) + if has_video and not is_video_campaign: + raise UserError( + _( + "The post contains a video, so it needs a campaign of the " + "'Single video' format. The campaign %(campaign)s uses the " + "'Standard update' format and LinkedIn does not allow " + "changing it once the campaign is created.", + campaign=campaign.display_name, + ) + ) + if is_video_campaign and not has_video: + raise UserError( + _( + "The campaign %(campaign)s uses the 'Single video' format, " + "so it only accepts posts containing a video.", + campaign=campaign.display_name, + ) + ) + + def _action_campaign_post(self, post_id): + """Create the sponsored creative of the post on LinkedIn. + + :param post_id: URN of the post published on LinkedIn. + :return: URN of the creative created on LinkedIn. + """ + res = super()._action_campaign_post(post_id) + if self._requires_campaign_post(): + campaign_linkedin_urn = self.social_campaign_id.remote_ref + if not campaign_linkedin_urn: + raise UserError( + _( + "The campaign %(campaign)s has not been created on " + "LinkedIn yet. Use the 'Create in LinkedIn' button " + "on the campaign before posting.", + campaign=self.social_campaign_id.display_name, + ) + ) + if campaign_linkedin_urn and post_id: + ad_account_id = self.account_id._get_linkedin_ad_account_id() + if not ad_account_id: + raise UserError( + _( + "No LinkedIn advertising account is available for " + "the account %(account)s.", + account=self.account_id.display_name, + ) + ) + response = self.account_id._request_linkedin( + method="POST", + endpoint=f"/adAccounts/{ad_account_id}/creatives", + headers=self.account_id.media_id._get_linkedin_headers( + self.account_id.sudo().access_token + ), + json_data={ + "campaign": campaign_linkedin_urn, + "intendedStatus": "ACTIVE", + "content": {"reference": post_id}, + }, + return_json=False, + ) + if response.status_code == 201: + res = response.headers.get("x-restli-id") + else: + raise UserError( + _( + "Error creating campaign post in Linkedin: %(error)s", + error=self.account_id._linkedin_error_message(response), + ) + ) + else: + raise UserError( + _( + "The campaign could not be generated for the post, " + "please try again later." + ) + ) + return res + + def _check_linkedin_publishable(self): + """Validate the campaign of a sponsored post before publishing. + + The sponsored creative is created after the post is online and its + failures are only reported, so everything that can be checked upfront + must fail here, while nothing irreversible has happened yet. + """ + res = super()._check_linkedin_publishable() + if self._requires_campaign_post(): + if not self.social_campaign_id.remote_ref: + raise UserError( + _( + "The campaign %(campaign)s has not been created on " + "LinkedIn yet. Use the 'Create in LinkedIn' button " + "on the campaign before posting.", + campaign=self.social_campaign_id.display_name, + ) + ) + self._check_linkedin_campaign_format() + self._linkedin_advertising_accounts() + return res + + def _register_creative_failure(self, post_entity): + """Log and report a creative that could not be created. + + The post itself is already online, so the failure is surfaced on the + post chatter instead of being raised. Publications imported from + LinkedIn have no parent post, hence the guard. + + :param post_entity: URN of the post published on LinkedIn. + """ + _logger.exception( + "Error creating the LinkedIn campaign creative for post %s", + post_entity, + ) + if self.post_id: + self.post_id.message_post( + body=_( + "The post was published on LinkedIn but the campaign " + "creative could not be created. Check the Ads permissions " + "of the account." + ) + ) + + def _linkedin_published_values(self, post_entity): + """Store the URN of the sponsored creative of the post. + + This runs inside :meth:`_publish_guard` after ``remote_ref`` has been + written, so no error may escape: rolling back the savepoint here would + drop the reference of a post that already exists on LinkedIn, and + retrying would publish it a second time. The only exception is the + concurrency errors that :meth:`_publish_guard` re-raises on purpose: + they must keep propagating so Odoo retries the whole transaction. + + :param post_entity: URN of the post published on LinkedIn. + :rtype: dict + """ + values = super()._linkedin_published_values(post_entity) + if not self._requires_campaign_post(): + return values + try: + values["creative_urn"] = self._action_campaign_post(post_entity) + except psycopg2.OperationalError as error: + if error.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + self._register_creative_failure(post_entity) + except Exception: # noqa: BLE001 - the post is already published + self._register_creative_failure(post_entity) + return values diff --git a/social_media_advertising_linkedin/pyproject.toml b/social_media_advertising_linkedin/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/social_media_advertising_linkedin/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/social_media_advertising_linkedin/readme/CONFIGURE.md b/social_media_advertising_linkedin/readme/CONFIGURE.md new file mode 100644 index 0000000000..d635711d56 --- /dev/null +++ b/social_media_advertising_linkedin/readme/CONFIGURE.md @@ -0,0 +1,32 @@ +The LinkedIn application. +--------------- + +- The campaign features (*Create in LinkedIn* and *Import campaigns*) require + an advertising account in the LinkedIn Campaign Manager and the following + products, which the *Social Media Linkedin* module does not request on its + own. Ask for them on the *Products* tab of your LinkedIn Developer App, + following the configuration steps of that module: + * Advertising API + * LinkedIn Ad Library +- The member who authorizes the account must also have a role on that + advertising account (`ACCOUNT_BILLING_ADMIN`, `ACCOUNT_MANAGER`, + `CAMPAIGN_MANAGER`, `CREATIVE_MANAGER` or `VIEWER`, the last one read + only). LinkedIn only answers the advertising accounts of the authorized + member, so without a role the import finds nothing. +- That requirement is unrelated to the formats LinkedIn accepts as an ad: a + post with several images cannot be sponsored with any product nor with any + advertising account, because LinkedIn does not turn a multi-image post into + an ad. See the ROADMAP section about it. + +Re-authorize the already associated accounts. +--------------- + +- Installing this module adds the `r_ads`, `rw_ads` and `r_ads_reporting` + OAuth scopes to the LinkedIn authorization. The tokens already issued keep + the scopes they were granted with — refreshing a token does not renegotiate + them — so every LinkedIn account that was authorized **before** installing + this module must be re-authorized from the account wizard (*Update + account* > *Update token*) before it can call the Ads API. +- These scopes are only requested while this module is installed, because + LinkedIn rejects the authorization of an application that asks for them + without the *Advertising API* product enabled. diff --git a/social_media_advertising_linkedin/readme/CONTRIBUTORS.md b/social_media_advertising_linkedin/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..ab4e0fb41b --- /dev/null +++ b/social_media_advertising_linkedin/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Binhex] (https://www.binhex.cloud): + - Edilio Escalona Almira diff --git a/social_media_advertising_linkedin/readme/DESCRIPTION.md b/social_media_advertising_linkedin/readme/DESCRIPTION.md new file mode 100644 index 0000000000..8480231467 --- /dev/null +++ b/social_media_advertising_linkedin/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +This module plugs LinkedIn Ads into *Social Media Advertising*. + +Main features: + +- Campaign group and campaign management synchronized with LinkedIn Ads: + create them in LinkedIn from Odoo (in draft state) or import the existing + ones, and link posts to campaigns as sponsored creatives. +- The LinkedIn statuses of campaigns, campaign groups and ads (Draft, + Active, Paused, Archived...) are declared as stages of *Social Media + Advertising*, so the badges and the status bars show the real LinkedIn + vocabulary. diff --git a/social_media_advertising_linkedin/readme/ROADMAP.md b/social_media_advertising_linkedin/readme/ROADMAP.md new file mode 100644 index 0000000000..c74ddf4baf --- /dev/null +++ b/social_media_advertising_linkedin/readme/ROADMAP.md @@ -0,0 +1,34 @@ +Campaign objectives +------------------- + +- LinkedIn defines seven campaign objectives: brand awareness, video views, + website visits, engagement, lead generation, website conversions and job + applicants. +- Only the first four are offered by this module. The other three need a + configuration that lives on LinkedIn and that the module does not create + nor check: lead generation needs a lead gen form, website conversions need + conversion tracking, and job applicants needs LinkedIn Talent Solutions and + is not compatible with the video format. Selecting them without that + configuration is refused by LinkedIn when the campaign is created. +- Campaigns imported from LinkedIn keep their real objective; the ones that + are not offered here are simply not shown in the field. + +Sponsored posts with several images +----------------------------------- + +- A post with more than one image is published as a multi-image post, and + LinkedIn does not sponsor that format: *"API partners can only create + non-sponsored multiImage posts"*. Linking such a post to a campaign is + refused before publishing, so the post is not left online without its ad, + and the post form warns about it as soon as the campaign is selected. + + https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/multiimage-post-api + +- This is a restriction of the content format, not of the access level of the + application nor of the advertising account: no LinkedIn plan sponsors a + multi-image post. +- The format LinkedIn does offer for several sponsored images is Carousel + ads (`content.carousel` with an `adContext`, on a campaign whose format is + set to carousel when it is created). This module does not implement it, so + the case is not supported here even though the platform allows it. +- Publish the post with a single image if it has to be sponsored. diff --git a/social_media_advertising_linkedin/readme/USAGE.md b/social_media_advertising_linkedin/readme/USAGE.md new file mode 100644 index 0000000000..dd08c17c2d --- /dev/null +++ b/social_media_advertising_linkedin/readme/USAGE.md @@ -0,0 +1,147 @@ +Create a campaign in LinkedIn. +--------------- + +To link a post to a campaign, the campaign must already exist in LinkedIn +(only campaigns already synchronized with LinkedIn can be selected on the +post). Publishing a post does not create the campaign anymore. + +- Go to *Social Media* > Advertising > Campaigns and open the campaign +- Fill in the campaign group, account, budget and currency fields +- Choose the *LinkedIn Ad Format*: *Standard update* for posts with text or + images, *Single video* for posts with a video. LinkedIn fixes the format + when the campaign is created and only accepts posts of that format + afterwards, so a post with a video needs its own video campaign. +- The *LinkedIn Objective* is required by LinkedIn for the video format. + See the ROADMAP section about the objectives that are not offered here. +- The *LinkedIn Political Intent* is the declaration LinkedIn requires to + create any campaign since the EU political advertising regulation. It + defaults to *Not political advertising*, which states that none of your + ads qualify as political advertising under the law of the targeted + countries, including EU law for ads targeted to the EU. Change it to + *Political advertising* when they do. +- Click on the *Create in LinkedIn* button +- The campaign group and the campaign are created in LinkedIn in **DRAFT** + status, so no budget is spent until you activate them from the LinkedIn + Campaign Manager. +- If the campaign cannot be created, the error is shown in a notification + and logged in the chatter instead of being raised: the campaign group is + already created on LinkedIn, which does not allow deleting it, so its + reference is kept to avoid importing it later as a duplicate. Fix the + reported problem and press *Create in LinkedIn* again; the existing group + is reused. +- A campaign group can also be created on LinkedIn on its own: open the + group and click its *Create in LinkedIn* button (it requires a positive + total budget and a currency). If you forget to do it, the group is + created automatically when the first campaign of the group is published. +- When a post linked to a campaign is published, it is attached to the + campaign as a sponsored creative. If the post is published but the creative + cannot be linked, the post is kept and the error is logged in the chatter. +- A post with more than one image cannot be sponsored: LinkedIn publishes it + as a multi-image post and does not accept that format as an ad, so linking + it to a campaign is refused before publishing. The post form warns about it + as soon as the campaign is selected, without waiting for the *Post* button. + +Import campaigns from LinkedIn. +--------------- + +- Go to *Social Media* > Configuration > Accounts +- Select the account +- Click on the *Import campaigns* button +- The campaign groups, campaigns and their sponsored creatives are imported + from LinkedIn Ads. Every creative is matched with the publication it + promotes by its remote reference. +- A publication brought from the wall with *Update* has no post in Odoo, so + its campaign is taken from the creative and its badge appears on the + dashboard. Publications published from Odoo keep the campaign of their + post and are never overwritten by the import. +- The Posts API does not return the campaign of a post, so only this import + can resolve it. The order does not matter, but the publications have to be + already in Odoo: if they are brought **after** importing the campaigns, + press *Import campaigns* again. +- Campaign groups created on LinkedIn often carry no total budget, which is + optional there. Those groups set no limit in Odoo either: the rule keeping + the daily budgets of the campaigns within the total budget of their group + only applies when the group has one, and it is never checked on the values + brought by the import. The rule is enforced from both sides: raising the + daily budget of a campaign, moving it to another group, and lowering the + total budget of a group are all refused when the sum goes over it. +- The real LinkedIn status of each campaign and campaign group (Draft, + Active, Paused, Archived...) is stored in its *Stage*, shown as a status + bar on the form and refreshed on every import. Campaigns flagged as test + campaigns on LinkedIn are marked as well. +- Campaigns and campaign groups deleted on LinkedIn (*Pending deletion*, + *Removed*) are hidden in the LinkedIn Campaign Manager, but the API + keeps returning them because of their performance data. They are + therefore kept in Odoo as history, and a note is logged in their chatter + when the deletion is detected. Keep in mind that deleting a campaign + group on LinkedIn also deletes all its campaigns and ads. + +Update a campaign in LinkedIn. +--------------- + +- When you modify in Odoo the editable fields of a campaign (name, unit + cost, daily budget, campaign group) or of a campaign group (name, total + budget) that already exists on LinkedIn, the record shows a *Pending + changes* ribbon, a warning banner explaining that the local changes have + not been pushed yet, and an *Update in LinkedIn* button. A record that + does not exist on LinkedIn yet carries the *Not synced* ribbon instead. The tracked fields also + log their changes in the chatter. + + ![CAMPAIGN_NOT_SYNCED](../static/img/readme/CAMPAIGN_NOT_SYNCED.png) + +- Click *Update in LinkedIn* to push the local values to LinkedIn. To move + the campaign to another campaign group, the target group must already + exist on LinkedIn. The political advertising declaration is pushed the + same way, since LinkedIn allows updating it when the targeting changes. +- Synchronization priority is hybrid: the LinkedIn-only fields (stage, + test flag) are always refreshed by *Import campaigns*; for the editable + fields, your local pending changes are preserved by the import — the + LinkedIn values are logged in the chatter so you can decide whether to + keep your changes (push them with *Update in LinkedIn*) or re-type the + LinkedIn ones. +- When the LinkedIn stage is *Archived*, *Canceled*, *Pending deletion* + or *Removed*, the campaign or campaign group cannot be modified in Odoo + (LinkedIn does not allow editing them either): the editable fields + become read-only, the *Update in LinkedIn* button is hidden and a red + banner explains the situation. The *Media* of a campaign that exists on + LinkedIn is read-only as well, so the lock cannot be escaped by taking the + campaign out of LinkedIn. The import keeps refreshing their stage, + so an archived record becomes editable again once it is unarchived on + LinkedIn. + +Archive a campaign or a campaign group in LinkedIn. +--------------- + +- LinkedIn does **not** allow deleting a campaign or a campaign group + through its API. Archiving is the only way to end them, and it is what the + *Archive in LinkedIn* button does. +- Go to *Social Media* > Advertising, open the campaign or the campaign + group and click *Archive in LinkedIn*. The button is only shown when the + record already exists on LinkedIn and is not already archived, canceled or + deleted. + + ![CAMPAIGN_ARCHIVE](../static/img/readme/CAMPAIGN_ARCHIVE.png) + +- LinkedIn refuses to archive a campaign while its campaign group is still + in **Draft**: activate the group in the Campaign Manager first, or archive + the group instead, which archives its campaigns. Odoo checks it before + calling LinkedIn and explains it. +- A campaign or a campaign group still in Draft keeps the start date given + when it was created, and LinkedIn rejects any later change because that + date already belongs to the past. Odoo therefore sends a new run schedule + when it updates or archives a record in Draft, which moves its start date + to the moment of the change. +- The campaign stops running on LinkedIn and disappears from the Campaign + Manager active lists, but it is kept in Odoo with its performance data. + Its stage becomes *Archived*, which makes the record read-only in Odoo, + because LinkedIn no longer accepts changes on it. +- **It cannot be reactivated from Odoo.** To unarchive it, use the LinkedIn + Campaign Manager and then run *Import campaigns* to refresh the stage. +- Archiving a campaign group archives its campaigns on LinkedIn as well, + except the ones still in Draft, which LinkedIn leaves untouched. Odoo does + not know about any of it until you run *Import campaigns*, so run it + afterwards to refresh them, and archive the draft campaigns one by one if + you want them archived too. +- Deleting the campaign in Odoo does not do anything on LinkedIn: the + campaign keeps running there and the next *Import campaigns* brings it + back. Use *Archive in LinkedIn* to actually end it. diff --git a/social_media_advertising_linkedin/social_advertising_linkedin_utils.py b/social_media_advertising_linkedin/social_advertising_linkedin_utils.py new file mode 100644 index 0000000000..31138583e6 --- /dev/null +++ b/social_media_advertising_linkedin/social_advertising_linkedin_utils.py @@ -0,0 +1,20 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +_URL_CAMPAIGN_MANAGER_LINKEDIN = "https://www.linkedin.com/campaignmanager/accounts/" + +_FIELDS_CAMPAIGN_LINKEDIN = "id,name,test,account,format,objectiveType,runSchedule" + +_ENDPOINT_AD_ACCOUNTS_LINKEDIN = "/adAccounts" +_ENDPOINT_AD_ACCOUNT_USERS_LINKEDIN = "/adAccountUsers" +_ENDPOINT_AD_CAMPAIGN_GROUPS_LINKEDIN = "/adAccounts/%s/adCampaignGroups" +_ENDPOINT_AD_CAMPAIGNS_LINKEDIN = "/adAccounts/%s/adCampaigns" +_ENDPOINT_AD_ANALYTICS_LINKEDIN = "/adAnalytics" + +_PAGE_SIZE_LINKEDIN = 100 +_FIELDS_STATISTIC_LINKEDIN = ( + "actionClicks,adUnitClicks,clicks,costInUsd," + "externalWebsiteConversions,impressions,pivotValues" +) + +_SCOPE_ADS_LINKEDIN = ["r_ads", "rw_ads", "r_ads_reporting"] diff --git a/social_media_advertising_linkedin/static/description/icon.png b/social_media_advertising_linkedin/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9cd4ce2988454e77625c47384dae98eee457a18d GIT binary patch literal 4950 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 H1DO8- + + + + +Social Media Advertising LinkedIn + + + +
+

Social Media Advertising LinkedIn

+ + +

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runboat

+

This module plugs LinkedIn Ads into Social Media Advertising.

+

Main features:

+
    +
  • Campaign group and campaign management synchronized with LinkedIn Ads: +create them in LinkedIn from Odoo (in draft state) or import the +existing ones, and link posts to campaigns as sponsored creatives.
  • +
  • The LinkedIn statuses of campaigns, campaign groups and ads (Draft, +Active, Paused, Archived…) are declared as stages of Social Media +Advertising, so the badges and the status bars show the real LinkedIn +vocabulary.
  • +
+

Table of contents

+ +
+

Configuration

+
+

The LinkedIn application.

+
    +
  • The campaign features (Create in LinkedIn and Import campaigns) +require an advertising account in the LinkedIn Campaign Manager and +the following products, which the Social Media Linkedin module does +not request on its own. Ask for them on the Products tab of your +LinkedIn Developer App, following the configuration steps of that +module:
      +
    • Advertising API
    • +
    • LinkedIn Ad Library
    • +
    +
  • +
  • The member who authorizes the account must also have a role on that +advertising account (ACCOUNT_BILLING_ADMIN, ACCOUNT_MANAGER, +CAMPAIGN_MANAGER, CREATIVE_MANAGER or VIEWER, the last one +read only). LinkedIn only answers the advertising accounts of the +authorized member, so without a role the import finds nothing.
  • +
  • That requirement is unrelated to the formats LinkedIn accepts as an +ad: a post with several images cannot be sponsored with any product +nor with any advertising account, because LinkedIn does not turn a +multi-image post into an ad. See the ROADMAP section about it.
  • +
+
+
+

Re-authorize the already associated accounts.

+
    +
  • Installing this module adds the r_ads, rw_ads and +r_ads_reporting OAuth scopes to the LinkedIn authorization. The +tokens already issued keep the scopes they were granted with — +refreshing a token does not renegotiate them — so every LinkedIn +account that was authorized before installing this module must be +re-authorized from the account wizard (Update account > Update +token) before it can call the Ads API.
  • +
  • These scopes are only requested while this module is installed, +because LinkedIn rejects the authorization of an application that asks +for them without the Advertising API product enabled.
  • +
+
+
+
+

Usage

+
+

Create a campaign in LinkedIn.

+

To link a post to a campaign, the campaign must already exist in +LinkedIn (only campaigns already synchronized with LinkedIn can be +selected on the post). Publishing a post does not create the campaign +anymore.

+
    +
  • Go to Social Media > Advertising > Campaigns and open the campaign
  • +
  • Fill in the campaign group, account, budget and currency fields
  • +
  • Choose the LinkedIn Ad Format: Standard update for posts with text +or images, Single video for posts with a video. LinkedIn fixes the +format when the campaign is created and only accepts posts of that +format afterwards, so a post with a video needs its own video +campaign.
  • +
  • The LinkedIn Objective is required by LinkedIn for the video format. +See the ROADMAP section about the objectives that are not offered +here.
  • +
  • The LinkedIn Political Intent is the declaration LinkedIn requires +to create any campaign since the EU political advertising regulation. +It defaults to Not political advertising, which states that none of +your ads qualify as political advertising under the law of the +targeted countries, including EU law for ads targeted to the EU. +Change it to Political advertising when they do.
  • +
  • Click on the Create in LinkedIn button
  • +
  • The campaign group and the campaign are created in LinkedIn in +DRAFT status, so no budget is spent until you activate them from +the LinkedIn Campaign Manager.
  • +
  • If the campaign cannot be created, the error is shown in a +notification and logged in the chatter instead of being raised: the +campaign group is already created on LinkedIn, which does not allow +deleting it, so its reference is kept to avoid importing it later as a +duplicate. Fix the reported problem and press Create in LinkedIn +again; the existing group is reused.
  • +
  • A campaign group can also be created on LinkedIn on its own: open the +group and click its Create in LinkedIn button (it requires a +positive total budget and a currency). If you forget to do it, the +group is created automatically when the first campaign of the group is +published.
  • +
  • When a post linked to a campaign is published, it is attached to the +campaign as a sponsored creative. If the post is published but the +creative cannot be linked, the post is kept and the error is logged in +the chatter.
  • +
  • A post with more than one image cannot be sponsored: LinkedIn +publishes it as a multi-image post and does not accept that format as +an ad, so linking it to a campaign is refused before publishing. The +post form warns about it as soon as the campaign is selected, without +waiting for the Post button.
  • +
+
+
+

Import campaigns from LinkedIn.

+
    +
  • Go to Social Media > Configuration > Accounts
  • +
  • Select the account
  • +
  • Click on the Import campaigns button
  • +
  • The campaign groups, campaigns and their sponsored creatives are +imported from LinkedIn Ads. Every creative is matched with the +publication it promotes by its remote reference.
  • +
  • A publication brought from the wall with Update has no post in Odoo, +so its campaign is taken from the creative and its badge appears on +the dashboard. Publications published from Odoo keep the campaign of +their post and are never overwritten by the import.
  • +
  • The Posts API does not return the campaign of a post, so only this +import can resolve it. The order does not matter, but the publications +have to be already in Odoo: if they are brought after importing +the campaigns, press Import campaigns again.
  • +
  • Campaign groups created on LinkedIn often carry no total budget, which +is optional there. Those groups set no limit in Odoo either: the rule +keeping the daily budgets of the campaigns within the total budget of +their group only applies when the group has one, and it is never +checked on the values brought by the import. The rule is enforced from +both sides: raising the daily budget of a campaign, moving it to +another group, and lowering the total budget of a group are all +refused when the sum goes over it.
  • +
  • The real LinkedIn status of each campaign and campaign group (Draft, +Active, Paused, Archived…) is stored in its Stage, shown as a +status bar on the form and refreshed on every import. Campaigns +flagged as test campaigns on LinkedIn are marked as well.
  • +
  • Campaigns and campaign groups deleted on LinkedIn (Pending deletion, +Removed) are hidden in the LinkedIn Campaign Manager, but the API +keeps returning them because of their performance data. They are +therefore kept in Odoo as history, and a note is logged in their +chatter when the deletion is detected. Keep in mind that deleting a +campaign group on LinkedIn also deletes all its campaigns and ads.
  • +
+
+
+

Update a campaign in LinkedIn.

+
    +
  • When you modify in Odoo the editable fields of a campaign (name, unit +cost, daily budget, campaign group) or of a campaign group (name, +total budget) that already exists on LinkedIn, the record shows a +Pending changes ribbon, a warning banner explaining that the local +changes have not been pushed yet, and an Update in LinkedIn button. +A record that does not exist on LinkedIn yet carries the Not synced +ribbon instead. The tracked fields also log their changes in the +chatter.

    +

    CAMPAIGN_NOT_SYNCED

    +
  • +
  • Click Update in LinkedIn to push the local values to LinkedIn. To +move the campaign to another campaign group, the target group must +already exist on LinkedIn. The political advertising declaration is +pushed the same way, since LinkedIn allows updating it when the +targeting changes.

    +
  • +
  • Synchronization priority is hybrid: the LinkedIn-only fields (stage, +test flag) are always refreshed by Import campaigns; for the +editable fields, your local pending changes are preserved by the +import — the LinkedIn values are logged in the chatter so you can +decide whether to keep your changes (push them with Update in +LinkedIn) or re-type the LinkedIn ones.

    +
  • +
  • When the LinkedIn stage is Archived, Canceled, Pending deletion +or Removed, the campaign or campaign group cannot be modified in +Odoo (LinkedIn does not allow editing them either): the editable +fields become read-only, the Update in LinkedIn button is hidden and +a red banner explains the situation. The Media of a campaign that +exists on LinkedIn is read-only as well, so the lock cannot be escaped +by taking the campaign out of LinkedIn. The import keeps refreshing +their stage, so an archived record becomes editable again once it is +unarchived on LinkedIn.

    +
  • +
+
+
+

Archive a campaign or a campaign group in LinkedIn.

+
    +
  • LinkedIn does not allow deleting a campaign or a campaign group +through its API. Archiving is the only way to end them, and it is what +the Archive in LinkedIn button does.

    +
  • +
  • Go to Social Media > Advertising, open the campaign or the campaign +group and click Archive in LinkedIn. The button is only shown when +the record already exists on LinkedIn and is not already archived, +canceled or deleted.

    +

    CAMPAIGN_ARCHIVE

    +
  • +
  • LinkedIn refuses to archive a campaign while its campaign group is +still in Draft: activate the group in the Campaign Manager first, +or archive the group instead, which archives its campaigns. Odoo +checks it before calling LinkedIn and explains it.

    +
  • +
  • A campaign or a campaign group still in Draft keeps the start date +given when it was created, and LinkedIn rejects any later change +because that date already belongs to the past. Odoo therefore sends a +new run schedule when it updates or archives a record in Draft, which +moves its start date to the moment of the change.

    +
  • +
  • The campaign stops running on LinkedIn and disappears from the +Campaign Manager active lists, but it is kept in Odoo with its +performance data. Its stage becomes Archived, which makes the record +read-only in Odoo, because LinkedIn no longer accepts changes on it.

    +
  • +
  • It cannot be reactivated from Odoo. To unarchive it, use the +LinkedIn Campaign Manager and then run Import campaigns to refresh +the stage.

    +
  • +
  • Archiving a campaign group archives its campaigns on LinkedIn as well, +except the ones still in Draft, which LinkedIn leaves untouched. Odoo +does not know about any of it until you run Import campaigns, so run +it afterwards to refresh them, and archive the draft campaigns one by +one if you want them archived too.

    +
  • +
  • Deleting the campaign in Odoo does not do anything on LinkedIn: the +campaign keeps running there and the next Import campaigns brings it +back. Use Archive in LinkedIn to actually end it.

    +
  • +
+
+
+
+

Known issues / Roadmap

+
+

Campaign objectives

+
    +
  • LinkedIn defines seven campaign objectives: brand awareness, video +views, website visits, engagement, lead generation, website +conversions and job applicants.
  • +
  • Only the first four are offered by this module. The other three need a +configuration that lives on LinkedIn and that the module does not +create nor check: lead generation needs a lead gen form, website +conversions need conversion tracking, and job applicants needs +LinkedIn Talent Solutions and is not compatible with the video format. +Selecting them without that configuration is refused by LinkedIn when +the campaign is created.
  • +
  • Campaigns imported from LinkedIn keep their real objective; the ones +that are not offered here are simply not shown in the field.
  • +
+
+ +
+
+

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

+
    +
  • BinhexTeam
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

edescalona

+

This module is part of the OCA/social project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/social_media_advertising_linkedin/static/img/readme/CAMPAIGN_ARCHIVE.png b/social_media_advertising_linkedin/static/img/readme/CAMPAIGN_ARCHIVE.png new file mode 100644 index 0000000000000000000000000000000000000000..0dd217e029e3ef288e941125259f31709bfd5c7b GIT binary patch literal 97676 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ⅇhRgCyfl8k^lA{hCc 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 literal 0 HcmV?d00001 diff --git a/social_media_advertising_linkedin/static/img/readme/CAMPAIGN_NOT_SYNCED.png b/social_media_advertising_linkedin/static/img/readme/CAMPAIGN_NOT_SYNCED.png new file mode 100644 index 0000000000000000000000000000000000000000..fd72d37601361a29570aae212dc4efac7ed6e162 GIT binary patch literal 129665 zcmaHSbzGEN*ES&CUD8N{q;v{McXxvzDbg_@DGkyo(jd|$-QCjN(nAk0FyEZ;9DSbW z{rHF9zVEqruDI4(*Z!cYEQ5hciV6b*gCQp?r49pwNCg7}XN!yoeew+HP7nqL8%9n_ zLemrWC=0<9Uxuh3;^TrP=R8;Mnd`}FcsD%ec#$v{utZi)8ma?YZ>>pjDh|Pe4eH}WjHof6Eh+K%4=mW%YoN;P> z;zfkrR!YwfUy?le>*>IVh4>=JY|22C?GYJ`0LB`R2~g+dt)C`F)ZWv6=G=*uhMfL zcFm7e%FaN!D(L}9ioYES7dC83#DA@YDh)o{`)>V17~f0DZX61n+aU~{3%A5!tGhd- z%|}`fDXCX3FVr2Tq+qMeX5&@e-<04 zC?%dw1OHDOdg^~Q2C1;I@iU`z)5PR8HF>&dY1CIHG+C&YDetKblo{sd^$Wjokx&Pf z)~)tN569w`5B0iwaK4?VxjY0Di5^~Ky@FH{7sVK<@H}3?R?ONQe%sP*-Ci$a)0F>B z)PITra25|6<}Xl*>3c?4XBP5nSN93m78l`g za8OR2+-_oo9w}-_V^4f?{im@85@p22ksmV}aqitSRT=~f=(pNTcwXM992U}}$+7&s z&h=3SDz={^|GnOya&9aTsPk38bB9ea4=yh+?`$u8XMzVq46;hVCZs8O;XVUA%n?~n zpTSYsI}Vf(y1uN!a<>-@>2-CtfQ(%rV`6g8nvY-C@VlPVk7c)nSY6zhkJr~AW5xE= z4P#Zlo%DwaA=!zFua>X&iL)rt*lK5!MnjeFhN!)vAAUB4Kl8HbO6ze<`JsyG{;~*v z?ak*Z3bKHhzS2eeWqc;fRe( zolO1Fxc_0-h+!CXCtt-rUL}6YPJ$H!a1&+Bk#(0J##X()b_4@^SuL2}BvW)HEcjHK z_F)zTo6_})S?E|yHP)F*cP|zoo$XbyXo3gA8Cgv#RhS5&X9P>6yw(|-B*S?Qh=dN8 zznLlIhL#t~*7ddACK1thw9r0x_r}Hs{vtU=2w4d z92YNaN!BlG{zUd16u`oQ=K1YDSH13rnIC*?Ch{ko?1c3x6`J@;OPYO6(+yQ-J2;sF zE(%HOTjdb*K$VEL6r_50?ITSSjns^c+ySAc?_Ohu&ktUMx&l=^BN>on4BX#e>|H$7 zp(wo|ty9QT&U?#W3*nk&6AMQ25b0QwF6r=0Mq-kYPhkoFQf1=H|%!+@iz*_GOY%wxaLEt>?WR|BFc8A-N_HEI!ORtY_aCUj*$05}PM)?Vuc1mC z7Z(mCOa%qktrWD#y^}s5wQH64nSZ9B_Y>m}z7HKFm0eU^H0fDGmCxyzALr0W5vp>Z zKx`LlK634#navj~4`j^|$Y2>#OZS+g-<$KA^V7z#{w;04NEfz(IOb1V1AXk7fdW+6 zL?e9u(%Biv(%G>ynl6GwojdM6>qUpape8%?Y(vg6IV~uwW?sUkFep|B25b%h1TH;Q zf&*7|r)&U%tl^%OiFpH2tM`W9pH1JH_2h_5e3pEXf4vr6hJ`~=qC*CZ6}?DSlwKVz zuNG;!!ChsFCjuxw;CDlQgfdgM7J>1qt9P9H((w44A=yZvu!7i+?|a_8DGr7@E9qCS zs10IRL5~eFsT{48q7R=r;lM#IY}aqlryDm3;a*Zn;=~R84^Z=mhfd4sFqnSnPc6uL zw2*v5Qodm*rj7W;pdI!ei_DX51`QRp>zZ6>vdn`rILwBOw?Y~g2M0$En+6YlyuA*a z1SiRK{_yfUPc%O2N+YQ%}Aazr}VfbnL!Ml1T)-oGmip zMFb#lp5IFo&s!u!oF?{)<`$#eIT*TW7Tn*&x;6T4P-wG>!f>k(;YVtPK1-Dq{vC;Q z7|4l}vi<5ml8u*~K~?xez=_!Iv)kp&ufoFsGz<6;~F$ojX3GxkCp^qS4MM@B|WmFfMZJXBP44Gek*V=vgX zsy)HQ`)@@Z?$EF3UY;6@$lj{*84by@Y6uAljbsZLA>17E!N9`84m}96;Z=dk$`Jvf z)%L5@mOT~Z#!@r}m^O-;oB@ge#Y_Ph$OuWex)mB}2oP%NKwgGAU?C4G6p)y7+Ih&? z+GqB75gWp!;~tWSvjx7OSEQKxV@{FD`Y`NYAB&ApN1uA-qz^Yt31^b$`ELi8@A zDibm1PmyI${BxFNLZTn{@LEiAkqe)L6M_=cc=lX*oD8I!Y?M~tuRS(kJLb!72%0p5 zVN@vZsf6l2(Bk{BH^{#+GaD-!@`b#SsmsF8Nln|IXfYyC%oan=Jk5dlMPzyz3dlzN zZWF^L#g%`tJ5ZP-t`2{)+=?V5WN>*rBNF3Pq#G~wvD(n<@Y;w`yEaaZ{tc-B&olJi zH6I3L|4cv=SAkMqL8Q;(<^Ft?E0On=3t2o-rv5NMBbM$vwwznAXrp*0-m^1N@y42w|Kq+ulOPf_h+yTYCJwk}Q}rsp|_9T73l{SKZb@hJ1NNU^exZh;2nLg}fJ{wL6+g8wUE z@Z3UPZt}niLEY84YJ&iAUUqjS-b*I~#9=%mA!El&&xl9Y#9;J{jBv=wqx)5|sV)f| zq%YTav_+4!hr$rAOk&SFVR||{DF7;DSh%WNOQ&I-)6;krhbCQ9M>?ucZHRm{??EEV z&Bq_g=~06~(&Op1cnq=2!C9>YWSREP*FEHl53Xeq*TyBn%zmE$;eui<5Ym&ivz}E4 zqG>7`Ti5>dW8I)^G zUfYCr%ioZ^DHW9r^?$(I1RCIF36ihx!$hg1$K|~*hrY*;K*$65DGeUT0ZGz$RkY3S zzF?|Ap15U`Oi#jPW6J8?k6EOgO9-li&`lS6o_{PJL%E9XZKdm+qq^w3{zGk1;>F#g zqbL`PR}!?blmLpDB9)cFs{@E}2Hg90ve#3k!enISRTf)%p!@wK z9XkhiJe3YeLRV9p^aRgC!6P5>^kY`Q`2AJiEm{hv)!mWQ(pw-_v~x+7VlPA7M(Du(e^v6%QnRY_3a^qB@&$L6r z-s=V6F^Xo4H&4-aL~HN4#O3^2-cPhhN;|jRIQp#b&M+{yNTszLF#d+_I?AZ23jW(8 z8%qT=q(k-rYo?@3C__nX(aTM5KbbuUG8z;(?LfjiS>FAP@37B*lvG zOe_&8V`y{snCq0%&o$Zt7R?9y8IUC;atBh@Fq+Q4t)Dj`HTrFpL};{(L;_wI5hQrA z*{1z&?tX@I|E4{h@B@o~yw=s%e-EgDp@+QP8!EBvG21YsZ}Tq7$r1DQdzh@&qivND z7NVzrC9Rf)>^A_4YtaQc-zl@JpR=4tDDAW-&No&JqGlG`?0swjyjlD#)T`)PTz zW~Ypsd~s!H`B2$b@{1zV!s$-BvL~oSqcIA5daxUqtA&GIU533lEDy-Q-Yowng)w%? z05QHL`NhrhB@bFw60!9QE-P8mzf@W_DgcD`Pqh9IqU?x7mXdz88McuJ>^J!+XZtCZ zL`nVww+(y8!UBqWeZ98|-u;JWA<*!LgPGD)kf^I&dZ-w^V%IW4WOOzw(Nc>8!_o0k zl>h$51Pisk#frD~OJNo`iaLuq zV<~BAI%0~oj|jJ!kW2&x;8LpC*;-T+zt1C_Q84FOo^`5gHZe%XNbm-H+2l|=Rhsbl z2s__;BmSPAk#Syk1PaGG@rEezAia0d20-AXg@vw^l;!(<3bAz|UxO1FN; zC^9~Zr+iLfVPQ64#zvPz-oo+hSeGqGXMI*P6sVpVlAZL2M5b)zFilrM*3E)L!umgS zTWt4~OoMLP(^5g>nnx=Di-h)7B%=U<>ixY4rpYeZ^`liIRRR4_&!5EImu?;8O=!}12b~Lk%Ie8G9#riVu%wS{i z?oy^ezZg``f zZEITfDJx`T%@@M$ezJ zlZ99DLd^FF{_$p7mQ}Bwc|kr(3Qe{MMrk^Z&%<$hDCy%JyC13-2*jyd!trL6rT}cF zjJS2USA^Q?0f9xtc%+C-{tC~6pUi3!#NKvGsif~sCnfv3c*tTm6JdO(6BRy5PB@Z) zSme(4>-pscS_imycp~pqT-Y1jLwxZ)%@t%0gnYNW{l4<#W8p~0`l&xxXpP0~!Br_G z7P&}FM#G7!2z{ALw^(iW#`4W_{YLvk%&<vC1J|0(Y&~gF~Ppd3$jPu(0|8!FO(&Fa?aR2D?PgezvYw-ge zvljCJYZUTh-@Fbn=jZ3mj|OtiB$}#G=yy_rR}77C(YZa{_a_ja+}}vZIv0DF=oU=_ zw{C*e>eLY=6W@u{+peGy!Q`;>Sto6r)I>4F4_o#ea zyvUzwCq(G+Xs_#vwOA6>X;44i>&}_>S6qcXGqsRk_a`iEV`B1 zq1*HivpN~jzKx=!^ih)sW!RZU|&6K2jG@-jEyWY2}#Jr#NLg{77tlitVCz$ zI&Drr-w`D)E@=^4dZF2JaSrG8T#hU(PMOH(U@|`bXFbLI8?KVApn*MnrI3pv)g$Gh zC`xX)nhzgJ)b6{~UivRVlZ~j{MT$L?7XF>v?R_-D!f+ZzeQJLwqAgbfzwwR()29AefFH++xzcMq5Cx$=X~ z2bX#TVdYSu@%c?rJ;-B|UlHv0%=_j?)U?gm)>IM;Syb7aQ$W9VtKbU`TQIRA=z(t} z{f@}j*U#l~la7FaiG`mZe#v7$39U@d0u0PGKI_`Yq!5gNTAFG+vO+R*l>cxLacRi1 z20!soOFVo!&zA~IyJ-d$s-^`e#>W#OlzLhY4rD9};jaD{WemiQ2ZQI|#rk6cM91S9 zb?R$voxe33G}wK>Jk~?0qB!KUSG+lIgaH5mzDh$#IyySwhdKDGd(tDwx6wDyf`DS) zO0@tDOInIt2BGceJPQurZGK%n{mrEc#6FzbxiTZP7o|GA*I7QDT|SatDHX;VE0POxS$!z1>JNv1Z~0B^gY)ZBh#(&&N{aVf%+?ji7!e-wfpmUR>m?0-RceIyp1eB4|D%JF+BKO6NXbatiiI9ZjPp3R#4 z>gvSvW(ff-4ZGJL3c$A>JfC51Q7X!QJ(DXmmMJnWF;siJj3*-YVA!@xq4Gktb#J=W z1>Ov(G?<>S0WgsABudmUjCD-e`R%)KZsRcDt1mfG@x9d0AdnOXmkFxxs0fY12w6g>v=`xtl+?Lq#Qf`c?+%wK zX#15^!Hn}DB5dq%tFk%Mp7FIjh5q`+cekY0V@8(fDg&KEDFXwtS4+sDdDN>wGs($X zHNS4A7C&F-hdVaQ@m=dn3mHvI&CJV8EWlugSz&0y&E5St=a0#Pph+P?K~iuA_Drd! zx<3*7e!D57?xz4jn@9x6DLxBaP}kt>EaJz$*pRYFG$fW9xBKUAJg?uyX7LBw>#bCJ ztV!_L&Y-X@x=Cgzi(nHI7jM#8RV4%g48sV7LK^{5w+Lv~D!reZrZwA1!3;`Sfeu3M zPnCBVf6Y)qLtrFW+mL_ob6g2ZC7DBasdQy^wYSCMLZw8Ij{e;4XnO0^d4fjjbAkA0 znAjBeL7i8)la)z~kJ(+@$2;XQSQMgNdHpJrRemhcRB)bLo;Sbej~}jL!e3}U65!)Y z@j^ohL$jvyC>uA1K+r*f>twr=N{hn#lTH(V#2y$VOv)F1wY9Y!DrOEFSxJ?03h!ItG1YHm4g8=yg zor;@Y?vQQntE5*;$#;H zmdPrW^DQZVy~9#6ViQz6NnuMW{yD4P!uh{q9cWtTot$)>vj{AAR@&R;rm{6InXFV@ zitOxzk-k2C^4l9S&<*k`yAi5{L?;LG7q4at0_tol+mg9u1o$5WIA`y-GOJ&1K>-lR zrHAqIL=4#kaG!_D_uvrL;^kVRlE6=ukzriY*)H9=XKz?-3F}nWU?|P1_mz?Ghd+ja)NwzC`s?Y$#Ts%2whXTwRnN`OiCOHk~6Q=#kKxhl{bLNFInadH>W- z*u?`t2#hc?YIg749nCOtDN)b-$;?AjazbeTpmaCN7k`_!}GE~(U+J=WmsoLN%JcOMB zzyuo`IK@U6w6%5%nzsy=XLB$uMp~XnI<#aA96+z$h(Zw)ihjTN!ObJ|$1HS>JX@EG zjc*hp?zk4Oz~S$2FGNzvS+o_WYcdcX#-*dK)*rzxJ+GIWA;??(523zZ7d@dsB9?{3 z`?D>EgV&WHnF2jEOa47t$k*+zRz@#c?I*VS=8 zde)vR_6+5)vlEVIgl4Y@l3JiY6~$GNJL_;G;XJspd0g`^*d!I1T7U*sfyS-g8>j6M z<-G(h^=oya-}m^Z@c$$zKw)uk{&%?oFP3gSY{ODZ7h%7H4{;N!z3ba0+^?6bX`zXY zkmLMR(+y6i)@~C9cm#y4L-sq1&LgH*)6m#|x%mdWt!nAISW%Jn5jH8K!Ex)cTLzjR zU|``aRLm(?hQff@`to$MZJ^nY?_UW#A>0IUs4P$e4t2o1yB#k*zS{I+4KcM_FevA` zbbljOdgq>X%J7Gq-0SZNA5|QJRP~Hv4>*`e0+?$1&cPgQdOXQiU7T zkJtYqRsTgPEi0?x18+!YY^+(X*+9-|T365mE?0VCU^~2kH#naYw0LFk<7%G><@Cpu-wK$%Kd7W#I;n47PlbVUf{d6UE^`TvJiNDS<7jh7AA@ueL-ZP`< zNm$J0_y-7ms)@V^1pJ0eyL_qme1|fAmy-9ZW^T(6eOU&Ja+YWq?Xz{!cbEKq;CNsP z8Clc2LEK{9T(ZXpC;B`8#~0pL_i#|0rtYu>{7(Eh`hji9z3V%%znDemrj>4k8QM_d za};)VCyUG#AFLjxx2J)UeH@aau{pxgc>T`RQTPlQ^+&45UO=Er<~w1+qa%B@t-Mmr z6YHbpRxFhj5e~Z}PiSswb9nrYq`EVJJv2eR>q+#%TwiW?f7GqvH(#w@vmHDZCQ&-N z-hJ*p=l!7$U_)#0}(Ro0*VvXj|A^9So zdIB64u0#gCbizYQA1Zb&zKQAypm=0Xg#Wd~{tw#VE( zv4DV{QfgHUEr>_5X-xABT`V}?Z7fxAUX}$GtCdIG>2JkOwikmC6{dNDraias7%rPp z$RvXLkf7-ybckXaPCs#dxuEQOznFK|%OU(5)8<}>t}w~ZP4E~pFPfeAxfuM$pg+~T zJxf;pxS2F8%nz(l@yWCN;KBKL$^_L8DrgOWQnKw1z0gDeYV88@%pX7RxD2fll##hO zSM~?Hv@;>hVYNr~#hn5QtFB=3WWWK!UT;fk<@9)GXH$hYJR)-NAdfFFuMWQR(BDEK zDH!x%n#g=h;eYq|G!Js2DxL^$|DD`d_vCE9Q?wg!e21 z^9;~ySejX1E3V#GW5~N<3cR)IoR(=rMxSs}xU41^WZb{DZw@!7b7j!aS{86M1<#vs zdhE8Rg8F4gQn_#-QItyZ?}@>DuGzTT6~=;C-Lfz_s`O}kioIP68hEJc{^0J>tyh@+ z%OoE)9~Jgg`j2cOJ-2K@xTmd=2sMSK8<~(w^GW-6_DiTwb=Y66#9qPeVXzRIbhzLpLexUrK$BPwuoWc*z`Yc_n3^AWH zeC|s4?cM9VA^WLM6i6B(yQX8cazn>kTWEx{7dm)ZLiN(!-gPte>fm1SwE6N3(c=~J z$US9^GmP2THNPM?7D8g>EwVMgqGIOQC7iYG@f4ljtn(X6ls?YH?w5}sTZ}zKcV=3= zH{C#)Uk` z>1vSA<*Vgyk39a5P9t8-5ksj*b)ixD=$yF)@|0=Id62n^7!tU$>em zWlZPCX*ta=DM4#0?Vn}i--A9GE~FICWW+y{xwf}sCC0~N*Lb5e-C^qG2Mmw<*myRfUdb`j^>nw&+EJnLrpPHW^q(#8A^HWXk4ZK^QSdK5VtM zN0fu!ITpAXon&M5e0vV0r>O1DQ);r8Cc|u$310iKHBsm4)7WSk_=HJKbboSX;A?@GXK=ze->2!oa2Bq{FEVV{DwXw0WRFj^=O)jPZCeQn6y7e zm)2-%|Nc!&G<^Q(FG)<<2q=0;pn0|deU{GxtDvATzqr`P`EY;Je|@|(U1vCGh-}FP zH%0be^Zzpe3I%(fTl$Qs;e20*^SfSBx#BPyedD1J_5@~@7&JKU+7;LS#de&{c4XpdA( zaGwKYBjSRgm76#_xIdzc$vhb|2Z!=={crD>-hH!~9NvO%Z`-*%EN&k35PGp8GJQo;&O1byNd|j<~$QsRqPlt0drxZuwNh@mndw>AB5T8 zruM6BXyOp_yq<8~f1%XP-VVm?2W<9ya2eBhM-%_wWr{yK`L~q%?VMj{-^t+f={4<@{{wCFYItGWG8CL8p$fn=~}e=e!T+9>Z zBA36E5Ne{g;q$$I+lIdyarl2Q$fRYvxS?CM@l@fHcpx03LH*HF13*dRf9@Rgla`lo z>WP0n|8@9(g|H$g;QN0ww4s^>joqPaOZ?m@%I7cs4^H+^e)FF@%@-4Ja{bVcO^f~) zhy5RxITOhfT%Eqi|C50}KrCyA{^PBcNB2{Q{KqN4PZIn;9sK)%>LM0)-(uh5C;vT; z6WA9GyYPNL`u?>vGwa_bYyAo@j`ZIZ{ubLn|S%#i=U0DoPb2=>VFnE z0-?)dq*`r!{t@-Bl4i#t1J#TFkWlEi#oyyRe&z!H9Qu#FF|&hm#A)i_>01BC$>fW{ zGD-Yuo}fAd6|uie$seLSry_3m%S>SZkx+5uznJcE`hTb_n}i(uzw`b3SF!*&_4mII z3T2f*3G2~^TqTvz7ysw6|4W9?PZm#SqJrkeop-to2%T8NjSUK3;H@1 z3;KcgDsP-E*XpGDJ!M}rW_x>f^HCu`26kG482iM4Gh74cJGYa>Z*R^eV3?|L$(?b3 zZaCFPJm3vR-?mFemEVJ&aO2^_EU|%sg1xRg++2l7!j&cJY&`F*Hu5uS32R+6>qOpp zT|Lxk{HV^_1HSCVJPY&fI{?O!#&no4(%FT9-FaLsQwn*HjkR^5q?Bd8p_tSB6<#|f z_mn_8m7RkFlLd2kREeqQ{ayF;64vD8r0=j|om?Fp4Kp+8Md49rs{uvH*C^0}qaai5!0 zzg{vd=0nlWJaRCzvOVLq@Z4IYP#kyoIu_$(*vf4XvLfQQp4^K91ygC|wo}=2QUO-O zlg~`tYa4u&Uc{N>(O^^HNJNAO1ix25O0h%e$-5VAbe$IIMNojF2GYPa^X34a&&Tov zj(fEcVlna5;2@)-4%#avT>q3G*@TWdh1d=(-<>jiWiChS=!wENT3_^>$>MR;(&|3g zE)-%eQ=2>eH;P!94xk8h2Z(w!e4xO92X2@Q(V?BIVLKxYSeAgZwa;|ff zt{n7)Dc0ENz92ssS=jyNrTZLf+#v(C;N}e%ys#g5{RdUVMv7TVwykFtE(K270gbv} z(aw0ih7sDJIC2jy5cpL`n376Z7%U(Lwd}SMR9y1Gn6{m2=YQG-D3BNw9!2pKJ_|C? z4&xcWQ5u?9=*83$^(HdV$tIwa;u4B!r-+V<2(6i35^+!Xz@j(lX;8-D-8fF;I!zFU z2u=251Q8JOjVV}A!|~UXi+@^NsIB!>g3s55+PA5A7zpO8Hn3im%~tO_RytR!QF-L2 zEDE|G2MyVL{L0NFvg-;E?90okAjagdtNQQPw#SlIj%Ns%=?c<2@YG*6@(T@|V&|t@ z9&nk`1p~yftF4mOkmSkb_J8Kp>8-SF+Ln0QITiw zknJn_PrFn&zU|k~QBg4^q@9RDyc3VWEEjL{^>&E(okfV z{&Y9{N&%KX+|m>Ku3S}|Pm^uDQ<9m^_t=6?M0_KBo2n(IkxdI<&m21S7@4OF$!MqU zZ@WBbU5A>8V-pm0LPLd^^Ba2X@Te#_2z{_`>-n9}aQ`@EGU0AzO`X?6Qaq_8DxBE{ z4iF7pyB#H-iS#m=kEWL;y&*G&$K+Qx^oQ$Fle`avxPlRR>Z00eYM;A8>_ z$KAZT)>A}J8)lbB(Xkg*e+7HYRKt{nJqLZTkp`|8n`Cd7Z{x6jmFJz#?_zQ3Z z>PU4gccHswB=&BQ>ul_igCpeqY9c?-(7`(;#c^ju`l7_YG8&8^f6QGW)2oRymr=+w zq|4aG2A-dVt+(c25 zLKCnljsz_}H_uS*U}0f-dJbd&_7Qctps4q6Or@?E5b_i@5SB?fWeR?lmk?@x zd3c`-WliN*Jo9HPhouzrI+q`kccgBCGyqIF3~MY4_mZ~jMg2CVrCz-cDRttS4vVFT zUuWdVk-4o5!58d(|!c= z_J1r=fjZ~}6rY1zdTA-C_0?9NaU@F^N)w29U2jTxv>Re^wC08tatk4XRI{VjWSdVN z{Nd8=$JmKtty}o+ghN}4Hy@x^#MDVB>G|YJw{`|kBUBJ)q}wYF+us=*-F+92#3aB| zTDyt#CKmim2c@^-MO%<^c&iijc1{lVeHYR?31u595q8~kQ@gD_&?L$70pu@}EPV65 zMWufbA`x;wQLM4^7yY3CzFR7mx-e+)7kL#;5p+THoSt;g>WSl8QxNr{?bybl215}{LY;rrsr^CZZ16i-@AewAbZfk3IC!m%d%Thw<)%QLBiwfo#0 zw&#g_C)9oOFfJ{^uyTKAxJc4F4(eDqug51}T|o0_x>a^0U@&;<%PP0EiezNH(qy#} zx3FLOW7mznK$dEWKIaz$PnsEr`8Ep%UWbK@RDTC<@8X!%`YNN)MBhN#Nt) zXawn@Sq^pv7-8oh*t`y_@7W9*m}(sEF*60-NLeT3hrJ8kj9D#nfOBH=4G>d(o9K>D z*ix^gY3syJ?$$dc?diYv#}xSsyV5qPJZAHdEIaz2MLcX~7&M%kE}SPp*?yAXPdq|m z&cL|R?(VSIDOQy+#t10G?2?|k^i64-c$3c!#ccUnct~Wj=~;r+{3}-pWR#(%s5#rl z@yEM86UAGnJC+JWF_ZH~0k=EF^yJY9TFQ19C{ewOp5W!sr%zP&EA>XBRm)CL`h?Ug z4N##LD(?DWK=7aC!y03EBa zVYwOTn~_2_xg@AeP?^qa6pRp=V6;q@a6FRH&arTY7tv@j=lI32l`xi~2tvw|d%gwg z725ELDT<{eBR?`eIIMlj=UD0%V&+#B^3|n{^6vh=pJjhk%u3tW=t3lYg``0G+nicF z7Xj*L*0kWE=Q<_WP-=UIs%rHE&ueB!d4Hs1=ygW3?_yb6-%_e5#DJYmA3a=5Z<12v zzD_wWDI||pETiZZ#jJF-krUzIP^75>FZ`KcfxZX9ewVLvZM|jEuu#|i4r+pFZO~2H z);-Tx8z3;&<7pZ0SRA!oA0!k%M}N^Q!7bG)e@_#F#_)%nzTF)N^maS^_l$-_z{k>Mc*|u zqqMV7?dQtlxBz3zCajkBQL9+jJ&`d>OiLblHT%a@!EmGZ6)E(rv z;06<`CqLK1rBlFX$bEO`?_kQvPAB6;34qe~jpb2X4rj>?TiwZ78{51Nd7tApwAO~& zP^ayubI@IR6}U{lrM6dhp)>(_Kf+zz3_FRroPfOyo8A&IC}(pG3ixF$)Yx?UKvT9{ z1quZQ664u@*-cZWl|UeJWrqTUF8F!x!gK#eOxx{<*oC9sgT>M|Mh+*%sOCvuZEWsk3@r@DiCoP4g7EAcc~oD?xCTeur)i z&2h>v;1nsv_C(P8h@UA0)QbqoPbkvkuMm(=Vhx-sm~z5Y_)^W(n&k{mykr>e2YFlX z)aGKgeD&PRG9O6RYVYF$*(A~q2MV|N#`P|6_R1rBU||9IxuQxA=bYBpbGuUoe4d|( zdB&-wr{X<~i7s+XkyqrlW;YKt}dq!8|sRYpI1=smZLR{rL9F%<9H@k}@-B zG}1}4p!&u0xvst*@q-fiEovh*V8i}EqU*aDbpzVY>3Z>L(DJR}<^G7tDaXa-WzSZB ztXichEU?Yx>_sLR@s+75m2SWoRs|NNC=YM6(~lN{$tUS?{L94HIg2V6gB*Q6!185kJ7X_e7>=dTuTy$oMr}(0k3N81ekak&FE;D=RCV zBMT0)Df66+jKraQ;ZQPKrVnPB4okncXl%OJUA@_}ekaYn~mN z+U#0`DEFk*6lpzpjc!t|tGua}wK`dDDhFc_R<$uh&5JxPGr-7R-uUU zuj&N&b1ExUgF`}kzjbS7*4FEy#PgYacR3jPnG4W9T6l-@Oj^<9x5AoJ%Y;YZ=?PcJU?*t;*;|BV-N7D$^>sY9gtM;P@KGKVr)7z>lJe zvDZSe^CGz+R%Nj@&SF4#H9iS@Hkojpl2#fOvj5}OR=m9p^aWBh)S~f)(et?Jw4;+> z;P&=DDDCWSiYH+1!pv-=&^ST`T&^IH-Q0{6ozv)of${_nZf23I>cxxui@TiuB5EaX z3P0Z`rgGy+*X~S^Z&_Z3?Jjs=^iGQ(=Shw1qi3Mavo4voS9+@57E==x{{C-^UI>Pf zfHx_Fek=s_SL~Cn_AQdWC*RjUA)Kov+n3Q>;3)xf!EjC zP{k~mi?5)D1W+|vxyZ+3C*^2>j`hFl$ap&*PLLkb!z`RurR-}r6s;_$tl0z%MG&r@6Jz!ZGC%A#isc)Q*v_JUC`4) z3+jXtcQ-pauzytKB<`)2*?0gS)*}ORa^-jAfdy{>Yk^N(xHhY5y-kbJFRxO!ot0D~Kecw(S(VDVgRQnm_~tpDTrB~7pXmlVO^$!{O}8BN9w zD^%0N1OiOnm1}L$*7a?=2AT1G;jx-U6cZ)&1V$1XCf}p&!o=hdhFVo*ueLe6x_o}2 zI%E}XB;=uKVYhAry}%&N{i5PR$9vf(^p39Hc$0r5a!)d5_@dMnfDrE+;l+)H$@d~N zxt}Cfa@QJpt8HCaV0pcNe}P??2AaK@9T`dKPqHt$nmO)5nH(!Ht?+493mBVB~tiKS=V?@w}-9i;^pVD!tXwZRblLPdcTL`>5{rH;!b~fO{fHw2n8U0fx zUJ)eUC{9gw>+=Vv@`>oh5}IeGb*E?4jWKM3+S<8#H9}^f9C8j|bD!NI<0D-D_xoE- zVuLT+ptXTn(0N!PX!E`g6nIBdvi1oSb8xfbRDRa3R?Z13+GSqx&}7%fcFC^j&J=Y` zPLvV!eV9qHJ2$E1II*!U)2WcCP`NW7TlV^~(2VtX)2&jP4x66{TEi0CtO+z(KXeNH zELrq?zSgToKduFxvlt%kE6zp4u`GT4 zAG;9V9ZX@Yp+8`Xc>BF?z0Xc6qABdSvjrieM_Rc&^kHa0^lJ804UpD{D-fs8xPi&C zC`$f@BT!=5WTYpTj;KnhUv91J{YFvF+3hEGf{Pq~ipOP|_ZOTm*8CwNp|KzW09b$` z=lbcVc^691-EG<{yoP_$oxli`)U>o*@48qH&@grBw%gOW;;j!F{gb7Bt|OU3{fbh+sU|OdzV_=XhCI2{>Zz&=vaYS z-x~Ty1Z-QA!kN|e3d9%#prz~OmUGQ4hPh3*srq9ftxNr96Xs#}8Xls=qj@s1oDO}g zBLK#lBnztYDw%Ie2zdal}jIk(V62| z2@|T;(rRpepf}3N$JugQ7xPu@sgqpnvPWGcghwRy5w;7QY@3E9BExLy1Sat&v1nBrNkKR9|!U^;byRA{m+5Vc~c)COlHWIbyMp0`D7Ic`1k(5=_Iz481G=(V(yG z;eIByQ~@=NOrZMd$0TMwIJ$^-neifP(?A>~;FX})0T0k^&_o6ZYIA=OL}IG(>&KiC z1J^(2PHT6Lj~ITx4Jc8;FM|{Yt-ZF6$$SkBx!(D)_FR)Ub7bwNAn<5=vDV-QbmMWc zDrPCqq3n(Ob@}bfMIgsCyG&bKtc@4$S${=B+vksJ;MzDe!->XuZZW?4a8!;yw>Pmt zdmy;!Od%;uS<>;YKHJliy7>*Ec{yF#GylguQ5QECSU_E#?cKX&00E0H43w=dOfSJ_ za}7Gfb2}dY>=o{H=B}MmVYowOmC9!#!=h8J=8AH|7OR&G3)N&1PY^Z)EEKZ4(`%=A zLG1auI*GVFNyKkgaipki>k@3$7g>MiqXtoPIZ%)|p&WZNo7tq>WcPxpvRi=BNC^hT zstLv?y%_}JdNs`h{C|ACby!?Yw)Pz?gkTBoEm1*(=g6j7*{=cnj$aW*2kN%om3TS=LdizZwx_-RRej%Z z1sJ3u*>C){KkmV2pLWmZi^JF#|6Jfd$Q5#xq>0O{YeviQyPjWA zP2BbJ+H#hpL*Jf^)MMO~BfPkL)b24V{YYSVvu&39YEzdUn#_)mY>BqG- zhm^=xB2+`d?^|}(y)v&udPaFUZtvHEqMi#+uX6MRX>!;_a)LXf10+tt0QZhx}jGE=7iBQT;sWHtvdrU|b!J>Zol5%mX6-^KLLj)RvGjuT09 zC27KNUQKE=0tOd-UwB+~uYBk4VNu-`@As>nt~@?|=p>p`A&yL@-7LB`RJ3mg$S@&z z!=>WwKkOxYNlTb+^_YkRo#B=_(y|Gq;N7hlrkROQX061-&?`prozOo#uBRL1kA5Xb zjo;ess*ql&+lx2)9f(B`%#0`S-0*g#hIeZm)vbSRof+PA825uo@;3IZi zq;gt?*}e1&G7<|1K5F+(F2@bk!|D}!%78}Or2|{qa@xSI zzvDhIIf*>)MK7hx?`Q6e`i5&+@OXO|$&85lZG03ZFvL4Gr#)aW@(~V_DD>>>GfODf zM&>g~5p*1j(Hp)LZ8b|_I*z*`U*dMMf}rwK0#M+Y!#V6e8fR;ZM-Q7P7W%Sfiz?1?HN$%%=L zjVf>FceBU9kf+y>b+J&%PRvHOI|9s2Cx5oWC2M24nQEy8eemzh*kd8V!-B_&PYteJjvZi$hQp}? zEtm=Vrp5)mBlT8@lCQSy6?cCG2L!eA-4dUi&Yr6QBK#uzwbs<2XrQ;lGF;a*nZO{M zaeW9bcV=JffL7Jjef44O6l(tCpp>o9;hvqGF;auzR5Ukp^Y@0(2T8lTbJ44oy&##N zn^U`9X+mJ!;gEIP9zaduv?BNG@a`KzWc=&G_(zCyeyz}H<&8eguJ z&nlHfU!38fUMc4;&4-gxq1ZY57S0#0#VqpZJ5~P3(Z}Pio(}p1hHNT2d9HI$7pq){ z+?O9XIdjdApA5*UI?30X6h*NZ6}}Gq!Uvp=KXL;eQ-f;f)YRB3O0uXTh1J4}fDaUf z#^*!auVw6V?6T4|b&q!>MO>u6uZjx#BlR0(EbTMf@6LFsIKz5U>W^n)_jjTtn@9C+ zLEs(VPbY$ETh%X;Jg}KmUUjhJp3|5TAvy`Z*RPhMgD+JtE%65*;?bbn0@60Mz0$jk>zc<` zRb4HmcZ<{)77?Lh99v0z z!?$?YmBJH_kC551AEpcz-omqXVKz8_#R4{Sk4P!f&hi;2UKchOXeH49O5%)`QmPj5 zm?_rPZkBzYQK$(s=5*G|en!Yq1UMWnH-M=Vm-9}MrAoc75rWdxY=vsv9)eREmy47x zsRKnmNb*&rz8YFe>^c1*(_q z*!oJyias9=mp%z@nezrnEqP9V#jg!VNBXL~u#k4muai*%ZUiGCuncf}d_7Mh%ItL4 zkl*eFSQ4m&GwOFpx7reSBNkei_L_X^K_CSnarJ$e8T&BIHg_)%Sb5`N_Hm()x9J;83SsHdT}dwq9EVG+?>)6bR`7Mo9cf{=A(gc@Zb5E?h_U*|P6j?fHy>2A4vX{dL`qkm#`4 z@Z#&CYOot&lU}M?dS8O{gV^_k1gJ0__hDR^ll!_VW4hA|?YXppt>i})=ZaRxJ}pvC z`k<6NaFgfZTQg{_?tS8Y7Fk-Fjl*7_&6X#uSMe&QpDv>8^zUqS7i?NDIdiSNPliX} za;$wWny8hpFP`6Ei%-t5uvk6x*(_9VGP10f5sR;YB+-;7FYIiInwk|p&W}3VStb6R ztJFxwnOa(MMhPaBsuxHomo;orLy7MR0grk&dLL1jNT5VCNof){5fM@R*BAs&)T3F| zN054%f^b5=h=tqTwJw|3j~8)bTo9dp7wJUuZ1l<`{=;s_+SB7gn`x=mpr1lT+R+^| zq7T|y)b>-2sX~DY-3Q9?dA3_w8`sWXcymXKHKsw*iHsIARWKb7Mr26h;SB!h$n&c! zD;Bc4N%83SlkxD{j0Q%V6t(_!ykIw}}*DBI!%@@kv0a?CAGOkG^W#voHSc;8QtKH4ek1 zMs>H-n>)MD78U^$QnXQ-FOXRU3(O~{@};w74yO#VO@U}KlYVmq5w|0mTN!Kq=gpp@ zX4z?W46q)|orfrf91gh{gFY?}Rg5t?wYvYh$UHkLzo#>t9qFjc{(~dl(DE!f|9g@_ z@A3oZ>Yi*|tiA2yToGg(cToGcLB~${$1Fj7s8S9)h+h5*lh(|ihIG*)rJ%u`PO+^? zX~81J%o{F!A3th3IX;Ut1~zJtPhSvN8z3S_%PXyi#AP+z4t|sbDW~mVoaZvp#WeeQ z`0m#g&=f06lhIYMEA5SRv!DR%e2C)+Pc;Sh3E)Z zR&-|D24z}Y4?l$sTI&<}!j))!4ZG4k-l@ zoiw0P!{lDlrX_KZg(s-L~8DJ1-PjS@n}7;n8xN{pv7KVpwq!No-br~aGAEidq>#5vb)6&tb6`a|0Nsb(Q>>Vwh?C>VtdhT0p_G4VD}@l(*R$?E#x855@DAR8IavM z+S;9P*`B8sTs=Yc?Z~?~xeJ*s0SV*FPG2DKr_E%CpzI3c2wq&kA#u+JX6EN3fN8q+ zL9&fZ>8gwK6}rlCH;c9V2QUwCH#BLRPNkO9=2^!36~icC@2ugi+f&}7pG2ipvOK}= z9-n!~&VGIN(XZmIFZdiQGQjBGJwi*~?U|3&x+s&$5{G^u6Gpg8#!!6AVyCxqi3)(% za9?#Uc7?-BL3=s_>V)dD#F!+upkrGnW#G$ZyubB}fIjo$@>WGZ;7baWWa489SeRM> z5494`Y%D$0Rd*@Nb~_r>d+}WSL76<@(mgT}oj-)HZ9ixV`FtS0e4paqxwd~8@7^%AJZfDt1%knmwto8EWtSnBVF*+mV z`|j1mv6_z`VADWe@LzZvAq<__a`e`GP6Fau&PCqaesE3+)*;X(1< zs~0!FEWvnh-)2`}M)`$&vxAhG(H}604x^%ZVbiF)ayEHA{7(CM4e45GmXpsA*zH9| z?c8Yu;%ShliOw%1TjdvAFbq2PzXn1F8nP?hA@+Ws;$UNYo7NX6W_N;GDe@LJj~T4g zICFCX)~(GdtTm8^ElFd#QlkG1%4|A!`BI0bqG@VCSSv~LM9!{v;_$X%?&8Larcqih zc|@%&&#RjPK)C4m$SOA}kb7&C(`y(^Xj9++PzqYC+H78mDLh>A#|FLh%RpQ#C(ZTL zBw$cqjE*d@Jh~HDc%m@oQNK*wfBBEv)81Q zW6dUDy7)znMyL#8nN~Z_iW&!@1*AIDuIwai^`$Xh@5ljkz+urkygkE1;A5ZHBn!Q3 z*`>O4%B}U&z}3g~Lb$+dW+i(P569iDKvvrdPmdhKQMw7|Dt1}X@iHcNFqPVP$|&pE zT5I%iALKH@>ZoI4c>0Sfed&zwkPb75N0ypwgz1+0ErZqcBiWFN<(N>nyGWReK+TWf zXlsqluB$Cfr81C*e*nV9_O|o@V(@V)_G_YneZB^0`ekPIxm~~QG(O&&SJ4YecCx$4 zu6gWRb@*d{L_;bj?3UPL*@qdZMlX$RY_f5N3=Q9Ue_`jTZ32ysI)9Ivl0S`&$Ca3= zGj}*S$#CHdaXHk+Y?H-AK1M%l4j8kNuf0Q-RrwVETCnDvfTN7BcyvJZVY_0?L6BVi zEoQZQ78S6ty?h|0xSVa8XO3leXQr0_zSk2R?BS|6P67$wFmh* z!RCF&CMHxI)(b@gGk^dH^q9HL;=8r}i_ zX*}^g!|lmxIDj@Ol7A8?sd!%M8`bG_-#JZ!-aR#bNI4GB|1JAGhCf7bh&*&@!?eac0O2R#&kS^U!=A^^iJj|>aE+AzpY;aGFX z(U`+=L1CvnjRQ3a(hA49DPW+5ljm_zBrzWwUw@cvje78_7VirER_a2E4qsn-0>Prn z^(IXl=HBJIs(sMHspRPBwUv`^Y21_k9M-Q&^{^9O`!|T_#7c`;Ifz=W7Ad~vDvh^; z7E?{xtdRtB>Jp+nQAu+HYvV6@zGUimv5|{8jvV~%c)xJ5Tm%t~{U|u)gigJAU&eja z<;8xLy74Ct*Wvxpkh({F6`)?~QO=X~TzC{Ye?m3t>UP(~is6{f=Mok#_5j`B-#{$` z|JuEn1Wf8?Jn5bp8xB_Q*yvI9!7Mw;zbEJBwA$Nt;$X=$3?DVt%)%P27g~HmJOzxz z(2$2Q=tG_ujWXr+utK);`v3`2ApWH`2r@v^Nv_@A0u5qjOP4e47tt^)m&px28e49u zw<_hiQb0pRRt2G`nZ!ezSM4)MB#5!o*iw;6n!Tuby(A7D?f~P;-AE6@RKNahT>Au zzwqMkeyDcWHa=q$6!ZiC`0aHD32JiMXBv(NxMQR;LE$t*7%pISFN42g0W}wsb#_}4 z?AL>X^>5+_IRXS5$2=$82HmcH%o-*5F<>?LIBc87%$euuCpv262o(+f@X5kf z=E&0GqUMMJ`owgc_dM_~4}y-8m=D%}F{|D#_S8NM_H&m^ zwrR6+1_bu90OU`N=8X?>1CJs-Zzg-DWMpx?q1V;Sq+RGJfFK=?@M%tt<-%V6o=iQt zo#uGrt(C*^gECj^Lbk_aSP4!?#{;7O@89;b9JcHJx=FKJUDKk>&cMwU%J8nRA2uqT z0ub~$CW|I=YV=|Rn#{>#`O-<^b3}pz zkY^mshu*do31e{zidO?EEvy@^40X!phPWk!bHhKX#6=5M1ZjN~fbDE1AZAc8_2b}BVH(`N1$kr*X{UIP zYfD`;aJEGvBGRphJFtj_46|3&h zNJjvw-8+nkc+^`+;((ydOy4^=hzp?>4ZT|nXM4F^Z&{x4=z6-AYT>x%C^5~Ro0Udc z)AMzZ<{6JEvv9^D<>lIN;4}!`?vlkkD$QG{V#RZ_k{YQjnVWD;ElA&_CfghV_&R9n zBH}<_A|jTuGiGg*{#K(swAdX=MwS2m7+%p0?w=>mM|uR=m0#%mvgIgq*Q~OycCe}z zskReOz3p%Jy6c|!bbncRwwQWAjh_Dr$_5uErU47-={KWTwDmjzCnKUajQC+C=(3xxT)>F=U4;4Cg)U_??b*rab}F z2DYOEh=iJ()86KIMI}3|?Gd9j$&RWcq-fm79SDk&W^j9{={}~#uPboc&_3qZPqL%l zJDLs#4xVkSS4mnz`OtSqW8umeIU?xB1NY$xQ;zSKwkj#yPpYf4n*;DIQwApWEKW{N zRlvw1;k~zxTeKS_(M4hFz<3Stk9N{g?ir~JWTmFN}`A9(&i0%hK_8uBo8;Z zTUQy?DP+SE!K>wg=Dpi0nuK1zg{D(t3*k&)^A$Ml51XbTyC2Km8t=Z;C&fZluhiQP zWX;IiQHw$`YX>-Xx2WdW!6hHpVCga`2Lr1~&_taolBX=7wO^HgJY&`=LnvDm0+DKk(} z5dlOwKn1H&s3T;Z~%lh5cpV7l{n1$!h_*uS;0`sCB zu0zANwzl7`^6Ok?Q#*9UOksW_0od=_8cF;>v1zxel~8hbn7%eYd=SHf_Y(8$O$^FE zr(qkrNS~$1W0YVSU)e4A{oqGac zI0`~h?dn%MoDg?M+AH;XHaL6C$1{-E9D^HI49FP;tsQyR!u+F=ahdxoR$CX7vo=4{ zBgb=9sg{}_>-#E41+k``*X%OP2rSj^jrNY~dd0u--!7~lI@&60;Fmo1K0Q5AP2_bw zfk&<~A4|#ohB(YNH=Mvm3x2-~s6-5BQ(V4x-W(;f?a$1~UEwmqo>zA|-h4@5)ov&Q zGC;~!KKd9{m#&DsZl?n~#nV-W+h%;8;LrV~`jt_hwmPl8aAueQ=@6QW_}ny6NpD1; zy(qcy17jBs*AM*btw5GSb^s9A&{@?^B{D!^U``TjX_y&E`Pyfl45VIfzUQeiA1N@w29zwt6}%))Cwt!DrF}{U zDx1$S!-7h@H=Tzix}|EBn`9~_3c|$7#w?x5&gu4~HD8B^CKcOX;R?!Ep!?m|THTST zBChFue>9xlE3b-LH3~;CNwWoH2^t+n2}(1n&s8Z(A7rvyu5g5bypi%euASeggWx;; zD0B@OOZT-+0k5B3I3n~)1e)H04mFKpQt{|I?9ul)V<}abfHbFXD}SU4P`g~H*)ZlY zYGl25jm>BPN$&w<`T@(MPMpu;{!%|>N=-ty==bEd_(OIufM?tfDCuXp_M1DkX6Z|S z^wd_f-&3&c26Umpht{2eHesPps^nS2Qn5@!an`&|7AIQlj7_Z}#UyTT9MbHxvDRDY zj5?U-u?7G>U>GPpD$Ecjty7}s%XHD9KB%(c2^F1LDr+wb?XyJ;^2ws5%~)NQP^yd$ z5QcuBe38A=mz}N(#!};R1w>cbSk9r;qepuM@V_AmOIR)I7w$`S9@1$C&5pYYVZr=f zcgANBguJd5an=&cjpJ1P_(;rTxu!~wH0%5t58dj}3ghwAZ3%I4B@{Ag9CMeQ572UI z7b7FkQodAzR2brwX1!;5N}MLQR;Mj-+|SCnp)mKe)hv~Fq)h{WXjzxX#efz}V2rqK zZ2dVjA@MjLl1Eh6(4fRj@hq_&4oC!1na%?pcki=gVkv?Wa_9lgkE!&rzRKQL07G>t zyz1wnEv5FwiA1}>8jd0Ka&HW-WBy`LofnHX;*10lQ1i<}n+-+LDkrMGk zMrJb^+5C)7%ma@73Ms6_q5KK|TyV2gbH?t?2WMz*C%Z@#8HWe`<;`QvPlx3(yqU-x6+GDxnMxSrGzo8 zcbEQ-p~hWlrO{9Da(4vTmy*%9+SS$dsGOAJJ)23^Vv6?vD9M1DWK#- z>_;m#2)_et&7{Z=$}cG|iShd$2jj9Y!%%_g5&B87PREdg`z*aGI1328fuXJ02IPFq z1M|OO*B6J7;-DEt!>X||jA0~R1ryrgHlk&PJO@Tk?6Ra19yA8k!Rs>@vT3=NDdIkr z=Eh+7_%;Kuxjo@93mcoV@JXd5!Arj@Kzi)0w51HBdY@ z7*%?8gwA%?(+AZ{$>q-WW#;TJ=|;aNA1J7~ueA4x1l5_ImRCQH*AO7x=%=ke2&9&x9G0?(9Gn_u(*|~zQKiPFu%p;=H;DyxMKn8Bp~}5SD4>b5b=-6W~6C?E-Z%|mo~!enXL$xy>Hv9$%SoN z8y=E|G&GFGwnxeGX0wXnfn_}cgN?ha*ZCSh9?6<2nPwaR=;L`3_3lsfTT-s?U0r1L z#F)?alnC4WMq@qLY82_be*PIxJ&J|ngWwqWa-wm&#!I#U{Bj?kC&&T!ke*F=x1qc6 zsw!AotJ$$B-{O%}lm#+ZUSK&vM^?u8Uhx|i43NC1(umFj5#P12=2fcI)0e20!ul#- z-KMv0Gy?7@Yc!k`CnA0aSAI4C4)$CdU0~^*b*$mHgSj`*qu@GU1SKbp2o4e~%i2Cp z)?cUrr1shlAotzR5DtDjmfnd(03^7vU*b5ELLlm0pD>UR>mNy`!MNs|I z+xpn!k$K_?Fhs_eWBwzXiIy{K7Pw%6Z0W_e2>HVraeW9Y7C* z-+PCM=ykzC4CC)#f?jJf6kw~%Tgr%2H{tXg(VjZlK` zUW2LGY$xE9C@vIrne*a}?{*o~H_u65*g64AXw|o9XlMtYoQN{PlQ%|kQUbjz;c*of zf5ifFEiKp0&3K3}f8RJJeOuEplalp^MX->wtz3ct%I_2>(xT=@v&iVKiNZ8ehNP6~ zTKf5n<kDOV2 zgrl?BLp5BpYLP1xU6=Rt*N@+=q^;Cxf32-eczv=K8IP-}(vec2=q%fO z&$G=9fPzlG=sMgiedOX=DCI-BvP+)I@4lkdZ$&r4KH=$eENtV-9mZzJ`LZh|rRJV= zR!G!nxh=lYc*(u8;ObL-610%#JhPwAH~p)orY_5MVe3%ZTesNqLF&s=t!HynQ;}8r zIrrMyn!)&xCbMOV0JCi(nrCCdVf0C%B@fwc+PO!&*DYC)m)wP2(gl}Oo<}wM4Q-|6 zo5$#LpOd=UdV~ql6EkDUJF5&_cil04yR}v~()+7jv2}sWug4<}T9ZE9gmj?Zp5AV$ z`r&Q+GUC z3k2V0hL2gv^@m<=WZ(whrUFHH^o0hSn|ya!I-n*TiDUdlTS4$4#(goSVA~h@h3>5! z`U;zX<|4OevBhl=Q_!~=5BOz;Rf8BSVl;Ed(6#HhX5D5Sbd^TavB_E}yHe1mDcrNK z&U`?MvEcL=>{Oy-%L`Dei3G{7mK#S@XP!}Yik5!SJ{}l;-?m+MPn1t+c6llu_M0w= zJA(N21#W{MgNmF;&xKIV*bqep(vg{!%07vI+B}~m1ig3IY^Z*2U(TSf=uY8v;p{jS zjNTgU_1kVV>vj7D%{!J!v+jjo z1YxRy|Fdj*gsC)+Kdz#;u|eZzEr%#<$4_3Dldt{A`^KA)<~(ma^l8$U4?rL$w4466RJHbHCOraN<`qR4KX`z{X@Gr}#CJos#3y9{eZzBY zG;Oi5u>YQ7YvR5-nb#|^gzb(wvo^qN$F8$x+JZ*@0WntsYW6Q|&c^Pm&-6fEO-aN6 zT=jOL+Ol!7OtXQc^NE!p=rs<90GWet0=5CJ>oz#t9$KT?jx_n{Jt)mC|NIxiDK9o+$02t}?wG2kkp*o$o zvy_~4EW=d|>sIN$BvS9$7Zo4bvK2_91gv>ovFl^?gYX52F0E<^Et zS2BaOwmt+Fi}ZXrQ|FK+;Di@MbkJ*d+GYJD8J9~*N6N@Jy19FLH+65BQqbXOI!^=)96uQ3uXzQ-y;C;Ja zx!N3Q%_Z+ks6Tx@j~QZ4Oa5+f-yUa_hnKgMPvh;cO-oaa-$`*5Nsj1z-zscgBuR}{y$4yyB_2s*} zBO4eE8rkp&e?r-0{A$+Cp49u)JxqD#h31zG-DV#W`dkcPukVgZwDSUpH_4ksMBqu=tX1%n*VyU)T4-j{$ z?(NsvvdqYe(Ug7yZP5#`Whvhb-n}&&2;wv2JIBjOYi==L#s%FqccLm z2ZSnTyC)OFuWtP&JwxvubTTTrnFxCc`mXh6%cW_R&|G#6Ox+o)jUpcXA1gRPdr5Js z24$wk#P1Hrz=N9)|S?>&P?#im)uUaPguENh>qZeM&2%caF zrcG!Pj!G!^0$+~;-{he&{n)l5pSO8MKsBsk8Cf|+ZE%5!VYc5fr5KVvE9<(3o~Z}3 z=2((kqRQh>&eS9(A&h^c;OrP-^x!PDhDj4q^sNe1d+qdmU`fwk#FwTx?)Hw%`Ogbl zV@Ow8((PjiGV6u37e#R@v^l z^W91S{O^>+4{oyC*Jh#A@|e%o6`F6};6?mSQV__KL@;7AjFJRJMW#H!?HZUBcp0uA zIP(BLBJy@<@}aZ6gX8Cf%kDZ`9d(?ST2vsJA<$G{ZGk28j-{5ms}ui16)y`wF0MqD zoleTOba+Aq5M`vcT=LPz71k9SHp3V{4)f~xWT$5mIo0yW=IU_khZyrU1#vSU(+01d zV5&*>*yrMoq8Eyex*QgZ;@}z4yc9iP67msF0PKs_eN0ZzyP5(^1DK8i>J@n` z>Y>g$6QuyG1qk&UJI%pX;fvVLstsJkA0@Bu>I>SNh;`IS*d%vNUvYABdTn|aTP``l z+KFoQ#tw!D1$q1cp@62hf3ChqUTOEfVAY>#lkxe#4O(yaE{)J7d2kwQvu-wM`k!4@&r#$DL*&5HD(Yji$Ij_(k;0zE%8< z1gwnUAV4+_cXK=^+e^gess zMI)P^buY7hXy^V6{VO~2&uAe4P1}VBHyckhAQ!E-pqvnWegR3m`Yd+^u<#7!fy&0r zq!;RkLz0vZ7}Qy?)}QY;4D3o9YB3644>457Uy_%GdyXXf-EAdcrN{+c=z-em)p$%Z zvujLu0SfU2IgDv=$%3{TzLOK1iHnokm70mkjP~3^(-@$Wi2#rSfsNkuz!y27Q<{J? zD2-bDYXW+9_U0>;*K&+u;7)RQXS>h*`6+)jS&tkBV6FUPfx9LSi|DE+vN(Ldxj{7? zvUA;8V7Xxvu+rWpc^{L8g+a>=U(*PG?EPeV7lh^S6&ZUU>4j@9!FO}%5m_ITll&Ev?mQ9wDi_ckD)VgtbBJZ%|W8^@WPWC7B? z(ycPog;YpCQm1J8WQa|M2mfbsQ|YSwAmB1b-^wFhg&7oC4=in zFB2$3{g2!BN3npP_BAjL6&6l}&Hep!qvr}VJ#{==v+(jzTY+YHbznfM{r%bDX;P@x zjc2NnjSYg@QPYSTYh@j!^EJxG-cdy1SkV&zXglTTP` zqk`WG^1Ka9M;Vx)k&ICYyP8DA7k#V8k!613Wp#hPu<67X2gT;%WqJkWaRrvYVrLNq zI$&NQBTv+wj#{G{@4|Kde7F46ww3|p5CJY)&-Ai-hloeJZfvoX`L3w@CwK^40IO>2 z+VwzH&DvBC-|X1udH|(1OavFq@q$!Ymk?luz43RG+8qd~U#C=G5wxC!g_|nD}I^fJS;e@bGVsfc9UQ0w7U~p%^{!#qe~>m9&j-pLavhZ$A9Ip4%o6v%s}MexaWZ0md1#+z?J;g{s7_8m2h zX~ckgMWC$)cKt_y(vpnYn#Uso@XFiKXfz0cu26{;4PfGaIu~~No$L3WKo#lP)WP+W8m>9; zS{5S7;^GddKS7G-0R2@!>Ou;sEwSe{)_Aa?@e&z5&&zQ0Ge4`* zP)bh4HXV{CGdK5Qmv05_=~fDEcKVwM13u@6lrkSrXZ7W~vkhOpLP-Qc2o|iCM(#NM z=<+O?Gxesa$_w%{U>MFt4hW{|fEB&vkB|Z+II@>_q^%=qX@&!^23E%99a!wN_f}eb zd3m9*0SrCZfS;SfH~IHJxrP@v6JrWHt3-ThdKSP$Ja+aYvZI-kEG^}I2dZ^#bMx2E z5|2%TIH5lS%%7fO2Lr=kgGAj)D9;mCMy=eG;p$q2Yer|1IMb5S zm@G>l^{w#fDmh}e&isLXfUYy*=k0D=GNT?XA|kJLGj1NJeZ%F}T>Kp`?~pmG2!9@0 z@cnPp@u*uV@^9=9NoKFVvzJ7h09f6>n3MoglmjD%<3A{F|H|%v6Ds(Fkd&FvFSd)> z`By{#9((?^`#;%{|N7^lEWnOEl-0Pz{aYgNO=t?9D){fu`hOh7U*-=)b;70C{oeuA z|2led=GQufzgc3R?f&2??RtK6{(Z0*c9_4lQGd1huV(SU0)azTUF3Y>|2pR1Kf0XY z`$YdfI1tMHpH2UN9o!ezP8WGv1|~cFA|Pn}tp);7i0W@{mL8#jum3+>Qq~K+n0+-5 z*CZA)o?wG5k>WXd}-=S`BT3qcs&Ji)A5soG&l@qf1WBJoX(*}vzs ze_zC(i~Vz#KJ;S#7DMQzxW*ZJc$&(qC{`Eh`_x0R5#IrnNe~SCud2=edII<%X^d(}Gn>`JYE&rh0?<)U&YzU!VsQo2En+^=E?( zF?cGO|8C-6Z^}Q)CId}wbNR!-opWhs*ZA)ZU-bNs7XsKbX6+AxcI_LrANl|L;ZBK= z1MTh7{s-h&H%U71uTk_Lr+;Di?~D1XJ^%6k$ftbm``=gnj}89=aQlBZ{OgDRbI-pT z9yhLU^zSqNueac@zVd%ITnO)jf6=d30(qq#*rs__a8yozuJPYvz(0PtTf$cA!^8^B z=H^m>D0(INNOcE;L`N3y=TZ#(n2FXo5WHoWU#ZL6_Vp6dPBT1m-HS^|Y>svyyh6JO z-OkjbJ-e$cxolcVqT7aa?_M}`KC()#|76b=_6?gXJ&NOeDf6=Ypr5kT{;>zDuGhMZ zd%Jnk(SNAMq@Qbk_x_W~CykPkCU+C+Gd+PA3xU|T*)xej8?2a-Sj<9xonc}7k0;l| zAeSZ=!#lQUDxH0F#-Pr-Z7L&bL9Xv-*ya|tAM-XE9MVzui}?pWNtAlIirjyiZp%nC zrB(?p&b*uEY!x4QmR zI>@r06kYAp9nGw3A!Ge4p{!3EAG+U5_>-;eCC4~8ARum@**60nJDReOiU#%Za%k0w ze%*;(yomKE`Y}3^$3yaXYVxDI_v49lL88eJ`5PRJqB%oNsR{36ENAs2kVv60ceb6+Jb;s^Savc+H5XYyBE!w zjzF62tMSv!=+u3;d~Ij}zJ&OE7EKtVh;eM3|;^Sk- zldvQSBNuC$WJ4NEdwWv_sbmTr;Au8~DR=ra-NGI6)mWRhd;f3@yvFQvc7F3&caWtz zV8!OjSLtrP4}O5|{lUhAAn*h{k!ZB9&dq=Fw}}TB7T-j?Lhql&Og+c|S^Ii=-cdx^ zJ%wPsnJgY#`{}!8hl*WUO1u5~z5H2PDP%gwM?Jc-$D?Ka!GlV_*yrh5Tt}bvb|GNPc$oqq5v}8o9L!+Y&|5ur~3UwD|_Xwo19KA{J4qg*IBW}X5B3N011f_p+2xJg7o{iEU==`N1BFw83&&xaCGDwarBm!ybw*x=sYN2fPN~fm`C|pvM zI?)okg#*hhDfHt%80s74lriMkZ-S)MB{o?<##h%ke2dez-8JikoU4XSOF{;g(xo5m zLBGBhyO7{h}YX!v^}n=S6<6tf;6kU`!iy zFgq8ya3uixb)*Xt(q(VDwAbvdup6M|)TEz2og^tYOuITHgvdT=PmwkEZ zZjb85bi6JQ9q}r#B)1cnL}?Ki0(;sSLf#}qFu!Ok&Pa+b?;oq>eY5d&Vx!E&^=l&T zay3M`b$^o_wCT98Zs5IozPWTb)?{yiz|wATAvecD)rH78aoSa^>vY(JNiN(mk?-Yw zj53aqNod~C{Wx^WwVB-wrAp)eI____5!6T%yl32?qKU?9Dz~ z_Y2zH$!_RAI#X|ZX~?_h7q=V)@&s`6ayDun7q=*rlM`!DpaLLQ*4&``2E7&UhEuyVy|H>`}iF_J*>X~x+;Elx9@;P?Ay(mMp}EwMDB6wrTz$JY?#Lc=A*GY+i*$YS z9rHZ}9~SWB`wmyHfRl=Yy#!c;1)F@aviqng_!R2<*N^>KMrXQh{G5;Lj%u}WUCBSV zw!W&An5SD6pd^&T6q{co{*szG2v;A3+RFX3y=-12h`YgbBS|$Ix=kPxlZjX{sj=yC z@U}kr9P7<(e_-lJ=@+O$fULWM*9J}B?^^^T&%r?`Q zC12v|x&D9$!7(3=CRkedXkz}UfndbD2kKTN3mWrjfpVpx-tD5MNV|_Wc^!5+>2rJw zg8!{kt9#Q+`GvAja`gYB>nj7|XqI+^1qlR#Cb+wV;O-%~yDXC6?zXtQ1PE@yWg!H2 zPjFw{-3ctP*xo(zp7Vb9e)k{D&h&IocU3=C^>o#p&q|T)Vs2zf)Ayd)1YhBjWw5Nbb9(epQMY8ZcyY3{SHuY__BWjx2bwUf%Xp zziQK3h9CYR$7NV?>NUU?ZLc9}_Z4mbq=-!%Jj=<-FJ9N;q@Mzjf)i zIVqnlR*NYdFyK>qg!6Ix=GUN{$+iOcdWub>?#s9*e}I#BHs?5I&D3N4m$TdN;%`A` zbLa#o%HZWBrIb34qy2@ZBhQtBhT_4Cw*BCq1`gGj0=xeD3+o zuzBd}Nlw1E--B@OnuwI^fCs_ZSb~CMYu*{)iGf!UCci3WX14E6z=@K^uo|** zp4S%^!lG4C>d7&XR`+emp9FP2R_Dt__JF-c(w4z&!008~rHH3V3r1WC^m(r(XF>SE zpep0fySp_UhaZDmpH!^Lnk=DKc7QuB2f%{tHszT}_I5uXL*s-x`HcL0m z@#5b#1#eN9m|?u*yO-NpxewDm`?%Fa?32Pr=09X;65@o?Z?Znhd_Ku!P2RgJz2%rB z^^po&FYsQ^IT~{vC^BF$dxER$D&=1o#SLR)S8Z1I+}M2M4zlm7kAE3FIWLf9hInU- zLx=R$xJzBuR?aFl(_f^x zJ7SKN8t$WI(wqZ?VqHGt ziqjIwYkq!97;6&=$G#h*xf!}#Ew3Dm{B8n0?UpgC^5!GRDTy}wEP3aZPQ$9NI(4Rs4op99ew$z5_QgS`*V(+d7puLV*t?+YVUfL{ zlz=rtL+Y&OiTIoSNyksK;fLp9PDtTG9^6O4yt9G zI(wWOHWYvDj0f}UimXEb%fE;AVDs+ynIv#kA)tTPoTc{5TDq=KaXN;|=pX8! ze!*y02beH!TI~0G55)a}OJMF=PcJ7{Z2-h-g});=lnh`7y<;EbH5RK!8lnH$TTV9f zsk!~zx0}IunLYvQWcqKRrz7-k-JPjjaP-ULMjAwbw>VP1fsN>u&xe8A%k-6;&88m#Th~Hi2e6wO(y-H4 z&$5tnh2V^^eh=<66+4Aq2pU097@h*mN-nr`oe@N^z9;~hVV0Md>|QJG@N!Zis>91=O8-Z}o>act+zt>bY-!dVehq?=+{5Iu?Z z^$LdDKMCZmK*6w zNF}(5!BaV9V@-C~d;41X5AfNM3;htjj+DS7!r6t#j=N%HQNNi9IJtl6Gg*a?JV-+%CNMTcI3=f~}ZYmcV#mWdOswz}zl*Qd`ahvY1Xf2_p0vi))Q1XOmMa zR^sK!vg;^X)5*a@r1DtDSswq-SJJFx>xc;7H#Y5*&vWBmoJXKxL)7fO@mb!&qs~!O z@s$j>Mg9i~B$m7z7vYzhA8EXjUW~6Fqc?2Zs`nc4N;BNj*%@45nH>apS>c3XmpI?E zWSl#n_A}cV@PGBfmc~SP;7_7p5+RDSjhC_=+QcyG%G&n}k}=PG+b5vx6j(|ev41l$ z5Y%99ooj;yxP2fET_(CU8|po;^D=xz%mtftuakkE>xqUB7CZ|IcKDWLw$+d>m-Q|z zeKhxd2&O1ALYjTM(6ps9N}(4~Ghp(%q%$vUb;MuGfkM5Iy!g`meT23D+DG#n%DD`h z_xr^G+(SV;MP+f7b>^2%Zdh0aqFDPT!(5?*b}A>v&nZU{_OHUtZj+OkS*b=Iev{ed z2>s$`fF?@@k1W^SF_>S&f8IDKfx#opkl)CL)O1`EhlWEJPGY#hYshU#a!wYXerAMb zwxvCVvT%DkW(oxEO;6!pKQ2&D(8A*FGZf8z>!NQH@%4lVfuJQXtEW88^y28J43T>$ zg5UkEKvOXz!0JUPzOMyg+V@aMU+|QY75h6S*E2yozy%vb2e92vDAzj*fkBGzpCw%% zKbTz+jOCasx%jQM#0_o;;<~tYuu~YtpK_n^jz4Bo4<^D{hF=02i=9Q0!H52Jj=P`W z>)pdF<+uZ$M#XIJetk_*b zdcgw)SOL>3A|$pYZhbr{BI}OXi*l~6vaRRih5i|`enQ)Wi=q4U;~XvY+>;4^vWiBJ z0@~D`xtz|AUK6{T*O^bf@1JZZmFgWieHGB1yDPS}mU`+Y!dB?r#UuF%;j=zm`fY11 zTy!#-8Nr~WxguHD!_qfk)Rmwj|$uW|BjIQR29r1dEv!PY2SDntfNr_o(HNV}akT8QZ+Dc}gezHM1=!>uXiKY7F(ChRD z*5`=iusL28!-i5 zrJ>wjsG*id*5v98b=s6nrQS6d)Dn=n?cpi!iEd=kgwQpD}?Y8}G!s&}z55-D_qkJeInL zpF-7+>9Dz%L)KWX8E16{dzjyyxqJD+-#!;)NAM{Q^4e?{a>kW$lx&{l3UG(FZHy0u zFbl)eKNy0B_$De`8zn;?d;H-kfv#*BnksFtMsxLk=6;hsuF0J}gYo~eQa;ZNGg64y zWL~A=8$VIWZew8|M5~L>vTWK~n-e)YdCS@1=2q-dPk_6l06E{G+D7e5M*{0L2q-r#t}qW^W0-JZHMk+if&cDunm-kZ$ekeVYeP<`8)sx;tAhmmbw#TiT1T%!zThoYerM%xaWO-O%%E*` z#2?k7$PDxLbNZ+zz!@)3wh?pR4g~F3mVh0>Z$Wg$%CzW$<5cAVtgH(ycO*N^hA2kX zCvNxf^GFSE;VXf!`&y|vvR1reoqn!SFb5Wx@POqd(po@1IdPH^W;uxhqO8 zjXlcJjlB~7Cj?2D(XR-@5xVFHcTpw2wd5P1+Ogi3m2bxHS~|JoOhKZv^8&&i3yp%qkJkO zqi9uUq#Hm=z&Is7Fp+jHVvf(yv~nP6p8HT>Gvj!9y(ULMC>4B3VhJvk-X)(!J=S_R zz~a#xCiN=P9skIkEXr&*Yo;Mtc2}<$9ocLK2%8u@W$8te~0j zg&)aamF5eLh(O7=nK|!yT5N}P(HgdiOI8uwH9uJ$^||-E;u*=t)jC2ZF6ij?>)qB4 zE@zb8*)FqRDj{`U+Mw}~x)QJ#4LhjBE`F_=Mz`3T=Z;|2R3nVD4=MZgY0h%AWgrm6 z%jXsuJ(#faLgV`Z`b|Kfb{>be{fVF**V($)prXu&x<4HnQ59z4ZRxL>*5Ur3imMkC zW+i@swK(uxcyl~AiUIGNeyT%$HcUjV$gnUsDcm65ck71XOtg@dQAF#UNp61@3I4=H zOhTm<@hg%NpM6H0^FaERL5QK%)1C~phcy#>KCQdfY4v^>xPqNZ;$ITXJ8>UASd8WK z-2O`VqoBGEAVHzo-=Ym;y~B?GvHh%2ZvEYn&(ZZ0eJDy>Wi63j+I-{eZC1}|_0nfX zxPe&gN8ZIK7v1+L$3(jE@IvW9S$3Htz0CP+M;oV!<3!z?OjihDvtSJ_R#_FYO%#nY zA~ShkrDn96%TsT2FsvS=TEWn-yNDRpx0u2n96do~aGU)d?tsq2Bu> zUi>k@2X@sl=RGug6|>7z_M?6-7M~Qn)+(poizL@W*-oN zvU%cZc2K9luki??p1+=IqGwDmh^;c;G5Kk$2Z*X)rkKlwqZV980H@6FM89PiRMMh? zyL6L^sf)WYu&;`JWf|OH)_f<~f6Bk=ByecSYQ%s-W_(M;OY6l~+3^y2Jk{XbqsYBj z@5v0k!oCgn0|7G%yOVk z^TxM}s9x(KUB8LGT&9S10+Hii(cVnr;miS@v&GG=iTxFdgV0N2B|qu?*6gbpMfAlU_YJCEOMFwSg3DQdmUO zy`2R*EZkk%e1@$4e7@j9CP_eFE$x$ws^425cMD(^hdOGzYUPaM9q6ydD}3+fp^x8> zj72N({e8~t52k4BX7hAbfAot=^ulQL`aR3^>iHG~edo|JTnYWQ-!x5eQ~1HN>>)2& zI!&3VXs_bNEunnbTi8egj6+5sXOJAS^p`Jy${EJYM;v-<>t7+f{AMO$IYHH-k-G0` zDs?a2qrZ0lVihh6MJ_i^IB}J!ZWC*v`lo|s9&XPLG^4EcpIR=WF!*%g8I84XM{D{T zRRKiZ5Dk_C(WcP*&jd$q_ec`{2^-Mh^ju8nFSWnLg;+;W7Wrqg)E^kA0YQRm_8@;Qq-}9~DbsI?0 zpLf~S$>P}sw7zw?YV$Ya7v6#AyyJMKCb#PKA~CVGvf0Y21-V|;CZ^ETia%_|AvOEi zr9@vTw}jJvGx{aL!97L0;4eR%ft$M2-8=kopuyEmCWAPSk~Q=JhrjvEaToAi(cb;@ zvAIw8iHI5Y=qQ_NyZWo2P!|MDE^(;%uge&nzL)P4}SB8rGV za=q#mjYoXNVz46Egupj`if}viz|*E0Fs5qCR2Q9UYZ04UB(6u?M>Si6VV{K5Y!R%BhnuT3%72*SBHm6E9THpaDmCnd zl(U;woQ93e28R{Xkgo|_#1gG1)tk7hBl0sj8W5|*vqRsIZWNC@<}L z5IpL$c)FGxsZA9BBX^$w@)hqZ$;e}pAYft7yv+-bhCBqrc72;L(3u+RG|&~{_ENa8 zj7^zqh!$vs5uOaihFz^Vy9_i@GJ#P_l}v?dt_`csAmL0_q4wGLX4M#_MzION1-Z-*ugiOC5= zvr$GWE_U>>eB;APteJhHnAv8hG@)g}#?#ZE67|a^9Lyeo6z+(z*!(m?eK+?>Xt3c805u1piMiDe-&WZq_Bf5TKD zd20Voojg|t_Hujs0<({;d!Ezj}w|(bdlr&Dg{7Kb&}4 zfXzf{M-~JvWOoOFhpi1VyC0=Ai3!iRq}-EVrWH55Vv-VzNR2~c<~b>KQDlVKzv@&s zFD-sRCWc}DufExaX>4itF8LTOmv0<@*LY*(P_V0dxJfx`)lN5GNP5V88D>J5Z|d zc@iy>fS^fTdeR7`-H-R)S*yx!WNmxQQis>JU%=U6izJ=7*t49ubfy1;&makp@#P0p zcu8ed(;$4ao~!=E4;t6oz{izF+ep$un za{%BkVY7I8q_TijP0S2Ed9>?BUWxYwWtLoSW<+@&Wh%fD%_2>X6H|}g%E*UjMV3V5 zhM6y!&#O!f`}y2fQQ2ghd!0PDj5~O5-3hf4#JP?$JZ=og^0I#3rNJo@qcvF;vW(e= zXCE|=J%!v6T0H9#)ce=e@K2Ckauyc##$6tJkSp~LV1L@UPKoA9*;?&%>VBL1Ed-^8 zbZ*9AoAkM%!Y-rr|tWE`*7F@ng~V>86GiZ2T->%FzbfXz|%_7AKbC0rU*oZSA_ zIFkU(A3Kw;OtoI+L0`0`z1BMMu9|=r(_cn)cZ|qt>R$PCZDvD^PV^tK zEZ&6ZiOqHQk;t)_Xqw@T3{u5hDq8BR{1K@G)(|7rmpfWw!_Xq;VIMo09+p4c27H5P z@R&i8Y(@@OxpGIk=X(YY_KDg~7xotO%I^%>vm%`WkRRxt=Ns{uWUn(0>~mVe)+9Wf zy73nHg|`)qX%&CTpvK)xitQcjgatJAm(S(X(JovMB(GCPTZ6?PC zQ7x>KAgu9yT6{!j%5>S-84qjrupw#4Ltcz`K&MmcR|&cFm*gCUm-`IV_~@2)FZN2I zjXy(oVPQyMKP5(%@FHkXJy7rm0XcC$s-ZyV``2QGHgEu{8ioS(?&h(S*yYuoztO`Q zmrmFtoH)uKjE#oct(46W>2lUdp53W6cfn7>$j+Z#ptk23_7^%U9o(0N&%tgaFqyLZ z4s*vY$yfRDOYMU?o1%jFn27UMb@Lw!5_gYjn+b^FS70DkC<2ursP$cvLJ!d z&tDQ_Kb6?N5gHn!cEoBE+hb@8{KYlE6tZ=B<)$B*E;#P0?_`t06g+L}MiIbERLLzoQA;xq=RL$GE%*$){Qci5-Cn22-~U0B zYwt5>&W;hSWw0d|%Ns3qvRw5NJILvqdmYP=X%{tSoJuM4Cl% zgRW~|6FZCNsWdkCI-Bp{^S6_)MdAtZcjp}0zfR8%>*r`G0;S!%73wJQ)OsU;PvwOq{fu`>tHu=q{O%cbk73DFrv&Xy8^9am>^d zl3omgM%1E&+noiL;g1ekY?pPCK0MdA$ZP(_Or&1cz62_kxL1@;V^0|CouScwPkxTZGYb34& zWGbU_`B-(i*wHDXc-_{V@L8CC+0I{a7mO=?+>XDZ&wDKB9sTrc?(7)F=JDr)3kqrW zmD%zmC`lWp`^(kmwySIfUWi(#lJ|*9X0iVL^IEEX*{Uh2&+`vJL-ESceG?Df$S=;D znC!uh(u%u|I53oI_Kg2S-Di&LCih=a8-bXAOyB+c zluSDOf$GBzYP@b(wrj~{i)RKM42zj1aFl+RKTb3O zF<1s&BYLh)v-yh&b1!mb|L{KJ@Z22UXxW03%ix}vh;RA@>LCctPELf%c6srL=RAmj zS6(5{T5tdXw|3azftv6v%iFR{1O4N$*>kOvd(y7lu7iC(-_ok@-SI+>;j|ier5N7m zBZKTDOlZLyMCYd$2W6kULjzb1Z{1x2B2^?ibJ+1yeF)o>nD&C0% zI1fb)oZnS%nggQz#Nq58CzszvD@mIL@YFAoAx~J!7Z6vE3GV)W=^~3PwSF58;#>b9 z#Ngrgw2gHWCD{Zdm}<8j#x$=B_b;A?S7~oL1Nlw>XVeW^aVjU@f`^*MS(<<+Bh+yH zuVcUMkx87ZV}D|(Wk``=aP(__Tj>7!MA`Q0K-+M)Gq3ddRbiDkWci&-KxW#z1DIlu z?NQ!Wc>ejKp*qK7lC68M6-{LWSt57E>fN2pltBun6{{S}Or7^k{3n2X*60r{2~Mq) z0V>k6VZFQ_hCcw`f9nvdB{>ts_yt%!%o)y=!hO93P+<<?O8Ue;#klxcj-*H!C3#oyV;V3Xk-+Kjmwf0zCxql+U*ZW~p~=I6*#|-hDBqsj zCYejBHc|)}JlSk0f1R|!7er?0l87^B3|R~zVGxl25o@bV__p6uSiOOa)`&kg}MtOLoTQh za?^?o6Tl#Q2|_`}_H{EBKB70^i4zlsxFB|T(Dq#%g)P73NJ8Q6Fm795i8Z9~lJH>Lh zaTGC^t7|LaYPUT7q!^d_&(H{Dbzh;4lTNywls{dC=iq@9q0&w0CyrmHn}^|t_fJJ^ z)nsN|vsQ+yQlIT$gFeLAx!54H#XDYZvAby=(-_2o%ARq7N@q;he#zpajT>l3vxgZQ zM!LhO@$V1H-#>pY_!Sm%RfY`#e7;LbOc~l~L8?qnm6c4Y;GIdh%-Z*O7lQ3LnJ^bj zzx?4HQp)B+n+rGk_mHV(FfC8|yX1q>QB%_#6w4cBh9Gsc=Jf@~nJNxvuNg6B{hHqM zp_>q6fst)faPqmPu2~uhlLsqnj+Jn%bei3O=1;$K?UE0{mFxxKNcd4QIL>Jd6e*uXu8t z_RqBW6nOHF<-yr6a#|Yjz)@X5oc~B$=m%y=@aui+%> zsDE0_fF=?dls13k1yVO}UEeJ^W{O=Z(&*CJ_CLpjEZ5i7*{JJOWeJKG!?_Pu!gYB= zPtDN?HwvTpjCw{WWXpn!zb3%j3y< zI);T6k!(KC;eDD2FjjFe9okw~9R0Lyn40NAa`P4)oP9n$8C}vIL7lv}zVh8|M_2@2 zg>j&d1a8*{$8o)^^r*Qn0dB=GV_&G>!b`7z2!=mlao*^1!?%YImef#D9hIIxrZhqY%$ECALY$W*IJgo}?@EB&fp+^@EG+F4`UT*AJ;2vG)?}A_uyf`k%GBsLzFXHnVxK*D4o6$^Rg7o;SVJ6)h0i z1>;-PVaK3bd{dmTgASW6_kx(Zv~}NCPz59A<{PJrb;FuRt$Z%@-~V`%R{kNYXJsWi z1ebd*6Bs5*N0LlwM&-eJpNZ@ordBImkGx$H#_%deLGvuOEuF9Yo`W15j7*AZ@-doJ z>RAB8FObXE!t9WuT8@7O9d5Wub>1FH#sJHB`xOp!OJ_b=3BICJ)xEbQVWsI|e^tHY zU#BWFA&+>MJm<5uaj&K;Tq8gj>ktcPXB3z{1x-##SzZoTUmVP%AHghi|H!hO7~hDc zvJ0fad9$;p!a>s%1Ho!>-P@ZN;cASACFrDP>+ODOqK(Q9PEPEhJQ}T)m%ZK#3nI%e zw`8obOj}~U$~0K4&-C4+6So0oLSXKiw$DXW-}tkN1_Bo^{9OoKWoNZ^N1?@p=#|2% z&6-LC6HK;mNY^fe(&8Os2=IiaA@5P(eitOA8BH(05jOCHyh)ptA{U&nODcBP;@auJ zYX3uZ?NkeTKS@PWtF{y3Ni=F#^7SM(q3 zh4CCSDLgUmQz0WGj!}4&hx6C5z({pty+s(fO)$o z3$Iin49VTN-DS9DB3xuB$7}@wFoDB5S!Kk+$jty4L5L+G=th zP0<5>f7R~nfoJm*iseT}oAlUXvbJ@+R6YlH@%JA67AaR*W~o0D>rkzonbdLv9^n^# z*Cw=ysS5lUsvb*Y%m@g#6u?!T6QVW(p@a1?nOEL+PUNC0j)mH7mRGsMjPpNS-KY>8 z{aXps%?8%*Z-!pd`M?K-)UXs^=sWOHOLQU*&h0{GIku~Hb&`8;)UYSDk-TlxdQ;?l zexyvQD@3YtPNw7RieDCOb`I8d&={FsLfFSvgtPP&hK1JK8b0a4$mjS-hQUxpC<;`h z=XVa?n}Y{5ZC>T!1h&qmy?|SDkFD!7IbEoZP-|}CTuZrD%{ZkyG2u}Ru+m=>@o+e< zGthOu5Dn|5cL`pU&O%;C{i|2^60Z=(*RsN`(sa0YF3Mnk{@b@X8M>Qk%=XWsvK4f*{h6X|iXc3)TP^B6CjqwOFyJgJHs z&7pqWMvB5*R(oCRLQoSv=K8Y2JK)R_QU*9LQXWdGU_?zPorMuNn?NRxT8%#4q7e>O zcI&cGjLDoB3N@hKfaaTeL_Ip{?AIt+OMSf*6)~4EGqG(A7&3L1CxQ7q4IKrow{xe3 zDk152L@-f%(=Rb;d2Am8Z|xiC_js;rilwaWr(O)O(D`?9Hr3L9nZWMPFP7iG+-TQV zSX>Yaff2BQ@pa^&ouTug3RXEli}QK`^-``77A9Slsc-ua%h6qeTaVB07>!$ARoRj8 zdq*p=t6RAiy3hE`++SIG&|Av~%)UlZnD1L75DNtR$CQDTCk=1TQ15*;li$(enBM-F zYIgD>WEw`qLnGk$OXL5`YnTW{KltE-SIPT8Ojp?mtjR=)){#AZ-8FEz-^2u^(wfO& zg2`h&6w2itGCry4d@Y|MV|zIY5BhX!nxgAL*DIsv0jzzf?hgfaW8_~wh>tv`(l>X~ zP)}ciBz6Y@`O*#XR#KNo?hi;5yfBPM4%^(qBZS3y=|HstV@KS#LqM({#_8*(Du_7B zee?!vTUH`gQOqF8r-@0Q4YSW<8nGd8Etr@yY$-RETUqpOFOUTme?7mLFier_rRBS;a5ekeYpeVkZO%i7VhLH6vMC2BTF5C zSK(-|kW&32zY;zLB0ugtemoW|yLm!t zQD3B-LMWdJBHO`Eqp9l$um3LJFeG3rO3)9nhgYN^t`6w-B`|;+W1ChFyXv@ui)BuY z6}-^cduu^?-~vi2$!m311x@Q72}ZRR0k|_=3b>9$F$=}d33pJ160rbu!id;4G;*@C z49c(Ww0n{%aIa(-bxJvg{bu8aGlGb#BP3_8)0J|@X_1-!1aW4x6gLcm#DCko=i<^~ zJ#v4-JNKImo_7b;ZD!-q1d5LiqWCVG2`iR%vjbYYb# zNKSi}KQ6gdt=q@iL^R}d#-k``>yTDX^RpYarcse>C+Rgjn{ie&J--=XX)5}|P(;3a z=`7Sy+r2a{x0_DqYe>ZmE*~RO&|F@xoDD}7F`u&85Z2XSes{&o56=d)I=*eCD7-xe zPrHgm@8CoCBA|Gtc0aFd*_xDJ%woy^!e%Qmq=;D~d5~~|Mk~`Vjud5>Q4Frmh6z^# z77A;E%yu)Uc{p+4?jGrzN4nUllLLKoQn~n zzoTBmD~)-d7?{lwLRz1L#@B0tK0bAO&)NwYK0PC}VUc~uRcamMstLNuJ9~Cpb>t2HUk4_Drof;MH5V| z#i%T=nXF;yDOjF69_{2M@+LE`0v z53z+R4qD3BZ9VU_TClX^{RYZMFW>VuX&So=B~X3Qb>3~H?lHLXhZPVreG7RMHnq(Q z^RkzY(}o{;unNz#_ZM%X@a&_~R{ah3pApq4&xu+}+O%C`HxBHVpp3<4f(^ z`A4&f(2Wfip^aqOdohu{cNZ@;Uq~M}W9A0*Ti7yCzo-so88asxO0HS;0I;b7VdJQ5 zYH`Rgqvxi$!SLtYZf8ZTga`Q3Mapn|n>(w4vdgB8Ak2$pZ$44Ddvvf|$+Zo4ck5tR zQ`8sp|K$rf@YnBDrfBVIDD_d6h?yv8S(v(XTbnKOx7hb4#&Qu2e@{`lJC815Db+K$|$DmauL86JmqcA>GXb8{zsa?JF z>QigebBxXRP=|$Wik}bB=dkT^ijO@P=N%YWhq+Cdkg|khcWB5w;bOPDJrv5yeejIU zJWCN;q12_d-A1;Wg>|Ty5GJpMdh714{#+Yy)=N=#g>B%6+9#IlEAX0xEvIqBKlELk-LJMJd*HfUxOr~P_iQ^S_j_t2c7Nr*s&xZhw*l?_$sH$5`1t zmHnxnzb0W%^IEKQWsg{_+aDtP$S9^$fs|KY4u*K{?tiou5lwKknG`J6pW%Q@z2w&d zh(+l%2&nFrb+ad8 zGRW)bzP%q`x9=I^w*2}a=6j3qR%R+3;qK{-J#5nVoQx?tIx@Ozx=!b00Y}0IPt^xR z%_}&K(!YjMj25k%R8n@mmV%?s?g<;UJ4=3rkm7{iGUEH8tzM|Uo@>Iv>;vFN8Hhax z=)Jdnd?p{6)eSH3D0*;Csep-346J!Xf35nw6jo!Wk6`D~wn&uJKrD*+Dhwa(MIwJVU>2%fteG-xka z4g-&>0%Wl(J=zM5&UZ@y1IeP;)r);0v*2;kD_7qq5(*9I;*o8Bn}A_Jz&yBIck#%= zfJ3s6di2tC(;Af#BfTsT)fe~z2(&Q9>Z#c3X?w z$Gu1CHPH=OlG3A%WdLe<9fy4Sw`V$C-&c$2-E6X+hz#)Av<7CYfu}}$QqR&=jn|xE znmC1nTK1}?Q>&9Hd0#3F<37j*7^N~eQXRM+12Elw~|g3?{`a{{!6Yl2+%dGe+7E$D57oE6$>ioq>RKXM z7DlaqQirEARSB}lb}Imt^k8zy)Yc}+ydueJl35PGJfQ6mN&YLSnx4y%53i$`d2Xg{ zTG6aNmO@)re+`{8RE>U?#pz@mVT9kRYh@ zr4Dx|`0MU(OCuWK>2)1SOe)X*%>LZ+K10y6j1IsNXg{oJ{Q{`seWay&3+N1{6V9q- zdS$Pd|B0zy1(vL@rZ=ktrEY1tK!BdRCOo zn%QhX7J&xd!5ql0dK@rwqR=sk_g?FG)uvNhq3og9Wae^PxKY16%&m?N{&l z)_}QGm4dBQ;Woedn?zO3GVhK*UTCSHW)x@&eYktE>y%~fWz4(CIDudm*lq0EuF{fm zf`*;Px#%0t+p_w=g?K{5bN*uj}MnnEPB( zs(j@B+*=%%8tD}?tkt#G4lHKj(_FVq=~CgJg)mnR8Y`{l-M8P9ww@P`M}1G{oqy<^ z1v^c%=cPUy=Z|QVQqxsGRBH~rWYjJR7oZ0es564ju)llBl%2cY>rOO)@;e=Oey2Zz z?@(slXLh0Ro*kTrXp#IB4=G6~oF;%tc~$ry{;{cOU&F$#fXVWNuFVhmD(gw25RQ5z zr;Z;B=DNs!`sRFPNF;Jr0m<}^=4fWG==3!5zB0F}Kx~N97b&e-ox_pqB){)|KL?I( zaOA#qq=Nuy3dPkrma`=1ZnK$>e1Rb)*itYtI_iKQAx-MhvCpuK=MZFI>{@jfOtZ`# zZ#HVv`|up<6HEOYIJKT~-Qhm~xh0`V+W9cy%mjYzo~Kzghpgmq9&NDPQAOXLry)ke z{Yh_tfuf>w;`s8=R#h1Y0t41_F{pXJs>3*{eEX2f^t)PGM8C{|i_gqMF@g_$?nP$- z>pF%3c`bnJhRIz5o3;Fyj`Fh1pEa8M7OoVbp?jPUkXFg+<2mFDyggXlm&)7#GDVpN z#{ABqLl1YdhA6)<`Al1YHVXB1qq6CdFQ}#{IAskWu?lYO zcsvrFU(R(s0&c!RQ6ky3@S1)2Z&5`Beuv0a6FGwCd=W*^dAM#&opTg^nxpIIh7y!yvq4+p6-4tVfmp zd-5YRbcMAH>e7)06?X`*RvMbQmP}pDTrkh$S#~~vUEmk@u~ta3;|tO7j>c2fFmsxd6=qmb4l}iiQ+_iz%_bDBYn5Mg}9k zUH2)l;`t0%jUr2%S+hO6j-|5mwR_7mO!6H>1h6ks9^1MelU}*I7;5y?Rg!%lpf3Uu z`5At{SpF$GkL;(`k!u#cFmrvA~ElAEUpdi z0w}JY6GL_555}ixigNy`gLB)S8<*z5*WV-WTX-WpQ&x-M`|N+WT=o$0==C&?zC~9k z)A{i|Z;kEhZ%2$b(3pI9sa-FRS!}PzUB`=^4UWlXd!2=o*J3##W1pMEY1{*ZMNSNU zQe3m;l6StWo`cK<$fd77Z9pGsoMeZry)61Yn@d_C9@5X&snzi9T|gq=r0#>4!>9`V zVb}aPI?#-QG0_Oo(<3`r_8ypMRJ!f`&t6d)Aba8d3l9VaAioW~NHCALk&#YhIDEQw z)f`&LP<(aJq=~ax^pNp#lnOOGT&CNiem`-omIOavyc?~y=$|;^zvE*R4|+jh@`4(P zC|>LDNaNpkp!p-jC;kh2h$Fzz-oMzSkZqLwHy{z8N2}HP4>tL4n~0i}NC=TH$f(+s z{tI3H`dR!XvTo{MEn?is2EwPD{|CXpn)&~=7!08NB>U@ybfU5D>zMyM{BKnBfuXzT zZ!`8t)BPeefOHY#pVRm`?u9|||CrH#ea1T!{$5Ig=A9Rz%V`WKaY8b=djI!G(1a0g zzWx1Wfy;zGzcJIv1D$pvgnr)9d4^*BvkT(Hh$}Mx%SbO?Ifdf?GDVEKL1cdM#{YLm z>&^>g%E$?DH-=<06dC!h@PBj+Q9m%J^lw+h9SA3TP%U*)WGI=9mc+!6DK%01-`9?=P&sBolaUeWDu5B=2&4!qTXwEc6gzb^cz z*>+TyW08%AjEUx*bP6P#^c5mr_(Xpv+~EDY5dC|0dN{h@sKb#gnmWQu%HZo>7u~-t z#tgg-wZHxQ&p$UP;5R;Q|I+^PF|hg{PyPb~|Fw|df8snMl2ZNi3Uc`iB)HXKD!}@m zSxgi_Apd`7mvNIJK1g! z0$Tb=7@S8CK-Vax@V|Wizdjt!#a*E&9M60_eZ=T-+&E4>oNb=X>Uhm zzZG%1?q~Yu_DCB~E)>)!o3K4OWzO$)hjv3D7~6pwztJBL!ns#s3$3ZhWzo0;e{sIv zAm4RWA5+NdSL5NcAN{(z+vy+V{QR?P96}0X;sHB`VY34d(_e(1B{mzCY1ZH~r+q0z z`2W2BAM5bbtw{HJkwE%?x=qxA%1oQ8#i)V1>&|S1xT`=U+qra#rj zkLvX-;Yw85qXF4h4#JoA8U=Q8=fKa6sRS@U_GRACHL2LFVVD0#H$~GR9V#VmwlXbk zf01TX;qw9Z^1s)Lp*BKp_W!$b7!jT6^eJ1%lf@4m2(M>QhSGO~Tq8C;+LYwIaXUD6 zJ%)v^hzKWLpOks_LHo}fhAcq3bke}dbI`66|FkFX{Qs2Qf2_(o45zW-Glf?%?e{YS_OtoZd~G7?T;AjdyfEgB(0e8~UP zRq>!~W=d+Dy5!OXYGyRGLYbE#IJre3pHE{$fl4%#e;i*@J^IM)CL3Hyz1T{A=zjTk z34wQO#~300zaQYclz6}A2VLp*9<(eCdW{vq(okj`;SchOGy49hH#Cn`jK&qCfJOLA;=5=-Yx!zG6)6)(qM8UB<;Ki`Q);8g&%6k zMolkX5Q3xDK#1_Gc&6(TRSTyu4ny)n4$U3UaQ^?$_10lgZr}SbDJ3N>jR*<~g3=9x zh;&J}gw&uk3|#{VsHk)+DIg#nLnDpSHAC0X3^mmI;5mBE`TjodKU@RX#WQO^d*!|E zwYPr_ilqL_vbe}^>%LmmJKThShkwAavsoNQ>e-iP0uusheuFZ2Zx`#~qlzG9GPX(( z5oja@?+N~^r5+qs15BIBeMM-GC-wu3mvp;c>nWT~Od9_z@^=OQm&?LZk!~Y=1ck17 zcT3ByL%KJ|-xZPR_;RqAfB(RH?yihX#(pa{bX|7wX`BPoBW$vX>-|Ijde||(^MCcE zIv@Z_R?1_Y}DqOa8hKP3n>3W^A2m4n5`uCc~6HEwaRcprJm;8#r% zHU<=xh{P?7FYE9jlSW- z9L@QUG67l9bwVgqL>2_WB4eBP-II|bW_?$G-(ty9sq(RQ-^uq1-YSZifyjK8HbB6# zIgTgf0pG*QcPanRBCl-G$Qm;(?U&qGI?|weW17-Ews*TMpa^Q{mxl4FXInkhv&UHs zWG)FZ(0h%?*QKBHJxKmL5irsY2>pK^8F(EZ3azSyexP}K9glTBQfOdC24DByM&UZf z51%ql{J93Z8tzsM&{i#+;r_qX8(5YRuQbMA8!eqd1ciFwvpq8b(T404e0ms1OK_U> zSb7n0UG`Jlz3dU%fbV&<$X|vUAcZ^rCk*~eoBRl zAs-*B!cYZUlC|(=bVWbFcO}#}DQf$m;9aO%8~mDj3Dg2^m#f&6uQ+SL#)HRxKz4eb$t?$<;I`c-IQ)R3)~=v46?hgwf`YQTm^ZwfT0_kKdl> z%0dbQpq}9z7EAIVmElv4m3BfOoT8mld zM^Dq)?CF;cf;+!8ijdU|;jqxUey``pCQGbZcuQIBidp$jQ1}yhr2k5NA7hf+zcWeK zyjoUAFxltHJeIac)x26{Z1B5-&EYbN;JWuxv1Ybn+$`^SmDP)%WHDtaq7y^POD*>N z4BMCkeP>jpQCs^#WY?d3GEKyUq6|wp{!wUYGJI$JKf46HOni61|95%*+9-V4>9E!H zp#TRG+11(BYErp50?9=pnlGyqLEl{-l+5EE=qXe4Q2CC#wFP#k;!0g2zq+IC6P+0j zN5(Mt@aRImf4U1SGGxKrkKF9mzrW@0!~zF~q3{H-?q55nbG5{rATo`ST8=kj? zaV;hBTW4>N8s1wXpn~w-!8za-*}Zs-%n#pkY#kQP2*PTDTe?5mK(>@Vp($QlX_Mlj zog4ELRcmc5qn>`rIeY?hw-*j>2PC7|Ign4k&yCt@z`WV3Al*~`7ia*ya)*5Q!r^7JKG8#>(ffv zD_2^en-$4=R?mIC^`k*Eo^P7lEx8|g1`M;;1h+?g0)B=%;+NOGy!SIKa-fc!B9~A{ zBfYMLuijXEBTKMsK7ERy)23aUZ2nO&8QqQ*P{`_CPOV#G2qET)4 zgP%dzoEsd+Z{FDAGNWgTf=OTE6Y+&x&$Zxo(kulev;vM(YogX}?aRiBU82)g6+&p? z<8skUocfmfA_g(K&-P@ z#6@MIA9g8O@>=ZQ!v1Au`s_{CzdRoc9zHZL@$8cyRL_gN%bAR=*mM6Lf$aE*vRed} z)3+Kf%+nm;SRDg#l;q5k%dQ~g*fOYpzSzE1qa>cFl%T6QJd?D8RlF@a1l0VxWN&uz zLU4GM%T}yA(A51zgljG`)K`a*#hWykY&p^hlF5T?=U)8w^K*~x?1g)*8RFvH*4fHe zJ71H)=Wwh2J{SIOd%Wq=w=Fbk<*fqK2c zhH_QY_^n>@@R||bkW)V?!-`}c5 zn=JErGwA*OdB8oQ3?FU^+jd6efn1U@d-_@TQl=J9;fy4V5d4ZdK4kKP_g>Vi!V3(3 z=&8`u@M)lg=DgXE5FnVn!KG(LvOB=WcjHhXI^a7@r_L!(Q!wSBBn6vQERU&V1{N6} z2~*qP+%0ly>M&lZQ~ZZ;2;=s>l-LiZeqnFO@M;fNqvLW_y#x;0kROYtzWSb$q$f)w zFv4RQniC(s&Dkn}oyYi|9wyeCFo;5ErOwZY=CzcEkyR=`JJ~r@Rxi$ac#l2CoQ#2q zK+X#xM>GK*DdXNT)}pl1cR%rA&hofy_S_0JSsnGkF!8k%%^FAp4)BCsT;N?J zrVl>H^DWUx`Y@up`DtqVV4lTwb$aOAZgSUIoIWV5E*Rr||D;+O~eBk%j;a*;=wyqmLiLkH5Hg%{-`SiG2uYTWMVJw+4I-NPH74Adrm%x(k z$4iyKL(8P=_B?LwDzLM#wDW8E_M_P6c*LEhmZM{u-QN4u_XUnrR;qt%nv!vBa}`X>fECO&I(0>TUui)gdQ(& zsE&@P{Is<5JvFxVoiZ*sQ+#Ciio&`-&Az+r)eln@9$3RHqp(e^q~LEXK%1pt;=WL( zX6W9r-H4sQ+zh?KyP!LF2QEi@YmS$y`i}Psb?$oPCXUJE#)riOIZ26&U+)7uMrO%J z-U*K#-2QBC*CHy|xA3Z^|8%>NLQQ;8ax+gK`J(eM0IXrEJfUs}orSBcu$f*Q=M zDdR0PDzXr2VAgSO)ijD?L}^>&l{} zg^dZ?*EdfIM~g0-4PBz_{PPH7RxizGTvi-8%#KqC15zY?I}B}FxJxW2B1zQ?9}7&o zE*y*lYVX--&kU})V3vmk%I!|jaBBdU%If6uT}+;k)*eF0b5;5912KF{r~a+azfzFE zX&x3f6Rf|po#%a1d|sj}&vrvx2m~Qj#^QCowWp5ewC7Ml$-LFpV^T)Xa~IAO#dN&g zwoBQagE#7D{LR-;EuJ(@nFyg=Rhg5$B{DjyP(`hrEcc=$PwO=$gM;?=aGOKR0(Uhv)@}vMGVK7NYG+hKORbB$0i+lHw**H{V6;VR^@m@-}brn2_ z7;zaOvcH@x$_+o2R%xx2Hr#KKE9QmyULTEF9y}y zIN7CRJ=)7|=jk31-ZQFiqgPif2#Gk`-*=y(EUmYK+K(-J++DYhA9Y02zIH!C z1e?r=S+2MJ7*xxC&2$twFoB}Eczdf;z5KBb)~}b9tD5Td!37*RQSn9U_OrnKpn45X5@~+(8m^vb0`aq^ z2HHm%q%rBj5q*Z0iK2Cf8||Z;{#j}RG&@F`srhk_<%Tb(pA`n=<+uISPWn~tH2D*J z{SA2qGTdQ#C!WuH6Ka>uV$+$mpUzM zGa5dS#=YaZvyfJ|#eeF5df-v6+Hib+)R(7`EgE*+y9+fJs>=9sV8$b-*t*i^U^NVV zur6B?m9ID=g8h;>7==f?^;a z-BlweJ8=V< zoRg;q8wCY*rH%@FhPe5Rl9wnUXq9!Z0C?AJ%+aw|$V@FX8EhAXw_T^?TkeuzY{Pbo z4}tY4i}Mb)nD>a4eDDr?yeH@qE^G_EIE?Oa>d+RgJUIw)g(^|AFl8;VRKy? zt)zKYpSu9a_t#>lpADoH;eN>6@U&I43;xUI&h+M>+dayOCZKeQC_aju*);dfzA5aP zmx%0yhv2t0F|}vy@nM#$-(QN_zw<+8E2KgC#p(727Z`VzUyBsLHu?N_HMJz2erk`5 zQqVr!)%oUgrY}BIDkhrew>eXVcyD9_BlU z+5FcHx8%=HKT2!6zJIa2^EO-98znqh`NFC4fKAO)`00mxF#|B8&&4n*eDA%|aA&0< zde>6L+e(x|$X11zgoBMLnuG%tm78*~&&hs7xxmF+mUAgDs#p4_RP<->k6Zpv*n!3O z7Nug^_!U@UvHTMBZcH$l_^L8nT8I=uyCLf}^SJj_-^FYoLgvDb#zRE%Z z2MGdZ3Kx<%Q(t2A{pfx`?ETg88lrV$H{yb^;;2J4L(FRu zBI#v6Ad2bwZ#mUMCW?%eAKxb|?9!{?zjWV*TXHlWXwg(4jd4?p8;c|E4pCHb#NkGL z&$FF&Z9Rb7C+<<|6l%+65ym;eYiBk_xAv(Ug1Og+LHU}u`{H=h{ThG1|6Bo$j(P~L z$VR+U^p8_9&TU9)8kNCsxLg=$Y+Jr>_@l%_w>Ne7=2pY0d;R_vp7p#zJQ6iD0Gk`V z6lkZ{>R9N$=9Y;de7c?dz?wFU#GBk^Agz5>Tg{=@uzI*Y`ez~FJ*E0~`pb?=t!J*y zFf^J}+)UO^M)RT&X(ysgP9D6^AizmW!tzP48g`7_&L=o5IqD<%KP#E{LgV>ROMZiD z?g^r4PZf{q8u3h$#i@CZwyn0(LzO-v?P!$!&(C=)w~u=`Yu?EYR8_r*=!h=fWhxBD zM&Es{Fgdd_ll#!;h#4hfQWc+oYJ>8qC2rL=5Ep+j9jjBqORRL53cSYkB-bNN*z0LY z?^t{n`~CId;ul*QjMaBFKRs||kt>OdEZv)`0PeVwr=a>aeCl`V%l(v+H}XaKa(h*7?5gy(7)jxvM|)0`yF%VZYx4cmW1fMw%ZVK@9B^?a|e zq)&9oqHoeIfzQ6u%VI7(S_;T&F6`z-$m2L)x7R7rEF4a0<8UouCidoDC^<8F0#zYa zW@aX>fHeagVlP?@+Y;?f6`4snlFcP2r{p#1Jv(Ty$$B<+zIoK=y;L<^CjW5%nFh1p z+1*C{hRLa7Kel-|9#ab#b{W{rOZoV>B7Cp+`QFL^m;?zwz=eFZ$QLS7zql1n z%4elt>MTm(9Gax3TDX~I_fCI%teL4HCL%&cww@wYOfp;O$EV$jK6R zW#=SgT_FdvQ5r+>!$E9Mk6oi_h$V%<3u9#!Cq#ZDU2W<9wlTaFwRnkr1dn)1)svjd z>Dru4iij1hu$!F>)6Hrmj7$|@nu?DZsO$tXjZA|N-OD8x2c<~Zr3Q1P&sIJ0cEi4) z(+lrgs43!#x^1WN5sy*WrHW*3ejYUfd`2Mp^?}{0^lH=Ch^|1>PV>1=*U4qA+g1RO z*yZ`07zoh$-#FWXjr0XK zADefQcO-M@8dDp$}*MFpTr*$9pkz*_>v`L&m$LHh;AR{)6%aGO#xlKUW4C z1$e)aoyzCjAf0sFC$wiLgjzDt7BAyL`;q(X1)ePvdKxa^QGL;h>+*2t_=>mQ`RiLx z$X)Yvy9zR-Of8U-bro|ZpZ&yg4*CMWrxt*n;?71~Gb$~KKE((%tUV414K+WU_BuY@ z_OCaV@?0gmf-_1NIP;^{TWYsQlgs*3MY9}}NAvW{uZgEKn-lVw?{*hz%_UBsEyUg* zFZbYevi;cx;oF)%5hA3%7tkDhZGJf!EL**fY*%%YPZb@nbQH@`_GJ{iI3rSm>*sJt z>l$smQU1_yfG)hFmE&5LNWUX4|1;~PXi&|~$oD`;4>otIT(K>Z=P<#)jD}SCVcQj$ zX`Fi>41;{+&-Reum4>3Y6KMqxtR*zwy)l!wa62#1 ziEC`lkKWmFuE2RpT?Ic>08`$zkXXvzPa9@wH$@|TM+$Y?izR$qsaN{jEdazMvz17q zPh{JB_9d04i;R@H8P_L00Oo0fsLD1%PU?M#sJ{k1hBGQA1}UoHto5B0#H^%^O}+Sh zQjTci+`fOPc%qe}z?_Y^R;U1rH`g7r@BgN&^4@6s=#vb#_bEfk6!$wyX@Jbu5EC8U z8X$A4cg1tnEb>;nA^%!!xMqFo2ROWLJHW&XQQPH^*UoHMl~`3)TdHU&emcBhN6PZV zZ*9Q0TE|xf$$2a1HFIb^x!2A}_hNv_E0yKnI`>2_pzil)psK`@mS*=71OaF^J_r)G zDFu(YTd7SCXy3J}^kA|AdK39vC2g{n%^7NPu%y*v7;A5)mf&^6BwKf}jUc?_6uF>D z$axgm@Ma=Z&EGwNA9jB}_E*QzEDbL6}ozpnq|z%=Ma4(uhx)8AM?(2W_NBl+@=j?RnUHI5gk zsBc=sf36+YZM7O7N!T`G)VZxaPa2Iow&@3DStU>XD6#=h?>EU=UxreHm3?t5>_*y< zU{$#(v8|2}#gdG3idf@3@0=qfm~hC%pO}@EOFLossiT{T1bKtQ3E{q=vIqPW<5@A9 zBmdDp_c^K+K=ho76-yd<=a;REdaE@hm)@R?4oPZWR-j4g{rOD?YdZiF`ntdEKf(?P zB7`vTkL7JqRrDQ>aqA|tbOp8dJW#^Dg%Z7Yk22l2RFm1;9APz~Pe z?Gou_amT8rKgs}#4|*3j>DKJ8YQu&mVF+5_RJGXn-h4-=ETqbbl?s2DK0jS^ymCQp zsL{_(F&_B|(fDF^R-^hbsqlfcu~35==VpQwjOxn|(Q>d~EjS1>-KO17J7{a$v`o1m zm1SoL_wB2Y?!|nVo4~*GhQFki5_RbiDAq3XVVTy`9Q3b%p)%j z_k%{w&rdU7KNa0p96~^dkKises%fGxB&E!{TBD--J8qG z>FX-7QhO{HCmuqF8#CZ;Egle#T9tpB(U8<7TFi=EHBCG;#@KtI@B>2D|5-DDln;K| z74QTqnIFfK_UhHntm){)nd9R)-d-Ut-};NFOT^`PB-aP70+-sI9&qwYt8?xh6h&;( zfc_ng`h=6$*v3L`1-ez0TrQS-gQfYe|H4`A_HO&K-tw6 zubLHnE+5#s>?Jza71`cdld%<`U8QB`w&PX$>=g3u_T~?O^87G4vnhU9J);Hq#=Q3w$h7w9*znI7p zX?*53T5@u2rnWvHH{Ic?mtnUKTI>oIKtQZGy8B2fs*y2MeT`^EW{X01^L1{DQrqS! zQKSl6P{cxqS?eUY)al^qqxy9&aAN;;NGUH$1a^XUG4eOQ1?V;ZYMGr*VS*#$gCA3B zlKTv(@aM>gi5pxP&g;W>m6P^vqD*b#@9F>y_sbsW-*cY7ial+L{q2aG&{PQ>f9oyZgM zaQmBrO<~M}iM(4fyoda`se5SGRp2583JPn5pY#1wQ%%-Y=9?Y}&dpup3a1~}=eX`< z2j8^hY=|@+`2_Rxp5L_*Yc;Q0IIJwnxa4Yld{D3hiD?30gdh2arid{9nuES1H_zQA zbI?BpBYr0pchYh_`QaP>wb=|w$YgKLV7h*$l)w4hJ1a@Il)Gsi$E)SMzTK{d zqT^XpPf|^8iV})>To9cn(rt6o80+S)m9Hcg2p;%n`Zd%gV6@N9S$+j0FvX`Ev0X88 z7ane^FrJU<8T%Y4Kpgem)F3*tniial_AO|iO4utD6+4~zj=$*7OMidt_YIpo(`iH@ z1B6KvNu%7x$tA6kl`&E!*1cr3wElb3-+3~nq=bKh_} z$PIBG5FZ}he9G(>$!#~*RwnP#yTY|<2|&2|OHpRB%kT%NHhQI*t3*E-eG+BRy4`m~5F!$@UA_m*7AQ7cUE?`SX(A-rgmJ~o7pam0p_ zgpG?k7UVc8uKd9JR(I)Cf7CyfAo}9l%^`+*KC60o(Z!1Sv#)F0@MoiS+A1toum;?_ z{Pv@cP4TAw$T$)PQ3pG~n-}VXr!L~vYKpf3@Yu?GcWT=ZxVXS&Mb+5VWU|2~eWGeD z*VH%Wk|LZ!qsvA0wobs@z!=WR z)Aqtf!x33Vr}td5vd#D`!{(bqJ#`%%v#l!6q~*Y;w`)?JAztYsh6{Gt?U8ihH&2DXAzUCBNwL)1V{peO1qO(Y27}pCs9mqJ93A~==?59hS9Ej>4 z0y!}xL9c1~2NylgYA80D{HeEncL)le<-Zp9+DjPd9t=I2aSJywmrhR`^yQ~ljIv3C zTrUUUq;E@ggv7)G2M2ulL&VYaiQV4Q`90!OwU@Q;4pAFQHGVtrI=5XfMf%TQR=WJZ zIs~fCSXVu24_7=2kyM!A|J<TT_G>SphoZ>4hzZv)K4n=-Ztj@WYpY1yeKi@ic6NV zIZcOD8kEYnRijXOkK+1Ll<0#hB_@Z|`NtD%JR`Y_<;a0>sLSLJNCMHR8ap2MbV2ty z#X{*ko)jMIY}i87E9u@jBbpF14A~j&wUgY-f_mq$8VlQ|}JKe?vK^JEy;v+#$MFvuSkM(EFhzomw2oWSd`sJ*>(#8 zTg`_g%%nJ#$^`BGV~odcSwy4=XA=3nnhc~DPaugTsB=;s7EkXJb317!bbqom%k)k+ zjf=FuUt6};jjxbkpoS~f8`mAd%sN^uUR6g9{?U)h>J>1y6@+1o>9vSEK+kj19i6{A2vpaFomUVGhXwS zB~LlbSyF5?2B$dx9n{O%F;8zgP&k*~@G#=;5%Ld7!$pNG2ooO$mvMNFbwQPi1k-j=!yma{j!N* z30bblPjm`r9PDxmUZ;VWh#otOcgNV9gb+%yOPpi`k9i*9f6!F}D9v*J<}T{r0*aupU%aR; zKg4sbAHZ?D&Ph2tB2C zo$KxfB!w6UyTei@(zsREh;#f(?P@Ap%R+jGuR~$#_E2bPr5Lq9&z7ig5nRSu@ zY*TYsM5eCG-C+Xx$d~8cK#VCTmd;a-Bu?hueo7+6w*z|)XEIbFwiLrM&@VM{?2l-F zV*zFQepa4XznUBPWSH*%>vgkG0_IdeL;g^fyvvg;3*-VCNg{oMe|G*;O{%SJYH(kW zelKJ~Ju8D4ri#Tytt58rT)K}m>&Pz`Ts>%Cx2o>NVMWD}P3cSl)S$|_!&3sS%CrNo zYb<5(1e<>+{=l-|L}Q6l{W0h7OjqlDa<$&SIGt<-9RSI4b^xdV{AhaPp!Nmr3&DOQ z4mUaPn6hge@SfX5D3SH*TgiP^C82z=EgO-`=!sOqLtUV%_Lv10t~!q*WGC?i(m39O ztV^~dx1ZE>iU0b#W1WEL-)!c8sX9P5ukj24_PF6!i0#@=cHp`>tH>5Kd@}yd5g?r3 zoc6meiywebHFU>#JeuB4M5EERBZzcm9v7QhWT)4VXG3C3y9l#0J7raTC_=15I0FIi z+CX)(UEpsZldv@E`iW$5UP%My=s(Ti@Lx{)cRs8Q{68-vL;8>qx|;8YOu2IU{7fNt z+~~7($XXC;vLWHsZ-sh4qb(2kB$RB}c6wkW^?Qmz`6iEG#0yMB3PY15<0D_tW62cS zENnOtJX#y+@+~jN$Bc;D81R!=LbCQ^i4Cs;&6_X`kN^D^fGMHj#YRsm&=uZr4isfm z{t^V4zv-GiCy^>-XI^(iYQJWw(M|&Q6;_F2VQ=+eCu0zebu477xL>*t{atHGyag(` z!L%?lj22iGp5bpA^ObQmSy9if{hO8S?1cjw{sQ@#|BGPAhX3Oi4|9H{%lGVvo0WW! z*<@3hlCmdV+*i`3BEu7trkncZd?tNLp2_6;RM~dXfqzv>;LT!M;l_JJ9%qRF4Yu}! zG@&dz(R$idPIW3G|v)N3U+ofE2R;~`OOdQd3>GUzY-I#pgBVIEcpB)~N zrCoWNB`Qle0_)#`{9b+F?^%EpPQbm>{c`WEGSKRIQT)=y`or@Yx$qnFHyp-~tNQaX zf(_gn?vsbIP3P%%MB^ogBoA$huD8`PlH_U3ZWfb-gyPqJdzs&aLFU#UgTxCnn+Q|a z8UqvkCfW~sVe#TOV}u~Ka0LHBxn2cfoc_xKuRgNaT+#J!UFki`eDVb{I3MV*$~{lL zcU?9wKu$J`{(};`T0GLoGmpM~ukOcw3}{<;i*7}>Sv;b;Hj+Z>b!Dc$1m}$#xUj8C z^B%b8H~;&XN@J%Y_-_Hi{sKY(iB%BW=6vTPN4T^e3f+WohjwR zsy*Vdsuvn5*4BoJaMY#J$s~F1YM*SRySayeg=Bv%0h6S_sUHAg0tkPkAMgK<}hD5d#aG}n`AQhKzvx_G5dSk zHn8V*)BgWEzxXN#pdyN0(ec^iWT5Q@_b*kzg-$G{2}-!MDXTtJM7#**Yv&=YKORP0 z#bO>xZnI9I);4kSOjW0gwE=%USZiQ{Fjc9#4^v}TW=A&vdb;6eWK4zmW+hq0Qm6ke zo;hNWgC&UUqE6NiWvTTaOTCKHfdS-Z3co$plPW+OWF&xFX~Z2ic3GDL6FH0ZkgFN7 z0s&^SOO>rctt|BrqNPao`7Xm7rFKv5T$`qk^eJ{R+X>DbM@JG>^yoF?=fy|zvIt`8 z9Vlc?1`#Tc?<`)XK($UAFJ=OB;rQ|wokrDO&RCKscX9lD6Inw1Zdmxl_}#cl=uF%M zAOBL6c*B5|e-Fa`olvmYT(xV1e`zme$Y68C_>KGII+oNd<~w6O#n!zbMmD)f-6_8KyRsq&T> zEh)ZQrOPdy9rIq0ZO|uSzxDr27qYXE$UpObz86(0{st>ves(YoJArS57*$*N&>KE+ z*EqkEaUlD2lkvx9C5dNd-;k}xeuHPDm5(?gOM(MM8zJRC<)`1#68G<|XTir2#`#0X zcw-La2u#1&s0%XCUoly9`o@;yw&ueEdVC_l4y!fFGAP>jYTj@T7onG@${v!B*LMa= zks7GUpH=omG%KYFQ|A+EkL;++%n-V}!tOJO<%DIZ>T~vYoOnHz{8yl^b_)R=EWEnd z-?!ymQTN=MX8C$LU`&f}W3RldZ->Zub)>ASwk3|=agf;K9Nj)AKpvh!nU5}GHajnx zo4?-@rlXG-DmarK=5vs0#Pd&|jE!4ST^NX~+gVxOOc=LkABEI+4>62U;+tW*r~VTq z{^gwUW5^?+9xPW|I1GY;%O-d(+?7i|PqNjb4R==J$kcg%DBjnO*X}<`?;&=W+HSg4 zqSt&)?ZsP&;G-`Nt?zT$zB)!S7KUMDGsVb#zQOf?+)Az?ttnUP^yIGrv>Ixth}p8T z)-@*0WeMNjegR;1F2B#k_?@&vuSor*$C;NSgM<$QV*8v5Ryf<*oR{6PNUv~$f3KL4M}3{ntf$Nm3+ zVEimi1^+UzL~1gPhi8fkzewvKvU$OMb{zIc`4wLb*JfK|ursTzz7pm38(>1Yxx;76 z7o7t+vpv=r$r=;mZuZt8eT7HbvzUMX}mM5f=Z^DQ|qh>RYl58gAv zDaP_GwslWWsgEyz5>axCv!= z6;9PpMk+rzF}T# z;UxWS_}${jjzrqR#=GiKmm|9~&+5+h2R1_%$}PC?Uy3T=umWcX?6!6=Yg9Flz+FST z$MUq84@N-eKP>IACZ*!QilTwwA$CbL9Z8(BWgI zHvKWW6mgg9z)(U*rd+u5&a4cBB!pHWmZ^Prq4VW(FC9oh0U$7!11!`~!C^JY)mfcX z3mx@)YOJfX!JVW9GfkBi+y7qh-x5wzjtCJtyT0>~bbI z0}y^_Omx&;v3?rc6|#{z91roVtXrAE1gg11I5e&%c=TCWS;{+e!}(gmUVH4~ND8hy z8sZEI6E*kuLKQrNNkuW{+zY=x7R8R;-QDHL$gF+4ELDXj$;M_v7p1v|%5mM|O+gZMPw`JNAm=VxY2e~(318)1p_{!=Oe8Z|2f;OP4{0-Qi{08-gG z<&XdaT_lTTjRRopGnN}}gvnnQ(SyAoD|)jE!}p~jCU@m60SD&Ql6AW zbTrA?!AD29xM=yoOwDH}TEdlzLnNae7r9T&gxjcp7ZuTIfoK7?Pj7u%T8Z+6=lbS* ztBc;Lvk5)-3Z1WJnD6gvU9;->C=*59=X~nu*tfX9yTDm*OeZsgE1zLFabMvwbspdyK2NPdk4q=L387X3BO+8igRn(uGt>QqUC`GH zXnLaNHl5l|v6)(aHmhYB^cgruiE8?~A$bL>2qg8U*7&@3m*%Rs=W=&CKRUj6hNxb% z(UN?zo}2?YO7j(VJ%$oSf|52Xkg!fAIfW!%zw%p*Q4Kyv3!ez{K4se_>GXphTO6e| zzb%uZDxK9Bp%eUVhqet&S_I%n`AepR}&c*ZAFYf_*( z{{IQC7=u8R#diXd2@>_+01KIZ=cNq(`3cGGJx(^T=VWs@Fui6wjEJ^;%kwZsteBv6 zm?T24L15IO%KjY&pUAl@&I4z~J9cg^=4S+nf9;_8Cb@s1^me&=28xJ?l_ zt!~?gEZu*XszA3e?5Q1tQamgQ?t7Y?+?V@`#VM~uec*ulU`cd;DR-weh+I#^wh`+ZH8Wd9O3fr2L!Re&=ck%#6^^N`77;h$(F z-XV=f|5zMayT_3M9tt1tk%{^nW@A$1>W$6||okuiD1&yI9f%XcD{$k}iUh zr)NqoE$?nCUFmdl9XGr)o3yGG=Y?Ce zDQ>pH^;Sc$a{HGx@N7P3dJ`wtP8l#p*F}Z>h?9)6PwV^CX+y3hc@igY!Dw-4Z8ZJD z%B_{13-D=a4_#QK8#3GZq#OUP9uD)Dsn_{Ru#U=^@VUyG5t>x05MZjxw_zF>{B=aZ z__4m|SIyPu-f-WZ$;ATwkW^53{MZ7$;Z?BpqW<(k;n=A)HV1OHI957;9;;%)>vwVB z@tRhQ06YBkBXBGw)#g-8adzw&IQDFTYv3j-20BU}|DEtV}LRe^2~E==^DhU$AWM_6S{|h;jC883Y!i5A&b3J%LsI!g1jnW(e%9*V9m?K-GM;|>Yv3O%wS3CH$; zzpU%Jtn@0^&o#QID?{6G9!EHAi=FDeIa%akj;!3YW?YTp$5l#}*3{FZt=#-6LwnGU zEq2H(65(Tr2qBkXY7M?mUuaG^>~Z^`eN%c&!NA-iHe4h372UY3nIyKXS{Ra-zlu<@ z`r%Hy`%H!IEilOB3)-Dgvk`teOF_wNJtLNO&bMMw3Jmk<4BqWvptxC}T1-ep-5Xo) zRdl*VLj|D~hukA!6b<_M`Ds3ye(+*~N7!xsTfvL-)3ZXIWABlh?=_n%VeS{fZ!I|% zY=0WYu3?Onr`9eg2zZ$mJJ6jK7JCf%1!o{`bcu>C^i&`l6@t-jFAbAF4uZW7;Nj_> z^gq^U!BY_Z9kL5cZ zC>O3#>#7J)NN=I9Ggve_Wtt#I{bL(Wj|v$wVlRO)LKO2OhD`HJe61Jlr|DNjx||fE ztHNAFHX!x?0Q=H`_b32d6FBh%C|q6B!9RiH7=V)#g2~#Gt-5SvS=OlNdB@$Q0kRy&|jx0&QRqZ5~Jk< z@p@qtw_e$-BWi5~d!45(rD|mk7|;gzlxe$(!UxKPNI|!s%{aM3wV1BS-lkAOtknut zTq4Rr?)_&<9NE-&dq#2wU;w$ye6C)^cBtU?#j$0f*Xd_^uv6F)H+(-o{*@fw6IZg< z@)GstAP%3s#k=LIHNw8=ZxiA!%iqRWWpQm$Cjd3hz8lL|gA2^J*pobf6BCmz67@a7 zh4y?I3$n8$T7NWhP1*R2&cxdV*TfrX%pmD@P0pr17VlJ5K8j~y=Q!^~*p=r7RUx%3 zv^A%3)rjt*(A(FS-qb@t@_e4V`W+Sx`r%EKA=QkpLNd`_<%^N)(Zx00?>!(n5(ZTc zVJ=+Ke9sp>wTlc0z>T6z#$pmt?cf1WBZOSJCG->|V9>P6wf$nE}Zw=9#MBV4wlviU2IYK62INL^8NA%8l zM|y+Grfzd6>~bcQmG_{%$xq_Jt5xag(Py|^H*VCsxpJ^JN5CaSvrmZz%%`EvT=?-& zIfWRr4XDiOXg>B9>24J8K)SoT8@@eC zjNkkHhX?M>?#|B6JUjEu`l(nGC}i1<(x!t?AaiE{k%H?%)?}U<$eN@_if+Fg6YF9` z=cBZVK)Iv$b9?rihpfZNtiBiA97o6O>Lqlq>Lm`Om)tMAO-MoJj^|P>QZiS>jQso^ z*G*Q&4VioBR056K9eOzm;$wYpc}4Zq2fz0}7^ zbjHT^#EEjLulk+)Ux6~>_74TpvyXIkCunCEx{`vW?5&nW&)C^fN7^Owuc#MHm$W!1 z?hY#zKc#Wrp2WPJzjg`a+Mw=r}=M?|c7lFDuFVE_ad?QZHb`3YqFH0t% z63}Y1PoJbW&{DuWzq01?d168jlpZ2K5ep~{Gbt|rS%q8gF|~v4%WSI$T1kKc!ND8* zoA1>ni-mUWbt258Yd+;*uFdP7`C0{PG+8G;BkLvhSSm-+hi_Fs$k)RLn@@o6fTcA& zl&;@!m&98fqSS}N@Hn4*swMsvn(k#!DHp2#{d8ide7@&ZCjK=ACjQmL>-}!wWbz_D zF?J13(foYvs^iVUa|PS@B*{iGlyJ&x5~@g& zbK;bPebAu-_41|Kq#BsVueHA{pfWSHR>Rdv;$xhLJ4U|&^9tIY;|m_whv;X`?mwm< zD-LvBkjZ)0H;VG3x)UUb8){8r^bb^EGHsYjwE|ia^g50Xv;9vpat<_Ts}5dK%dZG` z^6#@7QcS@vxVkH-POTej zh=M6xVAa1~yrX|L+qm14ra~-6qFH5Ov^^;b_9{1a5ifVnGdz7WK(OWM6zlv*va@h0 z+gB>iU5`OpvGCH$4s7CRL0ymuXCG@j;Oi9K|8O#NoJX_QW_odUiv2?D)EGd=)_a`d z5|-6{FJ_KI)}MSgNE0bx|Cwho-r|uM#-&JHP0E|C*iV{n-D`i{S@&XJWC@8k*DPGG)he4iIB4S2@P&3iNP;c+e`^fR?o0RpK32C?+KnX7L|V$0F}89DY1I}O;sak z>l=AN@77sqkn$x4a+eU{rJXEr7eK|SXGg~foRgKLIeozqzk(QE>{>wOh_bcJ1jvei zs28@7r}Pg=h-%1UOR$9FczbwxT{Tj^R)F(TDR1O+MbM5P+vwsq_SY6$1~=?*u?R9# z6~8;xpCWgYp|-&`{dyRoxb++$MTe=eW{A{Su~7u5A#ng|+-N!rXljs$cmrPCw@ru* z5@T27E5RMq-@0cqJvP+(wKw0$`U+QTWmwnZ51Tf=$3E^HN>G8^#dXcHeNV1NXDrWNUlv-bnDmImVSVoqS;np9j**qaPyxR@6BY|^d~)+M#4)=aVmO)RtwPYU#)@ypGkpf4p|jS}21efs)5?_QS=s4Bl|^w@S33)>B! zV}hQN?iD2t$Tu?oVrOs#9*jw9T$VF3Fjz5VFoi^EKR{`^Iiq<#&0=04AM!F4D40If}}MjQcpi#;`#?$ z8~3Va;}x6y-HF;(oZvactm`W5ozL_)o)NjAtyxLA#XqJ)3Nlx_t-bdUuH!fGP~}_f zaQJQGS1$Wqp5wfH)BGR32kPJgx3<$v93wi_qlN8~ch?LY_j~n|)i$qY@9eSN#i!E$ zzA`juO#mrb{aL+gfG7nBHrcOyl3yFnq!6#scWDwNai>pi&qvWO-{H%_uMa+7va?a7 zzmJ=&QuMG@*(*G}Z-Rf%7b;N6sqQie%5x0&hZUQlyQIB5Mf?sJ)`pu6E#3K>q>fun z4h-*CHqWrE%^H2@!`sQ9bYP3kn+j2P*+TA5QA6*|v6nD0BBrD??%JEm4B+<~c*&C` zUs4RRzKg53*!v;t>!ER74t1!kA5EMiR8vL0ajxDycg>@m;C)oUFtyy@2RuX*+{j40YH5Qn)C;6civ#Y{Pa_$ z#R?ANWTAsP;kG6!a6ktpc1iDV*-x5N|g?lPv zAR+?rs&Bb)fVEJICS)V{5ceFK|P+7x~FN2oH-{wSiEzH!_^8jk1ixe85&_hNDofc3hiujVUcn($h#wm|=Dkm$kp1bj=GIKKX zJW;DNI=h`}bx)7Li^-lwb)>gU3J@hdJ)Qt01WI;Gzx`V;V2LJ9R`tPcrRLucQ%(N6DOgh(+WR3953BBfI-oXpJpOK zf8{Tn_(R~uYb2NGXAWvRx^GWZWA2s{Q8Lw%GH)1Pktxx- zJR~O>(``rwU^aJxtUB&5qa?W_(s$F~lFJ+?eshdE>5t|w zX^D6?GiLMT32{yoki`+Uf^7T9&X3O*xOSfkCp`QN3T6hW$sZ38d?R*jGQgSTRVg*)Rhi+$;)gl(kZsPN4IdWz1 zC;CKxL4htYt>>9i%sSE`5Qu#-95ifJ0GaNRn|SSfK6BzBo;fS=U5Z;;uDm2QjWPem<;Y&^ z)=!VE|K6%BMLJ{H6%V#EBgY*jqefOtF(#b8Bh58q9RExz3j;jo%Qu$4h-oXx8LsPJ z2^u5ZXGYX7DC5sN|7BIRCJ(@g+q@USNJ=lkdPkq5FaThBa`lCH!&#D0&> z9M34>_=V9Y zmJwaFMbhaf{sE(+a6f0KAqNT4m)G7O-Ur-hA`i-XU)s;IdzX?(lWBumq3(xh+1$rA zzPP?+CE52=Nk2B}-jj)&b`|lOl-zBVRngB0Ye&nTRcyk`nk{iL<|<~utR!Dk2?lve zey{Pgvs`%@i^svUQW+NK3NcX}ymFw%Z=afm3CG*_fqJOfmxQhLo%wq!j)NR4vkp|A zcy=fRU-goUxq7)A7tUpadZ*Fg=LF2e9;Vu>H{Da5{Js8qB7z(y#{~|fi@w%bmxmst z9s4--uc#vu=B2|IO`%tZs8;tm5L?bFWOnC1w4F>1hQm=Tx2~ zHHX?l=_eW0R(j+*jty?C!I$hXY_|#i*pm18;to&WcQFOTLC#}w36qOo+lYdxnkEdC zEkT{j*7~h!7V9NKs6E&ynJ_djhtfpjcg%2D72>ioDX|W&!U@Sp<4_3aK8@VcCfZ!q z5rTLA^6nPmkh*`T-2REcLW~dCuW*H17Wab*^DSb|(^v~lC%ujv6T9B7i7g*E1#`xY zy=>PDQYw95^jJ*ip_O@PWgpXcz6^QUR`T}h?LR_!m%462c$pRFbIw=MiM-wf`!|aX zBM)?TlqiMV3`rQ2v)^#TR!2}JOG|;Zo$lbG?|ZaBQLK`6F}(&#Bd~S-?2js18fN50 zeJ74v7?^wfo|;ekYN9{Jy+$QqvJDDZ$vZ0*)^GSDXb%0_=|rHTw3s6fd%~Nb80KaF zR4O7D?G+}?OLmc`=h10|$&KZhf?T~!zM^2x8(9ven&BI6@t#ZK@cP&wE@YV;BGfSp z(IoL;kY^xDmnNf>E%RFEqzk!K1YU54zqJ$Z>|OD=v3O2cD`#3(^`RCZ2;ZwSF_M1u6_K0_P>6G^?N6F+Zv`+kR5r9D55-ADeF2*j_K%7;kslSS^SM;h!d3 zp(;M(GT&xW^aaO_HA!dQTnntTYQ|t<@n&kdWSFzWag4M zeyJdKWC-i`oP>KNK|E1;uof!~u$A_TNE&5YE$Rg^sLks?W7YaUb%>i*?9ruD*86ihIL(hTFLe+J1xdv`=?LQ=bwF$ zrz5`-eiMzr+7LE7Jc#{w!UHmODKlaVvADqNypf5O%F#<(gU)k$pIW=kMa58fLA~PhBtvo@+Z(>DAai6;DD_|8?rhxpj#fYQ zRHSMMzw&DC;=~&?d3s%Rn%835et!3HMr_@>6x*vxIX$1ug=vT6%ssB(H%32iY)j=^ zyie%svVnD;hf+=c!Coyx`V` zrDRmhrTcSwZ-;9d@Q<|dIx_R0Q>Eqy+Q`(O$as>QNV6{$Hx0L8xDe!xQ}toOtbkb1 zFLSt8KvSuO|BN6MtLmk*v>Z!>20GPBH}h`DPWF9M)WK-uH`gbG{ccIu+m*$3nNia; z`EAo95~^tu)3}FW`<3R^wi!93d}RM|uN(*nDFsa2Ir9f;e?%U7{_&5Nkr(?%iltl& zHH|0WHt`RckaO5y)fD*+%cKdTEWOB!>R%;WKFJHPo@`>ISty)+802VuuFj-lFgi_c2#`rK)vYOl|!HGwhwDo20&7wWuO=d1bPBI3M zQf~k}hv1Dq@>0+WLGES^HDb4Zr4eSEnmFwJ_;8=l()QY!$N`Z^88QWYq8?RxN=5m5 zmGz*;-~qDFFvv2hBjnuUHbkdMd~2@NDnoy{T-96F=xaE+C)C+|qrl;!egLxf8ME|T zhr2Xjj=QjP^5lHHLdZBz?}FT7uLCa$)LDRfPsMNy7eQ>~)t}HCqBS^?`Bk0^gby5C zEf$%x&Wn1WT*bXQ^!*+)Pz^VCBKP1X<{oYgEQ{O+$*yE}@~WbkH|Hp+UfPrdR8TyL z*cj3E;@Ml;c>=a2cRBgbZzp6xnV~<{At737Evc#L!?ZLxMA`Kjh@2eKp2H;kMJ?5w zaO=bL4O<9Txk%M*XDfAjj(M~AD38@pB0Q%2FJBz6zMpS`iYYbkY;{;Hy7Sg}4*IGO z57_mxR{I3F;4B*IOm|HwXGgxKKg#@ns^%*8Z>z4%ElgzaP#t!V76uAlK)uBi4r=R- z<+oy0x9^c!Mg>mgFZveA zo%mK*FGq8Pk!<{>O2xu61hF(DdM&*U-m89FjZ!<5o?p!t+@-;uJUNRf#sY-%-xRzP z^BTL~mE7XNdI0#z6Y0=t%3t^SCkUC)XV2gYDKW?-1`@wm%9V$?h1h~;Jz`Ij9HlbM z-heDx@(p`$3KCysW!T&m5NknvTvxZ4 zj3?Z}Naa=4D-tAzPBV1YwWub=cp4aI(}!5aTMb+Fm|l3`*d|_$7+b!K$j9_?$G))R zx;j!3)a;|Q=M-;c%5h!ZCS&kA-vRZYAoiDw|8+_s*1(3XTOL>nQjhp$e*}f`QoOBD zf-v*#c=CWMesZQ41KFO~Tx7Zx6|%w$lkmkcIU-GWK{Yc7CY7NC+dioHdO2bdefL>T z!beh-HSK;{LW$nSIPKq^A_mh)BpdK#bfOc~6X*bbzl&J^iv+^w3P^&K2|c`JguH)( zgMYslt*5`krvr_Q(xKHqfJ3>=OV|!w@ReCS%9GIL;s?BkAkFv64H64gVGv4 zte`tE?C5;0EyZl{%Q^y7FTHOXyxdRVjNr2DGXBno`<|?$xZiD?j>nAdu2HfM!3tqrjhIMFc5h`m6yl- z5<6L^9>iVpvawZ;T%51X%q{Q>yM3pDXO$&nuQ!X=*P(RmL~$Xh!~?D;|2p)iMtDHH z<*y995AU$Tbt#Ttw0$CSDVu<@Or6E*QV2mTbBOn}e&v%NxY4!5Iu(_xJ zaPBR%Cz$K;&itzp#7W;Ih%{&kVZwRf>2YBZsl;3b;WEmVGkf+4#)!68rbbz4h0V=w zOjl=PH=hLgG@6ur;#|=c;A^{mP94ws z2QQAf<2wX%*{|ZQd-r6gQgPv191N796AOIs!l$YRPv4qDBZ^;~E7rNpN3h;8yO7dL zf4I52(#^FCn4?0t4e-?*cBK%cQfG;Yr7oT@M2`p~CHjmoc9JD zT$a%O#^rH-Y$BBmyqH9&SNC~*m!g(ugwa};KI7(@%8!aBPZ)@;cb7NC=TmI?c-5HT zSEUgfh4ir^-&x9+gY8<4t>h5PAbam$YU{GAu~~?~d;KE`$flw|YI& zG4?sL)*og7;U!R$q1oabS|o@x2$oE25W5nO?R1KPKkJTV)-%)XMG35U#Yh>>d!+Y7 zN6#*T+7seB2V4P)iSlrYp$D0gF*px%<<;e@>K0hyQ3%;|;dk;fwA1ZJ-z$}jkrmz^ zxI@lwe@@+bH7nZ5u2#mPDXxf>q(4o?&nVobY737{GE{eQksdbq9b+|ptvd~o&}ADf zD@Oj%4R`mcM;w0pk;PWMQX2jDWoljooEy9Y*+IBEf(@C-0;E9`Z#I=EJmLjV17$Xr zN57z=Uj{&e^qXr#z)l;F;9|~XTf(m1XHpw$-nNm%?ThqF6WB+y{)~7&cmv?94+HZh z`>ZA(R~m8*WWZz0SG~UanVe2T+v1P&&3NdzB$Xy5Ce^vv!KN!6td>sE#0t)f33);F zLaL{&>!GS-$hrC*KY0Zb1AE(j17UA<=9I43rHn`LONjo&68tNL)9zrsd-w&)>VP*MZf}QA`w(Z@G?A^MO})DLT41=<|1yYoUM> zc4pEfS~V{Nk!E+P-!xNyp6S&^&NA70L^2g1;NU>^|2r=f5%i0XW8G;vo`CWZL(cjGN}ih)J5+W;zpxAx=tU=D2QTc5?i1fND`(uoK~V}cvI~aMK=8}w&#m56*=NfjL*x~o}z31KUL-o=0WHUvT$b)+A{$lr;tjL zVxxK8O%P@NYJ*a&Otkx0x$X=@+Yc)g@|&k;do=Omn{*)<92ih$hnRc}n0#c8`cDHQ z?)}*iL`>6$0ud&xO;aUv#TxOgEidcm$xyO5%-}B&p}TNH9>zTke4SDQJ-Szr5x{XXj}UVj<2W{PsL*QNN3zJy@XH z83@|XRxR}-zvy3I3+9bjK`+|UbjB;kc>$*a#ew7oBRC$#+<47NVdm=pX!GY~z0RLU{I5rk#1JyA@ zM%URZ=ph7bOZ z`aW6*!|^Vh{$iHEa50A0eCY?<4^*6Ienjsc0L)fKHBX2cj;z(wJS6BTvk=$-)EK;7 zAK{U`FKfxk5B4ndfYl|C7_9^IKX*>@3$g?Y5i2(l>o(Bc69?7Y6j4RK@OxX%5q)I* z--uA(BhJ0<@s7`aShtou1F>acVFQ~YO{hV2vNDGXaw_r=KMz#R^H}8mm$hB@&?EypZ#4j+f`yzYSIicG+|5`1#x@)DGCx`Y>d1`48X2a)5 z_K4_6^oj>?n_a4VlujpH8=+k71kx(}A#f1D#O6NKqW4AH9t{{>E4anYgcKAh zM8#`&sbHO|I@QT5#JyAl?nY}`&}=cP{MQqQ`yJQeBVCrE-UX|favgEt4+sE&!?8_a zqS6_J9gkGmL>xC@o+lG6i7m&h9>!RT$Mp8v3#}KpTJvrdrmQ$Y6zcPQn22-u6)pD^ z-NB)^2R@>}!af1xOin?gE(# zl{*hg`oIA*t1@nQM=e?k_?Ixzy|FfM1dyGE3pUS7mf2`>Up^5D?6)e!wcu0~dW!|{ zD?phMLW*I~pwJ@)ZBCcb9{j;TIm-1vXrB=JLVeJ287SGj_kofzV?E%V?(_FS#+ujG zLeTX5I?PcJYpezG)1QDgb3}_AmygW%P|oFy>^%+reJ|Xd{urCu1J%YtLbeY2eV7AN zdcX)l_z7qOMJVEINutq&(Hz{L18{Hw?zKGqy)otAJY@rdh%Y}V(+8?_a(^zg`vO7? zi3|!Hv{)pEe|5S(Xo4372}JZ+^f)lzfcB*G5ju@dsS)mawhbe&;LLXdYEFkd)DSgBNe)27Y!1;+cF!yik~bWrA#+6w+l7#smdD;7Z7DcHo=-8&fj> zgv5JvAJ%?Gxy%$8c-tCbMVYb(!ugP~iq1D_?_pDH#J&B5jP@P0=!*u_zGq^kFZq$& zAJYPP7kbV4+?NV=mk*{kDbs4*5aa^UmHD<62g2^iBn1yf7md z!*k4XSo7CIeD1cZjs}c%U>rIPrPoT&{KI|dM6mP*Bm9;;J;HKg?*+;bpH~7`+R>?3 zx-af7&l`)1dVJJ>`yGpbK+J7tb8&}lc5#H5fEIAt7KD6yPK1gXAi6Gqy7)8j0qBRr zk=dWwXyS8wbair~b6$JJ#V!}F!Z0gnO8A6MQpnNKJt1|vnA?eC3y&O!0x5KB>O*PDj;|PXLr9S!8aUi}xd;a(q>c>KUb2B4q2-bsc~TS)7~wy6UN- zRPucV`x@6(7B$U<9vb(2N@yGw@2XV1R#JsD8eNlAW4$(`3?({(9gg${=t`gkdI#IF z)icin*E#y3;nW6(6Q3{S)0!PYu7_kQE<~@8C(8hlw7C+Dn)(SY-?&YcJ1#AkqWaC$ zxHnrwDm$Pw=%d(!l06WcZQWL*wEHjb*M_HB7G^Gok6|$`jY5KS-DMLRfkX7 z)Me46Z<`>c-n~*Z&hGxlNS5MP&=Msd^U?31snVsfo{#i}T2rw$4`L-HBw@?h(ABJI zj(CVCaZc8=!AzIQrFB-_KCO{D7y*d3WOr449}KIz=@#bqn@ENmxu4C_SEzBIaz^P@ z_DJ)NOWFmQ6Ymx8kn-_>hUTI@MOz zBJregFrj9#t;GkCTF-osP5!z%KFivqPM5<>H0Huj-y%t;-B6-ul%)};uO_fw<}Z=9 zf4#XdXn^QGc~uyJ&SGI&v>kl>SpT*NTbtD=Wd^I%A*SFH)oTM0C~a7 zq#rGEib2IN)=KxK*D4wm~1}294?RT%no_Pg2h00WDQ5)G zfqp2QU?pWLR7S@W*jl*{O2ycP0zoQOl;-m-fmXQaBdEsAh6O#snuMq7%fKDSUW_Na z0R3{d$>X}0I)k)CF!l0RY>B9nkSgD$@R|-Z4xam5fBJ?71!79Bk57ket^vSO0N(OK z&~KOqsqAyH;rlK~6h&IU&a;k*MTC+%<~xJRx{o0E=ZdFmTO~dd9Zl1brJDwgFp)PE zuEYqJk}0@rHIbvhGi%ocX>Ea`8uqMY+PMoG+3!aP5M%Emw-MLiS0oXOJ=qx81Gz45@fnMLt*fM@QRAM56zd!&~EZr-SdLxaxy$We2OA`ZWvke!r z69zm|xx`=2zsr7wu8@B%faKrx0|zGhgTn+F8Z)k!0bSE~}VS zhh>pP(T}b+(HP5&BATQ z+IJK51Zy1M-iEoPPjy>G4xLF^B_>s5j}HLl8R1B^Mk!%18%J>WMD^=Tb^hRAfR_}2 z9NuTiJJxuJs~I3eiXNUGh$QBP#W-xm2yk*PlhOHrpLQ`55LU6;UB~@6owh7*&?OT*btq zZ2jg|(QR_8>9&k}RgaYK!|)8=D{sxYY?L`P^y%yyy!7_Q3WnwZe$T3&Nm-&GdX!#! zg3@8AO}T-|58-fyhDHR1>{Mwne-NjMn)c0>S1zCyQZMCu(2$(Y1&MB(uSPFS+LJ{b zv|B&d_%dyr)$bDej#^5JzNg~Kep)?_qQav`KrC`(MEd;_8V$46xjbg1=(45JI^Rx{ zqL=G^p3h~Ctx)Id<=t~;b2_b} zz;|4$nk;zX$IeBiTP}2$9``n$Yfn{1g%y{e0T|e=*I04_r}ovTwdX4d<$0Z#fEf2_c7dgRo8fd7?d)8|t9JV7WQoDSK_#yB z8};F<7_mhhMDr{WUd(X%_I9Xvy`na#5A^SBR+G1ii%w$N*XTr<2*1y{wD__La0xut zZQ4usimF>txmlDXS$)Q(b46$!-SL#8H}VvD&F^X+9r+?OOb+?Phb+ds!i z2xn3F%D@M^7?ghQJo7_&hgYlF!V7`BjCj-IV zxQawXMjG&V8(;ew56MNpLc8S&0XFn#*9uMe&u#jod1y8)OMUrjP$n4zFS$=>9B^k*1NJ9{up^+)9fNM%D=e zBQJ=FAp(@Zt0;zVem_8n%{~bBk;onH7|uUc^VfJX)c@GAe~x#! zW5Nq`|4{W);{li9uO0jMcwooSY>vi0d5252PTenvNi}DkdfNDa_J*W z^B>~@*ySf1Q0Si@D|!AN@x!&!$P-KDM$TA@B2MT7rdvdD=k zI1-E-Ec>a0`n#A|s>o}{eK}A4$D_n#O0bLn5YrqGkR9>@9v$7&ViM+}%y6IBoL?(; z^osItF-L$HzuTWvV*Y)I4%h$K)CK-a%-TGZ8PL4^$cP+>@oP2Z25`QN|F!nueWC=3m0hnT;GqQ?Uti+MITLJR(zCvs$SossMXcqr*`dl z9TAwF@Ue;cNei^Y{*BTx?CSQL^8DJ)Kw9*jORlv+v`_yspkO5zl8{b)(;pN6$1)y_ z3t-GZ+NVgDsaeQ){{q_HLtS-$m2R994?sdCoP3}~aUl19k622C@d^ATvR!A#v*Rfq zt@9rvMnUYsr6nG4cKYCX`R}bhlChNNAvHC)WUy-Ms-=I&*Z&+53H{V+{x2Cp;DL-d zl@K)!>#)-%;qMU{er@^xGvYZcguz5v%1CcMr4RoY5#qK2?*Aalt8L%@*Q^l-1Ofq< z98utVzldLZ@vjk)aXyKG%WH*Uw1?-;9a%TOEBV+wXgIup|IJ-Mc^o;9;4kWT)2X@JF{~VvV|!Pyg8S z(oy1p(Em7_ja0yBZ-@jL9ltmo8w^Mr_80A6TIdEi;(uxTz!LUC{mIuIE)7Cc5ABBw z*r7p!JK3iI2*jw=(`X&Py$GN@q>e%*1k85GK0grJ>2|&J*R%1#${&2i5pFy{;O2@8 z`QYbM2HMV<&Wp^NK?~M$x65jpKs18r3DrLQzMo^bDJ~Mqme*?xpji95y}fbuc{3h1 z`|@+L*~Yu;_QcJ)aJKG*ZU@P%v}=ff7P|QaYEzEOzcu)_z3*q$_-Dp6DBJ41!-j-m zZ+1o8&bnZBSVrWo+FBlX(w;dqvood-bA``H(RVo_4i^jXr@H~t3u?xGIIeQm-!p|ntCr2RGPAt(GG&@Aw zBJhl~PrYX8D$F@|@9Wkmw)2D&y*%{@h}$o)>YP=TaQe9o|B${F8s!|<)P3xKe_$@% zLwFtwUJwX6bz5q;>+UuIqTPusoqxHCEd9{#=Vdd{mBg>=6QfD#q-9v4+8YM7GaxWr zl(-z@TgWINfJ|F0eG7WW)+RP+Q@xqYu}eAIG#i@o`s&N!21R;}rVr01`r`^1yR&y$ zGS{vnkhV%-vD9p`>xv$u?)NS=-;K<3RXqp z>bCDM(2lV@dfC`0^NRcy;pX!Aa)Rn-^C~&PVYr+4(mXE6Vv5~)uD!fC^{L6!qoZ{m zkNM0c0gNpPY*_U}BLr>H{(G;ut~#mH%00sD=86NEP^VrQ%>xjdT-u{Mx(zH?ENgR~ zth5-K8&P{Y-RI(8s9dE8T=-O3DVjCD;{xX7120y!a~68Zzx53R%4_cwtC2TPp7@&EC`_Bbpr`Ep*vOQ zRph^nR6qqvlt|tkxRVfn5{;UJ+q9Gl1)0Q^%XZ<*MT^-m_WDPH#mQ6&YI}WRjq|hx z!)YahcGFiL4|b#v=T5uT6ip`h_pTVjR(|^OYvy#b8jW>t=(ZcfGTx9SwI{Dt5o}y=l4xHc(byJO zaOBfQz{+Ma$SWQi^AR2fi0b+iC&N7P1|;yN+L{WS9!5N3Z%VAXq0=9q9&8q^eg%h= zdBjL8M=|dqy2UHqXhGDWx9zc^km`QGt@hYOX1^~|Yx%yHv$^m*0yIvPON`G{Dj3|x zjck^y)x@)hto5Ne6NOqu zw0wN^*9Lhz6D3mo;zf^h@6RoCbdU4W{1Xf z5f-*N{Z?amKUswvKjO9ZoC$ZL(1e0Kv)Oac}kX$-2_q= zOGk{iauA_?^QCpcf`_Exvq_g)mcbSmZu@8Jl;}PXk1#0j(A2XZUe!@7F0>n*OzUGE zU43S+{Sz3~aDvZlWX1#Ae&LeuepNvnb%0<*TcNrrb%~c;rtAg#GzVn=!P%d-SZ2)CzUpeO9Uga;LTHF)<%m9qUCwRZ^pntK;SpVz1XJ z1kF&sO@8W&(7D9FauqPB#3{$xMEd$wEfSL&Ukl*LmTtJDx8EW1rQ4_j!DXhp=_hb* zE0Y><4>pXf(gNy`JwnT+ofoYjlU6EJ*UY&DS~{x`gabuSJ?&=@qM>XV-3xY6$Z>9K z+uk$K+p744NidW2gpf4Z{z-7PUUxn^;Co4QRy5UY{KBdqF(Ip7awm9x)5%5MwBYfW z4fZY?;L|j!jyFpS8BQ~lAw|d80kLSwpC}VULg+Wrpr}CH4prYF=?| z`lH0K0Wv$Opxu;;-g-x-?M#9+c-?K#`RTF~UnW4e^?a~Pup9l`Zq_#I!!q9na)3^D_YoL zvnO@lnN<8BUv@4t`g~0QOwuZqxv}iLe5K2gXVybsN(7u6p=*RBBu;(sBJgq>Yf#w! zxS5dMK%qeIF;SVTKySl1#|J(;oR9QM$Y^k(?KLV%E9d@x(jO7N?4xk&DNU3M#yMnL z0g+qlb92vz1lzX)K5T)?TlW&(9bMGqW1sB+f;OrFcyrnHky85IOaIQffNhmhbU_e7VR^IjD_O#`rgAUfuxtXtDZ<%SC zb(o4(o5lf+y;Z|YDN=Vv9BLZz}@1NqCYTh zgRa|V>CWAivHk;sr|%TBVu&|w<<1$^n|`oPBM!0^_-4LjNlF4`O0Op8D()xyDUF{U zM2PmTjG5hr`H7qS(NX%GS1L8L@4sR2M|LU*=jpbY5e3Xz<>eI(*{Sb&CZF?--&a;B zcD!ggxpVzIpy1uYKJyr^lcPL)jwy;$iV5JYox1lI9jY5RIfVSCmG?&OxSKW7!_7KRP#FMJ;8;1d=5XE_Ds3rPtwv2uMh#Bo_tVESB$ugzIbOUF! z3v5ouVP>H$mTz&GK>l+eg)!LbMD@zr{mQ?NR)$(Pw z?V+62c|0P$<{<@(S74=lun;T7VKjBcN|g;pe-Yrd`dMr?`2v0txH=`z)?Rpcgc!kv z_lu*tljJ|<>yrNNbubdX*>nZzjiYY|qK@PKlX%<<>&xkIWRDQ!P*S}-YP^#n?Rw+i zp$xgE3?e#v^ZxDgKk5Vm?T{&d%Y8f@PW%-^9LaM4u23_eqtGD{QBe4ac;T}){Q(ss zz&r@uyjw>{N8R3izd4Z~`+1QiHVc;){{$9CdK@6|x9V$DGtHfDuGkO+KRswGrxSxb z@77~YU|Lu>Zg&Dly7texAqY9=-p`@Bq>j(CpT;Un?beZ8#+JL`j$iTi(pdNvo%38$ zq4S%lfS#uhIi2#@XdRde(`P7rzXjAF&qff0`3qi`yQBUH$4;W7V+@G!4T$5=Mzu#z z@6aul(!&I(UlE!N@O};hzNZmLD!&5bLt_Po{ROaK-2)-H`%;Fv%&%WAqwu{x9rGXj z1{@#^>YeBiE2Gm9<6j~o;=|7X-w?7>=4LoPeg6{i&sNuu-ZL^|+Oxpem?Z?jXuUO&(-1s#mo>1 z9Y3~Ke{A&e$G~uaW0sfPbNs%O+Qq$@aTj)xCBIecg-kW}&usDyb-85WJd z={rQ4$X0*TMk#BZfig+`Vy4SJX~Vd4glzu#%H4s+Q_>#H#*(3?hJIkZ!R6COx}?Je z_C&WucoIO=fwSlcx@iE>yzlrv%H1=IG4+*2Itkw4< zSu^=&bL{LF?w3NY;4z*^n6E^0j1FKzLVTFHA2{YM}UyQUa7?@)8^*& zan2h4(+gLNkBE9_1Wl1ZX-)Mf#n{VqbYpxlr|q?*3`MDkU;yGMM)pw{@9K&tn#a_A zeplK6@_$kQrybMVO~6Sify>k`Yw%1X4FnvcPP@7QUmu4iqj!E}rTq(7;F>2Lq~R}R2N0NUmO;0=W^@|vZz6d*PmmV(FH@H@6#$bIdO(H9{RT3ag)I&YBb z{`er#HC*uW<;!wFKTbqSI<69xooZ}L5-S$FyQ2~K8;R*})H;2$VV>iGo zJFv{6DBMr_EW%-Tooumjn8NgKFY2?maebUdZG0;e4vXqavQ*3J-2sBl7t^5O+KE+F zRpEP-65}kbW7MD~=*U1pm`NH1ha>n6b{CsEGj-Deti}}~opx8;8#S2Tzhc!djjaTC z0yb^6I}xuZdPhusK-X#jIG>_lnW#AJ(G;9kFM|3w#`6s@gRa@80xC-~9olDRGaF_z zeVJmjg5h6lY(DP~+}Rg;GnW=8x^EoqLeOM%=jv*W>^pl3lJQFZ*19O!;I0JU+z1i$ z>;&qzq0iTDTK`PinDKo(<$Q0uZ^6<9Uc4K^WiPAZ=W40(3&;!ABP2Th~tGN?go6Rb4+rCRL&bd1iAQphFg>L=dLRa6z--KZe2<( zjAb=`)^w@;b}>;}(HvQ!Qm(B}r%UsV2D4r~&fx;Q{_U^nJH6{NC7QJJEG*mdQ8CI% z*;658ro;PT(bBt~8$Z5Pxxcge=NDf-#24HT+If%d%KwZuU>c2f~P z()1sEzLm7ZGQ;b$@gG)QEn+cS>C|iP2SmbeLu1|NFZln;yj2w)*q`Y|*C466gg%go zZCv))w?J0)Wp-`>nvD(C6%UB1jSa)xqn_U0UTk9t+hwRZ%X&2qpD?YLjzYEdShtL| zjl!esXK66G3!G-RZt24PF1x&GshJ6Y@v**t{bcop@4Ep|E>{~wU*g}NYloYW_++Ek zVSUo7?pp+}&5EA{h{GURiSqRlx4fH*9~)G@yog#O{h@JjUeFOzLD#V0uvfnEaRio+ zmpNA7`kdmmP%%FZr?B?-FycEe-wWSmr{d5$HCe5m~sI{ecubr7}{>L1#2-XEMhafrWexGimT7Ce`(CNfI9BYtlN2f5V4+WBQ02` zsPtN=`eHcxXk}vBgJ5dCt_7O&o<+02kRsctADzGkn*68$xuMI_Jv_Xhvnlku*{^Oz z?ir9T&f%&bG9X z<>HOj@zb|m9D1>LHymF5Njwte$zA;BChiQ~iVarUAjT&cMaJC92~)4FwlP*UFL+)L z6QL!^v1+zIr!Y=nex@VV2y>>}2I=iJ`O=UF6+s+Em$(XYwRWqe^6P(&Uc2e=gRdp= z0fm7A^PRWf-%|H~?oPKLooY{cKGtw)+R=Y^Yido@n@}xn`@?m92V9c#v*q0Z`_4NP znb+H0ib_YGWy*Y#ahANmkMOonqpM46ZpG(9V;a4V^u;(h@w>D~20tp$5q?XFxNA6~pkSdH&3*L$=sN4L zsLbgFw%E1>=pn@2Sit51^Gf^xpOxG7 z^Lmqb?sm$l=BZ;kY_d(duB$1AX;;aFxhcG^cA`e3;F7q<*m=$qB@~}jv;XyHa;E(t zqGqFEWedFGv#Tqv_tW%_kiAcK(K%W6VTc8a?)(jCYOU$|Hv_}6zdcgPXqan8I9^^v z_0mg{Y76zCei#|gsPmmMGq6jq%9lYZH%Flxt(+=7r4)MXJ7^`^YG-)y6bqe3iSAV4 z5l-ir!#(YB-?!Ap3rdT)0YZ*(yp8_5fG`UUsu!W#+iN5GHuotP0^&Pwo<;I15Tj9W zB2TfUPc3Elq%cq)-`@$XD;>R)2Dz`pRTf>N4LanuHHXJy33zCx%jsaggFAjXVJwG} z<=FdfGwz#goGKg8&Gf}E3!S~5+t$AB^i*kBPgGM{pJFuQGyAQ9Kozql>w@Uy1e8bpu%F5dJHEC`+03_i%20gjZ8S)4 zb;(eAVc@XM&R;XV{kRK=}@=~v3Tl7NqSE#rG4Jg^&ZdBKMqVTA7QX`LR*9pZwAhPP#g9|sh zmC_`=w=(jynmKn##+6&S(%z3{AT74uf85ud&A)s(-Mj_US()6=~z=D*HiiR3j>zwP@5l$}mK@q+kf3>mGy2)QhtNjwn4MWzrX z4Mew>oE%6TD6X?Ts?fX3lBE)6=_Ax;@SW4$rb__&OXQ0$#$+1AhVT!wt8OfdC9uGux322MFr|WsOepMe=#-DcR<6 zwqJIluVGgB>cq=)ODztQRelFT9uIoja1wNFZIXoJo{#>01>T~K*SCv`axuBjDz^GU z7{<&pC9f(71uT&n)ru(P(As1v&BB@APmGiA{LE>~Gh}Vp&?wOva~NB{>>abUwU=Id z0UG(Lh)&jQJj2*z>(FsID)l1e%|3yo+TlczxS`OfN( zGUGP1={y{h)V6fFfdmeA!|~U~LkZ*x#FG>ClSOlRTPc4y=P(G8l`z-joCuayP5XzU z&MTRv7>jjwQr!VZEvP!`XdRF`y*Td|J?T@BeE&5%C1`c1eNe@%vP`YS%@LD&eZUgsp1t|IMd_J@FKCTEYLtNZC(7~3t$i}iSnklH1 zT+9oq+C3v3Qm~aH2b6N9m>Tk7%Mj zWXc~@zWuy1e$q9mKE(zsK`x^EfiEK|$@L1BwC|6({!RgXWCrUR5LI>We-V^sqUD>L zOE=_>`I1I9Qasa(zenZg9UiNC6>@tJCMMmMM4jhWxEgPLeZJ0*g2)5K&Jf61cIW&R z>Y(fC!nRTxrDDo-S!GC|cJ0A(--HFdN&yXTkyZc;$t1Yxgn4m~4O^*+O|qX;ss}r= zEkACrfl|FFfl#1xW;k3reD`ej8>VOr#l_B=Sx>z$F6TlS&5~dYNF_2CY9gSc*^=~% zl+Y^?hVNJLyUP!7ZB8(fj}ZCK#|XSOMj^Ce#4lq?M+#;5bQ+z=0Pm=M8YOz9%#KU7 zL>*_$bm*t+_4$52MPdmM`B7xj&u7GE(RzG;_dEhyN=#rSPf@)qlq|xOMcZhv;q1=D z%mWJ#ISY}jDA`oAxUbhH@brV-*1ia-!1>t_KPKo6GZAFG$XBqs06g+lPoc_kQWn!( zPT+e3t49X~2@l~p`bu8A)LDk*}k-+hK z2M)-t)d~_9IYoLad(h#J;ztNh@SI9(es8dZt1+?sao7!*Ia91;XZ8MQyp9JGNX=wW zYUf4Y>DGIgm$CHWjype}%6;j4KmC>5dcG-tm=3WH?M2#6z|rQOHqZv@jfRlJq-D%q zp)i=Ytb4W^Lgh-6v8!)l9H_d;m&0xTle8In-mx{D4icto(-%*B`+lyTuH7Fco@(S9A8`Ejcwb7NGwk|YR>Fx!itL-W`t|Yuae9LdJI2Nu#und&tca0SPU7Lk7O%L z$*1wFFX>cSUcF@2tH7lY^Yk@pZAoCat{VD4hzLF14W~Uvx7!-h9Z2KCJXmV&F4|Fv z6w+W#m9cg)A1gozCl?CG&}~=&6o$y$Z`#%GOU7_$K1Fn)lPnJ=^JyMmZ~}C3?6O!? z@^Zrk=*{&ROBb56L{*;h&7P`1GBy^MDcbII{ZxvKwa#q4JH?lL3D)3HYXSw+$lSs+ z-;4FW2@=^f;b7vtPrGCtGi=z)FMeC#;S)25dAfAqYI}R^x;`AQ0q>83^P-euz7Z$T zaJ%jWIv7$np8p06>R^9eF6o*g1VYP=plA!=qC15o$@>w0kz0gyzd7_u5phMEs6F!Rw)x4Fkd|UfAK3b4QVh9^NRmc<~*khsY zY_;UeL@-48)#&uivzN;~KFy4~ZKHnfcE{Ti+<6)@=r29UmLrYY7tOIGXPuyzh9C@E zTxn*#Ee3-4Mn~}1VXmPReqWVtjI0-D=IrR708hEa(+yms)9ivTs!vi#G9XumV{1?J z`Bi*LrfkLyH08S^d4) zE_DK>vbOenx`P7e`r{KgJ!|d|d&`v9Y%dl)xeGM3w^shKwaLIH+gLi54bvhy0-jNg zU}D*B44Ulo&$y$)k%ozzQx%4U;PsWclWR4kHLviGE1tD;fPBE=3QZ|BoMkRW-!5km zIeM$1qC!-i=RLlE+R~_4T!5#t4=m92FqH>ewF6hqriQA3@BA5x`P`ydO$~1u zk7e@||KBJjm4-MX$!b?HJj$`^@h~{wkg)8m#RnPoCJHuCv-};E=|DnR`H`UF2~%Sg z%gY%0yUnNy``!11lii@42DFaDTINf-X%td{{j;P=5J`=P$o=cBry=ph(j}xbs$jO;m9F0(*4OV!IEVMh*SS`{c*+0-`aj%}%LvI(f+(3f zTWW6n294uj#T9*a-!}3=@6w6c*Ox!+-HH`Lx(NAxsCAT*WS&RN%BN|Kj%$VNCAje{5`Hg`d|9mXE=z)rRaU6!xx6N7u%br zLkSWT!@06)s-+}>K9hqaMr}pC3H+w?_=AaoZ+?XAaF0DlEGHf>4!&X>zEmy&a!DFQ zWjN4Uk*ei4{&L4DX^{QrMQvf8uJ3D~Ge;r-ttLNz!lu5g$LnA>Fe5-W>o=5}PmuCu@={Z4S-kEbVRMLQ_^+cdTOn~rjbk#Qk28q&lxaHle zhiCB@gmp^z%a!}(c>;2NB-?lLKpj{NasPSG{=YfxKQ+5cJ=o2^PY^)!9wnS*m`be8YXzb4Bs$@)QJyMo&VkPs|;1b!H7`wN!;WVH|Q4E|B};*XJwDSU*-5)1o+ zr@U;2S|hIyB zKPd%_l+)b$Zw>MHBwAMa2>Hj`|4)1Vk3l}X(G|9b*~FH$8TgO)^2a~F=fX_=W0L&m zFg;3PPW~+feT*JV;}^xvbh-ufRyhJ7H?gE zLX*zUzURA}gW7qj)rciJ^~AU&eZ2pic+2s~0HuTg_O$wff22l6qTCFQRGS3Jt2M%YiaHcJ@_jp2a)EC%K=4@C@1e)=k0 z*w)t3x3Te>&F?x2UAN8!J9jwc$Ara{cQ$|j@5zj{`e>T!vA-g$0J&m11BpD9ZcY(2 zp~a~=aJtdv-Z^7e`O0~3(Jz8RY_%gOLKD#JS#_IkbU_K67kgWIvr%*NB~?U~nc`7m zBgJMmDsdEPVxGs(YxjVNhnP;#u&>QhO8}&7MYHaG@G#AZTehtsmS`voX-%5=0axG?t6IBIU}ZYXA@qzSrPT#xYe>JdU!xSRNDr}y=21eg z%xVxOh3(NXo=%HBp4dq^KSlBtL*Y-p4_U*@D~7j667|}L^8q0)r!ij{<% z@G2SCr6)q=?s?d_JQ}~}Dy7?=oRjs;x4yZc5bn$$Gg~=7{^qS!wA#ACf7RwE1C&lc zOB3@pREuL;qW^CB`+K|L^fBOpc>uLM+%Y(is<~TRtU6kLwu+j$LbffJenm_cfSl9 z7)BVty5Roq&R2R9Hmn;gSr8L-)pUhAS}ntdM#zs0gbtz^2UixGuL*^`o(eicV5c%V zsGN-wpQH=B`l=O9tpd7i4n5F^nlv09G1KgzMYGacWT+6ets+$eyjrhA7`L%PEwlE$ zP0HNeZ?sTJ%yPD3VxGW38Xq-c!bvA6OuTz&f!!6%-6QRqzt!0rEjT3z@_p*rWOqVK zvk2NoCS&vCd-t=l7QO*!mcGMfrlmWs+w$&Nmr_d6NfK?yz20XaCYG$u0R(%2;2VLE z^Pc*C_=C*Lmdy3&XZzOhKB15uBY3t}yjCxKd$AkkY*+gokfsi`=5OWI9&W({bDP6D zJ7czfDPCQ7Hk+Ie$Wtz5XgJ*tQd}T=oo@;cBzdD8EVkD6k*KPh^Suv$$wPekSjBW; zQXxi^MZX#!&thEWVo~w^d?x%`(6%X$?N}8^Mt|emP)RcsB6yR;tO8Omw!-WktCM!G zg3RL#>wAcBd|84-t*^^3Ug--nSwAo$YxRl$y4 zig(^VK$pyRP$!{k$pn^_prcIe+c5SK&?C_+7pr*4A;u3dZU*L;ofH9eXu1*sm)sWgES8ncsu_!tqLC`bPrw+8z2m|{%?V8a=6 zHP*|~k(Sq>6F+VaYK<=zAHy#$m(mT5N8^N{0?F!Du+rh2k?@TAQQKd9MvFTIuYE;C z&sm#*Oo3^BNUn?ZO6RfhHgH7hOiDmRasE4%5&wNf0v4XJdQUA^lqm*q!r+F&j!% zfv2Y)DN@sJa^C{^cmhj)0+(}3Ne8!9vng5UP!%hkch>mF=OL4OwwK!+JUx}b_ExQ$ zGpC>hXRGh++q|C7mt&ab!%?~MJWOxSRtY`KnTDaa`Gd3QYG&7~a(Bj?cKA94b_okc~ zI34-6hzPB#WezVtXw#GT95PmnrC67+J&;UYG z#1JVnUUZCM(|&tZ+P1-@f1#F}<_Rc9BBuv)WT>VJFH+x5lv#J+7wJ?k>R=N5c8ZYo z;J&@Yl^G(PIl8-bPZ9Kd%LgoKwpCV3a%5u^ue)pGmsbTJed|{r`M{aT_*UkR0g=dFN<*u8`g5X@WIE?0TK&{QtU_kLoY90`N$N1k2u0MAfTEwcML*>ebf z*u>Gf{LPse3bSU@0Jv@{MT^DqK5%XQS-<+u@nG_J_s;+><%V$z@P!9n$z>gfq8h;n z=Gbo|KDfPQS0>s}6p{w_gS3)o8YL-i)&g>I%nY9im>xI2A3a`TIDy_!JRm3G`{5 zk#Fl~^7e-J4T4m<)u!+mWCA&BCRk<{i?<22%QlwtO$z!kQz+)g#~Vz$vz5c8h%8I|~%unMxd|kz4gw-}u%7TvUL&BnAXA{8M#RxeN43M#q7H$xe9p}awi0W4hR4# zRFCTIjiqAfqej14E2Nq>G(v)ju{4eL;BlGeuT#zYgmrUyaSDVlbg+$87BT`8=xqe51{XbWypAR*CRL$asNI za;>#g8dhYeQo2Yqk((prGkZ;q+f1ok7VV&NsWMIsvkE?^Y~;m-aAYp`O&>%fC4_jr zOzli5jUE5^@gxwImux{YOVmjF9w)T81<30H1k)`bYU_o1qoJoC@CqlzT|kNzR%32O zIyHoE(mlkC1}=kplAdv`$5y?eROv!v^hEhWBgb6W;_HEwDO=$p`|biqPi3R9P-9&fXy+g``JVtAYlq`S$s`i{20tr`bzPxe#22r?cEPnc?m@kTus{BchfKel5hA z$)Y9yCRh8+clE7U4DX&-y`O)bUVCmwYOF;T|NBIYY!8=FE5X~A4@n1rxy={Q_>0D_ zPuOHQ-C@!z?Fdv#M7uMMm_RVMEt${8(RGZ*{k-TCsX&F^ZI&j72%Bz&f7;y443$*i zsmt|#sve($V3k!C5{z;4yuLTs&%^bdI0hnL2S7vL%cI&3wwz7+Fbp`GzW zaD6A)ViF~yt)3e9T9?~q{A-%txqw`naANq&G+}|&?vPwo1F;cc(MlfwVRck4EMf1s zy~dTScQ9YxoYwz5)L7JLmWD8pDr9>$Kda%A1VOvzHfsv#Ajyd~Keb3yK9QsOIyx|0+0bQ0g>X%&A_T>C9q`z!;2wNlys4Y;NOx=^xWL^9&#^?a4c#+!{Uc!4zc zo9%KMB*Jx2@fV4-$8lFT0U>AQia%5?c?gNcLIc-z8)6opJ*eS2_p zZkfXrxyKb;k85AK5uIb2P3m->(F`8w^H&tT6pe z7x(4U{$ndtfIH|3jwni&9M4le9KTlNP)XUiK^@nMH1U56NZhGOUbn&6)D3!3@b=Az z(R9NxtyLoG1B5N+;RWYPz@m9t7$+YL;kEKXGySF*`{c}G%0J^y$o?jqVP@)Pp#B8< zd}4C5u&@b#d2p^a5{u)_bCbI>48fVw4jrfz>;4>Ho}b=r3YC&TPhkda7+IjSL(WrW z-^*= z)~t%iAFJ7NlPWo~R9sCSw=W++ohPjZN1V6rHg0FpJ9A9O&6qOrhMG;5-@NeDYZt?z z2bm_WtVmX{HNPr_E~|*QT+!Ou>o9f?c#@JTm&w|3ThBf@POS#&*vTIA1)zcJxLqrZ zBS``QLXpF@k9&GXT57t?5RO^Qv+wy$^ZORB0E}>crs#Fk#ZqRKBgd0!7MR^YOMXlIQ3UOQ!HGB>6{U!!?Vxk?Cg<3{@IDqIf`RQ zYZaD{j|Y7m6DwBQf4tJHcVih!5kc+t^0Yl&7TsNHhR5soZWj8Xx+$|6rv%>vyBM;? zc9`7Y!l*Kfs`CCIlMfAEE}FLOJhc%}fh-nq1sL9zn^)`I zjmllwd+&s-9&0w_hkxKQOT@oTrBjMRBPUObJdjA>lylGa{;3#TsZ6lE5sbA{ThxCu zwAWbi9?$7u!K~VQz4}d{A*EH)xcZgXPI(fJqr*O3||coi;vS=Pm1=*8Yjhs+&*)d+=NR7R%W-G{!{iRN5(ND!AX&RbOp+Yze_RnubyahLhQyvlI zEQ~B{7*vX5+gKH0_8MHv`Xz=+e-`el|kj}ywpX`)?_3EY}u`w0a7DicQefcd1 zbIoKqW$xGN%zErf6$u>#A+T5u+4rdOSHWxi)D=_E^KLiR`tyCgtP_CUp>yy&1LOL6QTeJ zyaWAno6k1pOR{7xUJ@Dao{dykA>q;=BjGdOxIJCde&$m+t&#C4Vw^3?|o>z1uKPH9Ltbo-VVCCialcTDq`F zJTPMkLMOm4)oVUFYhcdT$M^~8n3!Ws0mi!fI*lL*YoA(vmY;{z>UMdw%gQFLv`0Y7 zOeKSgQngL1XfbOrX)xvLb+Y8hS+g&>| z2N37){tR5N)vF~IQwzEBmJ3&4iIrsdM^sAQxwEqekGe0{4+WvzKtIl zQervbUCKoRZ$-v)SJmjC`tId`S4pc6I%OLmE3cfz#vi@KQTA zx(%ni!D|hX)410M13WLmnG{}3F5>pcdBQZ<=&~A)ItWd5>ygpX#SKy9fe3#++88>( zC#IzJi%z00Mqgt#>3y?kfbA9{&haDy*6I&mC9DrQW23LGLgG*BQw%jRon(ps?D_Tx z54Oe+E<@w{YQ={UnMl;I{(HS_%*$NGj#AUGx4r6s{5txj`a=)3k~-7%`Xjko$)7nOFgyZ2RByV@#O%qy3gg%)VK?1MJc)AHdPq6Ezc z@Hz@7YRu~zM>*YtGm_d#pS zuG?Z;<25*18+XQ*j;2>_IfrnpI?_2Rb=DinP`dNn;<}_chhC)B?W{p?=W*8kA{d!R zz~z!YFpM+;D9(|o#^sK8a+dq)BH&T|r$ZimcFXbmpDv3CcMp&^5Egqx-~%*erMBuS z66Cg+K|EM&mTDDkrld%X6E*JrLi9kncPVI7c2Cux;62ZO^NeOES?Vb$C3)_8GZ=v4 zVV7QW>Sts&DA)_n0i>O0c)9yE{z>_pwwo(k6*4nGl|%*9zmLUXf{5jLwMxVLP&K6! z0_O28bo)EX?mK*~*QJUqXpT9i40RNS~-sAtkBN&LKXwdjd-b`Vfp9HzJR510Cx zj{#6c9v+o0g1i&T!?ZHeNT-7&V019#O^LMSy6*eL1bBgKVYEo}~` z=8xqnqXQ-v6*FUB3jf3@pW$hT3zRCrE6s2N} zy~nYbvT3IdipxSr6f(xxYn)@MTC2OJKnAa-I&df3 zQ=Om)A7OPX^TrfPOh>YO#MY;j_XqU~qv{SMAWlY*ev>2KMbm-Pm)xi7MQ?LiYr3CT z&;(<%l}rVJCv9d6`d_-~(-AaV3b@`vS!%5Aq1$(%GpMK1cK~*%X?nf60fZ?t4rY7D%pQO$TsC!NvbTKkmg+0RoX@CAhl#~p5j$T!Vbg8-e2auZrBD!4(f3!J z8w(%s_v%H-W7l-*wO(u@UF@~jt8|Y5XUY=}A49@OBmFCkj@`tuGVfswG&{RV6C7Ik zukLlG6P(S~gov3kwPXDX$+HmdF&eX)T{pY+*BT-hHa{!f33T;lt6rh=%ktYBu&v)v zd)-`az5}wy#=3#Bvna7mZn#yk2g7q-8;~5Xuj*mp7Fr)QV(cMd8#&aupa3 zrt%Y|X0ugZJn~92;75vZ3P3GL+3Ci6tel|~3sL;rn;EibvjGn8sfTm}PznXf2?0w7 z(}Bjmp7NGA0QpOdwj+S%(nQ_EgH_DLt(UI=2TMSVIr84wa;}`@I7u^qeZSeg&iOt= zaEZa@&Yk+`53JNT31$u`f9S90W55SP)%cRtJVz-W{|quAF##ma&{5divVO?HVtDgB zTP~Wo-hTZfn*8$+Vzn(ls1YIJlV|dItTElH7J6b))H0KHZ)!Er#(g|3J5>nM6(BkF z9{#L0i^8(;ykV|~Z87-NFC2}B zup7>|1IpLvBt>Hgp1s8NgkiXPChMgV$=fS)B@c-^F&j>oO1q?2t$LI*ln`u(Lz|%p zUy4(fBG9DR^se($#IJ3Ech(TUq&~OrXWHDJrofe!oLG4Glwg;GTb&}GX?Xge0-_RDsyqr{HUa} zWJ$d-JDhgp!jkB(U6x0p*7D}^zSXA%794Oc8(0|mO%Cqa0R@%E zD{X>Ji-j`GmSAOL=&5FFKd`E?|0WxjYsDi!#Tq#abXB7P3a$j;rFm8>g{N+)eZ4xq z!|0{%hP)Y4CtH%+d}MAQK53#Ajtcy_HMt#HXe1Q1=Uz{K|0G?3EcaEDHlQ0QEoH)eP~BUf@=E$6H>Au{8urn32LZVh(q0q$0D-u3f|#!{%jOzas)Hav zkf&G3x;do(Nze1z@Gx=(ded=ysWoH)po9_cq(kyf{Iih(8Wxui=?L2o5bo}(NL~)a zIkX=}eaiHiLq!w-@KI?TyqY zKI<*f%{Q3{P1?*O61|R;T(VD@FH#l(rR;{gg=yy-?O4B`E2qnxk(ORan#g{P_?wEN z$69|#0_*h7pf7cb!a<#5Z(&2bfA8*gu^T=_Hn#@o@U~7wp)A_>5@F_QluCp*?^Aci zzkZqzHZijB!=~;w7*DW^Y%IE)X_D|hg~~u~C+*+i4qbcZF1_6}TYr^AgKa8L?q{7k zTM*qePp?VICRk&YLE)ZOX@IgGk&;r+f99ie7!T@y-LOs(DS_tt7S8J?n3(4={!rTI zHh5^J<<1j&9eOc05ai=kB`vi>*XiaHo&t5rMsk4*P}}o})+@oD&AlwCfN|V0S|X zGO1XT54lbu3wWOHj>>+xdqtAHVxPBfjvswGovGKiDLmazoy&*CDQ&CYH!sWawgFXS zuhJ~y0!e%skVAodwXDb4ia~n1o)ZjyZ@!2D>m>kb5)<>}`Gpew%mir81kn$n#QvXX zdIkAU!I_YqnQwk-BB?@ElYYEk@?|sN61bfRjgtXaOrnu@kloS1`$X65OVICbFg_zJ zWzGj)@+rbQ@j@Fn0>hUKWDmgt-PYq|YmGiV4le|_NbRnjM9L6xlwl9xk*fidz zQ{SxIHY@`kR5%VDFOWMJgb1<#3(gf9EdzH;jEMTC2TUGH`p0Do5B!K^DL~b=*e&(r z5Ns(?$jowoXesY(HH(HHt|``D?Hu*vcR!|ISCfWE;s?{A-ZfV(39P?!F3*&HIJ4g( zUr&UXaoC)rWy?m*AHazTsFN!lj4xi`Ad0zid8yO+At9V>Nk_jp8<&Q<(Q7r)2cm;Z z5s`mN=*8n|kQMb`kfL>5$iO}I$BeMl;<^&xH=IqFH;ETeLFAEDML9pSptecVDGR}$ zh$-4SX8Bb0p-Hd&8+8+%eeT|(~>X4sw_^I3(i$zW_d_yB=KgKYx> zkOvWqY;|m&@xKIwG=TlpFP{O+Rh7Sp-ng8gK7l{jComkT>fbaznL1MQhc&`4s zh%~<91hc?jAiWB1N(`xl4%<2 zuSNHl?}+RQw~PJPFG6m9pop8V)BY&4jRk!)ZS?;yc=+G|mY2}@5x@o;J0Xxr{sPVZ zJm>)wCrX*jZ>$U03;#ATKzeWE7)1OItNnr5{(0@6ys>X6U`*eE5(BBfgs+Et`b5%w zC;zAEDNi4e*VBD>pzielu;MS2R6G(Juu$X-p5=dZ^OQ`3asQRw^=lCRdJjbQqUUg0 z(El{zpRh49=Yxef8-3A#oopYr`~NiJANTYbdEkdf8V{uOhiCqm5&x_dE%5@Fgc7D2 zWmJEe?f)C`j~`&UJ>b}G=OXSe|I_3TGvqJS^?{Z8uN(Rc!=C>)NDSOia`<5eb^lAf zZOT*RvfSSTdPD@+=TV|=Ti`G6=D$t&#}9x}9F_)=emqnPJo1i){W`cbiDJ>)M~-@G zb;XZhvuaC8-{?3GWV_RhRdS1O)O8u=ae6u@H5oI`9D86O;Y<)*X%()woc5OFz1w3vp&kTk+*A^NtY;P`6KMAQd+oz_T6Z| zmqN~PTExON#&$N4JqTa}4Abt*U-fEJn%plGa}&=mh8C_*8#WL$u2K_3Ey!u*6Mfk9 zTedw}&aUtjQw4Gh@6Eccbt4!eVsu;FSVS)`Jw0!sVM7;(qtv?l-!rHQxeOlzNv%v| zh8#1;!x*xjKgsFhGiIf907;>|$E4W>kll1&^j)D*OdRBb_7AkLKJJLR zOJ4k}A&e!fD9%KT4eE}7Sa9yN{gb5QMOZh$0v8|O%)Ojb6nR}JG3MQmZ{Svlu`p5v z>V`RC|J5+jfZ=d|!CgL``JNbCto7ilimSwD^Hc5NFX4s@tn+4n05z%sAHKUjSi47$-yRxJ*^qe`}02l zB;8RUm@9w}%&*ti2gNx%xTHQXV%Mrgv!7?1<0LzIm6UIlv-dLPQ9o~=pwuA_KWJA_ zuldw-@$!7oJw@0J-A2E4Dz(AkgahiB5*%V?1IU9kvMJ0Milp_3TWKdpKL;TfM>As@ z4WVc+f%+k^wFEZh7IO^zpyUvN=-TZieO)5DhgHTm3MTnsN&GJMkhgv`d`^apLyPtT zouUA9h!m+#^K4ilwdg?<_Ktz#;1#OM#F5r>^h_|78pq_Mp*zKr;ir(Vd)Y0@7M zXEnPPc7gI)=e_k(KI$!rr=`&++&jys@;~v8V=Q5ssqrLGOcyDQzfA%#UkU;?y-_)PxcZIsR6-#`Vh=s` zyUQaS8U;nwXD0!#;?Ej_x~E_P2rb7Dm?%AwtmtSR-|2WLpP4A;!$K z^q9G=)UjKEL?nilws{11zDoN-N5|9nvsD1gDp%8zBgtlolTgeocDTRcu{%Tl>M7lS}>OQ zqf5RGE_iQ*;#GVp`<82A_%}Bp{&z$3SAy0{ckq#CCX7lEix`Bw_EmSaR+A~Nwq$~E zT&9B(Vlrl+6dswaRuF`@;Q}0zLH)w;aAd{Sp4`7Xf&#*Q<=m*TbeACLNgIep5)8}g z$6 zlr&a~6uBphmp&0s6uxVl*LuHLgtFe*)$#!~^{yC4pl!NT@sp{1G8%!IjHl$grKxD-0c|K5LXe)w87G|>E7r4@#x$>62>^u*y7F?d#WN@)tIJaZ2mr-_VSP-) zNWN-DN#dLMfYlhkPt>QY^<21HJEZZK8|_q4!CxC5*I_3Jm@t2#QOsrwY`pD90S8B$ zm6_hsI_QcLWX?&Ck*{xAU4*p7Wd5 z(3I7@FMLpVrdKL4A(sbM65of9q9TGu?b}qq%f+Atig6__OdVfeP#lV*Q})hlWIFqw z%2*nI?HB4ZKfkJMEa_l8)>q#L@-#YRBC?zY8MmhzNsRcF##+(8@+- zJ>rqu-6}$-RNNqnC_asX2TK;qS~kT`rQQ=D7vc7vvsb&mT|e(_ehPDv-+Js`@7HjF zpY9TUubL~Mr!!n9WJML$M8;+Ni5JnvaYs6ToHKlu*=!+0WDCKP2*;56jcnUCxc5NX z_ZK=PQ=;3_ewp#s5a8q)))fY(mQN@XY$biwvn&aalgjz;5PG6fI%L8mI+0gK3tUyW zt8G3-n6v8*v8`*qAb1m9I90M%yYwZ?Gg-YSm5C16c0RIb*W}|acZDTuwwUv*c87)F z4-#johnYu`sGANaVrpw^rvFX)Gq=m=9*-N!=Fio9W6^gNmSml+;p=8E?C0Uc*r=vAS>ct!o~A2;fb*A)FwOtR zY2{F98KNBJE#RvOa9Hu<(gRzUuTph@WM?16TM%lgODq0H*I+Wg`#C9iB0m5q9+YL< z=1a&ifOn1#k=mzpc5rye`oRK{KHWeEp8>);!+U9Qdpd)~vV9sHQV6Ud&o||XR=}&F z9NX`&poU;5lb3&l4CSo)M zH=Z|zdo1{^kB8U*$oPOBd@%;dV$oUWod7`W%#?_p%$xX1w9fEE?t}?&8sR{D+U+f_ z?&DVDPskcCMxDhZZ=*tH)hy zd&}O=IluQw8qf25fB&7A`mFoC?s47M3m67EQO9-$Cv__f~BN4gqXrgu`sb#psqIY)P# zDEyY4lk;HXH?sxbzQHHI5{k^2wKU^%%a;oxnx`H8al&uwflQkb;4541X;QMPw45N=N*w~B5n#-z6fw9OWc<2P zBtsP%IY?QfiLSs|!rOfzE#6`K7{`NA z0l`trcH$>Gb%Btup_~(yRPnarG1ua^&9HNwahH2DDO-w7b> zx6`|apj{j(M$ZYSP3I)Y`Vc*3I^Y)q4Ztb?yz{94ma3uD(P+IVv-A13uUeyCRVh_e z0xK@zmjsCCKE}V@dSOKm-7Nid!*ox2pvVAs>-lW}7~r<=y7NlJQPbkk0tl)+94R`V zqjD*t+&BJRxG+AwtJN|A>K))DgypGbN*c5=9Zz;ut++wp&@0UnopD#ck_*k+egfVY zJ!ErvBAfcv@xJuxFwfNFb!|hsO`XOQnuBmYNa4G^;+N?@-!9eJjwC31dwMAa9bLI+ zwPkkGmmh^`jcRciKV`J04`FCCQc|c0J7R=HBEe#fL}6r{uYC<8X(aObK50|~lf7S@ z;`E?>A3=i6+d&^dCxZUNy=Ng_#hg9R6R}^P@J@m~2Sg)x40k&=-2!n8xsD7Pee@TW zKpT&U^IA#3E45o$bIr}5r73}h8gR>GF04;@dVg8_ydvnKzx);7GWMFlpy4-z|ON-sFC!_kv;FyHixdSO7cx*L4=Cs>sX3{5D4nkKCj zH%bq%TIBhKjGrMy1e-V=t_|ZK;$kTrKic6;s zuUOQn0&mC`^zeQj@>o`HUJ@cAA`;uTG9l-<!^ zu;VB?2sf`eN3;YHs}@U`nL^xat(r&=ZdI%cOpP+hWZJxTB6xFO!rp~m$N)t-OWjnD zg4Dwd`2;=*St zLD}k}B)Vfga~_0~Ci!HcM!{=rX{?=E?r)K!-CLy)Y1Oz!QSp5 z0%{F1Db0vT+0e^6`VF)>cj8R;PUIGctB48)t3M=$s%_~lZ*L$W|(p;J;!Uys$63MW5wR9j`r1XmZ+L+*DKVV%FTOBrN*e}`X?0CFAll^8y{07xW#?jO&6)B?#LYL`Oa zs7$$tXP{k3kyqw1S#*FT?b<#gp07q%04n1plXuU_^6u9VGsqGIkEm5oPSQZA9(bu6 zR~^mv-fpxto!Rxw(GvQd5^dE+won7YrCSpIJ;P1(=hmP$=lF7!`syc#H)lGM7)56} zvtJYrEjGl`K`f3ql(4Qpn!s~V{rL4O*@wZ-$nlHpOCSmKZXFXZ^Fhkdk!?1Z6~+kv zfXCZD*v)!bdMEq$Wbct@6SnzzUH-yN0G^}`ZD(MX0==>E1?+EYxVlzuHi-#3VpU6K zg`A)2Da8N&%{QbhF6Xdb_|3M#_ai9i60#nf+K1cq7vO#^)_!=g@7S@XaV}_9;j&Im z8SVijZ(P-|$@&qyVb!^<&luOX8Kn${Dm+HK3SA;;<}(Ue_2_6X z0lvLkx$YiaPrX&tS&nl-qlcwtVfgY3w#7kmTiZk)3)E%7M^Etvm(z8UyO-A^l(+a} z)={%G$+zb^ T59Bl4nG<=ok%2NPbDFXv5^5F~x-g`?GL#E5T2P}}Da=Tt-I~4=W z*%$r$>Zx?4s?4rn-{NO(^4h(0JP5VH1>w6N!LtrQ{7;kvZ>^mD4Gq$sGsy8)?d|5u zZe}baL=D!s+jErSx+?Ry#&cBxo_qqJ6xKl#cyEEXR!3nmt0n}3efy4gZxUJ}*c++K z(==gXDiYtK>Af$3zHGsl)ypXPx>)7}ZGLm>dPv^Mb|0!&BF>zCFMV$;^=5v!mCAnY z?bG9raMWEy9btJl3D19v+h!DmEX^$=8VPNol0`X}z5s^(EhsxAMbt_(~ z88>C~%Y}E1WIqk7+PguoP_IK}yVe|r#P&#QS>l;t z<{xvr>i)J6G#9%Y65+ou~{gsj*8-bvsDnK!rRzDCr!VMtf{?fz6 zMee1L4OnHkb)#CnW^_HQKTA1Ob>`|+uPKz7$zjWg0*heAD!LpM^9>H03N@1=g$kkx z`?*#hXrBq&IR7?Nf5Bi`c3)Q|B|FRcF2_zq%6Z0{EjtRP-0tI#0aBLPWlb#PDu?Lw zr;+V>yxH|&_jVI0&L=~5eU}{Qo9}c^FAf$5G@(No!Ui)UB8o+fkIgPJuiuHs`B)FN zF}n3UE#3LpR=dIJ3f}yeFx!+dtXi=)ZelK5z42P-F+Hs=Xe^R*NF+;Ph2M=>plGO& z{7#a!hN!s?T!O(-?H~_}Pvb?7hFC%vkw>fNG<<~Gcn~3D=DbnLgT#~cWh+I1{8r20 z$W-#)o98cVKC&g4jc^mdoZ3pIluPaI>cSAqjXqmaH|Au&%IKTLcwI2lA0-tiDlWQ@ zZKv63suf?;@RJ^Am#a9J)gI)MHhWwWU+s4y#Ns zC89&`EjEc67NaXI7eN7A8>fRU5ftdLzi=5&F0MZ5RLeD;exAQLl1pnwT&KP%5l!dg z1zR^Mc-}lUx=TedE22J^6a+v%h{v>i7F}6(tg&kmG@>+HPP>46pbQYdY13F?RQd<) zu1Fg+Uw1xY52q1CJr~)1^q%SPz3k3nuXFEW?^^U7!VcOSPO=h|i8pi{HdHpq07RES z03uvqSZuZ&3aA;C(`wWA$_Ou9jm+-m5$nlZbE@6bweHGT0L=;;I|pcE+yWloAf4pS z4Nf-)cV}a$@De`ZNkk2vuy&kA;K-T*@jJ{D3A7^#DtJX$vo8XbO3+T~eK;jCW6+R) zo>}`n0g76$N?tq%7x&@=KB13E!c)!KsKbCcWBH3W+|A z=6>joWtuPBw%F2^tUmyocNAXma9Q&^s4#lnk8vK<-?Qw0Ex%U z_|x0ban~r_@^lL?ZNXA?4Jn2$9`Ji!t>T1Q95M z2rm!i`(GslBiu&HZ>hd=NBk?^#ZO_3@+y-jax0>Jv$*+;^2&K(r02b0`tf%49Z!S9 z5=s)S;=F4-*05j*LdUH8LXQ{oUV6YfUbljPbwhV6>j*&=9C=x$wnd0GPP7;aR8Fpif~m@ zwf9l)7L5a(mXm7?eYu=KbLx-TQ-oRmpz}#nV_l)I730*CBiDo2BUg>cX38FQtheNnA}K2g2wrpx;jcgFdK357tpEN%=)ATR zcQF!UILie9DIg~Gg1gEOZe?{-TH6$l<&X&BYN8RoeDMxwHS|`^k%|$Q*)8Y@IlT;M zJLY6CoFZ6G9KK1-7X-Ma-R;-+)OKto&_A5hZ*9_Fa863FwYyuSuBi>z5wRm8`!pU2 zDs&mn0cDBYUW6I45q_f)&+b3KC;Mp4^eel&p+Yx&8TX_9a6)&=+vPdmz7?S7?MGn?Y{K1QWb%Z4})te^hSPs+YoizY;<&>*=(T zG4ntv*`(|Jp$ciO!>&lq=a&{20lD&#l5VL6bKZUdUb$cAh6i!MW^sV*!y3zndpPCnk&#RO%Rl-#Uz|%{u)={0l z_mYpfejkLu6khN?omi|5?Jq<0oablZLFZdw@3Hq$dO0E}dGL4&)XL#WFdlix+EZ>C6)W#d!X8-&_`431yyThCc7ef z>diwWxt{~QxW~IWS!X?|Ft_HV$X?iOZ15zO?5+2`u<9u=73|PYA{?NL_)-kuI>aCg z-aij&JI(0*7CP$?5Nj<;MNLF<3fUipe__3wtEnNY9#C9Pl&v49o{VG^nU=++bFDvO zos9Z;zJh2Gq|X=VY*n zWV@g~70WUQ8&kb+WIR*EwFzlDn(Z^?3FIo?M>sV-!NimNO5)aBttwv-I=;1{};V5s;Py}6f>Y)Y7y^6X;RzRC9i8#nmQ70WSlnZ}%U<0G{? zFPh3F>PUo8Axbtnj#Yk-61+Wj zPYfOb=d^0Hx^nJlH<^|C=$&+v1cQCyknOuA#|3)w(OhK;e}iu#tveqo#WY$-v&kBl z1?BUeB3Ua3@(jqAymJc(;3vbwm!d^S=g?{^+j%=sWb>!nd#cSz0B;eRjmTc0*LH?Q z%{fpVD=bkp2+=JXRhKt3lAu;+Sxu4zvesu2P5!4)f zrbzF)mG$mB$-*i`pM>_-(y^~*N!$xHHqBh-G3(U=?NfEOhEDYl6L~yQ>zrI$jtF&C zxe@D$d^S(u)I0>}fc7L>%<5{9ULz{tc$Xd@F5`ASxupRaT5lKD4}K9-gOT){u-$$-)AuS;A8ftPv)v?Mf5j(9$oUi<@e@Tu z8tof(1!F+8jT=o7{{1>2sib)Cnoz+G@~9Az+58Y&G0&N;7llVb;kPbJ)-d6J{g~g! z^e%C!n8)P-74`csHc=T@D&BGvI5rPJFv`tT{IB7QnyeNHP)O7lr+!2UxTZECz2sas z8^is`2V1E2t4!B*TrZe-QrF|(^m9)N{e}ipCr3T1lhIIdI)dGYAbJ>S>E-*3FvnaK zNws5-IxP2;$XT{~d42$ALCTq^GDPoMr{EQKm2kIiJc4;8rzg*Vqe!<5=LOqyIl!*! zM3did!E$VIO+QP&Fh|o}lPd{ZYr*H%bMACyL8a+D{P*|d>#pZJKOZk^Hrc!k`YdAN z_SzkDS43=VY&hc3^h#8-V>ubeHYjHUb(UcjPPicp&#pL@I&|v6EEzv4&SipblP~Y_ z220HGEzoYscP`mhw@e2R4hXfg3;UuZZYQ&I)$?72g88meLQcY>`|02oCn^Vnk4S0pYf zG^@()+vR)v8E@1yMn)2a-Pu<&iti!A(*BT(TsVcQ zogk=+CrUWRcJaw$yE902DMmxQB}8ZXV4K)Z~u(IyyJy&&W$*c`zxxeQwV<2v6b&6A2lLJ>;W< zI}Si&w#+{+`XHH2QCT=f{k5U>om+66oabrJqdfEKTST z6)np0>(m)tRm?}dT=fvHbKT<7Z2pDNX#YY`d@Z#3G_;l~VElZrDf$K+>*~+V5pL=) zWN(sb4D(p*uMoXo{cw-ACMX#(f)*6v4?@IcBP%Nsqg`+y$qhd?_;45Xff>V@Hv{Er zxzKu(Zjv#_sGm^aIa*7qH+FA{BbW79Q`luiqv-+4(V4zEE6X(m0xZ<2wx`=Q3{+H9 zfkd?3H#Urtdb!ngUOb>A?#n6&8jhV=7m#ioBt#ov9N=gSyv(|$WG#KK)WV;66|ak zxBo;dwn(5-Audp;_)jwR05p81-8X_NEA^Wm>qP5VxqH|*brCH0wLP7ddhiAd)p5d= zO1obJnB1Rt_z3_d{z15jL4`!(CF{2bVKGJX-@e*m<;TRNm`L}!DE+FE!9^yT3w=4t z7cGxINB%~^8nU$%|9!(hz;CT8ejNXSLp?)KmLd_9r6U^FEN6rE196jzqWd?35`b^O ziybhxfi$HAYW08PP=7)w1&|O*f%<2^vL#5T!UN6vHxBhD<_u&#QE?Dv6tpK8!zRB< zQDMMrJZivV+nO03XBuCMZ3<%`8soX-EU!`}gSm9GxfsHsnt< z=Rq%x1Io1goc2$ShTmV!vay0$EBDJB#MVe8uC>cWs#92~6d2HdD1sud{V|{ttN?RK z`@&TC*VR97d$5Z({%ctRH0fW%?*Bi&`*cR0XCDQ+Zpe~1J(8?7VY#Qe~yu4 z4~bFOKQ2J-CqJ2oH!{H#IZzdj|C{gFrxB&*biB_-`45BskMaHq_XH)`C;%J&mqD8i z{B)7*?0=Z|w?QxMU<@<-Wh%PsKgd-UOTX>cc#0IK1*q(I zQC(%1f8VqPLOw|*6VU?14_fixq(5-QUW)W^b2B;4sTi(UyRo-cOJNj!BUY;mV@bKp z{_NZCPLczR>NL4j@Vn2YD!={;taNZ5l~Nm!2~nFxw?ej}XvCx!Y{klq`yVvN!onRF zB~a9IKkx9Z$unXTQ6g#m=BNAS_tzy7hniibx{k>wd=EA~nro;(A-vJX1XKKvEu!o= zncTBK36dM##KS;G75F)B|1k_d>?IjO9E}>3+DKh$3&UzYHl;AwG;DYP<%K=r+Su7S zi7$e_`tYqyZE$%IC%--_fDs5q9L;aVQyMmT;VxD98ZVZfk00@U=>GN9n*#3u-?}y; z{;PqcTRZB_S~LdD=+OQvDjGMtd{ijs&+@5$X!NRJg-bMO=|ulrji-2BV2bTIz3nH? z9Glbc+dGU)4pzSOeZ_5ckV5EnvoMgWMKVXt?{=q%oIo2r;j6*`YwFJhuZ2UY zaGqZ5=ogU~T|?xqM%l*Aq22cT)jsgAub&+9x#{3|mJ7gt90nFj9x!(LjC91&p^@PZ25Kqshev?SV-^!T<& z3JDXv-MYv&^~gSZqQ9R6fX@79P{@Ud9NIhMa-D<`R{_YOcb8Ve{sK@{6i5*D{QfFU z4rJg{E(LO9f^4XOwYH<)0E_BCODj1{J*2-yYH|HKaX>J^2LMnjUe?VSU0a7$e|p2~?b@0R0i5++=fx@g|8`&?u*h5E7{nbqLIV(nYBu=VI#o zGWi{@Q)Ax={kU<^Ns@KKfk{&+6$(lLQ@bleKJkY&9TCX_rWKcjVx*pd4kti$7;MRH zhGVuN+tqDYOHr?0Z?5a^u<$meet$ScaIO=kHg~gYiIytq0e0{ZqFR@qn0HI56T7g?S z)+Obx(_u9La)mAIUy@4cb)EufHI@77Iu@yYmbo=Rku? zh7M>Qq#A_m=^!oz_p^ENF0#D-_;||^li?;_@QtVU7^S8u>P#rTKnsz8xX(fO40g$i z&~NsU78PO~9u067zFSpv`Q5w@wrhG_I+c=FbpfD3$GgtSW2Ap(B{T>|cv>`99jHQgeLr2493jC-$8iY(Bz=KkkM* zJ*1!uu*AK6N?6!0jLYs1*{!ShWVn~L(Gks$>o9v8mlDud!1*GnYlDc^CA>d(T=`>H z=KxN|=ay6TVo+igIMrvCuJZHZYsDbGdWWJv!<`c%?%?YB0gzW~)INwr983kko1QF? zt0YPTsi_!Ii*vG(q`2z?a^9VnuSv@$EdkOm;I8(6L{(xRGtR`iiPlAR_a)%wP=;^K zg}sUB`+9k#f?lUQOGGKvD^Il;Z*?_es-xF5QONxg;0yMvU#{qGQFx8S!9~W&+qTFM z%DxMs)FBeNEa3#|PWBu0J)3V%H$WVHv{joBq?Z;l@?!PxK9?HFRqe8#dJ9+JG1F|% zbM-;EF2sPfx0Fj-pjG??Yj=4Oq=E=R&m9>E1*D!q@7)pr3}iB2%o$Y38$1yPWWEX^ zWE=$(%3aT|IUnCyE7ERahqGwP<|=qPEOb2;D8Ap)K5`y3$RW|-k2m{`+L`+FWlN+J`^vLKH zC*f_u(d&HLL4p%1j3>9`XWApeFg9n>0T1G1&qL%+7?hiv3#SPT4#o~5p!L6mD#F9Zd1!k}%0W!PumSmI}F? zVAw+6gb^kSw6*q9B6)mw&ryqCK-a^X0s*$$=37FbklC}Tn(0AkFx(zea$Ik*?lWHR zYnW?)K?sd&fbQK0NzGG_GTfWJ6++I%iIRU{#Z4dsp~#6?;$IInewj?x zFa%3<*D_VcCFf)xt$Oqn;2=!)#t%LG%(mlgI$jBrY|VCOZ)*Thj6^4o?OUeW+H>uP z_z4?YN%wBW53F2NnuDK^fZ9usw|0d~BWGJDf*F(r2fny!F@Qqw!FLDt-@cPHQnNvN zl*Zhbg*X;-SnzC>Ne4=T@;0~a4vvIO7xW$-U}_O5HVd7uYre?#ynn@`-IIZQCOh)h z`NpO(?w))J;Ma&!YOrbzm=_ehd1wA9nkE4IoFt;Dagw*?JUaG{Jejk-VILEd!Yhhy zW!U8EqF;IdcE%$h7&2BEe1Dy}Gg|6dFJq8NbVZzjN+P#^iG*)c1E9$Oz{Gz0E6>Q| z22tAbreu0+RnZG?sAkGar5SwVdH9;-Xm{x;4`Xn;JBg>*A{}Lg`R9zW8IcZ3bPe6? z6yNx3910>Kg=)=B6vCedyQG;Nwq%eZjtug3b1D#(Av~!y)sqM$9yCJ_2-rT9OAsN?uoCea@62AtcPz&12!0 zuU!D3*b^lpDgkza09WWS{!R)m=Choq`r)@M=@y|XX&^V^WN>XeCE3{J%-k%*447~j z4y&w7a!Q6|DXCHB+06VA1NkE)>Fm<4v#1K-D{6I+AzjaXiH8h=3ZOTN9d$1Vxf=g# zM``&&ZWM8L9R7^{S$=s*$A5 zb{^j~hSR`F&*$Vq8(cVo%g{`Fy7>`N%^%Hr#O0W1g|1+0VAuJ2I!n%8I#Aoce*b%G zTN`7~(f&vq2?t)5e9QpuU4ARP8=l9gcH&#tLL||N?q96nKfC1aKVQkG3B~Hv&@H4$ z0VDzI@o*$^0??jogZ>ksUr3pld~(e>P1Kb}CMe=v$v00QMQLZ(^m={_`=dTdyY7xPi=9cCLg^)Aikq+PxNced zMVyU2G6?)@`}}R_yL8-0Upf#}z?ZZr2J#Q(2^x+_eZdDxt1%364vw$b8vuSAw2SqP#nqXM@+Mq=6o#SWgP1}$tM9|o zAz}g8Bvg&^Y)J~L)H=-Bch}FRH(UZWK1b3BcY{Ts)J%u#uX#S1>vv;w<31tFNj!rv z$P-(2$I*L>kanbz_pYrXdD^Ib5pZ;%Pe1`=^WML{I*NSYsn#+M*Hb^3Lut{OY>1?{e}vl>pke$ zu?kp(;E;P3sb2>Y@qQ&1!yUbKMOnA??|pbwMW~Z-0$S2;MooD4)-=bJXjOB$r;2;C z>L6nkr=pX$Bi5*$HMjIHDp1R*KRR@8}Ql%~)v6e>B}7m$YnT`h9a)UIBu zjsrm}5k{@TnPt#qCIJM$ZnW>p?M2Xw;Lwiucy$OH(Cqc0qs8UiX{+B~S8cR3e-2ww zfmOCGmXWZBO@i~iRF!`gwr|1Nf1YW2k+WnuoaZesJ}^B1lth5$J^de`pDwcf|)$k_FY`n{FmBV{C@ zYF7;=(13B$d%g-Xz>qE0sg*6n-$CqvE z8#mdKe#PrRTu2in#W3Q*mXCHj{#P7%dOZ5zHMmXX%&286<}YZHgk1!AOrhO%d{CJ7 zpi6h^oMtg73OQ83P}2nPCLr^OAs0saKFwTlWw9??aq>%F_HK*ZiJSc{U7+$tDpLqK zk-kW^JkYs&ZMRMX*iu=~(TW$S0arN)d3;&aa3Udg%L66(Gb*n{im~N(p0&>csd+$4 ze8FRPHAv#wMY+QG&j7-x8Ftwa2ufH7qhW5PS9O_^bG$t442K9eakfX1QNj?P{Gldt z$Co<)beQol7?DZH%#`+=dy`uDk3G*e3FHEu?L^Pt?<9c?k#v^(P63H*?Ix3ppmQn! zK(9K}aT{FtWV#|tr*qhNo0H)|y$0WdjfPcVn+fb%A<0(KUMn;#1k}&tF zFm<@ng1)wI$NM~wy3{LIhA~s>_IeIaUUsY`e4nU~n(d6G4__P7x-$21zus_UP*Tp! z(Q9YX$zx1uxzIq|nH8T@QC}%Qqly$s z3ERJ^ z7gWK-Jok-G^ z!5aTz-QD5X0~VVwqu1$UI#PHBge@3N0&yr72Z*jHp7?`8 zvx0!H3@HB&8nuh9N<6;15TkXc`2HHAhT7@sX%I+!3)sPM6|oK5cs0-tI=V>AYgNET^nC*+RVPp!5GYnXX;>mb_!-4D;U7NjWyFM$af_);GRqc+F=^B*wcC1V(#_P#ct z7Q-p?Uf^6Ee?tW}cIm2u7pjKsKw*xwGcMC|?im2pacDwxu=l@w&(m%yIvgW39GlL3W3;!n6pl??e<&Y7-|i3KW*BIjttpJ+i0GrdM6V%A+D>jI>ROqO!<&riUP zYc#i9#VQ^}%7fijTEb=doaG38AVMQWgH21ahLbu*B5lYh$XI)UUyA{s^)olxt%Mst zxTt*mQ3R!a#(3<=^%l>*@;e=dES-?z@S}my+qL8s#r!gwMXt|d${t$A;5R_J@kwb_ z)!;eMT#_!`*Jjgu4Xuu7}^ME8Cl_d zG*O9*RZkEt=ZHf~e`B=LHuSslw+orC!V3Kp`-vmnMp98MNA0iczF!)icgMT_N z{w)_k&}`IMZg4h1nhgD}{Ma7K{RjlqOVm*Tr~C0EUBws%VIZFx#Cnm9PUgsoU_;#*jz#1soQp7Q+wDZu)a&qvCduko%HyJV~5@+{9y9-_PnSjK{mk z>i6to7f6+WEZLgpGd?a%xEiW0P}uruhJS8UXK(oDA&YdF4$;zc0PO+}fBj49-*e>W zU7%>;e}thpf6XQTb#kM>7Rb*#|DgD=M7n#HF8rc@xUk5>tfdCU^yg>&dshJ4sP>az zAMoo%e+VDmqZ>dv1OuyJM zu(QP%X-$M;>e%0_LOK)W_z&KYzeoJP$K_XP$`DzUX^vfRIkP(QnZ%bchb{gx#Gj^Y z!lVGKg1^?n&!4}-*b^;aqJNmuzlWto{8uXe^orl6-2LZz`8gwgifjVbkbj#p@FwbB w(%(x0Y04G^3JMAWu`r|E0)Aj2y%}^AMk?|IU!QCP@D~&XIc3=rX`?6q2cj5n<^TWy literal 0 HcmV?d00001 diff --git a/social_media_advertising_linkedin/tests/__init__.py b/social_media_advertising_linkedin/tests/__init__.py new file mode 100644 index 0000000000..4e47b081d8 --- /dev/null +++ b/social_media_advertising_linkedin/tests/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_common_advertising_linkedin +from . import test_social_account_advertising_linkedin +from . import test_social_campaign_linkedin +from . import test_social_campaign_group_linkedin +from . import test_social_post_advertising_linkedin diff --git a/social_media_advertising_linkedin/tests/test_common_advertising_linkedin.py b/social_media_advertising_linkedin/tests/test_common_advertising_linkedin.py new file mode 100644 index 0000000000..4dc543c444 --- /dev/null +++ b/social_media_advertising_linkedin/tests/test_common_advertising_linkedin.py @@ -0,0 +1,90 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.fields import Command + +from odoo.addons.social_media_linkedin.tests.test_common_linkedin import ( + TestSocialCommonLinkedin, +) + +PATCH_ADVERTISING_LINKEDIN = "odoo.addons.social_media_advertising_linkedin.models.{}" +PATCH_ADVERTISING_ACCOUNT_LINKEDIN = PATCH_ADVERTISING_LINKEDIN.format( + "social_account.SocialAccount.{}" +) +PATCH_ADVERTISING_CAMPAIGN_LINKEDIN = PATCH_ADVERTISING_LINKEDIN.format( + "social_campaign.SocialCampaign.{}" +) +PATCH_ADVERTISING_CAMPAIGN_GROUP_LINKEDIN = PATCH_ADVERTISING_LINKEDIN.format( + "social_campaign_group.SocialCampaignGroup.{}" +) +PATCH_ADVERTISING_POST_ACCOUNT_LINKEDIN = PATCH_ADVERTISING_LINKEDIN.format( + "social_post_account.SocialPostAccount.{}" +) + + +class TestSocialCommonAdvertisingLinkedin(TestSocialCommonLinkedin): + """Advertising fixtures shared by the LinkedIn connector tests. + + The campaigns are linked to the ``social.media`` record shipped as data + by ``social_media_linkedin`` because the LinkedIn stages of + ``social_media_advertising_linkedin`` belong to it, and the domain of + ``stage_id`` requires the campaign and the stage to share the media. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.SocialCampaign = cls.env["social.campaign"] + cls.SocialCampaignGroup = cls.env["social.campaign.group"] + cls.SocialStage = cls.env["social.stage"] + cls.media_linkedin_data_id = cls.env.ref( + "social_media_linkedin.social_media_linkedin" + ) + cls.SocialAccountLinkedin.advertising_account_id = "urn:li:sponsoredAccount:999" + + cls.SocialCampaignGroupLinkedin = cls.SocialCampaignGroup.create( + { + "name": "Campaign Group 1", + "remote_ref": "urn:li:sponsoredCampaignGroup:456", + "total_budget": 10000, + "currency_id": cls.env.ref("base.USD").id, + } + ) + + cls.SocialCampaignLinkedin = cls.SocialCampaign.create( + { + "name": "Campaign 1", + "campaign_group_id": cls.SocialCampaignGroupLinkedin.id, + "remote_ref": "urn:li:sponsoredCampaign:001", + "media_id": cls.media_linkedin_data_id.id, + "account_ids": [Command.link(cls.SocialAccountLinkedin.id)], + } + ) + + cls.SocialCampaignLinkedin2 = cls.SocialCampaign.create( + { + "name": "Campaign 2", + "campaign_group_id": cls.SocialCampaignGroupLinkedin.id, + "remote_ref": "urn:li:sponsoredCampaign:002", + } + ) + + cls.SocialPostCampaignLinkedin = cls.SocialPost.create( + { + "message": "Test Message for Campaign", + "account_ids": [Command.set(cls.SocialAccountLinkedin.ids)], + "social_campaign_id": cls.SocialCampaignLinkedin.id, + } + ) + cls.SocialPostAccountCampaignLinkedin = cls.SocialPostAccount.create( + { + "message": "Test Message for Campaign", + "account_id": cls.SocialAccountLinkedin.id, + "media_id": cls.media_linkedin_id.id, + "post_id": cls.SocialPostCampaignLinkedin.id, + } + ) + + def get_stage_linkedin(self, applies_to, code): + """Shortcut to the LinkedIn stage of a scope and code.""" + return self.SocialStage._get_stage("linkedin", applies_to, code) diff --git a/social_media_advertising_linkedin/tests/test_social_account_advertising_linkedin.py b/social_media_advertising_linkedin/tests/test_social_account_advertising_linkedin.py new file mode 100644 index 0000000000..b06f614d18 --- /dev/null +++ b/social_media_advertising_linkedin/tests/test_social_account_advertising_linkedin.py @@ -0,0 +1,965 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest.mock import MagicMock, patch + +from odoo.exceptions import UserError +from odoo.fields import Command +from odoo.tools import mute_logger + +from odoo.addons.social_media_base.social_utils import _generate_timestamps +from odoo.addons.social_media_base.tests.test_social_common import PATCH_ACCOUNT + +from .test_common_advertising_linkedin import ( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN, + TestSocialCommonAdvertisingLinkedin, +) + +PATCH_ACCOUNT_LINKEDIN = ( + "odoo.addons.social_media_linkedin.models.social_account.SocialAccount.{}" +) + + +class TestSocialAccountAdvertisingLinkedin(TestSocialCommonAdvertisingLinkedin): + def test_get_campaigns(self): + mock_response = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "elements": [ + {"id": 123, "name": "Campaign A"}, + {"id": 456, "name": "Campaign B"}, + ] + }, + } + ) + + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": mock_response, + } + ) + + with patch_request_linkedin as mock_request_linkedin: + result = self.SocialAccountLinkedin._get_campaigns( + start_date=self.start_datetime, + end_date=self.end_datetime, + ) + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["id"], 123) + self.assertEqual(result[1]["id"], 456) + mock_request_linkedin.assert_called_once() + self.assertEqual( + mock_request_linkedin.call_args.kwargs["endpoint"], + "/adAccounts/999/adCampaigns", + ) + params_values = mock_request_linkedin.call_args.kwargs["params_values"] + expected = ( + "true" if self.SocialAccountLinkedin.enviroment == "test" else "false" + ) + self.assertIn(f"test:{expected}", params_values["search"]) + + with patch_request_linkedin: + result = self.SocialAccountLinkedin._get_campaigns( + start_date=self.start_datetime, + end_date=self.end_datetime, + campaign_ids=["urn:li:sponsoredCampaign:123"], + ) + self.assertEqual( + [campaign["id"] for campaign in result], + [123], + msg="The finder has no campaign criteria, so they are " + "filtered here.", + ) + + patch_request_linkedin_failed = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": self.generate_magic_mock( + **{ + "status_code": 403, + } + ), + } + ) + with patch_request_linkedin_failed as mock_request_linkedin_failed: + with self.assertRaises(UserError): + self.SocialAccountLinkedin._get_campaigns( + start_date=self.start_datetime, + end_date=self.end_datetime, + campaign_ids=["urn:li:sponsoredCampaign:420"], + ) + mock_request_linkedin_failed.assert_called_once() + + def test_get_advertising_account_follows_the_environment(self): + """A production account never takes the test advertising account.""" + account = self.SocialAccountLinkedin + account.write({"enviroment": "production"}) + self.assertFalse( + account.advertising_account_id, + msg="Changing the environment drops the stored account.", + ) + users_response = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "paging": {"total": 2}, + "elements": [ + {"account": "urn:li:sponsoredAccount:1"}, + {"account": "urn:li:sponsoredAccount:2"}, + ], + }, + } + ) + test_account = self.generate_magic_mock( + **{"status_code": 200, "json_return_value": {"id": 1, "test": True}} + ) + production_account = self.generate_magic_mock( + **{"status_code": 200, "json_return_value": {"id": 2, "test": False}} + ) + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": account, + "method_patch": "_request_linkedin", + "side_effect": [users_response, test_account, production_account], + } + ) + with patch_request_linkedin: + self.assertEqual( + account._get_linkedin_advertising_account(), + "urn:li:sponsoredAccount:2", + ) + self.assertEqual(account.advertising_account_id, "urn:li:sponsoredAccount:2") + + def test_get_advertising_account_without_a_matching_one(self): + """Nothing is stored when no account matches the environment.""" + account = self.SocialAccountLinkedin + account.advertising_account_id = False + users_response = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": {"paging": {"total": 0}, "elements": []}, + } + ) + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": account, + "method_patch": "_request_linkedin", + "return_value": users_response, + } + ) + with patch_request_linkedin: + self.assertFalse(account._get_linkedin_advertising_account()) + self.assertFalse(account.advertising_account_id) + + def test_get_campaigns_filters_by_run_schedule(self): + """A campaign scheduled outside the requested range is dropped.""" + start_time, end_time = _generate_timestamps( + self.start_datetime, self.end_datetime + ) + mock_response = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "elements": [ + {"id": 1, "runSchedule": {"start": start_time}}, + {"id": 2, "runSchedule": {"start": end_time + 1}}, + { + "id": 3, + "runSchedule": { + "start": start_time - 2, + "end": start_time - 1, + }, + }, + {"id": 4}, + ] + }, + } + ) + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": mock_response, + } + ) + with patch_request_linkedin: + result = self.SocialAccountLinkedin._get_campaigns( + start_date=self.start_datetime, end_date=self.end_datetime + ) + self.assertEqual( + [campaign["id"] for campaign in result], + [1, 4], + msg="A campaign without run schedule is never dropped.", + ) + + def test_fetch_ad_entities_follows_the_cursor(self): + """The search finders paginate with a token, not with an index.""" + first_page = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "elements": [{"id": 1}], + "metadata": {"nextPageToken": "token-2"}, + }, + } + ) + second_page = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": {"elements": [{"id": 2}], "metadata": {}}, + } + ) + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "side_effect": [first_page, second_page], + } + ) + with patch_request_linkedin as mock_request_linkedin: + elements = self.SocialAccountLinkedin._fetch_linkedin_ad_entities( + "/adAccounts/999/adCampaigns" + ) + self.assertEqual([element["id"] for element in elements], [1, 2]) + self.assertEqual(mock_request_linkedin.call_count, 2) + self.assertEqual( + mock_request_linkedin.call_args.kwargs["params_values"]["pageToken"], + "token-2", + ) + + def test_get_statistics(self): + mock_response = self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "elements": [ + { + "campaign": "123", + "statistics": {"clickCount": 100, "impressionCount": 500}, + }, + { + "campaign": "456", + "statistics": {"clickCount": 200, "impressionCount": 600}, + }, + ] + }, + } + ) + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": mock_response, + } + ) + with patch_request_linkedin as mock_request_linkedin: + result = self.SocialAccountLinkedin._get_statistics( + ads_ids=["123", "456"], + start_date=self.start_datetime, + end_date=self.end_datetime, + ) + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["campaign"], "123") + self.assertEqual(result[1]["campaign"], "456") + mock_request_linkedin.assert_called_once() + + patch_request_linkedin_failed = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": self.generate_magic_mock( + **{ + "status_code": 403, + } + ), + } + ) + with patch_request_linkedin_failed as mock_request_linkedin_failed: + with self.assertRaises(UserError): + self.SocialAccountLinkedin._get_statistics( + ads_ids=["423", "756"], + start_date=self.start_datetime, + end_date=self.end_datetime, + ) + mock_request_linkedin_failed.assert_called_once() + + def test_get_statistics_ads(self): + ads_ids = [123, 456] + expected_result = [{"mock": "data"}] + patch_get_statistics = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccount, + "method_patch": "_get_statistics", + "return_value": expected_result, + } + ) + + with patch_get_statistics as mock_get_statistics: + result = self.SocialAccountLinkedin._get_statistics_ads( + ads_ids, self.start_datetime, self.end_datetime + ) + self.assertEqual(result, expected_result) + mock_get_statistics.assert_called_once() + + def test_load_ads(self): + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "side_effect": [ + self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "elements": [ + { + "id": "urn:li:sponsoredCreative:1", + "content": {"reference": "ref1"}, + "campaign": "urn:li:sponsoredCampaign:123", + "createdAt": 1735689600000, + "intendedStatus": "DRAFT", + "servingHoldReasons": ["UNDER_REVIEW"], + "isTest": True, + } + ] + }, + } + ), + self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "results": { + "ref1": { + "id": "ref1", + "commentary": "Test post", + } + } + }, + } + ), + ], + } + ) + patch_get_campaigns = self.generate_patch( + **{ + "model_patch": PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format( + "_get_campaigns" + ), + "return_value": [ + { + "id": 123, + "account": "urn:li:sponsoredAccount:999", + } + ], + } + ) + patch_get_statistics_ads = self.generate_patch( + **{ + "model_patch": PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format( + "_get_statistics_ads" + ), + "return_value": [ + { + "pivotValues": ["urn:li:sponsoredCreative:1"], + "clicks": 10, + } + ], + } + ) + patch_get_default_filter_date = self.generate_patch( + **{ + "model_patch": PATCH_ACCOUNT.format("_get_default_filter_date"), + "method_patch": "_get_default_filter_date", + "side_effect": ( + lambda s, e, time_date=False: ( + self.start_datetime, + self.end_datetime, + ) + if not time_date + else (self.start_datetime, self.end_datetime) + ), + } + ) + patch_ad_account = self.generate_patch( + **{ + "model_patch": PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format( + "_get_linkedin_ad_account_id" + ), + "return_value": "999", + } + ) + with ( + patch_request_linkedin as mock_request_linkedin, + patch_get_campaigns as mock_get_campaigns, + patch_get_statistics_ads as mock_get_statistics_ads, + patch_get_default_filter_date as mock_get_default_filter_date, + patch_ad_account, + ): + result = self.SocialAccountLinkedin._load_ads( + start_date=self.start_datetime, end_date=self.end_datetime + ) + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["id"], "urn:li:sponsoredCreative:1") + self.assertEqual(result[0]["post"]["name"], "Test post") + self.assertEqual(result[0]["campaign"]["id"], 123) + self.assertEqual(result[0]["statistic"]["clicks"], 10) + self.assertIn("url", result[0]) + self.assertEqual(result[0]["status"], "Draft") + self.assertEqual(result[0]["status_level"], "info") + self.assertEqual(result[0]["status_detail"], "UNDER_REVIEW") + self.assertEqual(mock_request_linkedin.call_count, 2) + mock_get_statistics_ads.assert_called_once() + mock_get_campaigns.assert_called_once() + mock_get_default_filter_date.assert_called_once() + + patch_request_linkedin_failed = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": self.generate_magic_mock(**{"status_code": 403}), + } + ) + with patch_request_linkedin_failed as mock_request_linkedin_failed: + with self.assertRaises(UserError): + self.SocialAccountLinkedin._load_ads( + start_date=self.start_datetime, end_date=self.end_datetime + ) + mock_request_linkedin_failed.assert_called_once() + + def test_load_ads_without_matching_campaign(self): + patch_request_linkedin = self.generate_patch( + **{ + "type_object": True, + "model_patch": self.SocialAccountLinkedin, + "method_patch": "_request_linkedin", + "return_value": self.generate_magic_mock( + **{ + "status_code": 200, + "json_return_value": { + "elements": [ + { + "id": "urn:li:sponsoredCreative:1", + "campaign": "urn:li:sponsoredCampaign:123", + "createdAt": 1735689600000, + "servingHoldReasons": ["STOPPED"], + "isTest": True, + } + ] + }, + } + ), + } + ) + patch_get_campaigns = patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_campaigns"), + autospec=True, + return_value=[], + ) + patch_get_statistics_ads = patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_statistics_ads"), + autospec=True, + return_value=[], + ) + patch_get_default_filter_date = self.generate_patch( + **{ + "model_patch": PATCH_ACCOUNT.format("_get_default_filter_date"), + "method_patch": "_get_default_filter_date", + "return_value": (self.start_datetime, self.end_datetime), + } + ) + patch_ad_account = self.generate_patch( + **{ + "model_patch": PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format( + "_get_linkedin_ad_account_id" + ), + "return_value": "999", + } + ) + with ( + patch_request_linkedin as mock_request_linkedin, + patch_get_campaigns as mock_get_campaigns, + patch_get_statistics_ads as mock_get_statistics_ads, + patch_get_default_filter_date as mock_get_default_filter_date, + patch_ad_account, + ): + result = self.SocialAccountLinkedin._load_ads( + start_date=self.start_datetime, end_date=self.end_datetime + ) + mock_request_linkedin.assert_called_once() + mock_get_campaigns.assert_called_once() + mock_get_statistics_ads.assert_called_once() + mock_get_default_filter_date.assert_called_once() + self.assertEqual(len(result), 1) + self.assertEqual( + result[0]["campaign"], + {}, + msg="An ad whose campaign is not returned by LinkedIn is kept.", + ) + self.assertEqual( + result[0]["status_level"], + "secondary", + msg="A status that is not mapped falls back to the neutral badge.", + ) + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_action_import_campaigns(self, mock_advertising, mock_request_linkedin): + advertising = "urn:li:sponsoredAccount:999" + mock_advertising.return_value = advertising + groups_response = MagicMock(status_code=200) + groups_response.json.return_value = { + "elements": [ + { + "id": 45, + "name": "Imported Group", + "account": advertising, + "totalBudget": {"amount": "100", "currencyCode": "USD"}, + }, + ], + "metadata": {}, + } + campaigns_response = MagicMock(status_code=200) + campaigns_response.json.return_value = { + "elements": [ + { + "id": 67, + "name": "Imported Campaign", + "account": advertising, + "campaignGroup": "urn:li:sponsoredCampaignGroup:45", + "format": "SINGLE_VIDEO", + "unitCost": {"amount": "1", "currencyCode": "USD"}, + "dailyBudget": {"amount": "10", "currencyCode": "USD"}, + }, + ], + "paging": {"total": 1}, + } + creatives_response = MagicMock(status_code=200) + creatives_response.json.return_value = { + "elements": [ + { + "id": "urn:li:sponsoredCreative:888", + "content": {"reference": "urn:li:share:5555"}, + "campaign": "urn:li:sponsoredCampaign:67", + "isTest": True, + }, + ], + "metadata": {}, + } + self.SocialPostAccountLinkedin.write({"remote_ref": "urn:li:share:5555"}) + mock_request_linkedin.side_effect = [ + groups_response, + campaigns_response, + creatives_response, + ] + res = self.SocialAccountLinkedin.action_import_campaigns() + self.assertTrue(res["success"]) + self.assertEqual(res["groups"], 1) + self.assertEqual(res["campaigns"], 1) + self.assertEqual(res["ads"], 1) + group = self.SocialCampaignGroup.search( + [("remote_ref", "=", "urn:li:sponsoredCampaignGroup:45")] + ) + self.assertEqual(group.name, "Imported Group") + self.assertEqual(group.total_budget, 100) + self.assertEqual( + mock_request_linkedin.call_args_list[0].kwargs["endpoint"], + "/adAccounts/999/adCampaignGroups", + ) + self.assertEqual( + mock_request_linkedin.call_args_list[1].kwargs["endpoint"], + "/adAccounts/999/adCampaigns", + ) + campaign = self.SocialCampaign.search( + [("remote_ref", "=", "urn:li:sponsoredCampaign:67")] + ) + self.assertEqual(campaign.campaign_group_id, group) + self.assertIn(self.SocialAccountLinkedin, campaign.account_ids) + self.assertEqual(campaign.daily_budget, 10) + self.assertEqual(campaign.linkedin_format, "SINGLE_VIDEO") + self.assertEqual( + self.SocialPostAccountLinkedin.creative_urn, + "urn:li:sponsoredCreative:888", + ) + self.assertEqual(group.campaign_count, 1) + action = group.action_view_campaigns() + self.assertEqual(action["domain"], [("campaign_group_id", "=", group.id)]) + mock_request_linkedin.side_effect = [ + groups_response, + campaigns_response, + creatives_response, + ] + res = self.SocialAccountLinkedin.action_import_campaigns() + self.assertTrue(res["success"]) + self.assertEqual(res["groups"], 0) + self.assertEqual(res["campaigns"], 0) + self.assertEqual(res["ads"], 0) + self.assertFalse(self.SocialPostAccountLinkedin.social_campaign_id) + + def _import_creatives_response(self, reference, campaign_urn): + """Return the three responses of an import with a single creative.""" + advertising = "urn:li:sponsoredAccount:999" + groups_response = MagicMock(status_code=200) + groups_response.json.return_value = { + "elements": [ + { + "id": 45, + "name": "Imported Group", + "account": advertising, + "totalBudget": {"amount": "100", "currencyCode": "USD"}, + } + ], + "paging": {"total": 1}, + } + campaigns_response = MagicMock(status_code=200) + campaigns_response.json.return_value = { + "elements": [ + { + "id": 67, + "name": "Imported Campaign", + "account": advertising, + "campaignGroup": "urn:li:sponsoredCampaignGroup:45", + "format": "STANDARD_UPDATE", + "unitCost": {"amount": "1", "currencyCode": "USD"}, + "dailyBudget": {"amount": "10", "currencyCode": "USD"}, + } + ], + "paging": {"total": 1}, + } + creatives_response = MagicMock(status_code=200) + creatives_response.json.return_value = { + "elements": [ + { + "id": "urn:li:sponsoredCreative:888", + "content": {"reference": reference}, + "campaign": campaign_urn, + "isTest": True, + } + ], + "metadata": {}, + } + return [groups_response, campaigns_response, creatives_response] + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_import_campaigns_links_the_campaign_of_an_imported_post( + self, mock_advertising, mock_request_linkedin + ): + """A publication brought from the wall gets its campaign here. + + It has no parent post, so the campaign can only come from the + creative that promotes it. + """ + mock_advertising.return_value = "urn:li:sponsoredAccount:999" + imported = self.SocialPostAccount.create( + { + "account_id": self.SocialAccountLinkedin.id, + "message": "Imported from the wall", + "remote_ref": "urn:li:share:9999", + "state": "posted", + } + ) + self.assertFalse(imported.post_id) + responses = self._import_creatives_response( + "urn:li:share:9999", "urn:li:sponsoredCampaign:67" + ) + mock_request_linkedin.side_effect = responses + res = self.SocialAccountLinkedin.action_import_campaigns() + self.assertEqual(res["ads"], 1) + self.assertEqual( + imported.social_campaign_id.remote_ref, "urn:li:sponsoredCampaign:67" + ) + self.assertEqual(imported.creative_urn, "urn:li:sponsoredCreative:888") + mock_request_linkedin.side_effect = self._import_creatives_response( + "urn:li:share:9999", "urn:li:sponsoredCampaign:67" + ) + res = self.SocialAccountLinkedin.action_import_campaigns() + self.assertEqual(res["ads"], 0) + self.assertEqual( + imported.social_campaign_id.remote_ref, "urn:li:sponsoredCampaign:67" + ) + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_import_campaigns_ignores_an_unknown_campaign( + self, mock_advertising, mock_request_linkedin + ): + mock_advertising.return_value = "urn:li:sponsoredAccount:999" + imported = self.SocialPostAccount.create( + { + "account_id": self.SocialAccountLinkedin.id, + "message": "Imported from the wall", + "remote_ref": "urn:li:share:8888", + "state": "posted", + } + ) + mock_request_linkedin.side_effect = self._import_creatives_response( + "urn:li:share:8888", "urn:li:sponsoredCampaign:404" + ) + res = self.SocialAccountLinkedin.action_import_campaigns() + self.assertTrue(res["success"]) + self.assertFalse(imported.social_campaign_id) + self.assertEqual(imported.creative_urn, "urn:li:sponsoredCreative:888") + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_campaign_pending_changes_hybrid_import( + self, mock_advertising, mock_request_linkedin + ): + advertising = "urn:li:sponsoredAccount:999" + mock_advertising.return_value = advertising + currency_usd = self.env.ref("base.USD") + group = self.SocialCampaignGroup.create( + { + "name": "Synced Group", + "remote_ref": "urn:li:sponsoredCampaignGroup:70", + "total_budget": 100, + "currency_id": currency_usd.id, + } + ) + campaign = self.SocialCampaign.create( + { + "name": "Synced Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "unit_cost": 1, + "daily_budget": 10, + "remote_ref": "urn:li:sponsoredCampaign:71", + } + ) + self.assertFalse(campaign.linkedin_needs_update) + self.assertFalse(group.linkedin_needs_update) + campaign.write({"unit_cost": 99}) + group.write({"total_budget": 500}) + self.assertTrue(campaign.linkedin_needs_update) + self.assertTrue(group.linkedin_needs_update) + groups_response = MagicMock(status_code=200) + groups_response.json.return_value = { + "elements": [ + { + "id": 70, + "name": "Renamed Group In LinkedIn", + "account": advertising, + "status": "ACTIVE", + "totalBudget": {"amount": "300", "currencyCode": "USD"}, + }, + ], + "paging": {"total": 1}, + } + campaigns_response = MagicMock(status_code=200) + campaigns_response.json.return_value = { + "elements": [ + { + "id": 71, + "name": "Renamed Campaign In LinkedIn", + "account": advertising, + "campaignGroup": "urn:li:sponsoredCampaignGroup:70", + "status": "PAUSED", + "test": True, + "unitCost": {"amount": "5", "currencyCode": "USD"}, + "dailyBudget": {"amount": "50", "currencyCode": "USD"}, + }, + ], + "paging": {"total": 1}, + } + creatives_response = MagicMock(status_code=200) + creatives_response.json.return_value = {"elements": [], "paging": {"total": 0}} + mock_request_linkedin.side_effect = [ + groups_response, + campaigns_response, + creatives_response, + ] + campaign_messages = len(campaign.message_ids) + group_messages = len(group.message_ids) + self.SocialAccountLinkedin.action_import_campaigns() + self.assertEqual(campaign.unit_cost, 99) + self.assertEqual(campaign.name, "Synced Campaign") + self.assertEqual(campaign.stage_id.code, "PAUSED") + self.assertTrue(campaign.linkedin_is_test) + self.assertTrue(campaign.linkedin_needs_update) + self.assertEqual(group.total_budget, 500) + self.assertEqual(group.name, "Synced Group") + self.assertEqual(group.stage_id.code, "ACTIVE") + self.assertTrue(group.linkedin_needs_update) + self.assertEqual(len(campaign.message_ids), campaign_messages + 1) + self.assertEqual(len(group.message_ids), group_messages + 1) + mock_request_linkedin.side_effect = None + mock_request_linkedin.return_value = MagicMock(status_code=204) + campaign.action_update_linkedin() + payload = mock_request_linkedin.call_args.kwargs["json_data"] + self.assertEqual(payload["patch"]["$set"]["unitCost"]["amount"], "99.0") + self.assertEqual( + payload["patch"]["$set"]["campaignGroup"], + "urn:li:sponsoredCampaignGroup:70", + ) + self.assertFalse(campaign.linkedin_needs_update) + group.action_update_linkedin() + payload = mock_request_linkedin.call_args.kwargs["json_data"] + self.assertEqual(payload["patch"]["$set"]["totalBudget"]["amount"], "500.0") + self.assertFalse(group.linkedin_needs_update) + + def test_deleted_on_linkedin_history_message(self): + currency_usd = self.env.ref("base.USD") + group = self.SocialCampaignGroup.create( + { + "name": "History Group", + "remote_ref": "urn:li:sponsoredCampaignGroup:90", + "total_budget": 100, + "currency_id": currency_usd.id, + "stage_id": self.get_stage_linkedin("group", "ACTIVE").id, + } + ) + campaign = self.SocialCampaign.create( + { + "name": "History Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "unit_cost": 1, + "daily_budget": 10, + "remote_ref": "urn:li:sponsoredCampaign:91", + "stage_id": self.get_stage_linkedin("campaign", "ACTIVE").id, + } + ) + group_messages = len(group.message_ids) + campaign_messages = len(campaign.message_ids) + elements_group = [ + { + "id": 90, + "name": "History Group", + "status": "PENDING_DELETION", + "totalBudget": {"amount": "100", "currencyCode": "USD"}, + } + ] + elements_campaign = [ + { + "id": 91, + "name": "History Campaign", + "status": "REMOVED", + "campaignGroup": "urn:li:sponsoredCampaignGroup:90", + "unitCost": {"amount": "1", "currencyCode": "USD"}, + "dailyBudget": {"amount": "10", "currencyCode": "USD"}, + } + ] + self.SocialAccountLinkedin._upsert_linkedin_campaigns( + elements_group, elements_campaign + ) + self.assertEqual(group.stage_id.code, "PENDING_DELETION") + self.assertEqual(campaign.stage_id.code, "REMOVED") + self.assertEqual(len(group.message_ids), group_messages + 1) + self.assertEqual(len(campaign.message_ids), campaign_messages + 1) + self.SocialAccountLinkedin._upsert_linkedin_campaigns( + elements_group, elements_campaign + ) + self.assertEqual(len(group.message_ids), group_messages + 1) + self.assertEqual(len(campaign.message_ids), campaign_messages + 1) + + def test_upsert_linkedin_campaigns_updates_the_archived_records(self): + """An archived record still owns its URN: never create it again.""" + currency_usd = self.env.ref("base.USD") + group = self.SocialCampaignGroup.create( + { + "name": "Archived Group", + "remote_ref": "urn:li:sponsoredCampaignGroup:95", + "total_budget": 100, + "currency_id": currency_usd.id, + } + ) + campaign = self.SocialCampaign.create( + { + "name": "Archived Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "remote_ref": "urn:li:sponsoredCampaign:96", + } + ) + group.write({"active": False}) + campaign.write({"active": False}) + counts = self.SocialAccountLinkedin._upsert_linkedin_campaigns( + [ + { + "id": 95, + "name": "Archived Group Renamed", + "status": "ACTIVE", + "totalBudget": {"amount": "150", "currencyCode": "USD"}, + } + ], + [ + { + "id": 96, + "name": "Archived Campaign Renamed", + "status": "ACTIVE", + "campaignGroup": "urn:li:sponsoredCampaignGroup:95", + "unitCost": {"amount": "2", "currencyCode": "USD"}, + "dailyBudget": {"amount": "20", "currencyCode": "USD"}, + } + ], + ) + self.assertEqual(counts, {"groups": 0, "campaigns": 0}) + self.assertEqual(group.name, "Archived Group Renamed") + self.assertEqual(group.total_budget, 150) + self.assertEqual(campaign.name, "Archived Campaign Renamed") + self.assertEqual(campaign.daily_budget, 20) + self.assertFalse(group.active) + self.assertFalse(campaign.active) + + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_load_ads")) + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("search")) + def test_load_ads_accounts(self, mock_search, mock_load_ads): + ads_linkedin = [ + { + "media_type": "linkedin", + "statistic": {"clicks": 10}, + } + ] + mock_load_ads.return_value = ads_linkedin + mock_search.return_value = self.SocialAccountLinkedin + res = self.SocialAccount._load_ads_accounts() + self.assertEqual(mock_search.call_count, 1) + self.assertEqual(res["ads"], ads_linkedin) + + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_load_ads")) + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("search")) + def test_load_ads_accounts_without_accounts(self, mock_search, mock_load_ads): + """With no LinkedIn account the payload keeps ``ads`` as a list.""" + mock_search.return_value = self.SocialAccount.browse() + res = self.SocialAccount._load_ads_accounts() + self.assertEqual(res["ads"], []) + mock_load_ads.assert_not_called() + + @mute_logger("odoo.addons.social_media_advertising_linkedin.models.social_account") + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_load_ads")) + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("search")) + def test_load_ads_accounts_skips_the_failing_account( + self, mock_search, mock_load_ads + ): + """A revoked token must not break the whole ads dashboard.""" + mock_load_ads.side_effect = UserError("Token revoked") + mock_search.return_value = self.SocialAccountLinkedin + res = self.SocialAccount._load_ads_accounts() + self.assertEqual(res["ads"], []) diff --git a/social_media_advertising_linkedin/tests/test_social_campaign_group_linkedin.py b/social_media_advertising_linkedin/tests/test_social_campaign_group_linkedin.py new file mode 100644 index 0000000000..765fa2c906 --- /dev/null +++ b/social_media_advertising_linkedin/tests/test_social_campaign_group_linkedin.py @@ -0,0 +1,145 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest.mock import MagicMock, patch + +from odoo.exceptions import UserError +from odoo.fields import Command + +from .test_common_advertising_linkedin import ( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN, + PATCH_ADVERTISING_CAMPAIGN_GROUP_LINKEDIN, + TestSocialCommonAdvertisingLinkedin, +) + +PATCH_ACCOUNT_LINKEDIN = ( + "odoo.addons.social_media_linkedin.models.social_account.SocialAccount.{}" +) + + +class TestSocialCampaignGroupLinkedin(TestSocialCommonAdvertisingLinkedin): + def test_get_linkedin_account(self): + """The account is taken from the campaigns before the fallback.""" + self.assertEqual( + self.SocialCampaignGroupLinkedin._get_linkedin_account(), + self.SocialAccountLinkedin, + ) + + def test_get_linkedin_account_without_campaigns(self): + """Without campaigns the fallback only works with a single account.""" + group = self.SocialCampaignGroup.create({"name": "Standalone Group"}) + accounts = self.SocialAccount.search([("media_type", "=", "linkedin")]) + self.assertGreater(len(accounts), 1) + with self.assertRaises( + UserError, msg="Guessing the advertiser is never acceptable." + ): + group._get_linkedin_account() + (accounts - self.SocialAccountLinkedin).write({"active": False}) + self.assertEqual(group._get_linkedin_account(), self.SocialAccountLinkedin) + + def test_get_linkedin_account_with_several_campaign_accounts(self): + self.SocialCampaign.create( + { + "name": "Campaign of another advertiser", + "campaign_group_id": self.SocialCampaignGroupLinkedin.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedinData.id)], + } + ) + with self.assertRaises(UserError): + self.SocialCampaignGroupLinkedin._get_linkedin_account() + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_group_publish_linkedin(self, mock_advertising, mock_request_linkedin): + mock_advertising.return_value = "urn:li:sponsoredAccount:999" + group = self.SocialCampaignGroup.create( + { + "name": "Standalone Group", + "total_budget": 100, + "currency_id": self.env.ref("base.USD").id, + } + ) + self.SocialCampaign.create( + { + "name": "Campaign of the standalone group", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + } + ) + mock_request_linkedin.return_value = MagicMock( + status_code=201, + headers={"Location": "/adAccounts/999/adCampaignGroups/555"}, + ) + group.action_publish_linkedin() + self.assertEqual(group.remote_ref, "urn:li:sponsoredCampaignGroup:555") + self.assertEqual(group.stage_id.code, "DRAFT") + self.assertFalse(group.linkedin_needs_update) + payload = mock_request_linkedin.call_args.kwargs["json_data"] + self.assertEqual(payload["status"], "DRAFT") + self.assertEqual(payload["totalBudget"]["amount"], "100.0") + with self.assertRaises(UserError): + group.action_publish_linkedin() + empty_group = self.SocialCampaignGroup.create({"name": "Empty Group"}) + with self.assertRaises(UserError): + empty_group.action_publish_linkedin() + + def test_group_archive_linkedin(self): + group = self.SocialCampaignGroupLinkedin + messages = len(group.message_ids) + with self._mock_linkedin( + MagicMock(status_code=204), self.SocialAccountLinkedin + ) as mock_request: + group.action_archive_linkedin() + self.assertEqual(group.stage_id.code, "ARCHIVED") + self.assertTrue(group.linkedin_locked) + self.assertFalse(group.linkedin_needs_update) + self.assertEqual(len(group.message_ids), messages + 1) + self.assertEqual( + mock_request.call_args.kwargs["json_data"], + {"patch": {"$set": {"status": "ARCHIVED"}}}, + ) + self.assertEqual( + mock_request.call_args.kwargs["endpoint"], + "/adAccounts/999/adCampaignGroups/456", + ) + with self.assertRaises( + UserError, msg="An archived campaign group is read only on LinkedIn." + ): + group.action_archive_linkedin() + + def test_group_archive_linkedin_errors(self): + group = self.SocialCampaignGroup.create( + { + "name": "Group Without Urn", + "total_budget": 100, + "currency_id": self.env.ref("base.USD").id, + } + ) + self.SocialCampaign.create( + { + "name": "Campaign of the group", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + } + ) + with self.assertRaises(UserError): + group.action_archive_linkedin() + group.write({"remote_ref": "urn:li:sponsoredCampaignGroup:457"}) + with patch( + PATCH_ADVERTISING_CAMPAIGN_GROUP_LINKEDIN.format("_get_linkedin_account"), + autospec=True, + return_value=self.env["social.account"], + ): + with self.assertRaises(UserError): + group.action_archive_linkedin() + error_response = MagicMock(status_code=400) + error_response.json.return_value = {"message": "Cannot archive"} + with self._mock_linkedin(error_response, self.SocialAccountLinkedin): + with self.assertRaises(UserError): + group.action_archive_linkedin() + self.assertNotEqual(group.stage_id.code, "ARCHIVED") diff --git a/social_media_advertising_linkedin/tests/test_social_campaign_linkedin.py b/social_media_advertising_linkedin/tests/test_social_campaign_linkedin.py new file mode 100644 index 0000000000..ec287fe750 --- /dev/null +++ b/social_media_advertising_linkedin/tests/test_social_campaign_linkedin.py @@ -0,0 +1,482 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest.mock import MagicMock, patch + +from odoo.exceptions import UserError, ValidationError +from odoo.fields import Command + +from odoo.addons.social_media_base.tests.test_social_common import ( + PATCH_SOCIAL_BASE_UTILS, +) + +from .test_common_advertising_linkedin import ( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN, + TestSocialCommonAdvertisingLinkedin, +) + +PATCH_ACCOUNT_LINKEDIN = ( + "odoo.addons.social_media_linkedin.models.social_account.SocialAccount.{}" +) +ADVERTISING_ACCOUNT_URN = "urn:li:sponsoredAccount:999" + + +class TestSocialCampaignLinkedin(TestSocialCommonAdvertisingLinkedin): + def test_compute_media_id(self): + campaign = self.SocialCampaign.create( + { + "name": "Test Campaign", + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "campaign_group_id": self.SocialCampaignGroupLinkedin.id, + } + ) + self.assertIn(self.SocialAccountLinkedin.media_id, campaign.allow_media_ids) + + def test_check_linkedin_single_account(self): + """A LinkedIn campaign refuses a second LinkedIn account.""" + second_account = self.SocialAccountLinkedin.copy( + {"name": "Second LinkedIn account", "remote_ref": "urn:li:organization:2"} + ) + with self.assertRaises(ValidationError): + self.SocialCampaignLinkedin.account_ids = [Command.link(second_account.id)] + + def test_validate_publish_linkedin_video_objective(self): + """LinkedIn requires an objective to create a video campaign.""" + campaign = self.SocialCampaignLinkedin + campaign.write( + { + "linkedin_format": "SINGLE_VIDEO", + "linkedin_objective": False, + "unit_cost": 2, + "daily_budget": 20, + } + ) + with self.assertRaises(UserError) as context: + campaign._validate_publish_linkedin() + self.assertIn("requires an objective", str(context.exception)) + + campaign.linkedin_objective = "VIDEO_VIEW" + campaign._validate_publish_linkedin() + + def test_linkedin_create_campaign_sends_format(self): + """The ad format travels to LinkedIn, which fixes it on creation.""" + campaign = self.SocialCampaignLinkedin + campaign.linkedin_format = "SINGLE_VIDEO" + campaign.linkedin_objective = "VIDEO_VIEW" + response = MagicMock(status_code=201) + response.headers = {"Location": "/adAccounts/999/adCampaigns/321"} + patch_request_linkedin = self.get_patch_exceptions_linkedin(response) + with patch_request_linkedin as mock_request_linkedin: + res = campaign._linkedin_create_campaign( + self.SocialAccountLinkedin, + "urn:li:sponsoredAccount:999", + "urn:li:sponsoredCampaignGroup:456", + ) + self.assertEqual(res, "urn:li:sponsoredCampaign:321") + self.assertEqual(campaign.stage_id.code, "DRAFT") + json_data = mock_request_linkedin.call_args.kwargs["json_data"] + self.assertEqual(json_data["format"], "SINGLE_VIDEO") + self.assertEqual(json_data["objectiveType"], "VIDEO_VIEW") + + def test_check_daily_budget(self): + with self.assertRaises(ValidationError): + self.SocialCampaignLinkedin.daily_budget = 5000 + self.SocialCampaignLinkedin2.daily_budget = 5001 + + def test_check_daily_budget_without_a_total_budget(self): + """A group without total budget sets no limit, as on LinkedIn.""" + self.SocialCampaignGroupLinkedin.with_context( + skip_linkedin_needs_update=True + ).total_budget = 0 + self.SocialCampaignLinkedin.daily_budget = 5000 + self.SocialCampaignLinkedin2.daily_budget = 5001 + self.assertEqual(self.SocialCampaignLinkedin2.daily_budget, 5001) + + def test_check_daily_budget_skipped_on_import(self): + """The import is never stopped by the local budget rule.""" + self.SocialCampaignLinkedin.with_context(skip_linkedin_budget_check=True).write( + {"daily_budget": 5000} + ) + self.SocialCampaignLinkedin2.with_context( + skip_linkedin_budget_check=True + ).write({"daily_budget": 5001}) + self.assertEqual(self.SocialCampaignLinkedin2.daily_budget, 5001) + + def test_check_daily_budget_lowering_the_total_budget(self): + """The rule is watched from the group side too.""" + self.SocialCampaignLinkedin.daily_budget = 6000 + with self.assertRaises(ValidationError): + self.SocialCampaignGroupLinkedin.with_context( + skip_linkedin_needs_update=True + ).total_budget = 5000 + + def test_check_daily_budget_moving_the_campaign_to_another_group(self): + small_group = self.SocialCampaignGroup.create( + { + "name": "Small Group", + "total_budget": 50, + "currency_id": self.env.ref("base.USD").id, + } + ) + self.SocialCampaignLinkedin.daily_budget = 100 + with self.assertRaises(ValidationError): + self.SocialCampaignLinkedin.with_context( + skip_linkedin_needs_update=True + ).campaign_group_id = small_group + + def test_linkedin_lock_covers_the_media(self): + """Taking the campaign out of LinkedIn used to escape the lock.""" + campaign = self.SocialCampaignLinkedin + campaign.with_context( + skip_linkedin_needs_update=True + ).stage_id = self.get_stage_linkedin("campaign", "ARCHIVED") + self.assertTrue(campaign.linkedin_locked) + with self.assertRaises(UserError): + campaign.media_id = False + + def test_action_publish_linkedin_validation(self): + group = self.SocialCampaignGroup.create({"name": "Test Group"}) + campaign = self.SocialCampaign.create( + { + "name": "Test Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + } + ) + with self.assertRaises(UserError) as context: + campaign.action_publish_linkedin() + self.assertIn("total budget must be positive", str(context.exception)) + self.assertIn("unit cost must be positive", str(context.exception)) + self.assertIn("daily budget must be positive", str(context.exception)) + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_action_publish_linkedin( + self, mock_advertising_account, mock_request_linkedin + ): + currency = self.env.ref("base.USD") + group = self.SocialCampaignGroup.create( + {"name": "Test Group", "total_budget": 100, "currency_id": currency.id} + ) + campaign = self.SocialCampaign.create( + { + "name": "Test Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "unit_cost": 1, + "daily_budget": 10, + } + ) + mock_advertising_account.return_value = "urn:li:sponsoredAccount:999" + mock_request_linkedin.side_effect = [ + MagicMock( + status_code=201, + headers={"Location": "/adAccounts/999/adCampaignGroups/45"}, + ), + MagicMock( + status_code=201, headers={"Location": "/adAccounts/999/adCampaigns/67"} + ), + ] + campaign.action_publish_linkedin() + self.assertEqual( + campaign.campaign_group_id.remote_ref, + "urn:li:sponsoredCampaignGroup:45", + ) + self.assertEqual(campaign.remote_ref, "urn:li:sponsoredCampaign:67") + self.assertEqual(campaign.stage_id.code, "DRAFT") + self.assertEqual(campaign.campaign_group_id.stage_id.code, "DRAFT") + for call in mock_request_linkedin.call_args_list: + self.assertEqual(call.kwargs["json_data"]["status"], "DRAFT") + + def test_campaign_locked_linkedin_statuses(self): + currency_usd = self.env.ref("base.USD") + group = self.SocialCampaignGroup.create( + { + "name": "Locked Group", + "remote_ref": "urn:li:sponsoredCampaignGroup:80", + "total_budget": 100, + "currency_id": currency_usd.id, + } + ) + campaign = self.SocialCampaign.create( + { + "name": "Locked Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "unit_cost": 1, + "daily_budget": 10, + "remote_ref": "urn:li:sponsoredCampaign:81", + } + ) + campaign.stage_id = self.get_stage_linkedin("campaign", "CANCELED") + group.stage_id = self.get_stage_linkedin("group", "PENDING_DELETION") + with self.assertRaises(UserError): + campaign.write({"unit_cost": 5}) + with self.assertRaises(UserError): + campaign.action_update_linkedin() + with self.assertRaises(UserError): + group.write({"total_budget": 200}) + with self.assertRaises(UserError): + group.action_update_linkedin() + campaign.with_context(skip_linkedin_needs_update=True).write({"unit_cost": 5}) + group.with_context(skip_linkedin_needs_update=True).write({"total_budget": 200}) + self.assertEqual(campaign.unit_cost, 5) + self.assertEqual(group.total_budget, 200) + campaign.stage_id = self.get_stage_linkedin("campaign", "PAUSED") + campaign.write({"unit_cost": 7}) + self.assertTrue(campaign.linkedin_needs_update) + + def test_campaign_archive_linkedin_draft_group(self): + """LinkedIn refuses to archive a campaign of a draft group.""" + campaign = self.SocialCampaignLinkedin + campaign.campaign_group_id.stage_id = self.get_stage_linkedin("group", "DRAFT") + with self.get_patch_exceptions_linkedin() as mock_request: + with self.assertRaises(UserError) as error: + campaign.action_archive_linkedin() + self.assertIn("still in draft status", str(error.exception)) + mock_request.assert_not_called() + + def test_campaign_archive_linkedin_refreshes_the_schedule(self): + """A draft campaign travels with a new schedule LinkedIn accepts. + + Its stored start date is the one given on creation, which LinkedIn + rejects as soon as it belongs to the past. + """ + campaign = self.SocialCampaignLinkedin + campaign.stage_id = self.get_stage_linkedin("campaign", "DRAFT") + with self._mock_linkedin( + MagicMock(status_code=204), self.SocialAccountLinkedin + ) as mock_request: + campaign.action_archive_linkedin() + values = mock_request.call_args.kwargs["json_data"]["patch"]["$set"] + self.assertEqual(values["status"], "ARCHIVED") + self.assertGreater(values["runSchedule"]["start"], 0) + self.assertGreater(values["runSchedule"]["end"], values["runSchedule"]["start"]) + self.assertEqual(campaign.stage_id.code, "ARCHIVED") + + def test_campaign_archive_linkedin(self): + campaign = self.SocialCampaignLinkedin + messages = len(campaign.message_ids) + with self._mock_linkedin( + MagicMock(status_code=204), self.SocialAccountLinkedin + ) as mock_request: + campaign.action_archive_linkedin() + self.assertEqual(campaign.stage_id.code, "ARCHIVED") + self.assertTrue(campaign.linkedin_locked) + self.assertFalse(campaign.linkedin_needs_update) + self.assertEqual(len(campaign.message_ids), messages + 1) + self.assertEqual( + mock_request.call_args.kwargs["json_data"], + {"patch": {"$set": {"status": "ARCHIVED"}}}, + ) + self.assertEqual( + mock_request.call_args.kwargs["endpoint"], "/adAccounts/999/adCampaigns/001" + ) + with self.assertRaises( + UserError, msg="An archived campaign is read only on LinkedIn." + ): + campaign.action_archive_linkedin() + + def test_campaign_archive_linkedin_errors(self): + campaign = self.SocialCampaign.create( + { + "name": "Campaign Without Urn", + "campaign_group_id": self.SocialCampaignGroupLinkedin.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + } + ) + with self.assertRaises(UserError): + campaign.action_archive_linkedin() + campaign.write({"remote_ref": "urn:li:sponsoredCampaign:003"}) + campaign.account_ids = [Command.clear()] + with self.assertRaises(UserError): + campaign.action_archive_linkedin() + campaign.account_ids = [Command.link(self.SocialAccountLinkedin.id)] + error_response = MagicMock(status_code=400) + error_response.json.return_value = {"message": "Cannot archive"} + with self._mock_linkedin(error_response, self.SocialAccountLinkedin): + with self.assertRaises(UserError): + campaign.action_archive_linkedin() + self.assertNotEqual(campaign.stage_id.code, "ARCHIVED") + + def test_archived_stage_locks_writes(self): + """A campaign archived on LinkedIn refuses local sync changes.""" + campaign = self.SocialCampaignLinkedin + campaign.stage_id = self.get_stage_linkedin("campaign", "ARCHIVED") + self.assertTrue(campaign.linkedin_locked) + with self.assertRaises(UserError): + campaign.write({"name": "Renamed"}) + + def test_publish_campaign_group_without_advertising_account(self): + """Without an advertising account nothing is sent to LinkedIn.""" + with self.get_patch_exceptions_linkedin() as mock_request: + self.assertFalse( + self.SocialCampaignLinkedin._linkedin_publish_campaign_group( + self.SocialAccountLinkedin, False + ) + ) + mock_request.assert_not_called() + + def test_publish_campaign_group_already_created(self): + """An existing group is only verified, never created again.""" + with self.get_patch_exceptions_linkedin( + MagicMock(status_code=200) + ) as mock_request: + urn = self.SocialCampaignLinkedin._linkedin_publish_campaign_group( + self.SocialAccountLinkedin, ADVERTISING_ACCOUNT_URN + ) + self.assertEqual(urn, self.SocialCampaignGroupLinkedin.remote_ref) + mock_request.assert_called_once() + + def test_publish_campaign_group_creates_the_missing_one(self): + """A URN unknown to LinkedIn is replaced by a newly created group.""" + patch_request = self.get_patch_exceptions_linkedin( + side_effect=[ + MagicMock(status_code=404), + MagicMock( + status_code=201, + headers={"Location": "/adAccounts/999/adCampaignGroups/456"}, + ), + ] + ) + with patch( + PATCH_SOCIAL_BASE_UTILS.format("_generate_timestamps"), + autospec=True, + return_value=(111111, 222222), + ), patch_request as mock_request: + urn = self.SocialCampaignLinkedin._linkedin_publish_campaign_group( + self.SocialAccountLinkedin, ADVERTISING_ACCOUNT_URN + ) + self.assertEqual(urn, "urn:li:sponsoredCampaignGroup:456") + self.assertEqual( + self.SocialCampaignGroupLinkedin.remote_ref, + "urn:li:sponsoredCampaignGroup:456", + ) + self.assertEqual(mock_request.call_count, 2) + + def test_publish_campaign_group_creation_error(self): + """The creation error of the group is reported to the user.""" + patch_request = self.get_patch_exceptions_linkedin( + side_effect=[ + MagicMock(status_code=404), + MagicMock(status_code=400, headers={"error": "Invalid request"}), + ] + ) + with patch_request as mock_request: + with self.assertRaises(UserError) as error: + self.SocialCampaignLinkedin._linkedin_publish_campaign_group( + self.SocialAccountLinkedin, ADVERTISING_ACCOUNT_URN + ) + self.assertIn("Error creating group campaign in Linkedin", str(error.exception)) + self.assertEqual(mock_request.call_count, 2) + + def test_validate_publish_linkedin_political_intent(self): + """LinkedIn requires the political advertising declaration.""" + campaign = self.SocialCampaignLinkedin + campaign.write( + { + "linkedin_political_intent": False, + "unit_cost": 2, + "daily_budget": 20, + } + ) + with self.assertRaises(UserError) as context: + campaign._validate_publish_linkedin() + self.assertIn("political advertising", str(context.exception)) + + campaign.linkedin_political_intent = "NOT_POLITICAL" + campaign._validate_publish_linkedin() + + def test_linkedin_create_campaign_sends_political_intent(self): + """The declaration travels to LinkedIn, which rejects it otherwise.""" + campaign = self.SocialCampaignLinkedin + self.assertEqual(campaign.linkedin_political_intent, "NOT_POLITICAL") + response = MagicMock(status_code=201) + response.headers = {"Location": "/adAccounts/999/adCampaigns/321"} + with self.get_patch_exceptions_linkedin(response) as mock_request_linkedin: + campaign._linkedin_create_campaign( + self.SocialAccountLinkedin, + ADVERTISING_ACCOUNT_URN, + "urn:li:sponsoredCampaignGroup:456", + ) + json_data = mock_request_linkedin.call_args.kwargs["json_data"] + self.assertEqual(json_data["politicalIntent"], "NOT_POLITICAL") + + def test_action_update_linkedin_sends_political_intent(self): + """A local change of the declaration is pushed to LinkedIn.""" + campaign = self.SocialCampaignLinkedin + campaign.linkedin_political_intent = "POLITICAL" + self.assertTrue(campaign.linkedin_needs_update) + with self._mock_linkedin( + MagicMock(status_code=204), self.SocialAccountLinkedin + ) as mock_request: + campaign.action_update_linkedin() + values = mock_request.call_args.kwargs["json_data"]["patch"]["$set"] + self.assertEqual(values["politicalIntent"], "POLITICAL") + self.assertFalse(campaign.linkedin_needs_update) + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + @patch( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_advertising_account") + ) + def test_action_publish_linkedin_keeps_the_group_reference( + self, mock_advertising_account, mock_request_linkedin + ): + """A campaign failure must not lose the group created on LinkedIn. + + LinkedIn cannot delete a campaign group, so its reference has to + survive the failure or the next import would duplicate it. + """ + currency = self.env.ref("base.USD") + group = self.SocialCampaignGroup.create( + {"name": "Test Group", "total_budget": 100, "currency_id": currency.id} + ) + campaign = self.SocialCampaign.create( + { + "name": "Test Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "unit_cost": 1, + "daily_budget": 10, + } + ) + mock_advertising_account.return_value = ADVERTISING_ACCOUNT_URN + error_response = MagicMock(status_code=400) + error_response.json.return_value = {"message": "Invalid campaign"} + mock_request_linkedin.side_effect = [ + MagicMock( + status_code=201, + headers={"Location": "/adAccounts/999/adCampaignGroups/45"}, + ), + error_response, + ] + messages = len(campaign.message_ids) + action = campaign.action_publish_linkedin() + self.assertEqual(action["params"]["type"], "danger") + self.assertIn( + "Error creating campaign in Linkedin", action["params"]["message"] + ) + self.assertEqual(len(campaign.message_ids), messages + 1) + self.assertEqual(group.remote_ref, "urn:li:sponsoredCampaignGroup:45") + self.assertEqual(group.stage_id.code, "DRAFT") + self.assertFalse(campaign.remote_ref) + + def test_publish_campaign_group_verification_error(self): + """An unexpected answer while verifying the group is reported too.""" + patch_request = self.get_patch_exceptions_linkedin( + side_effect=[MagicMock(status_code=400)] + ) + with patch_request as mock_request: + with self.assertRaises(UserError) as error: + self.SocialCampaignLinkedin._linkedin_publish_campaign_group( + self.SocialAccountLinkedin, ADVERTISING_ACCOUNT_URN + ) + self.assertIn("Error creating group campaign in Linkedin", str(error.exception)) + mock_request.assert_called_once() diff --git a/social_media_advertising_linkedin/tests/test_social_post_advertising_linkedin.py b/social_media_advertising_linkedin/tests/test_social_post_advertising_linkedin.py new file mode 100644 index 0000000000..a1deabe357 --- /dev/null +++ b/social_media_advertising_linkedin/tests/test_social_post_advertising_linkedin.py @@ -0,0 +1,350 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest.mock import MagicMock, patch + +from odoo import Command +from odoo.exceptions import UserError +from odoo.tools import mute_logger + +from odoo.addons.social_media_linkedin.tests.test_common_linkedin import ( + PATCH_ACCOUNT_LINKEDIN, +) + +from .test_common_advertising_linkedin import ( + PATCH_ADVERTISING_ACCOUNT_LINKEDIN, + TestSocialCommonAdvertisingLinkedin, +) + +LOGGER_POST_ACCOUNT_ADVERTISING_LINKEDIN = ( + "odoo.addons.social_media_advertising_linkedin.models.social_post_account" +) +LOGGER_POST_ACCOUNT_BASE = "odoo.addons.social_media_base.models.social_post_account" + + +class TestSocialPostAdvertisingLinkedin(TestSocialCommonAdvertisingLinkedin): + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + def test_linkedin_advertising_accounts_success(self, mock_request_linkedin): + users_response = MagicMock(status_code=200) + users_response.json.return_value = { + "paging": {"total": 1}, + "elements": [ + { + "account": "urn:li:sponsoredAccount:123", + "role": "ACCOUNT_MANAGER", + } + ], + } + account_response = MagicMock(status_code=200) + account_response.json.return_value = {"id": 123, "test": True} + mock_request_linkedin.side_effect = [users_response, account_response] + self.SocialAccountLinkedin.advertising_account_id = False + ad_account_id = self.SocialPostAccountLinkedin._linkedin_advertising_accounts() + self.assertEqual(ad_account_id, "urn:li:sponsoredAccount:123") + self.assertEqual( + mock_request_linkedin.call_args_list[0].kwargs["endpoint"], + "/adAccountUsers", + ) + self.assertEqual( + mock_request_linkedin.call_args_list[0].kwargs["params_values"]["q"], + "authenticatedUser", + ) + self.assertEqual( + mock_request_linkedin.call_args_list[1].kwargs["endpoint"], + "/adAccounts/123", + ) + self.assertEqual(mock_request_linkedin.call_count, 2) + self.assertEqual( + self.SocialAccountLinkedin.advertising_account_id, + "urn:li:sponsoredAccount:123", + ) + self.SocialPostAccountLinkedin._linkedin_advertising_accounts() + self.assertEqual(mock_request_linkedin.call_count, 2) + + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + def test_linkedin_advertising_accounts_error(self, mock_request_linkedin): + mock_response = MagicMock() + mock_response.status_code = 403 + mock_response.json.return_value = {"message": "Unauthorized"} + mock_request_linkedin.return_value = mock_response + self.SocialAccountLinkedin.advertising_account_id = False + with self.assertRaises(Exception) as context: + self.SocialPostAccountLinkedin._linkedin_advertising_accounts() + self.assertIn( + "Error get advertising account in Linkedin", str(context.exception) + ) + + def test_check_linkedin_campaign_format(self): + """The post and its campaign must share the LinkedIn ad format.""" + post_account = self.SocialPostAccountCampaignLinkedin + campaign = post_account.social_campaign_id + campaign.linkedin_format = "STANDARD_UPDATE" + post_account._check_linkedin_campaign_format() + + post_account.post_id.video_ids = [ + Command.set([self.create_attachment("test_video.mp4").id]) + ] + with self.assertRaises(UserError) as context: + post_account._check_linkedin_campaign_format() + self.assertIn("'Single video' format", str(context.exception)) + + campaign.linkedin_format = "SINGLE_VIDEO" + post_account._check_linkedin_campaign_format() + post_account.post_id.video_ids = [Command.clear()] + with self.assertRaises(UserError) as context: + post_account._check_linkedin_campaign_format() + self.assertIn("only accepts posts containing a video", str(context.exception)) + + def test_check_linkedin_campaign_format_multi_image(self): + """LinkedIn does not sponsor a post carrying several images.""" + post_account = self.SocialPostAccountCampaignLinkedin + post_account.social_campaign_id.linkedin_format = "STANDARD_UPDATE" + post_account.post_id.image_ids = [ + Command.set( + [ + self.create_attachment("image_1.png").id, + self.create_attachment("image_2.png").id, + ] + ) + ] + with self.assertRaises(UserError) as context: + post_account._check_linkedin_campaign_format() + self.assertIn("several images", str(context.exception)) + + post_account.post_id.image_ids = [ + Command.set([self.create_attachment("image_1.png").id]) + ] + post_account._check_linkedin_campaign_format() + + @patch(PATCH_ADVERTISING_ACCOUNT_LINKEDIN.format("_get_linkedin_ad_account_id")) + @patch(PATCH_ACCOUNT_LINKEDIN.format("_request_linkedin")) + def test_action_campaign_post(self, mock_request_linkedin, mock_get_ad_account_id): + creative_urn = "urn:li:sponsoredCreative:123456" + mock_get_ad_account_id.return_value = "999" + campaign = self.SocialPostCampaignLinkedin.social_campaign_id + campaign.remote_ref = "urn:li:sponsoredCampaign:001" + mock_request_linkedin.side_effect = [ + MagicMock(status_code=201, headers={"x-restli-id": creative_urn}), + ] + res = self.SocialPostAccountCampaignLinkedin._action_campaign_post( + self.SocialPostAccountCampaignLinkedin.id + ) + self.assertEqual(res, creative_urn) + self.assertEqual( + mock_request_linkedin.call_args.kwargs["endpoint"], + "/adAccounts/999/creatives", + ) + + mock_request_linkedin.side_effect = [ + MagicMock(status_code=404, headers={"x-restli-id": creative_urn}), + ] + with self.assertRaises(UserError): + self.SocialPostAccountCampaignLinkedin._action_campaign_post( + self.SocialPostAccountCampaignLinkedin.id + ) + + campaign.remote_ref = False + with self.assertRaises(UserError) as context: + self.SocialPostAccountCampaignLinkedin._action_campaign_post( + self.SocialPostAccountCampaignLinkedin.id + ) + self.assertIn("has not been created on LinkedIn", str(context.exception)) + self.assertEqual(mock_request_linkedin.call_count, 2) + + def test_compute_message_info_multi_image_campaign(self): + """The multi-image limitation is warned when the campaign is chosen.""" + warning = "does not sponsor that format" + post = self.SocialPost.create( + { + "message": self.test_message, + "account_ids": [Command.set(self.SocialAccountLinkedin.ids)], + "image_ids": [ + Command.set( + [ + self.create_attachment("one.jpg").id, + self.create_attachment("two.jpg").id, + ] + ) + ], + } + ) + self.assertFalse(post.message_info) + + post.social_campaign_id = self.SocialCampaignLinkedin + self.assertIn(warning, post.message_info) + self.assertIn(self.SocialCampaignLinkedin.display_name, post.message_info) + + post.image_ids = [Command.set([self.create_attachment("one.jpg").id])] + self.assertFalse(post.message_info) + + def test_compute_message_info_multi_image_without_linkedin_campaign(self): + """A campaign of another media does not create a sponsored post.""" + post = self.SocialPost.create( + { + "message": self.test_message, + "account_ids": [Command.set(self.SocialAccountLinkedin.ids)], + "social_campaign_id": self.SocialCampaignLinkedin2.id, + "image_ids": [ + Command.set( + [ + self.create_attachment("one.jpg").id, + self.create_attachment("two.jpg").id, + ] + ) + ], + } + ) + self.assertFalse(post.message_info) + + def _set_linkedin_campaign(self, remote_ref=False): + group = self.SocialCampaignGroup.create({"name": "Test Group"}) + campaign = self.SocialCampaign.create( + { + "name": "Test Campaign", + "campaign_group_id": group.id, + "media_id": self.media_linkedin_data_id.id, + "account_ids": [Command.link(self.SocialAccountLinkedin.id)], + "remote_ref": remote_ref, + } + ) + self.SocialPostLinkedin.write({"social_campaign_id": campaign.id}) + return campaign + + @mute_logger(LOGGER_POST_ACCOUNT_BASE) + def test_action_post_campaign_precheck_blocks_publish(self): + """A failing precheck must not reach LinkedIn. + + The guard turns the error into a failed line instead of propagating + it, so the other accounts of the post keep their own result. + """ + self.SocialPostAccountLinkedin.write({"state": "ready", "remote_ref": False}) + self._set_linkedin_campaign() + with patch.object( + type(self.SocialPostLinkedin), + "filter_by_media_types", + autospec=True, + return_value=self.SocialPostAccountLinkedin, + ), patch.object( + type(self.SocialPostAccountLinkedin), + "_linkedin_advertising_accounts", + autospec=True, + side_effect=UserError("Ads access denied"), + ), patch.object( + type(self.SocialPostAccountLinkedin.account_id), + "_linkedin_create_post", + autospec=True, + ) as mock_linkedin_create_post: + self.SocialPostAccountLinkedin._action_post(self.SocialPostLinkedin) + mock_linkedin_create_post.assert_not_called() + self.assertEqual(self.SocialPostAccountLinkedin.state, "failed") + self.assertIn( + "has not been created on LinkedIn yet", + self.SocialPostAccountLinkedin.failed_description, + ) + + @mute_logger(LOGGER_POST_ACCOUNT_BASE) + def test_action_post_multi_image_campaign_is_not_published(self): + """The multi-image guard must run before reaching LinkedIn.""" + self.SocialPostAccountLinkedin.write({"state": "ready", "remote_ref": False}) + self._set_linkedin_campaign(remote_ref="urn:li:sponsoredCampaign:100") + self.SocialPostLinkedin.write( + { + "image_ids": [ + Command.set( + [ + self.create_attachment("one.jpg").id, + self.create_attachment("two.jpg").id, + ] + ) + ] + } + ) + with patch.object( + type(self.SocialPostLinkedin), + "filter_by_media_types", + autospec=True, + return_value=self.SocialPostAccountLinkedin, + ), patch.object( + type(self.SocialPostAccountLinkedin.account_id), + "_linkedin_create_post", + autospec=True, + ) as mock_linkedin_create_post: + self.SocialPostAccountLinkedin._action_post(self.SocialPostLinkedin) + mock_linkedin_create_post.assert_not_called() + self.assertEqual(self.SocialPostAccountLinkedin.state, "failed") + self.assertIn( + "several images", self.SocialPostAccountLinkedin.failed_description + ) + + @mute_logger(LOGGER_POST_ACCOUNT_ADVERTISING_LINKEDIN) + def test_action_post_campaign_failure_keeps_posted(self): + self.SocialPostAccountLinkedin.write({"state": "ready", "remote_ref": False}) + self._set_linkedin_campaign(remote_ref="urn:li:sponsoredCampaign:100") + post_account_urn = "urn:li:share:122809890045" + fake_response = [ + { + "id": post_account_urn, + "content": {"media": {"id": "urn:li:image:1"}}, + } + ] + with patch.object( + type(self.SocialPostLinkedin), + "filter_by_media_types", + autospec=True, + return_value=self.SocialPostAccountLinkedin, + ), patch.object( + type(self.SocialPostAccountLinkedin), + "_linkedin_advertising_accounts", + autospec=True, + return_value="urn:li:sponsoredAccount:123", + ), patch.object( + type(self.SocialPostAccountLinkedin.account_id), + "_linkedin_create_post", + autospec=True, + return_value=(post_account_urn, []), + ), patch.object( + type(self.SocialPostAccountLinkedin.account_id), + "_get_posts", + autospec=True, + return_value=fake_response, + ), patch.object( + type(self.SocialPostAccountLinkedin), + "_get_assets_save", + autospec=True, + return_value=[], + ), patch.object( + type(self.SocialPostAccountLinkedin), + "_action_campaign_post", + autospec=True, + side_effect=UserError("Creative error"), + ): + self.SocialPostAccountLinkedin._action_post(self.SocialPostLinkedin) + self.assertEqual(self.SocialPostAccountLinkedin.state, "posted") + self.assertEqual( + self.SocialPostAccountLinkedin.remote_ref, + post_account_urn, + ) + self.assertFalse(self.SocialPostAccountLinkedin.creative_urn) + + def test_republish_keeps_the_imported_creative(self): + """A non-sponsored publication keeps a creative linked by the import.""" + self.SocialPostAccountLinkedin.creative_urn = "urn:li:sponsoredCreative:7" + self.assertFalse(self.SocialPostAccountLinkedin._requires_campaign_post()) + values = self.SocialPostAccountLinkedin._linkedin_published_values( + "urn:li:share:1" + ) + self.assertNotIn("creative_urn", values) + self.assertEqual( + self.SocialPostAccountLinkedin.creative_urn, "urn:li:sponsoredCreative:7" + ) + + def test_allow_social_campaign_ids_filters_unpublished(self): + """Only the LinkedIn campaigns already created on LinkedIn are offered.""" + # A post already published cannot change its campaign anymore. + self.SocialPostAccountLinkedin.write({"state": "ready", "remote_ref": False}) + campaign = self._set_linkedin_campaign() + self.SocialPostLinkedin.invalidate_recordset() + self.assertNotIn(campaign, self.SocialPostLinkedin.allow_social_campaign_ids) + campaign.remote_ref = "urn:li:sponsoredCampaign:100" + self.SocialPostLinkedin.invalidate_recordset() + self.assertIn(campaign, self.SocialPostLinkedin.allow_social_campaign_ids) diff --git a/social_media_advertising_linkedin/views/social_account_views.xml b/social_media_advertising_linkedin/views/social_account_views.xml new file mode 100644 index 0000000000..159c0d5d2a --- /dev/null +++ b/social_media_advertising_linkedin/views/social_account_views.xml @@ -0,0 +1,23 @@ + + + + + + social.account.view.form.inherit.advertising.linkedin + social.account + + + +