diff --git a/social_media_base/README.rst b/social_media_base/README.rst new file mode 100644 index 0000000000..431ed6700f --- /dev/null +++ b/social_media_base/README.rst @@ -0,0 +1,281 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================= +Social Media Base +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9ef0613fac59ec66dcecfb14f4bff101e89925782f08418a3cf3b64bdf731cd0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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/19.0/social_media_base + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-19-0/social-19-0-social_media_base + :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=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides the fundamental foundation for social media +management. It facilitates the integration of user accounts, posts, +reactions (likes), comments, and graph-based analysis. Designed to be +flexible and scalable, it allows developers and businesses to integrate +and customize social features according to their needs. + +Main features: + +- Integration of multiple user accounts. +- Basic methods that can be extended and adapted to suit the social + network. +- Basic business structure. +- Campaign group and campaign structure linked to posts, with an + extensible hook so each social network module can import its + campaigns. +- Dashboard of published posts with campaign, video and deletion + indicators. +- Scheduled synchronization of post statistics (monthly cron), plus an + initial synchronization right after an account is linked. +- Account credentials (OAuth tokens) are only visible to administrator + users. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Generate group campaign. +------------------------ + +- Go to *Social Media* > Campaigns > Campaign group > New +- A form view opens; fill in the required fields |CREATE_GROUP_CAMPAIGN| +- Save +- The *Campaigns* stat button on the form shows the number of campaigns + of the group and navigates to them. + +Generate campaign. +------------------ + +- Go to *Social Media* > Campaigns > Campaigns > New +- Fill in the fields. The social media is optional; when a social media + is selected, the campaign group and the account become required. + |CREATE_CAMPAIGN| +- Save +- Changes on the main campaign fields are logged in the chatter. +- The native campaign stage bar is hidden in the Social Media views; the + stages keep working in the other applications that use campaigns, + where they can also be managed. + +Link a campaign to a post. +-------------------------- + +- On the post form, select a campaign. Only campaigns whose social media + matches the accounts selected on the post can be chosen. + +- The campaign is shown as a badge on the post kanban and on the + dashboard cards. + + |DASHBOARD| + +Posts on the dashboard. +----------------------- + +- Posts that contain a video display a video indicator on their card, + and the posts kanban shows a *Video (N)* counter. + +- Posts deleted directly on the social network are marked as *Deleted on + * and kept in the dashboard as history. + + |POSTS_KANBAN| + +Charts. +------- + +- Go to *Social Media* > Charts + +- The view shows one chart per connected account with the statistics of + its publications. The statistics are requested to the social networks + when the view is opened, so a loader is displayed until they are + available. + + |CHARTS| + +Ads. +---- + +- Go to *Social Media* > Campaigns > Ads + +- The view lists the sponsored creatives of the connected accounts, each + one with its status, its campaign and the post it promotes. Clicking + an ad opens it on the social network. + +- The status is the one set by the advertiser on the social network; the + reason why the network is serving the ad or not is shown in the + tooltip of the badge. + +- The ads can be filtered by creation date and by text, which is + searched in the campaign name, the post name and the status. The *Sync + ads* button refreshes them from the social networks. + + |ADS| + +Archive an account. +------------------- + +- The account form provides an *Archive account* button, available to + the user responsible for the account. + + |ACCOUNT_FORM| + +- Archiving an account also archives its dashboard posts, campaigns, + campaign groups and the posts linked only to that account. + +- Nothing is removed from the social network, and unarchiving the + account restores everything. + +Delete an account permanently. +------------------------------ + +- The account form provides a *Delete permanently* button, available + only to the *Social Media / Administrator* group. A regular user can + only archive his accounts, he is not allowed to delete them. +- It deletes the account, its dashboard publications and the posts that + were linked only to that account, together with their metrics, + comments and attachments. +- Campaigns and campaign groups are **not** deleted, they only lose + their link to the account: campaigns are shared with the other Odoo + applications and may be linked to leads, orders or mailings. +- Nothing is deleted from the social network: the publications stay + online. +- This cannot be undone. To keep the history, archive the account + instead. + +Statistics synchronization. +--------------------------- + +- The scheduled action *Social: Sync posts statistics* updates the post + statistics monthly. +- An initial synchronization is also triggered automatically right after + an account is linked. + +Import campaigns. +----------------- + +- The account form provides an *Import campaigns* button. Each social + network module implements the actual import; a notification shows the + result. + +Account ownership. +------------------ + +- Every account has a *Responsible* user, set to whoever linked it. A + regular user of the *Social Media / User: Own Accounts* group only + sees and manages his own accounts, their posts, campaigns and + statistics. +- Linking an account that already exists in Odoo (relinking it after + archiving it, after reinstalling the connector or to renew its tokens) + is restricted to that responsible user and to the *Social Media / + Administrator* group, because it overwrites the credentials and the + access tokens stored in the account. +- Accounts are recognised by the identifier they have on the social + network, not by their name: renaming the account on the social network + keeps the history in Odoo, and a name reused by a different account + never overwrites an existing one. +- If another user completes the association of an account that is not + his, nothing is written and a notification explains that the account + belongs to somebody else. The same happens when the administrator of + the wizard tries to change the credentials of an account that is not + his. +- The *Update account* and *Archive account* buttons of the account form + are shown to the responsible user and to the *Social Media / + Administrator* group. The credentials themselves (client keys and + tokens) stay hidden from everybody but the system administrators, and + the connectors read and write them internally, so an administrator of + the application can renew them without ever seeing them. + +Campaign badge. +--------------- + +- The publications of the dashboard and of the posts kanban show the + campaign they belong to as a badge. Long campaign names are shortened + with an ellipsis so the card keeps its shape, and the whole name is + available in the tooltip. +- Clicking the badge opens that campaign in Odoo, so its budget, its + status on the social network and its history are one click away from + the post. + +.. |CREATE_GROUP_CAMPAIGN| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/CREATE_GROUP_CAMPAIGN.png +.. |CREATE_CAMPAIGN| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/CREATE_CAMPAIGN.png +.. |DASHBOARD| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/DASHBOARD.png +.. |POSTS_KANBAN| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/POSTS_KANBAN.png +.. |CHARTS| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/CHARTS.png +.. |ADS| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/ADS.png +.. |ACCOUNT_FORM| image:: https://raw.githubusercontent.com/OCA/social/19.0/social_media_base/static/img/readme/ACCOUNT_FORM.png + +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 Cloud] (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_base/__init__.py b/social_media_base/__init__.py new file mode 100644 index 0000000000..61cfedf150 --- /dev/null +++ b/social_media_base/__init__.py @@ -0,0 +1,11 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import controllers +from . import models +from . import wizards + + +def remove_social_media(env, media_type): + """Remove the data of a social media, called from a connector ``uninstall_hook``.""" + env["social.account"]._remove_social_media(media_type) diff --git a/social_media_base/__manifest__.py b/social_media_base/__manifest__.py new file mode 100644 index 0000000000..8f197aa655 --- /dev/null +++ b/social_media_base/__manifest__.py @@ -0,0 +1,49 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Social Media Base", + "summary": "Basic module for social media management.", + "version": "19.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": ["base", "web", "mail", "utm"], + "data": [ + "security/social_media_base_groups.xml", + "security/ir.model.access.csv", + "security/social_account_security.xml", + "security/social_post_security.xml", + "security/social_post_account_security.xml", + "security/wizard_social_account_security.xml", + "data/ir_cron_data.xml", + "views/social_media_views.xml", + "views/social_account_views.xml", + "views/social_post_views.xml", + "views/social_post_account_views.xml", + "views/utm_group_campaign_views.xml", + "views/utm_campaign_views.xml", + "views/social_action_client_views.xml", + "wizards/wizard_social_account.xml", + "views/social_media_base_menus.xml", + ], + "assets": { + "web.assets_backend": [ + ("include", "web.chartjs_lib"), + "social_media_base/static/src/xml/**/*.xml", + "social_media_base/static/src/scss/**/*.scss", + "social_media_base/static/src/js/app/**/*.js", + "social_media_base/static/src/js/services/**/*.js", + "social_media_base/static/src/components/**/*.xml", + "social_media_base/static/src/components/**/*.js", + "social_media_base/static/src/components/**/*.scss", + "social_media_base/static/src/js/views/**/*.xml", + "social_media_base/static/src/js/views/**/*.scss", + "social_media_base/static/src/js/views/**/*.js", + ], + }, + "exclude": ["social"], + "installable": True, +} diff --git a/social_media_base/controllers/__init__.py b/social_media_base/controllers/__init__.py new file mode 100644 index 0000000000..8aa4159f91 --- /dev/null +++ b/social_media_base/controllers/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import thread diff --git a/social_media_base/controllers/thread.py b/social_media_base/controllers/thread.py new file mode 100644 index 0000000000..7cafa0fe37 --- /dev/null +++ b/social_media_base/controllers/thread.py @@ -0,0 +1,39 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.http import request, route + +from odoo.addons.mail.controllers.thread import ThreadController +from odoo.addons.mail.tools.discuss import add_guest_to_context + + +class ThreadControllerSocial(ThreadController): + def _prepare_result(self): + return { + "author": { + "id": request.env.user.partner_id.id, + "name": request.env.user.partner_id.name, + "is_company": request.env.user.partner_id.is_company, + "user": { + "id": request.env.uid, + "isInternalUser": request.env.user._is_internal(), + }, + "type": "partner", + } + } + + @route("/mail/message/post", methods=["POST"], type="jsonrpc", auth="public") + @add_guest_to_context + def mail_message_post(self, thread_model, thread_id, post_data, context=None): + if thread_model == "social.post.account" and thread_id: + post = request.env[thread_model].browse(int(thread_id)).exists() + if not post: + return None + post.check_access_rights("write") + post.check_access_rule("write") + comment = post.create_comment(post_data, context) + request.env["bus.bus"]._sendone( + request.env.user.partner_id, "comments", comment + ) + return self._prepare_result() + return super().mail_message_post(thread_model, thread_id, post_data, context) diff --git a/social_media_base/data/ir_cron_data.xml b/social_media_base/data/ir_cron_data.xml new file mode 100644 index 0000000000..313a4e048d --- /dev/null +++ b/social_media_base/data/ir_cron_data.xml @@ -0,0 +1,32 @@ + + + + Social Post: Send post schedule + + code + model._run_send_post() + 5 + minutes + 1 + + + + Social: Checking social media updates + + code + model._run_check_media_updates() + 30 + minutes + 1 + + + + Social: Sync posts statistics + + code + model.update_posts_statistics() + 1 + months + 5 + + diff --git a/social_media_base/i18n/es_ES.po b/social_media_base/i18n/es_ES.po new file mode 100644 index 0000000000..8d6cdc63b0 --- /dev/null +++ b/social_media_base/i18n/es_ES.po @@ -0,0 +1,1715 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * social_media_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-27 20:16+0000\n" +"PO-Revision-Date: 2025-12-07 06:01+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_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"%(post)s: cannot be cancelled because it is published or in the process of " +"being published." +msgstr "" +"%(post)s: no se puede cancelar porque está publicado o en proceso de " +"publicación." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__state +msgid "" +"'Deleted' means the publication no longer exists on the social network " +"although it is kept in Odoo for history." +msgstr "" +"«Eliminado» significa que la publicación ya no existe en la red social, " +"aunque se conserva en Odoo como histórico." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +#, python-format +msgid "...see more" +msgstr "...ver más" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "" +"\n" +" Deleted on" +msgstr "" +"\n" +" Eliminado en" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "" +"\n" +" Video" +msgstr "" +"\n" +" Vídeo" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__access_token +msgid "Access Token" +msgstr "Token de Acceso" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__account_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__account_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__account_id +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__account_id +msgid "Account" +msgstr "Cuenta" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__account_remote_ref +msgid "Account Remote Reference" +msgstr "Referencia remota de la cuenta" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__account_url +msgid "Account Url" +msgstr "Url de la Cuenta" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.social_account_action +#: model:ir.ui.menu,name:social_media_base.social_account_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_search +msgid "Accounts" +msgstr "Cuentas" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_needaction +msgid "Action Needed" +msgstr "Acción necesaria" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__active +#: model:ir.model.fields,field_description:social_media_base.field_social_post__active +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__active +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__active +msgid "Active" +msgstr "Activo" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_exception_decoration +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_exception_decoration +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_exception_decoration +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decoración de la excepción de actividad" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_state +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_state +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_state +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_state +msgid "Activity State" +msgstr "Estado de la actividad" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_type_icon +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_type_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_type_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icono del tipo de actividad" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__actor_urn +msgid "Actor Urn" +msgstr "URN del actor" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/kanban_buttons.xml:0 +#, python-format +msgid "Add Post" +msgstr "Agregar publicación" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/kanban_buttons.xml:0 +#, python-format +msgid "Add account" +msgstr "Agregar cuenta" + +#. module: social_media_base +#: model:res.groups,name:social_media_base.group_social_media_manager +msgid "Administrator" +msgstr "Administrador" + +#. module: social_media_base +#: model:ir.actions.client,name:social_media_base.social_ads_account_action_client +#: model:ir.ui.menu,name:social_media_base.social_ads_account_action_client_menu +msgid "Ads" +msgstr "Anuncios" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__advertising_account_id +msgid "Advertising Account" +msgstr "Cuenta de publicidad" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment_dialog/social_comment_dialog.esm.js:0 +#: code:addons/social_media_base/static/src/js/app/social_post_account_mixin.esm.js:0 +#, python-format +msgid "All Images" +msgstr "Imágenes" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__allow_campaign_ids +msgid "Allow Campaign" +msgstr "Campañas permitidas" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__allow_media_ids +msgid "Allowed Media" +msgstr "Medios permitidos" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_wizard_social_account__csrf_state_token +msgid "Anti-CSRF state token used during the OAuth flow." +msgstr "Token de estado anti-CSRF usado durante el flujo OAuth." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__any_failed_post +msgid "Any Failed Post" +msgstr "Algún post fallido" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Archive account" +msgstr "Archivar cuenta" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +msgid "Archived" +msgstr "Archivado" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "" +"Archiving the account only from the system does not delete the account on " +"the social network." +msgstr "" +"Archivar la cuenta solo en el sistema no elimina la cuenta en la red social." + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "" +"Are you sure you want to archive the account? Please note that all data " +"associated with this account will be archived." +msgstr "" +"¿Seguro que quiere archivar la cuenta? Tenga en cuenta que se archivarán " +"todos los datos asociados a esta cuenta." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#, python-format +msgid "Are you sure you want to delete this comment?" +msgstr "¿Seguro que desea eliminar este comentario?" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "Are you sure you want to delete this post?" +msgstr "¿Seguro que desea eliminar esta publicación?" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.wizard_social_account_view_form +msgid "Associate" +msgstr "Asociar" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_wizard_social_account +msgid "Associate Social Media Account" +msgstr "Asociar Cuenta de Red Social" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_media_view_kanban +msgid "Associate account" +msgstr "Asociar cuenta" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_attachment_count +msgid "Attachment Count" +msgstr "Número de adjuntos" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_1920 +msgid "Avatar" +msgstr "Avatar" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_1024 +msgid "Avatar 1024" +msgstr "Avatar 1024" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_128 +msgid "Avatar 128" +msgstr "Avatar 128" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_256 +msgid "Avatar 256" +msgstr "Avatar 256" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_512 +msgid "Avatar 512" +msgstr "Avatar 512" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__campaign_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__campaign_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__campaign_ids +msgid "Campaign" +msgstr "Campaña" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__campaign_count +msgid "Campaign Count" +msgstr "Número de campañas" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__name +msgid "Campaign Identifier" +msgstr "Identificador de la campaña" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__title +msgid "Campaign Name" +msgstr "Nombre de la campaña" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.utm_group_campaign_action +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__campaign_group_id +#: model:ir.ui.menu,name:social_media_base.social_media_base_menu_utm_group_campaign_action +msgid "Campaign group" +msgstr "Grupo de Campañas" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_group_campaign_action +msgid "" +"Campaign groups let you share a total budget across several\n" +" LinkedIn campaigns and manage them as a unit." +msgstr "" +"Los grupos de campañas permiten compartir un presupuesto total entre varias\n" +" campañas de LinkedIn y gestionarlas como una unidad." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/utm_group_campaign.py:0 +#: model:ir.actions.act_window,name:social_media_base.utm_campaign_social_action +#: model:ir.ui.menu,name:social_media_base.social_campaign_menu +#: model:ir.ui.menu,name:social_media_base.social_media_base_menu_utm_campaign_action +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_group_campaign_view_form +#, python-format +msgid "Campaigns" +msgstr "Campañas" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_campaign_social_action +msgid "" +"Campaigns hold the budget and bidding settings used to promote\n" +" your posts on social media." +msgstr "" +"Las campañas contienen el presupuesto y la configuración de pujas que se " +"usan para promocionar\n" +" sus publicaciones en las redes sociales." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__allow_campaign_ids +msgid "Campaigns that can be linked to this post." +msgstr "Campañas que se pueden vincular a este post." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__can_manage_account +msgid "Can Manage Account" +msgstr "Puede gestionar la cuenta" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +#: model_terms:ir.ui.view,arch_db:social_media_base.wizard_social_account_view_form +#, python-format +msgid "Cancel" +msgstr "Cancelar" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__cancelled +msgid "Cancelled" +msgstr "Cancelado" + +#. module: social_media_base +#: model:ir.actions.client,name:social_media_base.social_chart_account_action_client +msgid "Chart" +msgstr "Gráfico" + +#. module: social_media_base +#: model:ir.ui.menu,name:social_media_base.social_chart_account_action_client_menu +msgid "Charts" +msgstr "Gráficos" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__click_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__click_count +msgid "Click Count" +msgstr "Cantidad de clics" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_clicks +#, python-format +msgid "Clicks" +msgstr "Clicks" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads/social_ads.xml:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +#, python-format +msgid "Clicks:" +msgstr "Clicks:" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.xml:0 +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "Comment" +msgstr "Comentario" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__comment_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__comment_count +msgid "Comment Count" +msgstr "Cantidad de comentarios" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment_dialog/social_comment_dialog.esm.js:0 +#, python-format +msgid "Comment created" +msgstr "Comentario creado" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#, python-format +msgid "Comment deleted" +msgstr "Comentario eliminado" + +#. module: social_media_base +#. odoo-javascript +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: code:addons/social_media_base/static/src/components/social_chart_account/social_chart_account.xml:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_comments +#, python-format +msgid "Comments" +msgstr "Comentarios" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__company_id +msgid "Company" +msgstr "Compañía" + +#. module: social_media_base +#: model:ir.ui.menu,name:social_media_base.social_configuration_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Configuration" +msgstr "Configuración" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_account_action +msgid "" +"Connect a social account and publish your first post. Published\n" +" posts and their statistics will show up here." +msgstr "" +"Conecte una cuenta social y cree su primera publicación. Las publicaciones\n" +" y sus estadísticas aparecerán aquí." + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_campaign_social_action +msgid "Create your first campaign" +msgstr "Cree su primera campaña" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_group_campaign_action +msgid "Create your first campaign group" +msgstr "Cree su primer grupo de campañas" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_action +msgid "Create your first post" +msgstr "Cree su primera publicación" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_media__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__create_date +#: model:ir.model.fields,field_description:social_media_base.field_social_media__create_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post__create_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__create_date +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__create_date +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__csrf_state_token +msgid "Csrf State Token" +msgstr "Token de estado CSRF" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.social_post_account_action +#: model:ir.ui.menu,name:social_media_base.social_network_stream_post_menu +msgid "Dashboard" +msgstr "Tablero" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "Delete" +msgstr "Eliminar" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#, python-format +msgid "Delete comment" +msgstr "Eliminar comentario" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Delete permanently" +msgstr "Eliminar permanentemente" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "Delete post" +msgstr "Eliminar publicación" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Delete the account and its publication history from Odoo." +msgstr "Elimina la cuenta y su histórico de publicaciones de Odoo." + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__deleted +msgid "Deleted" +msgstr "Eliminado" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_media__description +msgid "Description" +msgstr "Descripción" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__display_name +#: model:ir.model.fields,field_description:social_media_base.field_social_media__display_name +#: model:ir.model.fields,field_description:social_media_base.field_social_post__display_name +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__display_name +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__display_name +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__draft +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Draft" +msgstr "Borrador" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/wizards/wizard_social_account.py:0 +#, python-format +msgid "ERROR UPDATE ACCOUNT %(account)s: %(error)s" +msgstr "ERROR ACTUALIZANDO CUENTA %(account)s: %(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_account__engagement +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_engagement +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__engagement +#, python-format +msgid "Engagement" +msgstr "Engagement" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Engagement:" +msgstr "Engagement:" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__enviroment +msgid "Enviroment" +msgstr "Entorno" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "Error posting on [%(media)s]: %(error)s" +msgstr "Error al publicar en [%(media)s]: %(error)s" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment_dialog/social_comment_dialog.esm.js:0 +#, python-format +msgid "Error retrieving comments" +msgstr "Error recuperando comentarios" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__expire_access_token_date +msgid "Expire Access Token" +msgstr "Vencimiento del token de acceso" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__failed +msgid "Failed" +msgstr "Fallido" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__failed_description +msgid "Failed Description" +msgstr "Descripción fallida" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (contactos)" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_type_icon +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_type_icon +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_type_icon +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono de Font Awesome, p. ej. fa-tasks" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_campaign_view_form +msgid "General" +msgstr "General" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_account_action +msgid "" +"Go to Configuration > Social medias and use\n" +" Associate account to link your LinkedIn or X account." +msgstr "" +"Vaya a Configuración > Redes sociales y use\n" +" Asociar cuenta para vincular su cuenta de LinkedIn o " +"X." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "Go to account" +msgstr "Ir a la cuenta" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_search +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Group By" +msgstr "Agrupar por" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_ir_http +msgid "HTTP Routing" +msgstr "Enrutamiento HTTP" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__has_message +#: model:ir.model.fields,field_description:social_media_base.field_social_post__has_message +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__has_message +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__has_message +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__has_message +msgid "Has Message" +msgstr "Tiene mensajes" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__has_video +msgid "Has Video" +msgstr "Tiene vídeo" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__hide_post +msgid "Hide Post" +msgstr "Ocultar post" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__id +#: model:ir.model.fields,field_description:social_media_base.field_social_media__id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__id +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__id +msgid "ID" +msgstr "ID" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_exception_icon +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_exception_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_exception_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_exception_icon +msgid "Icon" +msgstr "Icono" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_exception_icon +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_exception_icon +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_exception_icon +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icono para indicar una actividad de excepción." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__remote_ref +#: model:ir.model.fields,help:social_media_base.field_social_post_account__account_remote_ref +msgid "" +"Identifier of this account on the social network. It is set by the connector " +"module of each social media." +msgstr "" +"Identificador de esta cuenta en la red social. Lo establece el módulo " +"conector de cada red social." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__remote_ref +msgid "" +"Identifier of this campaign group on the social network. It is set by the " +"connector module of each social media." +msgstr "" +"Identificador de este grupo de campañas en la red social. Lo establece el " +"módulo conector de cada red social." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__remote_ref +msgid "" +"Identifier of this campaign on the social network. It is set by the " +"connector module of each social media." +msgstr "" +"Identificador de esta campaña en la red social. Lo establece el módulo " +"conector de cada red social." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__remote_ref +msgid "" +"Identifier of this publication on the social network. It is set by the " +"connector module of each social media." +msgstr "" +"Identificador de esta publicación en la red social. Lo establece el módulo " +"conector de cada red social." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_needaction +#: model:ir.model.fields,help:social_media_base.field_social_post__message_needaction +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_needaction +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_needaction +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Si está marcado, hay nuevos mensajes que requieren su atención." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_has_error +#: model:ir.model.fields,help:social_media_base.field_social_post__message_has_error +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_has_error +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_has_error +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Si está marcado, algunos mensajes tienen un error de entrega." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_1920 +#: model:ir.model.fields,field_description:social_media_base.field_social_media__image +#: model:ir.model.fields,field_description:social_media_base.field_social_post__image_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__image_ids +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__image +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Image" +msgstr "Imagen" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_1024 +msgid "Image 1024" +msgstr "Imagen 1024" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_128 +msgid "Image 128" +msgstr "Imagen 128" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_256 +msgid "Image 256" +msgstr "Imagen 256" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_512 +msgid "Image 512" +msgstr "Imagen 512" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__image_urls +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__image_urls +#: model:ir.model.fields,field_description:social_media_base.field_social_post_mixin__image_urls +msgid "Image Urls" +msgstr "URLs de las imágenes" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "Importing campaigns is not available for this social media." +msgstr "La importación de campañas no está disponible para esta red social." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_impression +msgid "Impression" +msgstr "Impresiones" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__impression_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__impression_count +msgid "Impression Count" +msgstr "Cantidad de impresiones" + +#. module: social_media_base +#. odoo-javascript +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: code:addons/social_media_base/static/src/components/social_chart_account/social_chart_account.xml:0 +#, python-format +msgid "Impressions" +msgstr "Impresiones" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads/social_ads.xml:0 +#, python-format +msgid "Impressions:" +msgstr "Impresiones:" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__has_video +msgid "Indicates that the published post has at least one video attached." +msgstr "Indica que el post publicado tiene al menos un vídeo adjunto." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_interactions +msgid "Interactions" +msgstr "Interacciones" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__interactions_count +msgid "Interactions Count" +msgstr "Cantidad de interacciones" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__interactions_count +msgid "Interactions with the publication: clicks, likes, comments and shares." +msgstr "" +"Interacciones con la publicación: clics, me gusta, comentarios y veces " +"compartida." + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Interactions:" +msgstr "Interacciones:" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_is_follower +msgid "Is Follower" +msgstr "Es seguidor" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__is_property_account +msgid "Is Property Account" +msgstr "Es cuenta propia" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__last_update_account +msgid "Last Update Account" +msgstr "Última actualización de la cuenta" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_media__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__write_date +#: model:ir.model.fields,field_description:social_media_base.field_social_media__write_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post__write_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__write_date +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__write_date +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__like_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__like_count +msgid "Like Count" +msgstr "Cantidad de me gusta" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_likes +#, python-format +msgid "Likes" +msgstr "Me gusta" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_model.esm.js:0 +#, python-format +msgid "Likes are not available for this social media." +msgstr "Los «me gusta» no están disponibles para esta red social." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#, python-format +msgid "Loading ads..." +msgstr "Cargando anuncios..." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "Loading statistics..." +msgstr "Cargando estadísticas..." + +#. module: social_media_base +#: model:ir.module.category,description:social_media_base.module_category_social_media +msgid "Manage social media accounts, posts and campaigns." +msgstr "Gestione cuentas, publicaciones y campañas de redes sociales." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__media_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__media_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__media_id +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__media_id +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +msgid "Media" +msgstr "Medio" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__media_type +#: model:ir.model.fields,field_description:social_media_base.field_social_media__media_type +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__media_type +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__media_type +msgid "Media Type" +msgstr "Tipo de medio" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message +msgid "Message" +msgstr "Mensaje" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_has_error +msgid "Message Delivery error" +msgstr "Error de entrega del mensaje" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_info +msgid "Message Info" +msgstr "Información del mensaje" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +msgid "My Accounts" +msgstr "Mis cuentas" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__my_activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_social_post__my_activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__my_activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Fecha límite de mi actividad" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "My Posts" +msgstr "Mis publicaciones" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__name +#: model:ir.model.fields,field_description:social_media_base.field_social_media__name +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__author +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__name +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_campaign_social_view_tree +msgid "Name" +msgstr "Nombre" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__need_update +msgid "Need Update" +msgstr "Necesita actualizar" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "New Post" +msgstr "Nueva publicación" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "New updates are available, press the" +msgstr "Hay nuevas actualizaciones disponibles, presione el" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_image_dialog/social_image_dialog.xml:0 +#, python-format +msgid "Next" +msgstr "Siguiente" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fecha límite de la siguiente actividad" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_summary +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_summary +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_summary +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la siguiente actividad" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_type_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_type_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_type_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo de la siguiente actividad" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_account_action +msgid "No account connected yet" +msgstr "Todavía no hay ninguna cuenta conectada" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.esm.js:0 +#, python-format +msgid "No ads found in your connected accounts." +msgstr "No se han encontrado anuncios en sus cuentas conectadas." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "No data" +msgstr "Sin datos" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "No data to display" +msgstr "No hay datos que mostrar" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads/social_ads.xml:0 +#, python-format +msgid "No post available" +msgstr "No hay ninguna publicación disponible" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_account_action +msgid "No posts published yet" +msgstr "Todavía no hay publicaciones" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "No preview available" +msgstr "No hay vista previa disponible" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__send_post__now +msgid "Now" +msgstr "Ahora" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_has_error_counter +msgid "Number of errors" +msgstr "Número de errores" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_social_post__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Número de mensajes que requieren una acción" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_social_post__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de entrega" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#, python-format +msgid "Once you have ads registered in your accounts, you can view them here." +msgstr "" +"Una vez que tenga anuncios registrados en sus cuentas, podrá verlos aquí." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "Once you have recorded information, you can view it here as a graph." +msgstr "" +"Una vez que haya registrado la información, podrá verla aquí como un gráfico." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "Only a social media administrator can delete an account." +msgstr "Solo un administrador de redes sociales puede eliminar una cuenta." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_wizard_social_account__update_keys +msgid "Only enable this field if your credentials have changed" +msgstr "Habilite este campo solo si sus credenciales han cambiado" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__planned +msgid "Planned" +msgstr "Planificado" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__post_id +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Post" +msgstr "Publicar" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__post_account_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__post_account_ids +msgid "Post Account" +msgstr "Post de la cuenta" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__post_account_url +msgid "Post Account Url" +msgstr "URL del post de la cuenta" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_record.esm.js:0 +#, python-format +msgid "Post Comment" +msgstr "Comentario del post" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__post_preview +msgid "Post Preview" +msgstr "Vista previa del post" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "Post deleted [%(account)s]" +msgstr "Publicación eliminada [%(account)s]" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "Post on %(posts)s" +msgstr "Publicación en %(posts)s" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__posted +msgid "Posted" +msgstr "Publicado" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.social_post_action +#: model:ir.ui.menu,name:social_media_base.social_network_post_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Posts" +msgstr "Publicaciones" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Press the publish button to retry sending failed posts." +msgstr "" +"Presione el botón publicar para reintentar enviar las publicaciones fallidas." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_image_dialog/social_image_dialog.xml:0 +#, python-format +msgid "Prev" +msgstr "Anterior" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_account__enviroment__production +msgid "Production" +msgstr "Producción" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__published +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__published +msgid "Published" +msgstr "Publicado" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__published_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__published_date +msgid "Published Date" +msgstr "Fecha publicación" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__publishing +msgid "Publishing" +msgstr "Publicando" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_chart_account/social_chart_account.xml:0 +#, python-format +msgid "Reactions" +msgstr "Reacciones" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__ready +msgid "Ready" +msgstr "Listo" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.xml:0 +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "Recommend" +msgstr "Recomendar" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__refresh_access_token +msgid "Refresh Access Token" +msgstr "Token de refresco" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__remote_ref +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__remote_ref +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__remote_ref +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__remote_ref +msgid "Remote Reference" +msgstr "Referencia remota" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__user_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__user_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__user_id +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Responsible" +msgstr "Responsable" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_user_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_user_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_user_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_user_id +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__send_post__schedule +msgid "Schedule" +msgstr "Programar" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__send_post_date +msgid "Schedule date" +msgstr "Fecha de programación" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.xml:0 +#, python-format +msgid "Search..." +msgstr "Buscar..." + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__send_post +msgid "Send Post" +msgstr "Enviar publicación" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__share_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__share_count +msgid "Share Count" +msgstr "Número de veces compartido" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_shares +#, python-format +msgid "Shares" +msgstr "Compartidos" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_account +msgid "Social Account" +msgstr "Cuenta de red social" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#: model:ir.actions.act_window,name:social_media_base.social_media_action +#: model:ir.model,name:social_media_base.model_social_media +#: model:ir.module.category,name:social_media_base.module_category_social_media +#: model:ir.ui.menu,name:social_media_base.social_root_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_campaign_view_form +#, python-format +msgid "Social Media" +msgstr "Redes sociales" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_media_base_mixin.py:0 +#, python-format +msgid "" +"Social Media %(social_media)s [%(account)s]

%(message)s" +msgstr "" +"Red social %(social_media)s [%(account)s]

%(message)s" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_media_base_mixin +msgid "Social Media Base Mixin" +msgstr "Mixin base de redes sociales" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_post_mixin +msgid "Social Network Post Mixin" +msgstr "Mixin de posts de redes sociales" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_post +msgid "Social Post" +msgstr "Post social" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_post_account +msgid "Social Post Account" +msgstr "Post social de la cuenta" + +#. module: social_media_base +#: model:ir.actions.server,name:social_media_base.send_post_schedule_job_ir_actions_server +msgid "Social Post: Send post schedule" +msgstr "Red social: Enviar publicaciones programadas" + +#. module: social_media_base +#: model:ir.ui.menu,name:social_media_base.social_media_menu +msgid "Social medias" +msgstr "Red social" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Social network" +msgstr "Red social" + +#. module: social_media_base +#: model:ir.actions.server,name:social_media_base.webhook_schedule_job_ir_actions_server +msgid "Social: Checking social media updates" +msgstr "Red social: Verificando actualizaciones de redes sociales" + +#. module: social_media_base +#: model:ir.actions.server,name:social_media_base.initial_sync_account_job_ir_actions_server +msgid "Social: Sync posts statistics" +msgstr "Social: sincronizar estadísticas de posts" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__state +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__state +msgid "State" +msgstr "Estado" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "Statistics" +msgstr "Estadísticas" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_state +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_state +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_state +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Estado basado en actividades\n" +"Vencida: la fecha límite ya ha pasado\n" +"Hoy: la fecha de la actividad es hoy\n" +"Planificada: actividades futuras." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#, python-format +msgid "Sync ads" +msgstr "Actualizar anuncios" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_account__enviroment__test +msgid "Test" +msgstr "Prueba" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "The account %(account)s belongs to another company." +msgstr "La cuenta %(account)s pertenece a otra compañía." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The account %(account)s is already associated with another user. Ask its " +"responsible user or a social media administrator to relink it." +msgstr "" +"La cuenta %(account)s ya está asociada a otro usuario. Pida a su usuario " +"responsable o a un administrador de redes sociales que la vuelva a vincular." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "The data was updated successfully." +msgstr "Los datos se actualizaron exitosamente." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_record.esm.js:0 +#, python-format +msgid "The post does not exist or has been deleted." +msgstr "La publicación no existe o ha sido eliminada." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "The post was successfully deleted." +msgstr "La publicación fue eliminada exitosamente." + +#. module: social_media_base +#: model:res.groups,comment:social_media_base.group_social_media_manager +msgid "" +"The user is able to see every social media account and post of the companies " +"he has access to, and to configure the available social medias." +msgstr "" +"El usuario puede ver todas las cuentas y publicaciones de redes sociales de " +"las compañías a las que tiene acceso, y configurar las redes sociales " +"disponibles." + +#. module: social_media_base +#: model:res.groups,comment:social_media_base.group_social_media_user +msgid "" +"The user is only able to see the social media accounts and posts he is " +"responsible for." +msgstr "" +"El usuario solo puede ver las cuentas y publicaciones de redes sociales de " +"las que es responsable." + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "" +"This permanently deletes the account and its publication history in Odoo: " +"posts, metrics and comments. Campaigns are kept, and nothing is deleted from " +"the social network. This cannot be undone." +msgstr "" +"Esto elimina permanentemente la cuenta y su histórico de publicaciones en " +"Odoo: publicaciones, métricas y comentarios. Las campañas se conservan y no " +"se elimina nada de la red social. Esta acción no se puede deshacer." + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "This publication no longer exists on the social network" +msgstr "Esta publicación ya no existe en la red social" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__impression_count +msgid "" +"Total number of views, which may include multiple views by the same user." +msgstr "" +"Número total de visualizaciones, que puede incluir varias visualizaciones " +"del mismo usuario." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_exception_decoration +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_exception_decoration +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_exception_decoration +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo de la actividad de excepción en el registro." + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_utm_campaign +msgid "UTM Campaign" +msgstr "Campaña UTM" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_utm_group_campaign +msgid "UTM Group Campaign" +msgstr "Grupo de campañas UTM" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/social_utils.py:0 +#, python-format +msgid "Unsupported frequency: %(freq)s" +msgstr "Frecuencia no soportada: %(freq)s" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#: code:addons/social_media_base/static/src/js/views/kanban/kanban_buttons.xml:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.wizard_social_account_view_form +#, python-format +msgid "Update" +msgstr "Actualizar" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__update_keys +msgid "Update Keys" +msgstr "Actualizar claves" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__update_token +msgid "Update Token" +msgstr "Actualizar token de acceso" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Update account" +msgstr "Actualizar cuenta" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_wizard_social_account__update_token +msgid "Update token" +msgstr "Actualizar token" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__user_id +#: model:ir.model.fields,help:social_media_base.field_social_post_account__user_id +msgid "" +"User this account belongs to. Only the responsible user and the social media " +"administrators can see it." +msgstr "" +"Usuario al que pertenece esta cuenta. Solo el usuario responsable y los " +"administradores de redes sociales pueden verla." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__user_id +msgid "" +"User this post belongs to. Only the responsible user and the social media " +"administrators can see it." +msgstr "" +"Usuario al que pertenece esta publicación. Solo el usuario responsable y los " +"administradores de redes sociales pueden verla." + +#. module: social_media_base +#: model:res.groups,name:social_media_base.group_social_media_user +msgid "User: Own Accounts" +msgstr "Usuario: cuentas propias" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__username +msgid "Username" +msgstr "Usuario" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__video_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__video_ids +msgid "Video" +msgstr "Vídeo" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Video (" +msgstr "Vídeo (" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__can_manage_account +msgid "" +"Whether the current user may update or archive this account: its responsible " +"user and the social media administrators." +msgstr "" +"Indica si el usuario actual puede actualizar o archivar esta cuenta: su " +"usuario responsable y los administradores de redes sociales." + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_action +msgid "" +"Write once and publish it to every connected social account.\n" +" You can schedule it for later or send it right away." +msgstr "" +"Escriba una vez y publique en todas las cuentas sociales conectadas.\n" +" Puede programarla para más tarde o enviarla ahora mismo." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#: code:addons/social_media_base/static/src/js/app/social_post_account_mixin.esm.js:0 +#, python-format +msgid "You have liked the post." +msgstr "Te ha gustado el post." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "button to get them." +msgstr "botón para obtenerlos." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "comments" +msgstr "comentarios" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_group_campaign_view_form +msgid "e.g. Winter Campaigns" +msgstr "p. ej. Campañas de invierno" diff --git a/social_media_base/i18n/social_media_base.pot b/social_media_base/i18n/social_media_base.pot new file mode 100644 index 0000000000..ab50a3e0b8 --- /dev/null +++ b/social_media_base/i18n/social_media_base.pot @@ -0,0 +1,1655 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * social_media_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-27 20:16+0000\n" +"PO-Revision-Date: 2026-07-27 20:16+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_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"%(post)s: cannot be cancelled because it is published or in the process of " +"being published." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__state +msgid "" +"'Deleted' means the publication no longer exists on the social network " +"although it is kept in Odoo for history." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +#, python-format +msgid "...see more" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "" +"\n" +" Deleted on" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "" +"\n" +" Video" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__access_token +msgid "Access Token" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__account_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__account_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__account_id +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__account_id +msgid "Account" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__account_remote_ref +msgid "Account Remote Reference" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__account_url +msgid "Account Url" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.social_account_action +#: model:ir.ui.menu,name:social_media_base.social_account_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_search +msgid "Accounts" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_needaction +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__active +#: model:ir.model.fields,field_description:social_media_base.field_social_post__active +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__active +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__active +msgid "Active" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_ids +msgid "Activities" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_exception_decoration +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_exception_decoration +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_exception_decoration +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_state +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_state +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_state +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_state +msgid "Activity State" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_type_icon +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_type_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_type_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__actor_urn +msgid "Actor Urn" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/kanban_buttons.xml:0 +#, python-format +msgid "Add Post" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/kanban_buttons.xml:0 +#, python-format +msgid "Add account" +msgstr "" + +#. module: social_media_base +#: model:res.groups,name:social_media_base.group_social_media_manager +msgid "Administrator" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.client,name:social_media_base.social_ads_account_action_client +#: model:ir.ui.menu,name:social_media_base.social_ads_account_action_client_menu +msgid "Ads" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__advertising_account_id +msgid "Advertising Account" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment_dialog/social_comment_dialog.esm.js:0 +#: code:addons/social_media_base/static/src/js/app/social_post_account_mixin.esm.js:0 +#, python-format +msgid "All Images" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__allow_campaign_ids +msgid "Allow Campaign" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__allow_media_ids +msgid "Allowed Media" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_wizard_social_account__csrf_state_token +msgid "Anti-CSRF state token used during the OAuth flow." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__any_failed_post +msgid "Any Failed Post" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Archive account" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +msgid "Archived" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "" +"Archiving the account only from the system does not delete the account on " +"the social network." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "" +"Are you sure you want to archive the account? Please note that all data " +"associated with this account will be archived." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#, python-format +msgid "Are you sure you want to delete this comment?" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "Are you sure you want to delete this post?" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.wizard_social_account_view_form +msgid "Associate" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_wizard_social_account +msgid "Associate Social Media Account" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_media_view_kanban +msgid "Associate account" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_attachment_count +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_1920 +msgid "Avatar" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_1024 +msgid "Avatar 1024" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_128 +msgid "Avatar 128" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_256 +msgid "Avatar 256" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__avatar_512 +msgid "Avatar 512" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__campaign_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__campaign_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__campaign_ids +msgid "Campaign" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__campaign_count +msgid "Campaign Count" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__name +msgid "Campaign Identifier" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__title +msgid "Campaign Name" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.utm_group_campaign_action +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__campaign_group_id +#: model:ir.ui.menu,name:social_media_base.social_media_base_menu_utm_group_campaign_action +msgid "Campaign group" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_group_campaign_action +msgid "" +"Campaign groups let you share a total budget across several\n" +" LinkedIn campaigns and manage them as a unit." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/utm_group_campaign.py:0 +#: model:ir.actions.act_window,name:social_media_base.utm_campaign_social_action +#: model:ir.ui.menu,name:social_media_base.social_campaign_menu +#: model:ir.ui.menu,name:social_media_base.social_media_base_menu_utm_campaign_action +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_group_campaign_view_form +#, python-format +msgid "Campaigns" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_campaign_social_action +msgid "" +"Campaigns hold the budget and bidding settings used to promote\n" +" your posts on social media." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__allow_campaign_ids +msgid "Campaigns that can be linked to this post." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__can_manage_account +msgid "Can Manage Account" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +#: model_terms:ir.ui.view,arch_db:social_media_base.wizard_social_account_view_form +#, python-format +msgid "Cancel" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__cancelled +msgid "Cancelled" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.client,name:social_media_base.social_chart_account_action_client +msgid "Chart" +msgstr "" + +#. module: social_media_base +#: model:ir.ui.menu,name:social_media_base.social_chart_account_action_client_menu +msgid "Charts" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__click_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__click_count +msgid "Click Count" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_clicks +#, python-format +msgid "Clicks" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads/social_ads.xml:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +#, python-format +msgid "Clicks:" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.xml:0 +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "Comment" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__comment_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__comment_count +msgid "Comment Count" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment_dialog/social_comment_dialog.esm.js:0 +#, python-format +msgid "Comment created" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#, python-format +msgid "Comment deleted" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: code:addons/social_media_base/static/src/components/social_chart_account/social_chart_account.xml:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_comments +#, python-format +msgid "Comments" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__company_id +msgid "Company" +msgstr "" + +#. module: social_media_base +#: model:ir.ui.menu,name:social_media_base.social_configuration_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Configuration" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_account_action +msgid "" +"Connect a social account and publish your first post. Published\n" +" posts and their statistics will show up here." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_campaign_social_action +msgid "Create your first campaign" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.utm_group_campaign_action +msgid "Create your first campaign group" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_action +msgid "Create your first post" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_media__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__create_uid +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__create_uid +msgid "Created by" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__create_date +#: model:ir.model.fields,field_description:social_media_base.field_social_media__create_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post__create_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__create_date +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__create_date +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__create_date +msgid "Created on" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__csrf_state_token +msgid "Csrf State Token" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.social_post_account_action +#: model:ir.ui.menu,name:social_media_base.social_network_stream_post_menu +msgid "Dashboard" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "Delete" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#, python-format +msgid "Delete comment" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Delete permanently" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "Delete post" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Delete the account and its publication history from Odoo." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__deleted +msgid "Deleted" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_media__description +msgid "Description" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__display_name +#: model:ir.model.fields,field_description:social_media_base.field_social_media__display_name +#: model:ir.model.fields,field_description:social_media_base.field_social_post__display_name +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__display_name +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__display_name +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__display_name +msgid "Display Name" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__draft +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Draft" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/wizards/wizard_social_account.py:0 +#, python-format +msgid "ERROR UPDATE ACCOUNT %(account)s: %(error)s" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_account__engagement +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_engagement +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__engagement +#, python-format +msgid "Engagement" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Engagement:" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__enviroment +msgid "Enviroment" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "Error posting on [%(media)s]: %(error)s" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment_dialog/social_comment_dialog.esm.js:0 +#, python-format +msgid "Error retrieving comments" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__expire_access_token_date +msgid "Expire Access Token" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__failed +msgid "Failed" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__failed_description +msgid "Failed Description" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_follower_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_partner_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_type_icon +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_type_icon +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_type_icon +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_campaign_view_form +msgid "General" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_account_action +msgid "" +"Go to Configuration > Social medias and use\n" +" Associate account to link your LinkedIn or X account." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "Go to account" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_search +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Group By" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__has_message +#: model:ir.model.fields,field_description:social_media_base.field_social_post__has_message +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__has_message +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__has_message +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__has_message +msgid "Has Message" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__has_video +msgid "Has Video" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__hide_post +msgid "Hide Post" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__id +#: model:ir.model.fields,field_description:social_media_base.field_social_media__id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__id +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__id +msgid "ID" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_exception_icon +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_exception_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_exception_icon +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_exception_icon +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_exception_icon +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_exception_icon +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__remote_ref +#: model:ir.model.fields,help:social_media_base.field_social_post_account__account_remote_ref +msgid "" +"Identifier of this account on the social network. It is set by the connector" +" module of each social media." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__remote_ref +msgid "" +"Identifier of this campaign group on the social network. It is set by the " +"connector module of each social media." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__remote_ref +msgid "" +"Identifier of this campaign on the social network. It is set by the " +"connector module of each social media." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__remote_ref +msgid "" +"Identifier of this publication on the social network. It is set by the " +"connector module of each social media." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_needaction +#: model:ir.model.fields,help:social_media_base.field_social_post__message_needaction +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_needaction +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_needaction +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_has_error +#: model:ir.model.fields,help:social_media_base.field_social_post__message_has_error +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_has_error +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_has_error +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_1920 +#: model:ir.model.fields,field_description:social_media_base.field_social_media__image +#: model:ir.model.fields,field_description:social_media_base.field_social_post__image_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__image_ids +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__image +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Image" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_1024 +msgid "Image 1024" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_128 +msgid "Image 128" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_256 +msgid "Image 256" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__image_512 +msgid "Image 512" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__image_urls +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__image_urls +#: model:ir.model.fields,field_description:social_media_base.field_social_post_mixin__image_urls +msgid "Image Urls" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "Importing campaigns is not available for this social media." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_impression +msgid "Impression" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__impression_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__impression_count +msgid "Impression Count" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: code:addons/social_media_base/static/src/components/social_chart_account/social_chart_account.xml:0 +#, python-format +msgid "Impressions" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads/social_ads.xml:0 +#, python-format +msgid "Impressions:" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__has_video +msgid "Indicates that the published post has at least one video attached." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_interactions +msgid "Interactions" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__interactions_count +msgid "Interactions Count" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__interactions_count +msgid "Interactions with the publication: clicks, likes, comments and shares." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Interactions:" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_is_follower +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__is_property_account +msgid "Is Property Account" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__last_update_account +msgid "Last Update Account" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_media__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__write_uid +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__write_date +#: model:ir.model.fields,field_description:social_media_base.field_social_media__write_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post__write_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__write_date +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__write_date +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__write_date +msgid "Last Updated on" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__like_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__like_count +msgid "Like Count" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_likes +#, python-format +msgid "Likes" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_model.esm.js:0 +#, python-format +msgid "Likes are not available for this social media." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#, python-format +msgid "Loading ads..." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "Loading statistics..." +msgstr "" + +#. module: social_media_base +#: model:ir.module.category,description:social_media_base.module_category_social_media +msgid "Manage social media accounts, posts and campaigns." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__media_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__media_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__media_id +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__media_id +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +msgid "Media" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__media_type +#: model:ir.model.fields,field_description:social_media_base.field_social_media__media_type +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__media_type +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__media_type +msgid "Media Type" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message +msgid "Message" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_has_error +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_info +msgid "Message Info" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_ids +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_ids +msgid "Messages" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +msgid "My Accounts" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__my_activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_social_post__my_activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__my_activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "My Posts" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__name +#: model:ir.model.fields,field_description:social_media_base.field_social_media__name +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__author +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__name +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_campaign_social_view_tree +msgid "Name" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__need_update +msgid "Need Update" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "New Post" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "New updates are available, press the" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_image_dialog/social_image_dialog.xml:0 +#, python-format +msgid "Next" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_date_deadline +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_summary +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_summary +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_summary +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_type_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_type_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_type_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_account_action +msgid "No account connected yet" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.esm.js:0 +#, python-format +msgid "No ads found in your connected accounts." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "No data" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "No data to display" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads/social_ads.xml:0 +#, python-format +msgid "No post available" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_account_action +msgid "No posts published yet" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "No preview available" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__send_post__now +msgid "Now" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_needaction_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__message_has_error_counter +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_social_post__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_needaction_counter +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_social_post__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_social_post_account__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__message_has_error_counter +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#, python-format +msgid "Once you have ads registered in your accounts, you can view them here." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_chart/social_chart.xml:0 +#, python-format +msgid "Once you have recorded information, you can view it here as a graph." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "Only a social media administrator can delete an account." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_wizard_social_account__update_keys +msgid "Only enable this field if your credentials have changed" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__planned +msgid "Planned" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__post_id +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Post" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__post_account_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post__post_account_ids +msgid "Post Account" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__post_account_url +msgid "Post Account Url" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_record.esm.js:0 +#, python-format +msgid "Post Comment" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__post_preview +msgid "Post Preview" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "Post deleted [%(account)s]" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "Post on %(posts)s" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__posted +msgid "Posted" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.act_window,name:social_media_base.social_post_action +#: model:ir.ui.menu,name:social_media_base.social_network_post_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Posts" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Press the publish button to retry sending failed posts." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_image_dialog/social_image_dialog.xml:0 +#, python-format +msgid "Prev" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_account__enviroment__production +msgid "Production" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__published +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__published +msgid "Published" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__published_date +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__published_date +msgid "Published Date" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__state__publishing +msgid "Publishing" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_chart_account/social_chart_account.xml:0 +#, python-format +msgid "Reactions" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post_account__state__ready +msgid "Ready" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.xml:0 +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "Recommend" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__refresh_access_token +msgid "Refresh Access Token" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__remote_ref +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__remote_ref +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__remote_ref +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__remote_ref +msgid "Remote Reference" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__user_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__user_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__user_id +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_search +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Responsible" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__activity_user_id +#: model:ir.model.fields,field_description:social_media_base.field_social_post__activity_user_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_campaign__activity_user_id +#: model:ir.model.fields,field_description:social_media_base.field_utm_group_campaign__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_post__send_post__schedule +msgid "Schedule" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__send_post_date +msgid "Schedule date" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.xml:0 +#, python-format +msgid "Search..." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__send_post +msgid "Send Post" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__share_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__share_count +msgid "Share Count" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#: model:ir.model.fields,field_description:social_media_base.field_social_post__count_post_shares +#, python-format +msgid "Shares" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_account +msgid "Social Account" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#: model:ir.actions.act_window,name:social_media_base.social_media_action +#: model:ir.model,name:social_media_base.model_social_media +#: model:ir.module.category,name:social_media_base.module_category_social_media +#: model:ir.ui.menu,name:social_media_base.social_root_menu +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_campaign_view_form +#, python-format +msgid "Social Media" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_media_base_mixin.py:0 +#, python-format +msgid "" +"Social Media %(social_media)s [%(account)s]

%(message)s" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_media_base_mixin +msgid "Social Media Base Mixin" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_post_mixin +msgid "Social Network Post Mixin" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_post +msgid "Social Post" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_social_post_account +msgid "Social Post Account" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.server,name:social_media_base.send_post_schedule_job_ir_actions_server +msgid "Social Post: Send post schedule" +msgstr "" + +#. module: social_media_base +#: model:ir.ui.menu,name:social_media_base.social_media_menu +msgid "Social medias" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Social network" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.server,name:social_media_base.webhook_schedule_job_ir_actions_server +msgid "Social: Checking social media updates" +msgstr "" + +#. module: social_media_base +#: model:ir.actions.server,name:social_media_base.initial_sync_account_job_ir_actions_server +msgid "Social: Sync posts statistics" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__state +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__state +msgid "State" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "Statistics" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_state +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_state +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_state +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_ads_account/social_ads_account.xml:0 +#, python-format +msgid "Sync ads" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields.selection,name:social_media_base.selection__social_account__enviroment__test +msgid "Test" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "The account %(account)s belongs to another company." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The account %(account)s is already associated with another user. Ask its " +"responsible user or a social media administrator to relink it." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "The data was updated successfully." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/js/views/kanban/social_kanban_record.esm.js:0 +#, python-format +msgid "The post does not exist or has been deleted." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "The post was successfully deleted." +msgstr "" + +#. module: social_media_base +#: model:res.groups,comment:social_media_base.group_social_media_manager +msgid "" +"The user is able to see every social media account and post of the companies" +" he has access to, and to configure the available social medias." +msgstr "" + +#. module: social_media_base +#: model:res.groups,comment:social_media_base.group_social_media_user +msgid "" +"The user is only able to see the social media accounts and posts he is " +"responsible for." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "" +"This permanently deletes the account and its publication history in Odoo: " +"posts, metrics and comments. Campaigns are kept, and nothing is deleted from" +" the social network. This cannot be undone." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_kanban +msgid "This publication no longer exists on the social network" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__impression_count +msgid "" +"Total number of views, which may include multiple views by the same user." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__activity_exception_decoration +#: model:ir.model.fields,help:social_media_base.field_social_post__activity_exception_decoration +#: model:ir.model.fields,help:social_media_base.field_utm_campaign__activity_exception_decoration +#: model:ir.model.fields,help:social_media_base.field_utm_group_campaign__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_utm_campaign +msgid "UTM Campaign" +msgstr "" + +#. module: social_media_base +#: model:ir.model,name:social_media_base.model_utm_group_campaign +msgid "UTM Group Campaign" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/social_utils.py:0 +#, python-format +msgid "Unsupported frequency: %(freq)s" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#: code:addons/social_media_base/static/src/js/views/kanban/kanban_buttons.xml:0 +#: model_terms:ir.ui.view,arch_db:social_media_base.wizard_social_account_view_form +#, python-format +msgid "Update" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__update_keys +msgid "Update Keys" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_wizard_social_account__update_token +msgid "Update Token" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Update account" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_wizard_social_account__update_token +msgid "Update token" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__user_id +#: model:ir.model.fields,help:social_media_base.field_social_post_account__user_id +msgid "" +"User this account belongs to. Only the responsible user and the social media" +" administrators can see it." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__user_id +msgid "" +"User this post belongs to. Only the responsible user and the social media " +"administrators can see it." +msgstr "" + +#. module: social_media_base +#: model:res.groups,name:social_media_base.group_social_media_user +msgid "User: Own Accounts" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__username +msgid "Username" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_post__video_ids +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__video_ids +msgid "Video" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +msgid "Video (" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__can_manage_account +msgid "" +"Whether the current user may update or archive this account: its responsible" +" user and the social media administrators." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.actions.act_window,help:social_media_base.social_post_action +msgid "" +"Write once and publish it to every connected social account.\n" +" You can schedule it for later or send it right away." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_comment/social_comment.esm.js:0 +#: code:addons/social_media_base/static/src/js/app/social_post_account_mixin.esm.js:0 +#, python-format +msgid "You have liked the post." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "button to get them." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/xml/app/common_templates.xml:0 +#, python-format +msgid "comments" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.utm_group_campaign_view_form +msgid "e.g. Winter Campaigns" +msgstr "" diff --git a/social_media_base/models/__init__.py b/social_media_base/models/__init__.py new file mode 100644 index 0000000000..2c3d3522e9 --- /dev/null +++ b/social_media_base/models/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import ir_http +from . import social_media_base_mixin +from . import social_post_mixin +from . import social_account +from . import social_media +from . import social_post +from . import social_post_account +from . import utm_group_campaign +from . import utm_campaign diff --git a/social_media_base/models/ir_http.py b/social_media_base/models/ir_http.py new file mode 100644 index 0000000000..d3015d3e41 --- /dev/null +++ b/social_media_base/models/ir_http.py @@ -0,0 +1,20 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.http import request + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + def session_info(self): + """Deliver once the notification kept by the social media callbacks. + + See ``social.media.base.mixin._notify_user_session``. + """ + result = super().session_info() + notification = request.session.pop("social_media_notification", None) + if notification: + result["social_media_notification"] = notification + return result diff --git a/social_media_base/models/social_account.py b/social_media_base/models/social_account.py new file mode 100644 index 0000000000..bf7b967182 --- /dev/null +++ b/social_media_base/models/social_account.py @@ -0,0 +1,576 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import json +import logging + +from dateutil.relativedelta import relativedelta + +from odoo import Command, api, fields, models +from odoo.exceptions import AccessError +from odoo.tools import file_open + +from ..social_utils import _generate_timestamps, get_weeks + +_logger = logging.getLogger(__name__) + + +class SocialAccount(models.Model): + """Account associated with a social media.""" + + _name = "social.account" + _inherit = [ + "mail.thread", + "mail.activity.mixin", + "avatar.mixin", + "social.media.base.mixin", + ] + _description = "Social Account" + + @api.model + def _default_image(self): + return base64.b64encode(file_open("base/static/img/avatar.png", "rb").read()) + + name = fields.Char() + active = fields.Boolean(default=True) + username = fields.Char() + media_id = fields.Many2one("social.media", ondelete="restrict") + media_type = fields.Selection(related="media_id.media_type") + company_id = fields.Many2one("res.company", default=lambda self: self.env.company) + user_id = fields.Many2one( + "res.users", + string="Responsible", + required=True, + index=True, + default=lambda self: self.env.user, + tracking=True, + help="User this account belongs to. Only the responsible user and the " + "social media administrators can see it.", + ) + remote_ref = fields.Char( + string="Remote Reference", + copy=False, + index=True, + help="Identifier of this account on the social network. It is set by " + "the connector module of each social media.", + ) + advertising_account_id = fields.Char() + last_update_account = fields.Datetime() + post_account_ids = fields.One2many("social.post.account", "account_id") + image_1920 = fields.Image(default=lambda self: self._default_image()) + + comment_count = fields.Integer(default=0) + like_count = fields.Integer(default=0) + click_count = fields.Integer(default=0) + share_count = fields.Integer(default=0) + interactions_count = fields.Integer( + compute="_compute_interactions_count", + store=True, + default=0, + help="Interactions with the publication: clicks, likes, comments and shares.", + ) + impression_count = fields.Integer( + default=0, + help="Total number of views, which may include multiple views by the " + "same user.", + ) + engagement = fields.Float(default=0, digits=(16, 2)) + + account_url = fields.Char(compute="_compute_account_url") + enviroment = fields.Selection( + [("test", "Test"), ("production", "Production")], default="test" + ) + need_update = fields.Boolean(default=False) + + access_token = fields.Char(groups="base.group_system") + refresh_access_token = fields.Char(groups="base.group_system") + expire_access_token_date = fields.Date(string="Expire Access Token") + is_property_account = fields.Boolean( + default=False, compute="_compute_is_property_account" + ) + can_manage_account = fields.Boolean( + compute="_compute_can_manage_account", + help="Whether the current user may update or archive this account: " + "its responsible user and the social media administrators.", + ) + + @api.depends("user_id") + @api.depends_context("uid") + def _compute_is_property_account(self): + for account in self: + account.is_property_account = self.env.user == account.user_id + + @api.depends("user_id") + @api.depends_context("uid") + def _compute_can_manage_account(self): + is_manager = self.env.user.has_group( + "social_media_base.group_social_media_manager" + ) + for account in self: + account.can_manage_account = is_manager or account.user_id == self.env.user + + def update_account(self): + return { + "res_model": "wizard.social.account", + "views": [[False, "form"]], + "target": "new", + "type": "ir.actions.act_window", + "context": { + "default_account_id": self.id, + "default_media_id": self.media_id.id, + "social_update_account": True, + }, + } + + def action_archive_account(self): + """Archive the accounts and their whole footprint. + + Nothing is removed from the social network: relinking the account + reactivates everything. + """ + self.write( + { + "active": False, + } + ) + + @api.model + def _find_account_to_associate(self, media_type, remote_ref, username=None): + """Return the account already linked to ``remote_ref`` on this media. + + The remote reference is the only immutable identifier: a user name + can be renamed and reused by somebody else. ``username`` is a + fallback for the accounts created before it was stored. + """ + accounts = self.sudo().with_context(active_test=False) + account = ( + accounts.search( + [ + ("media_type", "=", media_type), + ("remote_ref", "=", remote_ref), + ], + limit=1, + ) + if remote_ref + else self.browse() + ) + if not account and username: + account = accounts.search( + [ + ("media_type", "=", media_type), + ("username", "=", username), + ("remote_ref", "in", [False, ""]), + ], + limit=1, + ) + return account + + def _check_can_associate(self): + """Check the current user may relink this already existing account. + + Associating writes the credentials of whoever completes the OAuth + flow, so it is restricted to the responsible user and to the + managers to prevent taking over somebody else's account. + """ + self.ensure_one() + account_sudo = self.sudo() + if ( + account_sudo.company_id + and account_sudo.company_id not in self.env.companies + ): + raise AccessError( + self.env._( + "The account %(account)s belongs to another company.", + account=account_sudo.display_name, + ) + ) + if self.env.user.has_group("social_media_base.group_social_media_manager"): + return + if account_sudo.user_id != self.env.user: + raise AccessError( + self.env._( + "The account %(account)s is already associated with " + "another user. Ask its responsible user or a social " + "media administrator to relink it.", + account=account_sudo.display_name, + ) + ) + + def action_purge_account(self): + """Delete the accounts and their publication history from Odoo only. + + Campaigns are kept: ``utm.campaign`` is shared with other + applications and may be linked to leads, orders or mailings. + + :return: the accounts list action, the current record no longer exists. + :rtype: dict + """ + if not self.env.user.has_group("social_media_base.group_social_media_manager"): + raise AccessError( + self.env._("Only a social media administrator can delete an account.") + ) + accounts = self.with_context(active_test=False) + post_accounts = accounts.post_account_ids + linked_posts = ( + self.env["social.post"] + .with_context(active_test=False) + .search([("account_ids", "in", accounts.ids)]) + ) + posts = linked_posts.filtered(lambda post: not (post.account_ids - accounts)) + shared_posts = linked_posts - posts + _logger.info( + "%s permanently deletes the social media accounts %s", + self.env.user.login, + accounts.mapped("display_name"), + ) + post_accounts.unlink() + posts.unlink() + if shared_posts: + shared_posts.write( + {"account_ids": [Command.unlink(account.id) for account in accounts]} + ) + accounts.unlink() + action = self.env["ir.actions.act_window"]._for_xml_id( + "social_media_base.social_account_action" + ) + action["target"] = "main" + return action + + @api.model + def _get_removal_domain(self, media_type): + return [("media_type", "=", media_type)] + + @api.model + def _remove_social_media(self, media_type): + """Drop the credentials and archive the accounts of an uninstalled media. + + ``remote_ref`` is kept, so reinstalling the connector and relinking + the account restores its history instead of duplicating it. + """ + accounts = ( + self.sudo() + .with_context(active_test=False) + .search(self._get_removal_domain(media_type)) + ) + if accounts: + accounts.write(accounts._get_removal_values()) + + def _get_removal_values(self): + """Return the values written on an account when its module is uninstalled. + + Connector modules override it to complete these generic values. + + :rtype: dict + """ + return { + "access_token": False, + "refresh_access_token": False, + "expire_access_token_date": False, + "active": False, + } + + def write(self, vals): + to_toggle = ( + self.filtered(lambda account: account.active != vals["active"]) + if "active" in vals + else self.browse() + ) + res = super().write(vals) + if to_toggle: + to_toggle._propagate_active_to_related(vals["active"]) + return res + + def _propagate_active_to_related(self, active): + """Archive or unarchive the whole footprint of these accounts. + + Dashboard posts, campaigns, campaign groups and the posts whose only + account is one of them. + """ + SocialPostAccount = self.env["social.post.account"].with_context( + active_test=False + ) + SocialPost = self.env["social.post"].with_context(active_test=False) + UtmCampaign = self.env["utm.campaign"].with_context(active_test=False) + SocialPostAccount.search( + [("account_id", "in", self.ids), ("active", "!=", active)] + ).write({"active": active}) + campaigns = UtmCampaign.search( + [("account_id", "in", self.ids), ("active", "!=", active)] + ) + campaigns.write({"active": active}) + post_ids = SocialPost.search( + [("account_ids", "in", self.ids), ("active", "!=", active)] + ) + post_ids.filtered(lambda post: len(post.account_ids) == 1).write( + {"active": active} + ) + groups = campaigns.campaign_group_id + if active: + groups.filtered(lambda group: not group.active).write({"active": True}) + else: + for group in groups.filtered("active"): + has_active_campaign = ( + self.env["utm.campaign"] + .with_context(active_test=True) + .search_count([("campaign_group_id", "=", group.id)], limit=1) + ) + if not has_active_campaign: + group.write({"active": False}) + + @api.depends("name", "media_type") + def _compute_display_name(self): + for account in self: + account.display_name = ( + f"[{account.media_type.upper()}] {account.name}" + if account.media_type + else account.name + ) + + def _fields_account_url(self): + """Return the account URLs as ``(media_type, url)`` tuples. + + Each connector module appends its own. + + :rtype: list + """ + return [] + + @api.depends("media_type", "remote_ref", "username") + def _compute_account_url(self): + for account in self: + account.account_url = "" + for val_url in account._fields_account_url(): + if len(val_url) < 2: + continue + if account.media_type == val_url[0]: + account.account_url = val_url[1] + break + + @api.depends("click_count", "like_count", "share_count", "comment_count") + def _compute_interactions_count(self): + for account in self: + account.interactions_count = ( + account.click_count + + account.like_count + + account.share_count + + account.comment_count + ) + + def _filter_statistics(self, entity_statistics): + post_statistics = { + "click_count": 0, + "like_count": 0, + "comment_count": 0, + "share_count": 0, + "engagement": 0, + "impression_count": 0, + } + for __, statistics in entity_statistics.items(): + post_statistics["click_count"] += statistics[0] + post_statistics["like_count"] += statistics[1] + post_statistics["comment_count"] += statistics[2] + post_statistics["share_count"] += statistics[3] + post_statistics["engagement"] += statistics[4] + post_statistics["impression_count"] += statistics[5] + return post_statistics + + def _get_chart_account_statistics(self, start_date, end_date, granularity): + """Return the account statistics formatted for the chart view. + + :param start_date: start of the period. + :param end_date: end of the period. + :param granularity: WEEK, MONTH or YEAR. + :rtype: list + """ + return [] + + def get_chart_account_statistics( + self, account_id=None, start_date=None, end_date=None, granularity="WEEK" + ): + account = self.browse(account_id) if account_id else self + return account._get_chart_account_statistics(start_date, end_date, granularity) + + def _update_posts_statistics(self, post_id, domain): + """Update the posts and their statistics. + + :param post_id: post to update, all of them when not set. + :param domain: additional domain on the posts. + :rtype: list + """ + return [] + + def update_posts_statistics(self, post_id=None, domain=None): + statistics = self._update_posts_statistics(post_id, domain) + return json.dumps(statistics) + + def validate_access_token(self): + """Hook for the connector modules to refresh an expired token.""" + + def action_import_campaigns(self): + """Import the campaign groups and campaigns from the social network. + + :return: ``success``, ``message`` and the number of imported + ``groups``, ``campaigns`` and ``ads``. + :rtype: dict + """ + self.ensure_one() + return { + "success": False, + "message": self.env._( + "Importing campaigns is not available for this social media." + ), + "groups": 0, + "campaigns": 0, + "ads": 0, + } + + def action_import_campaigns_notify(self): + """Import the campaigns and show the result as a notification.""" + res = self.action_import_campaigns() + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "type": "success" if res.get("success") else "danger", + "message": res.get("message"), + }, + } + + def _load_ads_accounts(self): + """Return the ads accounts of this social media account. + + ``ads`` is always a list so the client action always receives an + iterable, whatever the connector modules add to it. + + :rtype: dict + """ + return {"ads": []} + + def load_ads_accounts(self): + return self._load_ads_accounts() + + def _run_check_media_updates(self): + """Check for new updates on the social network. + + :return: whether new updates were found. + :rtype: bool + """ + return False + + def _trigger_initial_sync(self): + """Run the posts-statistics sync now so the dashboard is populated + right after linking an account.""" + cron = self.env.ref( + "social_media_base.initial_sync_account_job", raise_if_not_found=False + ) + if cron: + cron.sudo()._trigger() + + def _need_update(self, need_update=True): + self.env["bus.bus"]._sendone( + self.env.user.partner_id, + "social_need_update", + {"need_update": need_update}, + ) + + @api.model + def _get_social_dashboard_url(self): + """Return the URL of the Social Media dashboard. + + Used by the OAuth callbacks to land the user on the dashboard + instead of the default app. + """ + menu = self.env.ref( + "social_media_base.social_network_stream_post_menu", + raise_if_not_found=False, + ) + if menu and menu.action: + return f"/web#menu_id={menu.id}&action={menu.action.id}" + return "/web" + + def _get_default_filter_date(self, start_date, end_date, time_date=False, months=1): + start = start_date or (fields.Datetime.now() - relativedelta(months=months)) + end = end_date or fields.Datetime.now() + if time_date: + return _generate_timestamps(date_start=start, date_end=end) + return start, end + + def _map_chart_statistics(self, account_statistics, **values): + data_chart = [] + statistics_values = ( + account_statistics.values() + if isinstance(account_statistics, dict) + else account_statistics + ) + if statistics_values and self.media_type: + chart_weeks = get_weeks( + values.get( + "start_date", + ), + values.get( + "end_date", + ), + freq=values.get("freq", "W-MON"), + env=self.env, + ) + + def map_chart_data(chart_statistics, label, key_data=0): + dataset = { + "pointStyle": "circle", + "pointRadius": 10, + "pointHoverRadius": 15, + "label": label, + "data": [ + statistics[key_data] + for statistics in chart_statistics + if len(statistics) > key_data + ], + } + return dataset + + impression_count = sum( + [ + statistics[5] + for statistics in statistics_values + if len(statistics) > 5 + ] + ) + comment_count = sum( + [ + statistics[2] + for statistics in statistics_values + if len(statistics) > 2 + ] + ) + reaction_count = sum( + [ + statistics[1] + statistics[3] + for statistics in statistics_values + if len(statistics) > 1 and len(statistics) > 3 + ] + ) + data_chart.append( + { + "id": self.id, + "name": f"[{self.media_type.upper()}] {self.name}", + "impressionCount": impression_count, + "commentCount": comment_count, + "reactionCount": reaction_count, + "chartLabel": self.env._("Statistics"), + "labels": [week for week in chart_weeks], + "datasets": [ + map_chart_data(statistics_values, self.env._("Clicks"), 0), + map_chart_data(statistics_values, self.env._("Shares"), 3), + map_chart_data(statistics_values, self.env._("Likes"), 1), + map_chart_data(statistics_values, self.env._("Comments"), 2), + map_chart_data( + statistics_values, + self.env._("Impressions"), + 5, + ), + map_chart_data(statistics_values, self.env._("Engagement"), 4), + ], + } + ) + return data_chart diff --git a/social_media_base/models/social_media.py b/social_media_base/models/social_media.py new file mode 100644 index 0000000000..19f1210434 --- /dev/null +++ b/social_media_base/models/social_media.py @@ -0,0 +1,28 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SocialMedia(models.Model): + """Social media supported by an installed connector module.""" + + _name = "social.media" + _inherit = "social.media.base.mixin" + _description = "Social Media" + + name = fields.Char() + description = fields.Text() + media_type = fields.Selection( + [], + readonly=True, + ) + image = fields.Binary() + + def open_action_account(self): + """Return the wizard action to associate an account. + + Connector modules override it with their own wizard. + + :rtype: dict + """ diff --git a/social_media_base/models/social_media_base_mixin.py b/social_media_base/models/social_media_base_mixin.py new file mode 100644 index 0000000000..38e541ded4 --- /dev/null +++ b/social_media_base/models/social_media_base_mixin.py @@ -0,0 +1,109 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from markupsafe import Markup, escape + +from odoo import models +from odoo.http import request + + +class SocialMediaBaseMixin(models.AbstractModel): + _name = "social.media.base.mixin" + _description = "Social Media Base Mixin" + + def _notify_user_session(self, notif_message, message_type="danger"): + """Keep a message in the session until the web client is loaded again. + + The OAuth callbacks answer with a redirect, so a message sent + through the bus at that moment never reaches the user. These ones + are delivered by ``ir.http.session_info`` instead. + + The message is rendered as markup by the web client, so only the + messages built as ``Markup`` keep their tags: anything else, such as + the answer of a social network, is escaped. + + :param notif_message: the message to display to the user. + :param message_type: the notification type, ``danger`` by default. + """ + if request: + if not isinstance(notif_message, Markup): + notif_message = escape(str(notif_message)) + request.session["social_media_notification"] = { + "message": str(notif_message), + "message_type": message_type, + } + + def _format_user_notification( + self, + notif_message, + media=False, + social_name=False, + account_name=False, + message_type="danger", + ): + """Build the message of a social media notification. + + :param notif_message: the message to display to the user. + :param media: media type to prefix the message with. Without it there + is nothing to notify. + :param social_name: social media name to append to the media type. + :param account_name: account name shown instead of the media name. + :param message_type: the notification type, ``danger`` by default. + :rtype: markupsafe.Markup + """ + if not media: + return Markup() + social_media_name = ( + media.upper() + " " + social_name if social_name else media.upper() + ) + return Markup( + self.env._( + "Social Media %(social_media)s " + "[%(account)s]

%(message)s" + ) + ) % { + "social_media": social_media_name, + "account": social_media_name if not account_name else account_name, + "message": Markup("%s %s") + % ( + "ERROR:" if message_type == "danger" else "", + notif_message or "", + ), + } + + def _notify_user_client( + self, + target=None, + notif_type=None, + notif_message=None, + media=False, + social_name=False, + account_name=False, + ): + """Notify the user of an event through the bus. + + :param target: partner to notify, the current user by default. + :param notif_type: bus notification type, ``danger`` by default. + :param notif_message: the message to display to the user. + :param media: media type to prefix the message with. + :param social_name: social media name to append to the media type. + :param account_name: account name shown instead of the media name. + """ + message_type = notif_type.split("_")[-1] if notif_type else "danger" + message = self._format_user_notification( + notif_message, + media=media, + social_name=social_name, + account_name=account_name, + message_type=message_type, + ) + + if message: + self.env["bus.bus"]._sendone( + target or self.env.user.partner_id, + notif_type, + { + "message_type": message_type, + "message": message, + }, + ) diff --git a/social_media_base/models/social_post.py b/social_media_base/models/social_post.py new file mode 100644 index 0000000000..f20009db86 --- /dev/null +++ b/social_media_base/models/social_post.py @@ -0,0 +1,352 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from odoo import Command, api, fields, models +from odoo.exceptions import MissingError, UserError + + +class SocialPost(models.Model): + _name = "social.post" + _inherit = ["mail.thread", "mail.activity.mixin", "social.post.mixin"] + _description = "Social Post" + + account_ids = fields.Many2many("social.account", required=True, ondelete="restrict") + active = fields.Boolean(default=True) + user_id = fields.Many2one( + "res.users", + string="Responsible", + required=True, + index=True, + default=lambda self: self.env.user, + tracking=True, + help="User this post belongs to. Only the responsible user and the " + "social media administrators can see it.", + ) + message = fields.Text(required=True, tracking=True) + campaign_id = fields.Many2one("utm.campaign") + allow_campaign_ids = fields.Many2many( + "utm.campaign", + relation="social_post_allow_campaign_rel", + compute="_compute_allow_campaign_ids", + help="Campaigns that can be linked to this post.", + ) + send_post = fields.Selection( + [("now", "Now"), ("schedule", "Schedule")], + required=True, + default="now", + tracking=True, + ) + send_post_date = fields.Datetime( + string="Schedule date", compute="_compute_send_post_date", store=True + ) + published_date = fields.Datetime(tracking=True) + state = fields.Selection( + [ + ("draft", "Draft"), + ("planned", "Planned"), + ("publishing", "Publishing"), + ("published", "Published"), + ("cancelled", "Cancelled"), + ], + default="draft", + tracking=True, + ) + post_account_ids = fields.One2many("social.post.account", "post_id") + count_post_likes = fields.Integer( + compute="_compute_post_statistics", default=0, string="Likes" + ) + count_post_comments = fields.Integer( + compute="_compute_post_statistics", default=0, string="Comments" + ) + count_post_clicks = fields.Integer( + compute="_compute_post_statistics", default=0, string="Clicks" + ) + count_post_shares = fields.Integer( + compute="_compute_post_statistics", default=0, string="Shares" + ) + count_post_impression = fields.Integer( + compute="_compute_post_statistics", default=0, string="Impression" + ) + count_post_engagement = fields.Float( + compute="_compute_post_statistics", default=0, string="Engagement" + ) + count_post_interactions = fields.Float( + compute="_compute_post_statistics", default=0, string="Interactions" + ) + image_ids = fields.Many2many( + "ir.attachment", + column1="post_id", + column2="image_id", + ondelete="restrict", + relation="social_network_post_image_rel", + ) + video_ids = fields.Many2many( + "ir.attachment", + relation="social_network_post_video_rel", + column1="post_id", + column2="video_id", + ondelete="restrict", + ) + post_preview = fields.Html(compute="_compute_post_preview", store=True) + message_info = fields.Text(compute="_compute_message_info") + any_failed_post = fields.Boolean(compute="_compute_any_failed_post") + hide_post = fields.Boolean(compute="_compute_hide_post") + + def _get_allow_campaign_domain(self): + """Return the domain of the campaigns that can be linked to this post. + + Connector modules extend it with their own restrictions. + + :rtype: list + """ + self.ensure_one() + return [ + ( + "media_id.media_type", + "in", + self.account_ids.mapped("media_id.media_type"), + ) + ] + + @api.depends("account_ids") + def _compute_allow_campaign_ids(self): + UtmCampaign = self.env["utm.campaign"] + for post in self: + post.allow_campaign_ids = [ + Command.set(UtmCampaign.search(post._get_allow_campaign_domain()).ids) + ] + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("send_post", False) == "schedule": + vals["state"] = "planned" + return super().create(vals_list) + + def write(self, vals): + res = super().write(vals) + if "send_post" in vals or "send_post_date" in vals: + to_plan = self.filtered( + lambda post: ("send_post" in vals and vals["send_post"] == "schedule") + or ("send_post_date" in vals and post.send_post == "schedule") + ) + if to_plan: + to_plan.state = "planned" + return res + + @api.depends("state", "any_failed_post", "send_post", "message", "account_ids") + def _compute_hide_post(self): + for post in self: + post.hide_post = ( + ( + post.state in ("planned", "publishing", "published", "cancelled") + and not post.any_failed_post + ) + or not post.message + or not post.account_ids + ) + + @api.depends("post_account_ids") + def _compute_any_failed_post(self): + for post in self: + post.any_failed_post = any( + post_account.state == "failed" for post_account in post.post_account_ids + ) or (post.state == "publishing" and not post.post_account_ids) + + @api.depends("account_ids") + def _compute_message_info(self): + """Default when no social media module overrides this compute.""" + self.message_info = False + + @api.depends("account_ids.media_id") + def _compute_display_name(self): + for post in self: + post.display_name = self.env._( + "Post on %(posts)s", + posts=", ".join(post.account_ids.mapped("media_id.name")), + ) + + @api.depends("send_post") + def _compute_send_post_date(self): + for post in self: + if post.send_post == "schedule": + post.send_post_date = fields.Datetime.now() + timedelta(hours=1) + else: + post.send_post_date = None + + @api.depends( + "post_account_ids.like_count", + "post_account_ids.comment_count", + "post_account_ids.click_count", + "post_account_ids.share_count", + "post_account_ids.engagement", + "post_account_ids.impression_count", + ) + def _compute_post_statistics(self): + for post in self: + post.count_post_clicks = sum(post.mapped("post_account_ids.click_count")) + post.count_post_shares = sum(post.mapped("post_account_ids.share_count")) + post.count_post_likes = sum(post.mapped("post_account_ids.like_count")) + post.count_post_engagement = sum(post.mapped("post_account_ids.engagement")) + post.count_post_impression = sum( + post.mapped("post_account_ids.impression_count") + ) + post.count_post_comments = sum( + post.mapped("post_account_ids.comment_count") + ) + post.count_post_interactions = ( + post.count_post_clicks + + post.count_post_likes + + post.count_post_comments + + post.count_post_shares + ) + + def filter_by_media_types(self, media_types, add_domain=None): + domain = [ + ("media_type", "in", media_types), + ("post_id", "=", self.id), + ("state", "in", ("ready", "failed")), + ] + if add_domain: + domain += add_domain + return self.env["social.post.account"].search(domain) + + def action_draft(self): + for post in self: + post.state = "draft" + + def action_cancel(self): + for post in self: + if post.state in ("publishing", "published"): + raise UserError( + self.env._( + "%(post)s: cannot be cancelled because it " + "is published or in the process of being published.", + post=post.display_name, + ) + ) + post.state = "cancelled" + + def _render_values_preview(self): + """Return the extra rendering values of the preview. + + :rtype: dict + """ + return {} + + def _render_template_preview(self): + """Render one preview template per account of the post. + + :rtype: str + """ + render_template = "" + IrQweb = self.env["ir.qweb"] + for account in self.account_ids: + values = { + "media_id": account.media_id, + "author": account.name, + "message": self.message, + "image_ids": self.image_ids[0:2], + } + try: + render_template += """\n\n""" + IrQweb._render( + f"social_media_{account.media_id.media_type}" + ".social_media_post_preview", + values | self._render_values_preview(), + ) + except (ValueError, MissingError): + render_template += """\n\n""" + IrQweb._render( + "social_media_base.social_media_post_preview", + values | self._render_values_preview(), + ) + return ( + render_template if render_template else self.env._("No preview available") + ) + + @api.depends("account_ids", "message", "image_ids", "video_ids") + def _compute_post_preview(self): + """Render the preview of the post, one template per selected media. + + Templates are looked up as + ``social_media_{media_type}.social_media_post_preview``, falling + back to the generic one of this module. + """ + for post in self: + post.post_preview = post._render_template_preview() + + def _default_account_ids(self): + """Return the account ids preselected when creating a post. + + :rtype: list + """ + return [] + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + account_ids = self._default_account_ids() + if account_ids: + res["account_ids"] = [(6, 0, account_ids)] + return res + + def action_create_post_account(self): + """Publish the post on all its social media accounts.""" + self._action_create_post_account() + + def _prepare_post_account_values(self): + """Return the ``social.post.account`` commands, one per account. + + :rtype: list + """ + posts_account = [] + for account in self.account_ids: + if account.id not in self.post_account_ids.mapped("account_id").ids: + posts_account.append( + Command.create( + { + "post_id": self.id, + "account_id": account.id, + "state": "ready", + "message": self.message, + } + ) + ) + return posts_account + + def _action_create_post_account(self): + """Send the posts to the social networks and update their state.""" + SocialPostAccount = self.env["social.post.account"] + for post in self: + post.write( + { + "state": "publishing", + "post_account_ids": post._prepare_post_account_values(), + } + ) + SocialPostAccount._action_post(post_id=post) + if all(post_acc.state == "posted" for post_acc in post.post_account_ids): + post.write({"state": "published"}) + elif all(post_acc.state == "failed" for post_acc in post.post_account_ids): + post.write({"state": "draft"}) + + def _message_error_post(self, message, media_id): + self.message_post( + body=self.env._( + "Error posting on [%(media)s]: %(error)s", + media=media_id, + error=message, + ), + ) + + def _run_send_post(self): + """Send the posts whose scheduled date has been reached.""" + posts = self.env["social.post"].search( + [ + ("state", "in", ("planned", "publishing")), + ("send_post", "=", "schedule"), + ("send_post_date", "<=", fields.Datetime.now()), + ] + ) + posts.with_context(social_post_cron=True)._action_create_post_account() diff --git a/social_media_base/models/social_post_account.py b/social_media_base/models/social_post_account.py new file mode 100644 index 0000000000..c468f69a07 --- /dev/null +++ b/social_media_base/models/social_post_account.py @@ -0,0 +1,226 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +import requests + +from odoo import Command, api, fields, models + + +class SocialPostAccount(models.Model): + _name = "social.post.account" + _inherit = ["mail.thread", "social.post.mixin"] + _description = "Social Post Account" + _rec_name = "message" + + post_id = fields.Many2one("social.post", ondelete="restrict") + active = fields.Boolean(default=True) + account_id = fields.Many2one("social.account", ondelete="restrict", required=True) + media_id = fields.Many2one( + "social.media", related="account_id.media_id", required=True + ) + media_type = fields.Selection(related="media_id.media_type") + campaign_id = fields.Many2one(related="post_id.campaign_id") + user_id = fields.Many2one( + related="account_id.user_id", + string="Responsible", + store=True, + index=True, + readonly=True, + ) + + state = fields.Selection( + [ + ("ready", "Ready"), + ("posted", "Posted"), + ("failed", "Failed"), + ("deleted", "Deleted"), + ], + default="ready", + help="'Deleted' means the publication no longer exists on the " + "social network although it is kept in Odoo for history.", + ) + published_date = fields.Datetime() + published = fields.Boolean(default=True) + message = fields.Text(required=True) + remote_ref = fields.Char( + string="Remote Reference", + copy=False, + index=True, + help="Identifier of this publication on the social network. It is set " + "by the connector module of each social media.", + ) + account_remote_ref = fields.Char( + related="account_id.remote_ref", string="Account Remote Reference" + ) + + comment_count = fields.Integer() + like_count = fields.Integer() + click_count = fields.Integer() + share_count = fields.Integer() + impression_count = fields.Float() + engagement = fields.Float() + video_ids = fields.Many2many( + "ir.attachment", + relation="social_post_account_video_rel", + column1="post_id", + column2="video_id", + ondelete="restrict", + ) + + image_ids = fields.Many2many( + "ir.attachment", + column1="post_id", + column2="image_id", + ondelete="restrict", + relation="social_post_account_image_rel", + ) + has_video = fields.Boolean( + default=False, + help="Indicates that the published post has at least one video attached.", + ) + failed_description = fields.Html() + post_account_url = fields.Char() + author = fields.Char(related="account_id.name", store=True) + actor_urn = fields.Char() + + def action_like_post(self, author_urn=None): + """Like the publication on the social network. + + :param author_urn: actor urn performing the like. + :return: ``success`` and ``message`` of the action. + :rtype: dict + """ + return {"success": True, "message": ""} + + def action_like_comment(self, author_urn=None): + return {"success": True, "message": ""} + + def _action_post(self, post_id): + """Publish on the social network, implemented by each connector. + + :param post_id: the ``social.post`` being published. + """ + + def _action_campaign_post(self, post_id): + """Publish the campaign post on the social network. + + :param post_id: the ``social.post`` being published. + :return: ``success`` and ``message`` of the action. + :rtype: dict + """ + + def _delete_post_account(self): + """Delete the publication on the social network. + + :return: ``success`` and ``message`` of the action. + :rtype: dict + """ + + def delete_post_account(self): + self.ensure_one() + self._delete_post_account() + account_id = self.account_id + post_id = self.post_id + self.unlink() + if not post_id.post_account_ids: + post_id.unlink() + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": self.env._( + "Post deleted [%(account)s]", account=account_id.name + ), + "type": "success", + "message": self.env._("The post was successfully deleted."), + "next": {"type": "ir.actions.client", "tag": "reload"}, + }, + } + + def get_comments(self): + """Retrieve the comments of the publication. + + :return: ``success`` and ``data`` of the action. + :rtype: dict + """ + return {"success": False, "data": []} + + def create_comment(self, post_data, context=None): + """Create a comment on the social network. + + :param post_data: message and other data of the comment. + :param context: optional context used to render the comment. + :return: ``success`` and ``data`` of the action. + :rtype: dict + """ + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records._anchor_media_attachments() + return records + + def write(self, vals): + res = super().write(vals) + if "image_ids" in vals or "video_ids" in vals: + self._anchor_media_attachments() + return res + + def _anchor_media_attachments(self): + """Attach the downloaded medias to their publication. + + They are created together with it, so they end up with an empty + ``res_id``: in that state only the administrators can read them and + everybody else gets a placeholder instead of the image. + """ + for post_account in self: + attachments = (post_account.image_ids | post_account.video_ids).filtered( + lambda attachment: not attachment.res_id + ) + if attachments: + attachments.sudo().write( + {"res_model": post_account._name, "res_id": post_account.id} + ) + + def _get_medias_account(self, medias): + """Return the names of the medias already downloaded for this account. + + ``sudo()`` is needed to avoid downloading a media twice: a user who + cannot read another user's attachments would otherwise duplicate + every image on each synchronization. + + :rtype: list + """ + if not medias: + return [] + return ( + self.env["ir.attachment"] + .sudo() + .search( + [ + ("name", "in", medias), + ("res_model", "=", self._name), + ] + ) + .mapped("name") + ) + + def _map_medias_account(self, **values): + attach_values = values or {} + media_content = ( + requests.get(values["url"], timeout=10) + if values.get("url", False) + else None + ) + if media_content and media_content.status_code == 200: + attach_values.update( + { + "type": "binary", + "res_model": self._name, + "res_id": self.id, + "datas": base64.b64encode(media_content.content), + } + ) + return Command.create(attach_values) diff --git a/social_media_base/models/social_post_mixin.py b/social_media_base/models/social_post_mixin.py new file mode 100644 index 0000000000..88dc5010fe --- /dev/null +++ b/social_media_base/models/social_post_mixin.py @@ -0,0 +1,25 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from odoo import api, fields, models + + +class SocialPostMixin(models.AbstractModel): + _name = "social.post.mixin" + _description = "Social Network Post Mixin" + + image_urls = fields.Char(compute="_compute_image_urls", store=True) + + @api.depends(lambda self: ["image_ids"]) + def _compute_image_urls(self): + """Build the image URLs of the post. + + The dependency is a lambda because ``image_ids`` is declared by the + models inheriting this mixin, not by the mixin itself. + """ + for post in self: + post.image_urls = json.dumps( + [f"/web/image/{image_id}" for image_id in post.image_ids.ids] + ) diff --git a/social_media_base/models/utm_campaign.py b/social_media_base/models/utm_campaign.py new file mode 100644 index 0000000000..bddc7a3e23 --- /dev/null +++ b/social_media_base/models/utm_campaign.py @@ -0,0 +1,52 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command, api, fields, models + + +class UtmCampaign(models.Model): + _name = "utm.campaign" + _inherit = ["utm.campaign", "mail.thread", "mail.activity.mixin"] + + name = fields.Char(tracking=True) + title = fields.Char(tracking=True) + campaign_group_id = fields.Many2one( + "utm.group.campaign", string="Campaign group", tracking=True + ) + allow_media_ids = fields.Many2many( + "social.media", string="Allowed Media", compute="_compute_allow_media_ids" + ) + media_id = fields.Many2one( + "social.media", string="Media", domain="[('id','in',allow_media_ids)]" + ) + account_id = fields.Many2one( + "social.account", string="Account", domain="[('media_id','in',allow_media_ids)]" + ) + remote_ref = fields.Char( + string="Remote Reference", + copy=False, + index=True, + help="Identifier of this campaign on the social network. It is set by " + "the connector module of each social media.", + ) + + @api.depends("media_id", "account_id") + def _compute_allow_media_ids(self): + SocialMedia = self.env["social.media"] + for campaign in self: + campaign.allow_media_ids = [ + Command.set( + SocialMedia.search( + [("media_type", "in", campaign._available_campaign())] + ).ids + ) + ] + + def _available_campaign(self): + """Return the media types allowed on a campaign. + + Connector modules append their own. + + :rtype: list + """ + return [] diff --git a/social_media_base/models/utm_group_campaign.py b/social_media_base/models/utm_group_campaign.py new file mode 100644 index 0000000000..2577ea8dcb --- /dev/null +++ b/social_media_base/models/utm_group_campaign.py @@ -0,0 +1,49 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class UtmGroupCampaign(models.Model): + _name = "utm.group.campaign" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "UTM Group Campaign" + + name = fields.Char() + active = fields.Boolean(default=True) + campaign_ids = fields.One2many("utm.campaign", "campaign_group_id") + campaign_count = fields.Integer(compute="_compute_campaign_count") + remote_ref = fields.Char( + string="Remote Reference", + copy=False, + index=True, + help="Identifier of this campaign group on the social network. It is " + "set by the connector module of each social media.", + ) + + @api.depends("campaign_ids") + def _compute_campaign_count(self): + counts = dict( + self.env["utm.campaign"]._read_group( + [("campaign_group_id", "in", self.ids)], + ["campaign_group_id"], + ["__count"], + ) + ) + for group in self: + group.campaign_count = counts.get(group, 0) + + def action_view_campaigns(self): + """Open the campaigns linked to this campaign group.""" + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.env._("Campaigns"), + "res_model": "utm.campaign", + "view_mode": "list,form", + "domain": [("campaign_group_id", "=", self.id)], + "context": { + "default_campaign_group_id": self.id, + "from_social_media": True, + }, + } diff --git a/social_media_base/pyproject.toml b/social_media_base/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/social_media_base/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/social_media_base/readme/CONTRIBUTORS.md b/social_media_base/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..0bfe91dd47 --- /dev/null +++ b/social_media_base/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Binhex Cloud] (https://www.binhex.cloud): + - Edilio Escalona Almira diff --git a/social_media_base/readme/DESCRIPTION.md b/social_media_base/readme/DESCRIPTION.md new file mode 100644 index 0000000000..d70741f064 --- /dev/null +++ b/social_media_base/readme/DESCRIPTION.md @@ -0,0 +1,17 @@ +This module provides the fundamental foundation for social media management. +It facilitates the integration of user accounts, posts, reactions (likes), +comments, and graph-based analysis. Designed to be flexible and scalable, +it allows developers and businesses to integrate and customize social features +according to their needs. + +Main features: + +- Integration of multiple user accounts. +- Basic methods that can be extended and adapted to suit the social network. +- Basic business structure. +- Campaign group and campaign structure linked to posts, with an extensible + hook so each social network module can import its campaigns. +- Dashboard of published posts with campaign, video and deletion indicators. +- Scheduled synchronization of post statistics (monthly cron), plus an + initial synchronization right after an account is linked. +- Account credentials (OAuth tokens) are only visible to administrator users. diff --git a/social_media_base/readme/USAGE.md b/social_media_base/readme/USAGE.md new file mode 100644 index 0000000000..4c1001af5f --- /dev/null +++ b/social_media_base/readme/USAGE.md @@ -0,0 +1,147 @@ +Generate group campaign. +--------------- + +- Go to *Social Media* > Campaigns > Campaign group > New +- A form view opens; fill in the required fields + ![CREATE_GROUP_CAMPAIGN](../static/img/readme/CREATE_GROUP_CAMPAIGN.png) +- Save +- The *Campaigns* stat button on the form shows the number of campaigns of + the group and navigates to them. + +Generate campaign. +--------------- + +- Go to *Social Media* > Campaigns > Campaigns > New +- Fill in the fields. The social media is optional; when a social media is + selected, the campaign group and the account become required. + ![CREATE_CAMPAIGN](../static/img/readme/CREATE_CAMPAIGN.png) +- Save +- Changes on the main campaign fields are logged in the chatter. +- The native campaign stage bar is hidden in the Social Media views; the + stages keep working in the other applications that use campaigns, where + they can also be managed. + +Link a campaign to a post. +--------------- + +- On the post form, select a campaign. Only campaigns whose social media + matches the accounts selected on the post can be chosen. +- The campaign is shown as a badge on the post kanban and on the dashboard + cards. + + ![DASHBOARD](../static/img/readme/DASHBOARD.png) + +Posts on the dashboard. +--------------- + +- Posts that contain a video display a video indicator on their card, and + the posts kanban shows a *Video (N)* counter. +- Posts deleted directly on the social network are marked as *Deleted on + \* and kept in the dashboard as history. + + ![POSTS_KANBAN](../static/img/readme/POSTS_KANBAN.png) + +Charts. +--------------- + +- Go to *Social Media* > Charts +- The view shows one chart per connected account with the statistics of its + publications. The statistics are requested to the social networks when the + view is opened, so a loader is displayed until they are available. + + ![CHARTS](../static/img/readme/CHARTS.png) + +Ads. +--------------- + +- Go to *Social Media* > Campaigns > Ads +- The view lists the sponsored creatives of the connected accounts, each one + with its status, its campaign and the post it promotes. Clicking an ad + opens it on the social network. +- The status is the one set by the advertiser on the social network; the + reason why the network is serving the ad or not is shown in the tooltip of + the badge. +- The ads can be filtered by creation date and by text, which is searched in + the campaign name, the post name and the status. The *Sync ads* button + refreshes them from the social networks. + + ![ADS](../static/img/readme/ADS.png) + +Archive an account. +--------------- + +- The account form provides an *Archive account* button, available to the + user responsible for the account. + + ![ACCOUNT_FORM](../static/img/readme/ACCOUNT_FORM.png) + +- Archiving an account also archives its dashboard posts, campaigns, + campaign groups and the posts linked only to that account. +- Nothing is removed from the social network, and unarchiving the account + restores everything. + +Delete an account permanently. +--------------- + +- The account form provides a *Delete permanently* button, available only to + the *Social Media / Administrator* group. A regular user can only archive + his accounts, he is not allowed to delete them. +- It deletes the account, its dashboard publications and the posts that were + linked only to that account, together with their metrics, comments and + attachments. +- Campaigns and campaign groups are **not** deleted, they only lose their + link to the account: campaigns are shared with the other Odoo applications + and may be linked to leads, orders or mailings. +- Nothing is deleted from the social network: the publications stay online. +- This cannot be undone. To keep the history, archive the account instead. + +Statistics synchronization. +--------------- + +- The scheduled action *Social: Sync posts statistics* updates the post + statistics monthly. +- An initial synchronization is also triggered automatically right after an + account is linked. + +Import campaigns. +--------------- + +- The account form provides an *Import campaigns* button. Each social + network module implements the actual import; a notification shows the + result. + +Account ownership. +--------------- + +- Every account has a *Responsible* user, set to whoever linked it. A regular + user of the *Social Media / User: Own Accounts* group only sees and + manages his own accounts, their posts, campaigns and statistics. +- Linking an account that already exists in Odoo (relinking it after + archiving it, after reinstalling the connector or to renew its tokens) is + restricted to that responsible user and to the *Social Media / + Administrator* group, because it overwrites the credentials and the access + tokens stored in the account. +- Accounts are recognised by the identifier they have on the social network, + not by their name: renaming the account on the social network keeps the + history in Odoo, and a name reused by a different account never overwrites + an existing one. +- If another user completes the association of an account that is not his, + nothing is written and a notification explains that the account belongs to + somebody else. The same happens when the administrator of the wizard tries + to change the credentials of an account that is not his. +- The *Update account* and *Archive account* buttons of the account form are + shown to the responsible user and to the *Social Media / Administrator* + group. The credentials themselves (client keys and tokens) stay hidden + from everybody but the system administrators, and the connectors read and + write them internally, so an administrator of the application can renew + them without ever seeing them. + +Campaign badge. +--------------- + +- The publications of the dashboard and of the posts kanban show the campaign + they belong to as a badge. Long campaign names are shortened with an + ellipsis so the card keeps its shape, and the whole name is available in + the tooltip. +- Clicking the badge opens that campaign in Odoo, so its budget, its status + on the social network and its history are one click away from the post. diff --git a/social_media_base/security/ir.model.access.csv b/social_media_base/security/ir.model.access.csv new file mode 100644 index 0000000000..436e2fe6a9 --- /dev/null +++ b/social_media_base/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_social_media_user,social.media user,model_social_media,social_media_base.group_social_media_user,1,0,0,0 +access_social_media_manager,social.media manager,model_social_media,social_media_base.group_social_media_manager,1,1,1,1 +access_social_account_user,social.account user,model_social_account,social_media_base.group_social_media_user,1,1,1,0 +access_social_account_manager,social.account manager,model_social_account,social_media_base.group_social_media_manager,1,1,1,1 +access_social_post_user,social.post user,model_social_post,social_media_base.group_social_media_user,1,1,1,1 +access_social_post_manager,social.post manager,model_social_post,social_media_base.group_social_media_manager,1,1,1,1 +access_social_post_account_user,social.post.account user,model_social_post_account,social_media_base.group_social_media_user,1,1,1,1 +access_social_post_account_manager,social.post.account manager,model_social_post_account,social_media_base.group_social_media_manager,1,1,1,1 +access_utm_group_campaign_user,utm.group.campaign user,model_utm_group_campaign,social_media_base.group_social_media_user,1,0,0,0 +access_utm_group_campaign_manager,utm.group.campaign manager,model_utm_group_campaign,social_media_base.group_social_media_manager,1,1,1,1 +access_wizard_social_account_user,wizard.social.account user,model_wizard_social_account,social_media_base.group_social_media_user,1,1,1,1 +access_wizard_social_account_manager,wizard.social.account manager,model_wizard_social_account,social_media_base.group_social_media_manager,1,1,1,1 diff --git a/social_media_base/security/social_account_security.xml b/social_media_base/security/social_account_security.xml new file mode 100644 index 0000000000..79830edeab --- /dev/null +++ b/social_media_base/security/social_account_security.xml @@ -0,0 +1,34 @@ + + + + + Social media accounts: only the ones the user is responsible for + + [('user_id', '=', user.id)] + + + + + Social media accounts: all + + [(1, '=', 1)] + + + + + Social media accounts: multi-company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + diff --git a/social_media_base/security/social_media_base_groups.xml b/social_media_base/security/social_media_base_groups.xml new file mode 100644 index 0000000000..bf11d47c5e --- /dev/null +++ b/social_media_base/security/social_media_base_groups.xml @@ -0,0 +1,31 @@ + + + + Social Media + 17 + + + + + Social Media + + + + + + User + + + + + + + Manager + + + + + diff --git a/social_media_base/security/social_post_account_security.xml b/social_media_base/security/social_post_account_security.xml new file mode 100644 index 0000000000..e55f1d22f7 --- /dev/null +++ b/social_media_base/security/social_post_account_security.xml @@ -0,0 +1,26 @@ + + + + + Social media post accounts: only the ones of his own accounts + + [('user_id', '=', user.id)] + + + + + Social media post accounts: all + + [(1, '=', 1)] + + + diff --git a/social_media_base/security/social_post_security.xml b/social_media_base/security/social_post_security.xml new file mode 100644 index 0000000000..dd83cfdc03 --- /dev/null +++ b/social_media_base/security/social_post_security.xml @@ -0,0 +1,26 @@ + + + + + Social media posts: only the ones the user is responsible for + + [('user_id', '=', user.id)] + + + + + Social media posts: all + + [(1, '=', 1)] + + + diff --git a/social_media_base/security/wizard_social_account_security.xml b/social_media_base/security/wizard_social_account_security.xml new file mode 100644 index 0000000000..a05272588a --- /dev/null +++ b/social_media_base/security/wizard_social_account_security.xml @@ -0,0 +1,10 @@ + + + + + Social media account wizard: own records only + + [('create_uid', '=', user.id)] + + diff --git a/social_media_base/social_utils.py b/social_media_base/social_utils.py new file mode 100644 index 0000000000..3f640ed6a5 --- /dev/null +++ b/social_media_base/social_utils.py @@ -0,0 +1,188 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import re +from datetime import date, datetime, timedelta +from urllib.parse import quote, urlencode + +import pytz + +from odoo.exceptions import UserError +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo.tools.date_utils import add + + +def convert_to_days(seconds=None, miliseconds=None): + """ + Converts the given duration in seconds or miliseconds into days. + + :param int seconds: duration in seconds + :param int miliseconds: duration in miliseconds + :return: duration in days + :rtype: int + """ + if seconds: + return seconds / 60 / 60 / 24 + elif miliseconds: + return miliseconds / 1000 / 60 / 60 / 24 + return 0 + + +def convert_to_date( + date_add=None, + seconds=None, + miliseconds=None, + expire_date=True, + time_zone=None, + format_date=None, +): + if time_zone and isinstance(time_zone, str): + time_zone = pytz.timezone(time_zone) + if expire_date: + if not date_add: + date_add = date.today() + return_date = add( + date_add + timedelta(days=convert_to_days(seconds, miliseconds)) + ) + else: + return_date = datetime.fromtimestamp(miliseconds / 1000, tz=time_zone) + if format_date: + return_date = return_date.strftime(format_date) + return return_date + + +def convert_date_in_time(miliseconds, timezone=None): + timezone = timezone if timezone else pytz.utc + if isinstance(timezone, str): + timezone = pytz.timezone(timezone) + val_date = convert_to_date( + miliseconds=miliseconds, expire_date=False, time_zone=timezone + ) + current_date = datetime.now(timezone) + diff_date = current_date - val_date + seconds = diff_date.total_seconds() + minutes = seconds / 60 + hours = minutes / 60 + days = hours / 24 + months = days / 30 + years = months / 12 + + if seconds < 60: + date_in_time = f"{int(seconds)} seconds" + elif minutes < 60: + date_in_time = f"{int(minutes)} minutes" + elif hours < 24: + date_in_time = f"{int(hours)} hours" + elif days < 30: + date_in_time = f"{int(days)} days" + elif months < 12: + date_in_time = f"{int(months)} months" + else: + years_exacts = int(years) + months_exacts = int(months % 12) + date_in_time = f"{years_exacts} years y {months_exacts} months" + return date_in_time + + +def replace_repetitions(text, character_replace, character_new, repetitions): + positions = [m.start() for m in re.finditer(re.escape(character_replace), text)] + text_result = list(text) + count = 0 + for repetition in repetitions: + if int(repetition) - 1 < len(positions): + if count == 0: + start = positions[int(repetition) - 1] + count += 1 + else: + start = positions[int(repetition) - 1] - ( + (len(character_replace) - 1) * count + ) + count += 1 + fin = start + len(character_replace) + text_result[start:fin] = character_new + return "".join(text_result) + + +def social_url_encode( + param_field, params_values, params_values_char_ignore, format_quote=False +): + values = {param_field: params_values[param_field]} + if isinstance(params_values[param_field], list): + values = ( + "List(" + + ",".join( + quote(str(param_value), safe=",") + for param_value in params_values[param_field] + ) + + ")" + ) + url_format = f"{param_field}={quote(values, safe='()%,')}".replace("+", "") + elif format_quote: + url_format = ( + f"{param_field}={quote(str(params_values[param_field]), safe='()%,')}" + ) + else: + url_format = urlencode(values) + if params_values_char_ignore and params_values_char_ignore.get(param_field, False): + for params_values_char in params_values_char_ignore[param_field]: + for key, character in params_values_char.items(): + if quote(character) in url_format and key == "all": + url_format = url_format.replace(quote(character), character) + else: + url_format = replace_repetitions( + url_format, quote(character), character, key.split(",") + ) + return url_format + + +def _generate_timestamps(date_start=None, date_end=None): + if isinstance(date_start, str): + date_start = datetime.strptime(date_start, DEFAULT_SERVER_DATE_FORMAT) + if isinstance(date_end, str): + date_end = datetime.strptime(date_end, DEFAULT_SERVER_DATE_FORMAT) + + if date_start: + date_start_time = date_start.timestamp() * 1000 + else: + date_start_time = datetime.now().timestamp() * 1000 + + if date_end: + date_end_time = date_end.timestamp() * 1000 + else: + date_end_time = date_start_time + (30 * 86400000) + return int(date_start_time), int(date_end_time) + + +def get_weeks(start_date, end_date, freq="W-MON", env=None): + if isinstance(start_date, str): + start_date = datetime.fromisoformat(start_date) + if isinstance(end_date, str): + end_date = datetime.fromisoformat(end_date) + + result = [] + + if freq == "D": + current = start_date + while current <= end_date: + result.append(current.strftime("%d/%m/%Y")) + current += timedelta(days=1) + + elif freq == "ME": + current = start_date.replace(day=1) + while current <= end_date: + next_month = (current.replace(day=28) + timedelta(days=4)).replace(day=1) + last_day = next_month - timedelta(days=1) + if last_day <= end_date: + result.append(last_day.strftime("%m/%Y")) + current = next_month + + elif freq == "W-MON": + days_ahead = (0 - start_date.weekday()) % 7 + current = start_date + timedelta(days=days_ahead) + while current <= end_date: + result.append(current.strftime("%W/%Y")) + current += timedelta(weeks=1) + + else: + raise UserError(env._("Unsupported frequency: %(freq)s", freq=freq)) + + return result diff --git a/social_media_base/static/description/icon.png b/social_media_base/static/description/icon.png new file mode 100644 index 0000000000..f10b3a7b31 Binary files /dev/null and b/social_media_base/static/description/icon.png differ diff --git a/social_media_base/static/description/icon.svg b/social_media_base/static/description/icon.svg new file mode 100644 index 0000000000..c77511d417 --- /dev/null +++ b/social_media_base/static/description/icon.svg @@ -0,0 +1,83 @@ + + diff --git a/social_media_base/static/description/index.html b/social_media_base/static/description/index.html new file mode 100644 index 0000000000..2f1b3812ba --- /dev/null +++ b/social_media_base/static/description/index.html @@ -0,0 +1,644 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Social Media Base

+ +

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

+

This module provides the fundamental foundation for social media +management. It facilitates the integration of user accounts, posts, +reactions (likes), comments, and graph-based analysis. Designed to be +flexible and scalable, it allows developers and businesses to integrate +and customize social features according to their needs.

+

Main features:

+
    +
  • Integration of multiple user accounts.
  • +
  • Basic methods that can be extended and adapted to suit the social +network.
  • +
  • Basic business structure.
  • +
  • Campaign group and campaign structure linked to posts, with an +extensible hook so each social network module can import its +campaigns.
  • +
  • Dashboard of published posts with campaign, video and deletion +indicators.
  • +
  • Scheduled synchronization of post statistics (monthly cron), plus an +initial synchronization right after an account is linked.
  • +
  • Account credentials (OAuth tokens) are only visible to administrator +users.
  • +
+

Table of contents

+ +
+

Usage

+
+

Generate group campaign.

+
    +
  • Go to Social Media > Campaigns > Campaign group > New
  • +
  • A form view opens; fill in the required fields CREATE_GROUP_CAMPAIGN
  • +
  • Save
  • +
  • The Campaigns stat button on the form shows the number of campaigns +of the group and navigates to them.
  • +
+
+
+

Generate campaign.

+
    +
  • Go to Social Media > Campaigns > Campaigns > New
  • +
  • Fill in the fields. The social media is optional; when a social media +is selected, the campaign group and the account become required. +CREATE_CAMPAIGN
  • +
  • Save
  • +
  • Changes on the main campaign fields are logged in the chatter.
  • +
  • The native campaign stage bar is hidden in the Social Media views; the +stages keep working in the other applications that use campaigns, +where they can also be managed.
  • +
+
+
+

Link a campaign to a post.

+
    +
  • On the post form, select a campaign. Only campaigns whose social media +matches the accounts selected on the post can be chosen.

    +
  • +
  • The campaign is shown as a badge on the post kanban and on the +dashboard cards.

    +

    DASHBOARD

    +
  • +
+
+
+

Posts on the dashboard.

+
    +
  • Posts that contain a video display a video indicator on their card, +and the posts kanban shows a Video (N) counter.

    +
  • +
  • Posts deleted directly on the social network are marked as Deleted on +<media> and kept in the dashboard as history.

    +

    POSTS_KANBAN

    +
  • +
+
+
+

Charts.

+
    +
  • Go to Social Media > Charts

    +
  • +
  • The view shows one chart per connected account with the statistics of +its publications. The statistics are requested to the social networks +when the view is opened, so a loader is displayed until they are +available.

    +

    CHARTS

    +
  • +
+
+
+

Ads.

+
    +
  • Go to Social Media > Campaigns > Ads

    +
  • +
  • The view lists the sponsored creatives of the connected accounts, each +one with its status, its campaign and the post it promotes. Clicking +an ad opens it on the social network.

    +
  • +
  • The status is the one set by the advertiser on the social network; the +reason why the network is serving the ad or not is shown in the +tooltip of the badge.

    +
  • +
  • The ads can be filtered by creation date and by text, which is +searched in the campaign name, the post name and the status. The Sync +ads button refreshes them from the social networks.

    +

    ADS

    +
  • +
+
+
+

Archive an account.

+
    +
  • The account form provides an Archive account button, available to +the user responsible for the account.

    +

    ACCOUNT_FORM

    +
  • +
  • Archiving an account also archives its dashboard posts, campaigns, +campaign groups and the posts linked only to that account.

    +
  • +
  • Nothing is removed from the social network, and unarchiving the +account restores everything.

    +
  • +
+
+
+

Delete an account permanently.

+
    +
  • The account form provides a Delete permanently button, available +only to the Social Media / Administrator group. A regular user can +only archive his accounts, he is not allowed to delete them.
  • +
  • It deletes the account, its dashboard publications and the posts that +were linked only to that account, together with their metrics, +comments and attachments.
  • +
  • Campaigns and campaign groups are not deleted, they only lose +their link to the account: campaigns are shared with the other Odoo +applications and may be linked to leads, orders or mailings.
  • +
  • Nothing is deleted from the social network: the publications stay +online.
  • +
  • This cannot be undone. To keep the history, archive the account +instead.
  • +
+
+
+

Statistics synchronization.

+
    +
  • The scheduled action Social: Sync posts statistics updates the post +statistics monthly.
  • +
  • An initial synchronization is also triggered automatically right after +an account is linked.
  • +
+
+
+

Import campaigns.

+
    +
  • The account form provides an Import campaigns button. Each social +network module implements the actual import; a notification shows the +result.
  • +
+
+
+

Account ownership.

+
    +
  • Every account has a Responsible user, set to whoever linked it. A +regular user of the Social Media / User: Own Accounts group only +sees and manages his own accounts, their posts, campaigns and +statistics.
  • +
  • Linking an account that already exists in Odoo (relinking it after +archiving it, after reinstalling the connector or to renew its tokens) +is restricted to that responsible user and to the Social Media / +Administrator group, because it overwrites the credentials and the +access tokens stored in the account.
  • +
  • Accounts are recognised by the identifier they have on the social +network, not by their name: renaming the account on the social network +keeps the history in Odoo, and a name reused by a different account +never overwrites an existing one.
  • +
  • If another user completes the association of an account that is not +his, nothing is written and a notification explains that the account +belongs to somebody else. The same happens when the administrator of +the wizard tries to change the credentials of an account that is not +his.
  • +
  • The Update account and Archive account buttons of the account form +are shown to the responsible user and to the Social Media / +Administrator group. The credentials themselves (client keys and +tokens) stay hidden from everybody but the system administrators, and +the connectors read and write them internally, so an administrator of +the application can renew them without ever seeing them.
  • +
+
+
+

Campaign badge.

+
    +
  • The publications of the dashboard and of the posts kanban show the +campaign they belong to as a badge. Long campaign names are shortened +with an ellipsis so the card keeps its shape, and the whole name is +available in the tooltip.
  • +
  • Clicking the badge opens that campaign in Odoo, so its budget, its +status on the social network and its history are one click away from +the post.
  • +
+
+
+
+

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_base/static/img/readme/ACCOUNT_FORM.png b/social_media_base/static/img/readme/ACCOUNT_FORM.png new file mode 100644 index 0000000000..4b052b44a0 Binary files /dev/null and b/social_media_base/static/img/readme/ACCOUNT_FORM.png differ diff --git a/social_media_base/static/img/readme/ADS.png b/social_media_base/static/img/readme/ADS.png new file mode 100644 index 0000000000..acee10b718 Binary files /dev/null and b/social_media_base/static/img/readme/ADS.png differ diff --git a/social_media_base/static/img/readme/CHARTS.png b/social_media_base/static/img/readme/CHARTS.png new file mode 100644 index 0000000000..d3bc5ca7b8 Binary files /dev/null and b/social_media_base/static/img/readme/CHARTS.png differ diff --git a/social_media_base/static/img/readme/CREATE_CAMPAIGN.png b/social_media_base/static/img/readme/CREATE_CAMPAIGN.png new file mode 100644 index 0000000000..a9028b0c02 Binary files /dev/null and b/social_media_base/static/img/readme/CREATE_CAMPAIGN.png differ diff --git a/social_media_base/static/img/readme/CREATE_GROUP_CAMPAIGN.png b/social_media_base/static/img/readme/CREATE_GROUP_CAMPAIGN.png new file mode 100644 index 0000000000..ea007c2f8f Binary files /dev/null and b/social_media_base/static/img/readme/CREATE_GROUP_CAMPAIGN.png differ diff --git a/social_media_base/static/img/readme/DASHBOARD.png b/social_media_base/static/img/readme/DASHBOARD.png new file mode 100644 index 0000000000..ffe92349df Binary files /dev/null and b/social_media_base/static/img/readme/DASHBOARD.png differ diff --git a/social_media_base/static/img/readme/POSTS_KANBAN.png b/social_media_base/static/img/readme/POSTS_KANBAN.png new file mode 100644 index 0000000000..49e957b8b4 Binary files /dev/null and b/social_media_base/static/img/readme/POSTS_KANBAN.png differ diff --git a/social_media_base/static/src/components/social_account/social_account.esm.js b/social_media_base/static/src/components/social_account/social_account.esm.js new file mode 100644 index 0000000000..b902bf5f6c --- /dev/null +++ b/social_media_base/static/src/components/social_account/social_account.esm.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import {Component, onWillStart, useState} from "@odoo/owl"; +import {useBus, useService} from "@web/core/utils/hooks"; +import {formatFloat} from "@web/views/fields/formatters"; + +export class SocialAccount extends Component { + static template = "social_media_base.SocialAccount"; + static props = { + socialAccounts: {type: Array}, + }; + + setup() { + super.setup(); + this.orm = useService("dialog"); + this.state = useState({ + needUpdate: false, + }); + onWillStart(async () => { + this.state.needUpdate = + this.props.socialAccounts.filter((item) => item.need_update).length > 0; + }); + useBus(this.env.bus, "SOCIAL:NEED-UPDATE", async ({detail: data}) => { + this.state.needUpdate = data.needUpdate; + }); + } + + /** + * Formats the engagement value with two decimal places, avoiding + * floating point representation artifacts (e.g. 1.1500000000000001). + * + * @param {Number} value - The engagement value to format. + * @returns {String} The formatted value. + */ + formatEngagement(value) { + return formatFloat(value || 0, {digits: [16, 2]}); + } +} diff --git a/social_media_base/static/src/components/social_account/social_account.scss b/social_media_base/static/src/components/social_account/social_account.scss new file mode 100644 index 0000000000..41a574c70e --- /dev/null +++ b/social_media_base/static/src/components/social_account/social_account.scss @@ -0,0 +1,5 @@ +div.social-account-oca { + > div { + margin: 0 0 5px 0 !important; + } +} diff --git a/social_media_base/static/src/components/social_account/social_account.xml b/social_media_base/static/src/components/social_account/social_account.xml new file mode 100644 index 0000000000..fbc2834511 --- /dev/null +++ b/social_media_base/static/src/components/social_account/social_account.xml @@ -0,0 +1,74 @@ + + + + +
+ + +
+
+
+ Social Media +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
diff --git a/social_media_base/static/src/components/social_ads/social_ads.esm.js b/social_media_base/static/src/components/social_ads/social_ads.esm.js new file mode 100644 index 0000000000..4dcbb6b38a --- /dev/null +++ b/social_media_base/static/src/components/social_ads/social_ads.esm.js @@ -0,0 +1,56 @@ +/** @odoo-module **/ + +import {Component} from "@odoo/owl"; +import {browser} from "@web/core/browser/browser"; + +export class SocialAds extends Component { + static template = "social_media_base.SocialAds"; + static props = { + socialAds: {type: Object, required: true}, + }; + + /** + * Gets the list of ads. + * + * @returns {Object[]} - The list of ads. + */ + get ads() { + return this.props.socialAds; + } + + /** + * Gets the statistic object of the ads. + * + * @returns {Object} - The statistic object of the ads. + */ + get statistic() { + return this.props.socialAds.statistic; + } + + /** + * Gets the campaign object from the social ads. + * + * @returns {Object} - The campaign object of the ads. + */ + get campaign() { + return this.props.socialAds.campaign; + } + + /** + * Gets the post object from the social ads. + * + * @returns {Object} - The post object of the ads. + */ + get post() { + return this.props.socialAds.post; + } + + /** + * Opens the ad link in a new tab. + * + * @returns {void} + */ + onAdsClick() { + browser.open(this.ads.url); + } +} diff --git a/social_media_base/static/src/components/social_ads/social_ads.scss b/social_media_base/static/src/components/social_ads/social_ads.scss new file mode 100644 index 0000000000..76f84aae7c --- /dev/null +++ b/social_media_base/static/src/components/social_ads/social_ads.scss @@ -0,0 +1,4 @@ +div.social-network-ads { + border: 1px solid var(--border-color, #dee2e6); + background-color: white; +} diff --git a/social_media_base/static/src/components/social_ads/social_ads.xml b/social_media_base/static/src/components/social_ads/social_ads.xml new file mode 100644 index 0000000000..ce419e68be --- /dev/null +++ b/social_media_base/static/src/components/social_ads/social_ads.xml @@ -0,0 +1,41 @@ + + + + + + diff --git a/social_media_base/static/src/components/social_ads_account/social_ads_account.esm.js b/social_media_base/static/src/components/social_ads_account/social_ads_account.esm.js new file mode 100644 index 0000000000..38cb2070bc --- /dev/null +++ b/social_media_base/static/src/components/social_ads_account/social_ads_account.esm.js @@ -0,0 +1,142 @@ +/** @odoo-module **/ + +import {Component, onMounted, useState} from "@odoo/owl"; +import {ControlPanel} from "@web/search/control_panel/control_panel"; +import {SocialAds} from "../social_ads/social_ads.esm"; +import {SocialFilter} from "../social_filter/social_filter.esm"; +import {_t} from "@web/core/l10n/translation"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +const {DateTime} = luxon; + +/** + * Client action listing the ads of every connected social account. + * + * Its props are the ones injected by the action service, hence the wildcard. + */ +export class SocialAdsAccount extends Component { + static template = "social_media_base.SocialAdsAccount"; + static props = ["*"]; + static components = { + ControlPanel, + SocialAds, + SocialFilter, + }; + + /** + * Initializes the component by setting up services and initializing state. + * + * This method sets up the ORM and notification services, and initializes + * the state variables for social ads, campaigns, and posts. The ads are + * loaded once the component is mounted: the request hits the social + * networks, so waiting for it before the first render would leave the + * action blank instead of showing the loader. + */ + setup() { + this.ormService = useService("orm"); + this.notification = useService("notification"); + this.socialAdsAccount = []; + this.campaigns = []; + this.posts = []; + this.social_state = useState({ + socialAds: [], + loaderAds: true, + }); + onMounted(async () => { + await this._loadAdsAccount(); + }); + } + + filter_ads(item, startDate, endDate, val_search) { + const created = DateTime.fromFormat(item.created, "dd/MM/yyyy"); + const start_date = DateTime.fromFormat(startDate, "yyyy-MM-dd"); + const end_date = DateTime.fromFormat(endDate, "yyyy-MM-dd"); + return ( + (start_date ? created >= start_date : false) && + (end_date ? created <= end_date : false) && + (val_search + ? (item.campaign.name + ? item.campaign.name.includes(val_search) + : false) || + (item.post.name ? item.post.name.includes(val_search) : false) || + item.status.includes(val_search) + : true) + ); + } + + onFilterAds({startDate, endDate, val_search}) { + if (val_search || startDate || endDate) { + this.social_state.socialAds = this.socialAdsAccount.ads.filter((item) => { + return this.filter_ads(item, startDate, endDate, val_search); + }); + } else { + this.clearFilter(); + } + } + + /** + * Clears the filter and displays all ads again. + * + * When called, this method resets the `socialAds` state to the original + * list of ads retrieved from the server, effectively clearing any + * filtering criteria. + */ + clearFilter() { + this.social_state.socialAds = this.socialAdsAccount.ads; + } + + /** + * Gets the ads after applying the filter criteria. + * + * @returns {Object[]} - The ads after applying the filter criteria. + */ + get ads() { + return this.social_state.socialAds; + } + + /** + * Loads all ads again from the server. + * + * This method is triggered by the "Sync ads" button and is used to + * reload all ads from the server. It will clear any filtering criteria + * and display all ads again. + * + * @returns {Promise} + */ + async onUpdateAllAds() { + await this._loadAdsAccount(); + } + + /** + * Loads all ads from the server. + * + * This method is called when the user clicks on the "Sync ads" button. + * It will clear any filtering criteria, set the `loaderAds` state to + * `true`, and load all ads from the server. After loading the ads, + * it sets the `loaderAds` state to `false` and updates the component's + * state with the retrieved ads. + * + * @returns {Promise} + */ + async _loadAdsAccount() { + this.social_state.loaderAds = true; + const adsAccount = await this.ormService.call( + "social.account", + "load_ads_accounts", + [[]] + ); + this.socialAdsAccount = {...adsAccount, ads: adsAccount.ads || []}; + this.social_state.socialAds = this.socialAdsAccount.ads; + this.campaigns = adsAccount.campaigns || []; + this.posts = adsAccount.posts || []; + this.social_state.loaderAds = false; + if (!this.social_state.socialAds.length) { + this.notification.add(_t("No ads found in your connected accounts."), { + type: "info", + }); + } + } +} + +registry.category("actions").add("social_ads_account", SocialAdsAccount); diff --git a/social_media_base/static/src/components/social_ads_account/social_ads_account.scss b/social_media_base/static/src/components/social_ads_account/social_ads_account.scss new file mode 100644 index 0000000000..4c6d182540 --- /dev/null +++ b/social_media_base/static/src/components/social_ads_account/social_ads_account.scss @@ -0,0 +1,9 @@ +div.social-network-ads-account { + min-height: 100% !important; + height: 100% !important; + overflow: auto; + + div.social-ads { + margin: 0 !important; + } +} diff --git a/social_media_base/static/src/components/social_ads_account/social_ads_account.xml b/social_media_base/static/src/components/social_ads_account/social_ads_account.xml new file mode 100644 index 0000000000..90e19e4472 --- /dev/null +++ b/social_media_base/static/src/components/social_ads_account/social_ads_account.xml @@ -0,0 +1,65 @@ + + + +