diff --git a/social_media_base/README.rst b/social_media_base/README.rst new file mode 100644 index 0000000000..2ed96e37c5 --- /dev/null +++ b/social_media_base/README.rst @@ -0,0 +1,297 @@ +================= +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/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/17.0/social_media_base + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-17-0/social-17-0-social_media_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=17.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 + media. +- Basic business structure. +- Dashboard of published posts with 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 +===== + +Posts on the dashboard. +----------------------- + +- The dashboard lists the publications that exist on the social media, + together with their statistics. A scheduled publication is not online + yet and a failed one never got there, so neither of them is listed: + they are followed from the *Posts* menu, on the post they belong to, + and the scheduled ones also in the calendar. + +- 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 media are marked as *Deleted on + * and kept in the dashboard as history. + +- Commenting a publication from the dashboard publishes the comment on + the social media, under the account of the publication, which is what + the composer announces. + +- Deleting a post deletes the publications it created. A post whose + publications are still online cannot be deleted: remove them from the + dashboard first, which deletes them on the social media as well, or + archive the post to keep its history. + + |DASHBOARD| + + |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 media + when the view is opened, so a loader is displayed until they are + available. + +- The statistics can be grouped by day, by week or by month, and the + period can be narrowed with the two dates. Each connector module + offers only the granularities its own social media supports, so the + list of the filter depends on the media of the account. + +- The date range has to cover at least one whole unit of the granularity + selected: the social media group their statistics by that unit and + refuse a shorter range. A range of a single day grouped by month, for + example, is rejected before the network is even called. + + |CHARTS| + +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 publications and the + posts left without any active account. A post that still targets an + active account stays visible, and it is archived as soon as its last + account is. + +- Archiving a post archives its publications too. Archived posts are + found in the *Posts* menu with the *Archived* filter, and their form + shows an *Archived* ribbon and an *Unarchive* button. + +- Nothing is removed from the social media. The archived account shows + an *Archived* ribbon and an *Unarchive account* button that restores + everything. + +- A scheduled post whose date passed while the account was archived + comes back as *Draft*, with the reason in its chatter: unarchiving + never hands it to the scheduled action to be published on the spot. + Reschedule it to publish it. + +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. +- The records of the other applications that reference the account only + lose the link, they are **not** deleted. +- Nothing is deleted from the social media: the publications stay + online. +- This cannot be undone. To keep the history, archive the account + instead. + +Scheduled publishing. +--------------------- + +- Setting *Send post* to *Schedule* proposes a date one hour from now + and leaves the post in the *Planned* state. The date can be changed. +- The scheduled action *Social Post: Send post schedule* sends the due + posts every 5 minutes, so a post is published up to 5 minutes after + its date. +- While the post is being sent it stays in the *Publishing* state. +- The scheduled action never sends a post twice: as soon as the + publication is over, the post leaves the states it looks for. What + happens when it does not go through everywhere is described in + *Partial failures* below. + +Partial failures. +----------------- + +- A post is sent to each of its accounts independently: if one account + fails, the publications that already succeeded are kept, with their + reference on the social media, and only the failed one is retried. +- The reason of the failure is shown on the failed publication, inside + the *Posts* tab of the post, and is also logged in the post chatter. +- If at least one account published, the post ends in the *Partially + Published* state. A message in the chatter names the accounts that + failed and notifies the user responsible for each of them. +- A *Partially Published* post is **never** retried by the scheduled + action: solve the problem and press *Post* again to send it to the + accounts that failed. The publications already online are not sent a + second time. +- Its content is frozen from that moment on: message, accounts, images, + videos and schedule can no longer be changed, because they describe + something that already exists on a social media. To publish something + different, create a new post. +- If **every** account fails, nothing reached the social media: the post + goes back to *Draft*, where it can be corrected, and it is no longer + retried automatically. The corrected message is the one sent when + *Post* is pressed again. + +Expired credentials. +-------------------- + +- The token of an account is renewed on its own before publishing, and + again by the scheduled action *Social: Checking social media updates*, + so a post planned days ago does not meet a token that ran out in the + meantime. +- If the social media refuses the credentials anyway, the token is + renewed and the publication is sent a second time. That answer means + nothing reached the network, so nothing can be published twice. +- When the credentials cannot be renewed at all, because the + authorization was revoked or expired for good, the account is marked + as needing an update: the warning appears on the dashboard and its + responsible user is told in the chatter of the account. The + publication is left as failed with the reason on it, and pressing + *Post* again works once the account is authorized. +- Whether the account can renew its own token depends on the social + media, and each connector module documents it. + +Statistics synchronization. +--------------------------- + +- The scheduled action *Social: Sync posts statistics* updates the post + statistics monthly. It also downloads the medias of the publications + created outside of Odoo, so those images appear on the dashboard only + after a synchronization. +- An initial synchronization is also triggered automatically right after + an account is linked. + +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 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 + media, not by their name: renaming the account on the social media + 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*, *Archive account* and *Unarchive 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. + +.. |DASHBOARD| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_base/static/img/readme/DASHBOARD.png +.. |POSTS_KANBAN| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_base/static/img/readme/POSTS_KANBAN.png +.. |CHARTS| image:: https://raw.githubusercontent.com/OCA/social/17.0/social_media_base/static/img/readme/CHARTS.png +.. |ACCOUNT_FORM| image:: https://raw.githubusercontent.com/OCA/social/17.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..bb1c8e865f --- /dev/null +++ b/social_media_base/__manifest__.py @@ -0,0 +1,47 @@ +# 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": "17.0.1.0.0", + "category": "Social Network", + "development_status": "Beta", + "license": "AGPL-3", + "author": "BinhexTeam,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "maintainers": ["edescalona"], + "depends": ["base", "web", "mail"], + "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/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..839e2f4f8a --- /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.models.discuss.mail_guest 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="json", 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..0bc9ecfdcb --- /dev/null +++ b/social_media_base/data/ir_cron_data.xml @@ -0,0 +1,37 @@ + + + + + Social Post: Send post schedule + + code + model._run_send_post() + 5 + minutes + -1 + 1 + + + + Social: Checking social media updates + + code + model._run_check_media_updates() + 30 + minutes + -1 + 1 + + + + Social: Sync posts statistics + + code + model._run_initial_sync() + 1 + months + -1 + 5 + + diff --git a/social_media_base/exceptions.py b/social_media_base/exceptions.py new file mode 100644 index 0000000000..ec9522fc8e --- /dev/null +++ b/social_media_base/exceptions.py @@ -0,0 +1,14 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError + + +class SocialCredentialsError(UserError): + """The social media refused the credentials of the account. + + Told apart from the other errors because it is the only failure worth + retrying on the spot: refreshing the token may be all it takes for the + same call to go through. Anything the social media refuses about the + content itself is not going to change by trying again. + """ diff --git a/social_media_base/i18n/es.po b/social_media_base/i18n/es.po new file mode 100644 index 0000000000..e9ec72f8e3 --- /dev/null +++ b/social_media_base/i18n/es.po @@ -0,0 +1,1793 @@ +# 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-08-03 02:57+0000\n" +"PO-Revision-Date: 2026-08-03 02:57+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: 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 has no active account: restore the account before publishing it." +msgstr "" +"%(post)s no tiene ninguna cuenta activa: restaure la cuenta antes de " +"publicarlo." + +#. 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 +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"%(posts)s cannot be modified because it is already published on " +"%(accounts)s. Create a new post instead." +msgstr "" +"%(posts)s no se puede modificar porque ya está publicada en %(accounts)s. " +"Cree una publicación nueva en su lugar." + +#. 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 media " +"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_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 +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 +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 +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 +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 +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 +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 +#. 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,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_form +#: 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_form +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_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 media." +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 +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_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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.xml:0 +#, python-format +msgid "Clear the dates" +msgstr "Limpiar las fechas" + +#. 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, python-format +msgid "Clicks" +msgstr "Clicks" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +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 as %(account)s…" +msgstr "Comentar como %(account)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 "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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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:ir.model.fields,field_description:social_media_base.field_social_post__content_locked +msgid "Content Locked" +msgstr "Contenido bloqueado" + +#. 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_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_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 +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__effective_date +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +msgid "Date" +msgstr "Fecha" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__send_post_date +msgid "" +"Date the post is sent to the social media. Switching to 'Schedule' proposes " +"one hour from now, and it can be changed." +msgstr "" +"Fecha en la que la publicación se envía a la red social. Al cambiar a " +"'Programar' se propone una hora a partir de ahora, y se puede modificar." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Day" +msgstr "Día" + +#. 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_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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#. 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 +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 +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 +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono de Font Awesome, p. ej. fa-tasks" + +#. 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 +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_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 +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 +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 media. 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_social_post_account__remote_ref +msgid "" +"Identifier of this publication on the social media. 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 +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 +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 +#: 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +msgid "Interactions" +msgstr "Interacciones" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__interactions_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_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 +#: model:ir.model.fields,help:social_media_base.field_social_post_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 +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_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_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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "LinkedIn refused the post" +msgstr "LinkedIn rechazó la publicación" + +#. 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 and posts." +msgstr "Gestione las cuentas y las publicaciones 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_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 +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 +msgid "Messages" +msgstr "Mensajes" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Month" +msgstr "Mes" + +#. 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 +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_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +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 +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 +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 +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_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_chart/social_chart.xml:0 +#, python-format +msgid "No data to display" +msgstr "No hay datos que mostrar" + +#. 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 +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 +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 +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 +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_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__partially_published +msgid "Partially Published" +msgstr "Publicado parcialmente" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__pending_initial_sync +msgid "Pending Initial Sync" +msgstr "Sincronización inicial pendiente" + +#. 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,help:social_media_base.field_social_post_account__effective_date +msgid "" +"Publication date, or the date the post is scheduled for while it is not " +"published yet." +msgstr "" +"Fecha de publicación, o la fecha para la que está programada la publicación " +"mientras no se haya publicado." + +#. 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 +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 +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Restore the account and everything archived with it." +msgstr "Restaura la cuenta y todo lo archivado con ella." + +#. 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 +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__is_scheduled +msgid "Scheduled" +msgstr "Programada" + +#. 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#, 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 Media 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_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Social media" +msgstr "Red social" + +#. 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: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 +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 +#: model:ir.model.fields,help:social_media_base.field_social_account__pending_initial_sync +msgid "" +"Technical field: the account was just associated and its posts still have to" +" be imported by the initial sync cron." +msgstr "" +"Campo técnico: la cuenta se acaba de asociar y sus publicaciones todavía " +"tienen que ser importadas por el cron de sincronización inicial." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The access token expired" +msgstr "El token de acceso ha caducado" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The access token was revoked" +msgstr "El token de acceso ha sido revocado" + +#. 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-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The credentials of the account are no longer valid and could not be renewed:" +" %(error)s. Update the account to authorize it again." +msgstr "" +"Las credenciales de la cuenta ya no son válidas y no se han podido renovar: " +"%(error)s. Actualice la cuenta para autorizarla de nuevo." + +#. 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/components/social_chart_account/social_chart_account.esm.js:0 +#, python-format +msgid "The date range has to cover at least one %s." +msgstr "El rango de fechas tiene que cubrir al menos un %s." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The message is too long" +msgstr "El mensaje es demasiado largo" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post %(posts)s cannot be deleted because it is still published on " +"%(accounts)s. Delete the publication from the dashboard, which removes it " +"from the social media as well, or archive the post to keep its history." +msgstr "" +"La publicación %(posts)s no se puede eliminar porque todavía está publicada " +"en %(accounts)s. Elimine la publicación desde el tablero, lo que también la " +"elimina de la red social, o archive la publicación para conservar su " +"historial." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The post could not be sent" +msgstr "No se pudo enviar la publicación" + +#. 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/tests/test_social_post_base.py:0 +#, python-format +msgid "The post is locked" +msgstr "La publicación está bloqueada" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__is_scheduled +msgid "The post of this publication is scheduled and not published yet." +msgstr "" +"La publicación de este envío está programada y todavía no se ha publicado." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__content_locked +msgid "" +"The post reached at least one social media, so what it says can no longer be" +" changed." +msgstr "" +"La publicación ha llegado al menos a una red social, por lo que su contenido" +" ya no se puede cambiar." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "" +"The post was deleted on the social media, but its Odoo record could not be " +"removed: %(error)s" +msgstr "" +"La publicación se eliminó en la red social, pero no se pudo eliminar su " +"registro en Odoo: %(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post was published on %(published)s but failed on %(failed)s. It is not " +"sent again automatically: solve the problem and press Post to retry the " +"accounts that failed." +msgstr "" +"La publicación se publicó en %(published)s pero falló en %(failed)s. No se " +"vuelve a enviar automáticamente: resuelva el problema y pulse Publicar para " +"reintentar las cuentas que fallaron." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post was set back to draft: its scheduled date (%(date)s) had already " +"passed when it was unarchived. Reschedule it to publish it." +msgstr "" +"La publicación volvió a borrador: su fecha programada (%(date)s) ya había " +"pasado cuando se desarchivó. Reprográmela para publicarla." + +#. 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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "" +"The posts of the account are being imported in the background, they\n" +" will show up here as soon as they are ready." +msgstr "" +"Las publicaciones de la cuenta se están importando en segundo plano,\n" +" aparecerán aquí en cuanto estén listas." + +#. 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/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "The posts of the account were updated." +msgstr "Las publicaciones de la cuenta se han actualizado." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The schedule date of %(post)s (%(date)s) is already past. Choose a date in " +"the future to plan the post." +msgstr "" +"La fecha programada de %(post)s (%(date)s) ya ha pasado. Elija una fecha " +"futura para planificar la publicación." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The scheduled publication failed and the post was set back to draft: " +"%(error)s" +msgstr "" +"La publicación programada falló y la publicación volvió a borrador: " +"%(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The scheduled publication failed, the post stays planned and will be " +"retried: %(error)s" +msgstr "" +"La publicación programada falló, la publicación sigue planificada y se " +"reintentará: %(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The statistics are grouped by %(granularity)s, so the date range has to " +"cover at least one of them. Widen the range or pick a smaller granularity." +msgstr "" +"Las estadísticas se agrupan por %(granularity)s, por lo que el rango de " +"fechas tiene que cubrir al menos uno. Amplíe el rango o elija una " +"granularidad menor." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The token could not be renewed" +msgstr "El token no se ha podido renovar" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_account_base.py:0 +#, python-format +msgid "The token was revoked" +msgstr "El token ha sido revocado" + +#. 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. Nothing is deleted from the social media. This " +"cannot be undone." +msgstr "" +"Esto elimina permanentemente la cuenta y su historial de publicaciones en " +"Odoo: publicaciones, métricas y comentarios. 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_view_form +msgid "" +"This post is already published on at least one social\n" +" media, so its content can no longer be changed. Create a\n" +" new post to publish something different." +msgstr "" +"Esta publicación ya está publicada en al menos una red\n" +" social, por lo que su contenido ya no se puede cambiar. Cree\n" +" una publicación nueva para publicar algo diferente." + +#. 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 media" +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 +msgid "Type of the exception activity on record." +msgstr "Tipo de la actividad de excepción en el registro." + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Unarchive" +msgstr "Desarchivar" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Unarchive account" +msgstr "Desarchivar cuenta" + +#. 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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Week" +msgstr "Semana" + +#. 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" diff --git a/social_media_base/i18n/es_ES.po b/social_media_base/i18n/es_ES.po new file mode 100644 index 0000000000..2499252850 --- /dev/null +++ b/social_media_base/i18n/es_ES.po @@ -0,0 +1,1793 @@ +# 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-08-03 02:57+0000\n" +"PO-Revision-Date: 2026-08-03 02:57+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 has no active account: restore the account before publishing it." +msgstr "" +"%(post)s no tiene ninguna cuenta activa: restaure la cuenta antes de " +"publicarlo." + +#. 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 +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"%(posts)s cannot be modified because it is already published on " +"%(accounts)s. Create a new post instead." +msgstr "" +"%(posts)s no se puede modificar porque ya está publicada en %(accounts)s. " +"Cree una publicación nueva en su lugar." + +#. 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 media " +"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_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 +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 +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 +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 +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 +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 +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 +#. 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,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_form +#: 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_form +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_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 media." +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 +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_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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.xml:0 +#, python-format +msgid "Clear the dates" +msgstr "Limpiar las fechas" + +#. 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, python-format +msgid "Clicks" +msgstr "Clicks" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +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 as %(account)s…" +msgstr "Comentar como %(account)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 "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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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:ir.model.fields,field_description:social_media_base.field_social_post__content_locked +msgid "Content Locked" +msgstr "Contenido bloqueado" + +#. 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_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_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 +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__effective_date +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +msgid "Date" +msgstr "Fecha" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__send_post_date +msgid "" +"Date the post is sent to the social media. Switching to 'Schedule' proposes " +"one hour from now, and it can be changed." +msgstr "" +"Fecha en la que la publicación se envía a la red social. Al cambiar a " +"'Programar' se propone una hora a partir de ahora, y se puede modificar." + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Day" +msgstr "Día" + +#. 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_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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#. 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 +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 +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 +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono de Font Awesome, p. ej. fa-tasks" + +#. 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 +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_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 +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 +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 media. 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_social_post_account__remote_ref +msgid "" +"Identifier of this publication on the social media. 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 +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 +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 +#: 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +msgid "Interactions" +msgstr "Interacciones" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__interactions_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_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 +#: model:ir.model.fields,help:social_media_base.field_social_post_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 +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_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_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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "LinkedIn refused the post" +msgstr "LinkedIn rechazó la publicación" + +#. 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 and posts." +msgstr "Gestione las cuentas y las publicaciones 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_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 +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 +msgid "Messages" +msgstr "Mensajes" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Month" +msgstr "Mes" + +#. 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 +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_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +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 +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 +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 +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_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_chart/social_chart.xml:0 +#, python-format +msgid "No data to display" +msgstr "No hay datos que mostrar" + +#. 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 +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 +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 +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 +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_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__partially_published +msgid "Partially Published" +msgstr "Publicado parcialmente" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__pending_initial_sync +msgid "Pending Initial Sync" +msgstr "Sincronización inicial pendiente" + +#. 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,help:social_media_base.field_social_post_account__effective_date +msgid "" +"Publication date, or the date the post is scheduled for while it is not " +"published yet." +msgstr "" +"Fecha de publicación, o la fecha para la que está programada la publicación " +"mientras no se haya publicado." + +#. 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 +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 +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Restore the account and everything archived with it." +msgstr "Restaura la cuenta y todo lo archivado con ella." + +#. 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 +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__is_scheduled +msgid "Scheduled" +msgstr "Programada" + +#. 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#, 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 Media 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_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Social media" +msgstr "Red social" + +#. 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: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 +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 +#: model:ir.model.fields,help:social_media_base.field_social_account__pending_initial_sync +msgid "" +"Technical field: the account was just associated and its posts still have to" +" be imported by the initial sync cron." +msgstr "" +"Campo técnico: la cuenta se acaba de asociar y sus publicaciones todavía " +"tienen que ser importadas por el cron de sincronización inicial." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The access token expired" +msgstr "El token de acceso ha caducado" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The access token was revoked" +msgstr "El token de acceso ha sido revocado" + +#. 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-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The credentials of the account are no longer valid and could not be renewed:" +" %(error)s. Update the account to authorize it again." +msgstr "" +"Las credenciales de la cuenta ya no son válidas y no se han podido renovar: " +"%(error)s. Actualice la cuenta para autorizarla de nuevo." + +#. 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/components/social_chart_account/social_chart_account.esm.js:0 +#, python-format +msgid "The date range has to cover at least one %s." +msgstr "El rango de fechas tiene que cubrir al menos un %s." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The message is too long" +msgstr "El mensaje es demasiado largo" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post %(posts)s cannot be deleted because it is still published on " +"%(accounts)s. Delete the publication from the dashboard, which removes it " +"from the social media as well, or archive the post to keep its history." +msgstr "" +"La publicación %(posts)s no se puede eliminar porque todavía está publicada " +"en %(accounts)s. Elimine la publicación desde el tablero, lo que también la " +"elimina de la red social, o archive la publicación para conservar su " +"historial." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The post could not be sent" +msgstr "No se pudo enviar la publicación" + +#. 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/tests/test_social_post_base.py:0 +#, python-format +msgid "The post is locked" +msgstr "La publicación está bloqueada" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__is_scheduled +msgid "The post of this publication is scheduled and not published yet." +msgstr "" +"La publicación de este envío está programada y todavía no se ha publicado." + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__content_locked +msgid "" +"The post reached at least one social media, so what it says can no longer be" +" changed." +msgstr "" +"La publicación ha llegado al menos a una red social, por lo que su contenido" +" ya no se puede cambiar." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "" +"The post was deleted on the social media, but its Odoo record could not be " +"removed: %(error)s" +msgstr "" +"La publicación se eliminó en la red social, pero no se pudo eliminar su " +"registro en Odoo: %(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post was published on %(published)s but failed on %(failed)s. It is not " +"sent again automatically: solve the problem and press Post to retry the " +"accounts that failed." +msgstr "" +"La publicación se publicó en %(published)s pero falló en %(failed)s. No se " +"vuelve a enviar automáticamente: resuelva el problema y pulse Publicar para " +"reintentar las cuentas que fallaron." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post was set back to draft: its scheduled date (%(date)s) had already " +"passed when it was unarchived. Reschedule it to publish it." +msgstr "" +"La publicación volvió a borrador: su fecha programada (%(date)s) ya había " +"pasado cuando se desarchivó. Reprográmela para publicarla." + +#. 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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "" +"The posts of the account are being imported in the background, they\n" +" will show up here as soon as they are ready." +msgstr "" +"Las publicaciones de la cuenta se están importando en segundo plano,\n" +" aparecerán aquí en cuanto estén listas." + +#. 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/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "The posts of the account were updated." +msgstr "Las publicaciones de la cuenta se han actualizado." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The schedule date of %(post)s (%(date)s) is already past. Choose a date in " +"the future to plan the post." +msgstr "" +"La fecha programada de %(post)s (%(date)s) ya ha pasado. Elija una fecha " +"futura para planificar la publicación." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The scheduled publication failed and the post was set back to draft: " +"%(error)s" +msgstr "" +"La publicación programada falló y la publicación volvió a borrador: " +"%(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The scheduled publication failed, the post stays planned and will be " +"retried: %(error)s" +msgstr "" +"La publicación programada falló, la publicación sigue planificada y se " +"reintentará: %(error)s" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The statistics are grouped by %(granularity)s, so the date range has to " +"cover at least one of them. Widen the range or pick a smaller granularity." +msgstr "" +"Las estadísticas se agrupan por %(granularity)s, por lo que el rango de " +"fechas tiene que cubrir al menos uno. Amplíe el rango o elija una " +"granularidad menor." + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The token could not be renewed" +msgstr "El token no se ha podido renovar" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_account_base.py:0 +#, python-format +msgid "The token was revoked" +msgstr "El token ha sido revocado" + +#. 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. Nothing is deleted from the social media. This " +"cannot be undone." +msgstr "" +"Esto elimina permanentemente la cuenta y su historial de publicaciones en " +"Odoo: publicaciones, métricas y comentarios. 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_view_form +msgid "" +"This post is already published on at least one social\n" +" media, so its content can no longer be changed. Create a\n" +" new post to publish something different." +msgstr "" +"Esta publicación ya está publicada en al menos una red\n" +" social, por lo que su contenido ya no se puede cambiar. Cree\n" +" una publicación nueva para publicar algo diferente." + +#. 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 media" +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 +msgid "Type of the exception activity on record." +msgstr "Tipo de la actividad de excepción en el registro." + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Unarchive" +msgstr "Desarchivar" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Unarchive account" +msgstr "Desarchivar cuenta" + +#. 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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Week" +msgstr "Semana" + +#. 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" 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..6b4c8cd581 --- /dev/null +++ b/social_media_base/i18n/social_media_base.pot @@ -0,0 +1,1704 @@ +# 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-08-03 02:57+0000\n" +"PO-Revision-Date: 2026-08-03 02:57+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 has no active account: restore the account before publishing it." +msgstr "" + +#. 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 +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"%(posts)s cannot be modified because it is already published on " +"%(accounts)s. Create a new post instead." +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 media " +"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_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 +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 +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 +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 +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 +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 +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 +#. 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,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_form +#: 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_form +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_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 media." +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 +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_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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.xml:0 +#, python-format +msgid "Clear the dates" +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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, python-format +msgid "Clicks" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_kanban +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 as %(account)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 "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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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:ir.model.fields,field_description:social_media_base.field_social_post__content_locked +msgid "Content Locked" +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_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_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 +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__effective_date +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +msgid "Date" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__send_post_date +msgid "" +"Date the post is sent to the social media. Switching to 'Schedule' proposes " +"one hour from now, and it can be changed." +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Day" +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_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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#. 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 +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 +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 +msgid "Font awesome icon e.g. fa-tasks" +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 +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_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 +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 +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 media. 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 media. 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 +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 +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 +#: 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +msgid "Interactions" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__interactions_count +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__interactions_count +msgid "Interactions Count" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_account__interactions_count +#: model:ir.model.fields,help:social_media_base.field_social_post_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 +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_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_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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "LinkedIn refused the post" +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 and posts." +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_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 +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 +msgid "Messages" +msgstr "" + +#. module: social_media_base +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Month" +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 +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_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +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 +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 +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 +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_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_chart/social_chart.xml:0 +#, python-format +msgid "No data to display" +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 +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 +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 +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 +msgid "Number of messages with delivery error" +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__partially_published +msgid "Partially Published" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,field_description:social_media_base.field_social_account__pending_initial_sync +msgid "Pending Initial Sync" +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 +#: 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,help:social_media_base.field_social_post_account__effective_date +msgid "" +"Publication date, or the date the post is scheduled for while it is not " +"published yet." +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 +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 +msgid "Responsible User" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Restore the account and everything archived with it." +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 +#: model:ir.model.fields,field_description:social_media_base.field_social_post_account__is_scheduled +msgid "Scheduled" +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 +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_account_view_tree +#, 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 +#, 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 Media 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_terms:ir.ui.view,arch_db:social_media_base.social_post_view_search +msgid "Social media" +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: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 +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 +#: model:ir.model.fields,help:social_media_base.field_social_account__pending_initial_sync +msgid "" +"Technical field: the account was just associated and its posts still have to" +" be imported by the initial sync cron." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The access token expired" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The access token was revoked" +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-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The credentials of the account are no longer valid and could not be renewed:" +" %(error)s. Update the account to authorize it again." +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/components/social_chart_account/social_chart_account.esm.js:0 +#, python-format +msgid "The date range has to cover at least one %s." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The message is too long" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post %(posts)s cannot be deleted because it is still published on " +"%(accounts)s. Delete the publication from the dashboard, which removes it " +"from the social media as well, or archive the post to keep its history." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The post could not be sent" +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/tests/test_social_post_base.py:0 +#, python-format +msgid "The post is locked" +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post_account__is_scheduled +msgid "The post of this publication is scheduled and not published yet." +msgstr "" + +#. module: social_media_base +#: model:ir.model.fields,help:social_media_base.field_social_post__content_locked +msgid "" +"The post reached at least one social media, so what it says can no longer be" +" changed." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post_account.py:0 +#, python-format +msgid "" +"The post was deleted on the social media, but its Odoo record could not be " +"removed: %(error)s" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post was published on %(published)s but failed on %(failed)s. It is not " +"sent again automatically: solve the problem and press Post to retry the " +"accounts that failed." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The post was set back to draft: its scheduled date (%(date)s) had already " +"passed when it was unarchived. Reschedule it to publish it." +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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_account/social_account.xml:0 +#, python-format +msgid "" +"The posts of the account are being imported in the background, they\n" +" will show up here as soon as they are ready." +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/js/views/kanban/social_kanban_controller.esm.js:0 +#, python-format +msgid "The posts of the account were updated." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The schedule date of %(post)s (%(date)s) is already past. Choose a date in " +"the future to plan the post." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The scheduled publication failed and the post was set back to draft: " +"%(error)s" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_post.py:0 +#, python-format +msgid "" +"The scheduled publication failed, the post stays planned and will be " +"retried: %(error)s" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/models/social_account.py:0 +#, python-format +msgid "" +"The statistics are grouped by %(granularity)s, so the date range has to " +"cover at least one of them. Widen the range or pick a smaller granularity." +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_post_base.py:0 +#, python-format +msgid "The token could not be renewed" +msgstr "" + +#. module: social_media_base +#. odoo-python +#: code:addons/social_media_base/tests/test_social_account_base.py:0 +#, python-format +msgid "The token was revoked" +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. Nothing is deleted from the social media. This " +"cannot be undone." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "" +"This post is already published on at least one social\n" +" media, so its content can no longer be changed. Create a\n" +" new post to publish something different." +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 media" +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 +msgid "Type of the exception activity on record." +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_post_view_form +msgid "Unarchive" +msgstr "" + +#. module: social_media_base +#: model_terms:ir.ui.view,arch_db:social_media_base.social_account_view_form +msgid "Unarchive account" +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 +#. odoo-javascript +#: code:addons/social_media_base/static/src/components/social_filter/social_filter.esm.js:0 +#, python-format +msgid "Week" +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 "" diff --git a/social_media_base/models/__init__.py b/social_media_base/models/__init__.py new file mode 100644 index 0000000000..3b51072138 --- /dev/null +++ b/social_media_base/models/__init__.py @@ -0,0 +1,10 @@ +# 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 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..ade8b8affb --- /dev/null +++ b/social_media_base/models/social_account.py @@ -0,0 +1,753 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import json +import logging + +import psycopg2 +from dateutil.relativedelta import relativedelta + +from odoo import Command, _, api, fields, models +from odoo.exceptions import AccessError, UserError +from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY +from odoo.tools import file_open + +from ..social_utils import _generate_timestamps, get_weeks + +_logger = logging.getLogger(__name__) + +CHART_GRANULARITIES = { + "DAY": relativedelta(days=1), + "WEEK": relativedelta(weeks=1), + "MONTH": relativedelta(months=1), +} + + +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 media. It is set by " + "the connector module of each social media.", + ) + last_update_account = fields.Datetime() + post_account_ids = fields.One2many("social.post.account", "account_id") + image_1920 = fields.Image(default=_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") + need_update = fields.Boolean(default=False) + pending_initial_sync = fields.Boolean( + default=False, + copy=False, + help="Technical field: the account was just associated and its posts " + "still have to be imported by the initial sync cron.", + ) + + 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.", + ) + post_count = fields.Integer(compute="_compute_post_count") + + @api.depends("post_account_ids.post_id") + def _compute_post_count(self): + counts = dict( + self.env["social.post"]._read_group( + domain=[("account_ids", "in", self.ids)], + groupby=["account_ids"], + aggregates=["__count"], + ) + ) + for account in self: + account.post_count = counts.get(account, 0) + + def action_open_posts(self): + """Open the posts this account is one of the targets of.""" + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Posts"), + "res_model": "social.post", + "view_mode": "kanban,tree,form", + "domain": [("account_ids", "in", self.ids)], + "context": {"default_account_ids": [Command.set(self.ids)]}, + } + + @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 media: relinking the account + reactivates everything. + """ + self.write( + { + "active": False, + } + ) + + def action_unarchive_account(self): + """Restore the accounts and everything archived with them. + + The scheduled posts whose date passed while the account was archived + are sent back to draft instead of being published on the spot: that is + handled by ``social.post.write`` for every way of reactivating a post, + see :meth:`~odoo.addons.social_media_base.models.social_post.SocialPost. + _reset_overdue_schedule`. + """ + self.write( + { + "active": True, + } + ) + + @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( + _( + "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( + _( + "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. + + The records of the other applications that reference an account lose + the link instead of being deleted. + + :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( + _("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 and the posts left without any active account. Other + modules extend it with their own related records. + """ + SocialPostAccount = self.env["social.post.account"].with_context( + active_test=False + ) + SocialPost = self.env["social.post"].with_context(active_test=False) + SocialPostAccount.search( + [("account_id", "in", self.ids), ("active", "!=", active)] + ).write({"active": active}) + posts = SocialPost.search( + [("account_ids", "in", self.ids), ("active", "!=", active)] + ) + if not active: + posts = posts.filtered(lambda post: not post.account_ids.filtered("active")) + if not posts: + return + posts.write({"active": active}) + + @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: one of the keys of ``CHART_GRANULARITIES``. + :rtype: list + """ + return [] + + def _get_chart_granularities(self): + """Granularities the chart view offers for these accounts. + + Every social media groups its statistics by its own units, so a + connector narrows the list down to the ones its API supports. + + :rtype: list + """ + return list(CHART_GRANULARITIES) + + @api.model + def _check_chart_date_range(self, start_date, end_date, granularity): + """Refuse a range that does not cover one unit of the granularity. + + The social media answer an error of their own when the range is + shorter than the unit they group the statistics by, so the case is + caught here to answer something the user can act on. The check is + kept on the public method so it also covers the calls made through + RPC by the chart view. + + :raises UserError: when the range is shorter than one unit. + """ + unit = CHART_GRANULARITIES.get(granularity) + if not (unit and start_date and end_date): + return + start = fields.Date.to_date(start_date) + end = fields.Date.to_date(end_date) + if start and end and end < start + unit: + raise UserError( + _( + "The statistics are grouped by %(granularity)s, so the " + "date range has to cover at least one of them. Widen the " + "range or pick a smaller granularity.", + granularity=granularity.lower(), + ) + ) + + def get_chart_account_statistics( + self, start_date=None, end_date=None, granularity="DAY" + ): + self._check_chart_date_range(start_date, end_date, granularity) + return self._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. + + Called before every operation on the social media, so connectors + keep it cheap and answer from the stored expiry dates. + """ + + def action_validate_access_token(self): + """Check the token against the social media, from the account form. + + The user asking whether the token works expects a real answer: the + stored dates cannot tell a token that was revoked on the network side. + ``check_remote_token`` is what connectors use to tell this deliberate + check from the guard that runs before every call. + """ + self.ensure_one() + return self.with_context(check_remote_token=True).validate_access_token() + + def _refresh_credentials(self): + """Renew the credentials of this account without asking the user. + + Hook for the connector modules whose social media allows it. Called + when a publication was refused because of the credentials, so it must + answer whether the caller can try again. + + :return: whether the account can be used again. + :rtype: bool + """ + return False + + def _flag_credentials_expired(self, message): + """Record that this account needs the user to authorize it again. + + The credentials cannot be renewed from Odoo anymore, and whoever + notices is a cron or somebody publishing on another account: the flag + is what puts the warning on the dashboard, and the note is what + reaches the user in charge of the account. + + :param message: the reason the social media gave. + """ + self.ensure_one() + if not self.need_update: + self.sudo().write({"need_update": True}) + self._need_update() + self.message_post( + body=_( + "The credentials of the account are no longer valid and could " + "not be renewed: %(error)s. Update the account to authorize " + "it again.", + error=message, + ), + partner_ids=self.user_id.partner_id.ids, + ) + + def _run_check_media_updates(self): + """Check for new updates on the social media. + + Every run also renews the credentials that are about to expire, so a + token does not run out between two publications: the connectors answer + from their stored expiry dates, so an account whose token is still + good costs nothing. Each account is checked in its own savepoint, and + the one that cannot be renewed is flagged instead of dropping the run + of the others. + + The cron record does not set a user, so the search needs ``sudo()`` to + reach the accounts of every responsible, and the tokens themselves are + restricted to the administrators. + + :return: whether new updates were found. + :rtype: bool + """ + for account in self.sudo().search([]): + try: + with self.env.cr.savepoint(): + account.with_context(not_notify=True).validate_access_token() + except psycopg2.OperationalError as error: + if error.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + _logger.exception( + "Error checking the credentials of the account %s", account.id + ) + except Exception as error: # noqa: BLE001 - one account must not stop the rest + _logger.exception( + "Error checking the credentials of the account %s", account.id + ) + account._flag_credentials_expired(str(error)) + return False + + def _trigger_initial_sync(self): + """Run the posts-statistics sync now so the dashboard is populated + right after linking an account. + + Called on the accounts that were just associated: they are flagged so + the cron only syncs them and not every account of every user. + """ + if not self: + return + self.sudo().write({"pending_initial_sync": True}) + cron = self.env.ref( + "social_media_base.initial_sync_account_job", raise_if_not_found=False + ) + if cron: + cron.sudo()._trigger() + + @api.model + def _run_initial_sync(self): + """Import the posts of the accounts that were just associated. + + Each account is synced in its own savepoint: a network failure on one + of them must not lose what the previous ones already imported. + + The flag is cleared and the user notified whatever happens: the + dashboard shows the account as syncing while the flag is set, and this + cron only runs once a month, so keeping it after a failure would leave + the view waiting for an import nobody is going to retry. The failure + itself is already reported by the connectors through + ``_notify_user_client``, and the user can retry with the *Update* + button. + """ + for account in self.search([("pending_initial_sync", "=", True)]): + try: + with self.env.cr.savepoint(): + account.update_posts_statistics() + except psycopg2.OperationalError as error: + if error.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + _logger.exception( + "Error on the initial sync of the account %s", account.id + ) + except Exception: # noqa: BLE001 - one account must not stop the rest + _logger.exception( + "Error on the initial sync of the account %s", account.id + ) + account.pending_initial_sync = False + account._notify_posts_updated() + + def _notify_posts_updated(self): + """Tell the responsible user that the posts of the account changed. + + The initial sync runs in a cron, so the dashboard the user is looking + at knows nothing about it: this is what makes it reload itself. The + message names the account because a user may be responsible for + several of them. + """ + for account in self: + self.env["bus.bus"]._sendone( + account.user_id.partner_id, + "social_posts_updated", + { + "account_id": account.id, + "message_type": "info", + "message": account._format_user_notification( + _("The posts of the account were updated."), + media=account.media_type or account.media_id.name, + account_name=account.name, + message_type="info", + ), + }, + ) + + def _need_update(self, need_update=True): + """Flag pending updates on the dashboard of the responsible users. + + The check runs in a cron, whose user is not the one owning the + account, so the message has to be addressed to each responsible user. + """ + partners = self.user_id.partner_id or self.env.user.partner_id + for partner in partners: + self.env["bus.bus"]._sendone( + partner, + "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"), + ) + + 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": _("Statistics"), + "granularities": self._get_chart_granularities(), + "labels": [week for week in chart_weeks], + "datasets": [ + map_chart_data(statistics_values, _("Clicks"), 0), + map_chart_data(statistics_values, _("Shares"), 3), + map_chart_data(statistics_values, _("Likes"), 1), + map_chart_data(statistics_values, _("Comments"), 2), + map_chart_data( + statistics_values, + _("Impressions"), + 5, + ), + map_chart_data(statistics_values, _("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..fa3b2fe051 --- /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 media, 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( + _( + "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..d4167c845d --- /dev/null +++ b/social_media_base/models/social_post.py @@ -0,0 +1,669 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +from datetime import timedelta + +from odoo import Command, _, api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import format_datetime + +_logger = logging.getLogger(__name__) + +LOCKED_CONTENT_FIELDS = ( + "account_ids", + "message", + "image_ids", + "video_ids", + "send_post", + "send_post_date", +) + + +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) + 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, + readonly=False, + help="Date the post is sent to the social media. Switching to " + "'Schedule' proposes one hour from now, and it can be changed.", + ) + published_date = fields.Datetime(tracking=True) + state = fields.Selection( + [ + ("draft", "Draft"), + ("planned", "Planned"), + ("publishing", "Publishing"), + ("partially_published", "Partially Published"), + ("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") + content_locked = fields.Boolean( + compute="_compute_content_locked", + help="The post reached at least one social media, so what it says " + "can no longer be changed.", + ) + + @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): + self._check_content_not_locked(vals) + to_toggle = ( + self.filtered(lambda post: post.active != vals["active"]) + if "active" in vals + else self.browse() + ) + res = super().write(vals) + if to_toggle: + lines = to_toggle.with_context(active_test=False).post_account_ids + if vals["active"]: + lines = lines.filtered(lambda line: line.account_id.active) + lines.filtered(lambda line: line.active != vals["active"]).write( + {"active": vals["active"]} + ) + if vals["active"]: + to_toggle._reset_overdue_schedule() + 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 + + def unlink(self): + """Delete the posts together with the publications they created. + + ``social.post.account.post_id`` is ``restrict`` on purpose, so that + the history of what exists on the social media is never dropped by + accident. The post is what knows whether that is the case: a + publication that never reached the network, or one already gone from + it, is deleted with its post; a publication still online stops the + deletion with an explanation. + """ + lines = self.with_context(active_test=False).post_account_ids + online = lines.filtered( + lambda line: line.remote_ref and line.state != "deleted" + ) + if online: + raise UserError( + _( + "The post %(posts)s cannot be deleted because it is still " + "published on %(accounts)s. Delete the publication from " + "the dashboard, which removes it from the social media " + "as well, or archive the post to keep its history.", + posts=", ".join(online.post_id.mapped("display_name")), + accounts=", ".join(online.account_id.mapped("display_name")), + ) + ) + lines.unlink() + return super().unlink() + + def _get_locked_content_fields(self): + """Return the fields frozen once the post reached a social media. + + Extension point for the modules adding a field that changes what is + published or how. + + :rtype: tuple + """ + return LOCKED_CONTENT_FIELDS + + def _check_content_not_locked(self, vals): + """Refuse to change what has already been sent to a social media. + + The publication is irreversible: once an account has published, the + post is the description of something that exists outside of Odoo, and + editing it would only make the two disagree. The form already shows + those fields as readonly, so this guards the ways in that do not go + through it, and the post that comes back to ``draft`` by any route + while one of its publications is still online. + + :param vals: the values about to be written. + """ + if not any(field in vals for field in self._get_locked_content_fields()): + return + locked = self.filtered("content_locked") + if locked: + raise UserError( + _( + "%(posts)s cannot be modified because it is already " + "published on %(accounts)s. Create a new post instead.", + posts=", ".join(locked.mapped("display_name")), + accounts=", ".join( + locked._get_published_lines().account_id.mapped("display_name") + ), + ) + ) + + def _get_published_lines(self): + """Return the publications of these posts that reached a social media. + + Archived publications count: they describe something that is still + online, and archiving the post never removed it from the network. + + :rtype: odoo.models.Model + """ + return self.with_context(active_test=False).post_account_ids.filtered( + lambda line: line.remote_ref or line.state == "posted" + ) + + @api.depends("post_account_ids.remote_ref", "post_account_ids.state") + def _compute_content_locked(self): + for post in self: + post.content_locked = bool(post._get_published_lines()) + + @api.constrains("send_post", "send_post_date") + def _check_send_post_date_not_past(self): + """Refuse to schedule a post for a date already reached. + + The cron sends every planned post whose date is past, so a date behind + the clock is not a schedule: it is an immediate publication that + nobody asked for. Only the states where the date is still a decision + are checked, so a post already sent keeps the date it was sent on. + """ + now = fields.Datetime.now() + for post in self: + if ( + post.send_post == "schedule" + and post.state in ("draft", "planned") + and post.send_post_date + and post.send_post_date < now + ): + raise ValidationError( + _( + "The schedule date of %(post)s (%(date)s) is already " + "past. Choose a date in the future to plan the post.", + post=post.display_name, + date=format_datetime(self.env, post.send_post_date), + ) + ) + + @api.depends("state", "any_failed_post", "send_post", "message", "account_ids") + def _compute_hide_post(self): + """Hide the post button when publishing is not a decision to take. + + A planned post keeps the button: the schedule says when the cron will + send it, not that the user gave up on sending it right away. Once the + post left for the social media the button only comes back to retry the + publications that failed. + """ + for post in self: + post.hide_post = ( + ( + post.state + in ( + "publishing", + "partially_published", + "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 = _( + "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", + "post_account_ids.interactions_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 = sum( + post.mapped("post_account_ids.interactions_count") + ) + + 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", "partially_published", "published"): + raise UserError( + _( + "%(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, media): + """Return the extra rendering values of the preview of one media. + + Connector modules override it to make the preview match what their + social media really publishes, which is why they receive the + media: a post may target several media at once, and what one of + them drops the others may publish. + + :param media: the ``social.media`` the preview is rendered for. + :rtype: dict + """ + return {} + + def _render_template_preview(self): + """Render one preview template per media of the post. + + The preview answers how the content will look on a network, not on a + given account: the message and the attachments belong to the post, so + several accounts of the same media would render the very same card. + + :rtype: str + """ + render_template = "" + IrQweb = self.env["ir.qweb"] + for media in self.account_ids.media_id: + values = { + "media_id": media, + "author": media.name, + "message": self.message, + "image_ids": self.image_ids[0:2], + "video_ids": self.video_ids[0:2], + } + values |= self._render_values_preview(media) + try: + render_template += """\n\n""" + IrQweb._render( + f"social_media_{media.media_type}.social_media_post_preview", + values, + ) + except ValueError: + render_template += """\n\n""" + IrQweb._render( + "social_media_base.social_media_post_preview", + values, + ) + return render_template if render_template else _("No preview available") + + @api.depends("account_ids.media_id", "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 _sync_pending_lines_message(self): + """Give the pending publications the current text of the post. + + The message of a publication is a copy taken when the line is created, + and the line is only created once per account: without this, correcting + the text of a post that failed everywhere would change what the user + reads in Odoo and not what is sent to the social media, while the + images, read from the post itself, would be updated. Nothing is + touched once a publication is online: what it says is already public, + and the whole post is frozen by then. + """ + self.ensure_one() + if self.content_locked: + return + pending = self.post_account_ids.filtered( + lambda line: line.state in ("ready", "failed") + and line.message != self.message + ) + if pending: + pending.write({"message": self.message}) + + def _action_create_post_account(self): + """Send the posts to the social media and update their state.""" + SocialPostAccount = self.env["social.post.account"] + for post in self: + if not post.account_ids: + raise UserError( + _( + "%(post)s has no active account: restore the account " + "before publishing it.", + post=post.display_name, + ) + ) + post.write( + { + "state": "publishing", + "post_account_ids": post._prepare_post_account_values(), + } + ) + post._sync_pending_lines_message() + SocialPostAccount._action_post(post_id=post) + post._close_publication() + + def _close_publication(self): + """Set the state of the post from the result of its publications. + + A publication that reached the social media cannot be taken back, so + a partial success is an outcome of its own: the post is neither + published nor pending, it is ``partially_published``. That state is + out of the domain of :meth:`_run_send_post` on purpose, so the failed + accounts are only retried when somebody presses the button again, + instead of every five minutes forever. The post only goes back to + ``draft``, where it can be corrected, when nothing at all was sent. + """ + self.ensure_one() + lines = self.post_account_ids + if lines and all(line.state == "posted" for line in lines): + self.write( + { + "state": "published", + "published_date": fields.Datetime.now(), + } + ) + elif any(line.state == "posted" for line in lines): + values = {"state": "partially_published"} + if not self.published_date: + values["published_date"] = fields.Datetime.now() + self.write(values) + self._notify_partial_publication( + lines.filtered(lambda line: line.state == "failed") + ) + elif lines and all(line.state == "failed" for line in lines): + self.write({"state": "draft"}) + + def _notify_partial_publication(self, failed_lines): + """Tell the users in charge of the accounts that did not publish. + + The post is already online somewhere, so the failure is not going to + be noticed by whoever opens the post next: the responsible of each + account that failed is notified on the chatter, where the reason of + every failure has already been logged by + :meth:`~odoo.addons.social_media_base.models.social_post_account. + SocialPostAccount._register_publish_failure`. + + :param failed_lines: the ``social.post.account`` that failed. + """ + self.ensure_one() + if not failed_lines: + return + partners = failed_lines.account_id.user_id.partner_id + self.message_post( + body=_( + "The post was published on %(published)s but failed on " + "%(failed)s. It is not sent again automatically: solve the " + "problem and press Post to retry the accounts that failed.", + published=", ".join( + self.post_account_ids.filtered( + lambda line: line.state == "posted" + ).account_id.mapped("display_name") + ), + failed=", ".join(failed_lines.account_id.mapped("display_name")), + ), + partner_ids=partners.ids, + ) + + def _message_error_post(self, message, media_id): + self.message_post( + body=_( + "Error posting on [%(media)s]: %(error)s", + media=media_id, + error=message, + ), + ) + + def _register_cron_failure(self, error, back_to_draft=False): + """Leave in the chatter the reason why the cron could not send a post. + + The failure already rolled back its savepoint, so the note is the only + trace the responsible user gets. A business error is not going to solve + itself on the next run: the post goes back to draft, out of the reach + of the cron, instead of failing every five minutes. Anything else keeps + the post planned so the cron retries it. + """ + self.ensure_one() + try: + if back_to_draft: + self.write({"state": "draft"}) + body = _( + "The scheduled publication failed and the post was set " + "back to draft: %(error)s", + error=str(error), + ) + else: + body = _( + "The scheduled publication failed, the post stays planned " + "and will be retried: %(error)s", + error=str(error), + ) + self.message_post(body=body) + except Exception: # noqa: BLE001 - the note must not drop the batch + _logger.exception( + "Could not record the failure of the scheduled post %s", + self.display_name, + ) + + def _reset_overdue_schedule(self): + """Send back to draft the posts whose scheduled date is already past. + + Called when a post becomes active again: while it was archived the + cron could not see it, and reactivating it as ``planned`` with a date + already reached would publish it on the social media within the + next run, without anybody asking for it. Back in draft, the user + reschedules it and posts it again deliberately. + + A ``partially_published`` post is left alone: it is already out of the + reach of the cron, and its content cannot be edited anymore. + """ + overdue = self.filtered( + lambda post: post.state in ("planned", "publishing") + and post.send_post == "schedule" + and post.send_post_date + and post.send_post_date <= fields.Datetime.now() + ) + if not overdue: + return + overdue.write({"state": "draft"}) + for post in overdue: + post.message_post( + body=_( + "The post was set back to draft: its scheduled date " + "(%(date)s) had already passed when it was unarchived. " + "Reschedule it to publish it.", + date=fields.Datetime.to_string(post.send_post_date), + ) + ) + + @api.model + def _run_send_post(self): + """Send the posts whose scheduled date has been reached. + + The cron record does not set a user, so it runs as the one who + installed the module: the search needs ``sudo()`` to see the posts of + every responsible. Each post is then published on behalf of its own + responsible, and isolated in its own savepoint so a failure outside + the per-account guard does not drop the rest of the batch. + + ``partially_published`` is deliberately out of the domain: a post that + already reached a social media is only retried by hand, otherwise a + permanently failing account would be retried on every run. + """ + posts = ( + self.env["social.post"] + .sudo() + .search( + [ + ("state", "in", ("planned", "publishing")), + ("send_post", "=", "schedule"), + ("send_post_date", "<=", fields.Datetime.now()), + ] + ) + ) + for post in posts: + try: + with self.env.cr.savepoint(): + post.with_user(post.user_id).with_context( + **{ + "social_post_cron": True, + } + )._action_create_post_account() + except (UserError, ValidationError) as error: + post._register_cron_failure(error, back_to_draft=True) + except Exception as error: # noqa: BLE001 - one post must not drop the batch + _logger.exception( + "Error sending the scheduled post %s", post.display_name + ) + post._register_cron_failure(error) 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..211bc6cab0 --- /dev/null +++ b/social_media_base/models/social_post_account.py @@ -0,0 +1,446 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import logging +from contextlib import contextmanager + +import psycopg2 +import requests + +from odoo import Command, _, api, fields, models +from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY +from odoo.tools import plaintext2html + +from ..exceptions import SocialCredentialsError + +_logger = logging.getLogger(__name__) + + +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") + 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 media although it is kept in Odoo for history.", + ) + published_date = fields.Datetime() + published = fields.Boolean(default=True) + effective_date = fields.Datetime( + string="Date", + compute="_compute_effective_date", + store=True, + help="Publication date, or the date the post is scheduled for while " + "it is not published yet.", + ) + is_scheduled = fields.Boolean( + string="Scheduled", + compute="_compute_is_scheduled", + store=True, + help="The post of this publication is scheduled and not published yet.", + ) + message = fields.Text(required=True) + remote_ref = fields.Char( + string="Remote Reference", + copy=False, + index=True, + help="Identifier of this publication on the social media. 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() + interactions_count = fields.Integer( + compute="_compute_interactions_count", + store=True, + help="Interactions with the publication: clicks, likes, comments and " + "shares.", + ) + 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() + + @api.depends("click_count", "like_count", "share_count", "comment_count") + def _compute_interactions_count(self): + for post_account in self: + post_account.interactions_count = ( + post_account.click_count + + post_account.like_count + + post_account.share_count + + post_account.comment_count + ) + + @api.depends("published_date", "post_id.send_post_date") + def _compute_effective_date(self): + for post_account in self: + post_account.effective_date = ( + post_account.published_date or post_account.post_id.send_post_date + ) + + @api.depends("published_date", "post_id.send_post_date") + def _compute_is_scheduled(self): + for post_account in self: + post_account.is_scheduled = bool( + post_account.post_id.send_post_date and not post_account.published_date + ) + + def action_like_post(self, author_urn=None): + """Like the publication on the social media. + + :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 media, implemented by each connector. + + :param post_id: the ``social.post`` being published. + """ + + @contextmanager + def _publish_guard(self): + """Isolate the publication of one account in its own savepoint. + + Publishing is an irreversible external effect: once the social media + has accepted the post, an error raised while publishing on another + account must not roll back the reference of this one. Connectors wrap + the body of their per-account loop with this guard so a failure is + recorded on its own line, the accounts already published keep their + ``remote_ref`` and the retry only targets the failed ones. + """ + self.ensure_one() + try: + with self.env.cr.savepoint(): + yield + except psycopg2.OperationalError as error: + if error.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + self._register_publish_failure(error) + except Exception as error: # noqa: BLE001 - external effect, see above + self._register_publish_failure(error) + + def _publish_attempt(self, publish, **kwargs): + """Publish, renewing the credentials of the account when needed. + + The token is renewed first when the connector knows from its stored + dates that it expired, which is what the hook is for. Those dates + cannot see a token revoked on the network side, nor one that runs out + between the check and the call: that only shows up as the social media + refusing the credentials, and it means nothing was published, so the + same call is safe to run a second time once the token has been + renewed. Any other error is left alone: the social media is not going + to change its mind about the content. + + Called from inside :meth:`_publish_guard`, so an account whose + credentials cannot be renewed fails its own line, with the reason on + it, and the other accounts of the post go out as usual. + + :param publish: the bound method that publishes on the social media. + :param kwargs: the arguments of that method. + :return: whatever ``publish`` returns. + """ + self.ensure_one() + self.account_id.with_context(not_notify=True).validate_access_token() + try: + return publish(**kwargs) + except SocialCredentialsError as error: + if not self.account_id._refresh_credentials(): + self.account_id._flag_credentials_expired(str(error)) + raise + _logger.info( + "Credentials renewed while publishing on %(media)s for account " + "%(account)s, publishing again", + {"media": self.media_type, "account": self.account_id.name}, + ) + return publish(**kwargs) + + def _register_publish_failure(self, error): + """Mark this publication as failed and keep the reason on the line. + + Called from the ``except`` block of :meth:`_publish_guard`, once the + savepoint has been rolled back and the ORM cache cleared. + + :param error: the exception raised while publishing. + """ + _logger.exception( + "Error publishing the post on %(media)s for account %(account)s", + {"media": self.media_type, "account": self.account_id.name}, + ) + self.write( + { + "state": "failed", + "failed_description": plaintext2html(str(error)), + } + ) + if self.post_id: + self.post_id._message_error_post(str(error), self.media_type) + + def _delete_post_account(self): + """Delete the publication on the social media. + + :return: ``success`` and ``message`` of the action. + :rtype: dict + """ + + def _register_delete_failure(self, error): + """Record that the remote publication is gone but the line survived. + + Called once the savepoint of :meth:`delete_post_account` has been + rolled back. The social media has already deleted the publication at + that point, so the line is kept as failed and without its remote + reference, which is the truth, instead of raising and rolling back the + whole transaction into a line pointing at something that no longer + exists. + + :param error: the exception raised while deleting the local records. + :return: a notification action describing what is left to do. + :rtype: dict + """ + _logger.exception( + "The post was deleted on %(media)s for account %(account)s but " + "its Odoo records could not be removed", + {"media": self.media_type, "account": self.account_id.name}, + ) + self.write( + { + "remote_ref": False, + "post_account_url": False, + "state": "failed", + "failed_description": plaintext2html(str(error)), + } + ) + if self.post_id: + self.post_id._message_error_post(str(error), self.media_type) + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Post deleted [%(account)s]", account=self.account_id.name), + "type": "danger", + "message": _( + "The post was deleted on the social media, but its Odoo " + "record could not be removed: %(error)s", + error=str(error), + ), + "sticky": True, + "next": {"type": "ir.actions.client", "tag": "reload"}, + }, + } + + def delete_post_account(self): + self.ensure_one() + self._delete_post_account() + account_id = self.account_id + post_id = self.post_id + try: + with self.env.cr.savepoint(): + self.unlink() + if not post_id.post_account_ids: + post_id.unlink() + except psycopg2.OperationalError as error: + if error.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + return self._register_delete_failure(error) + except Exception as error: # noqa: BLE001 - external effect, see above + return self._register_delete_failure(error) + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Post deleted [%(account)s]", account=account_id.name), + "type": "success", + "message": _("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 media. + + :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): + """Download a media of the social media and attach it here. + + Nothing is created when the download fails: an attachment without + ``datas`` would still take the name of the media, and + :meth:`_get_medias_account` would then consider it already downloaded + and never retry it. + + :return: the command creating the attachment, or False on failure. + :rtype: dict | bool + """ + attach_values = values or {} + if not values.get("url", False): + return Command.create(attach_values) + try: + media_content = requests.get(values["url"], timeout=10) + except requests.exceptions.RequestException: + _logger.warning("Could not download the media %s", values["url"]) + return False + if media_content.status_code != 200: + _logger.warning( + "Could not download the media %(url)s: %(status)s", + {"url": values["url"], "status": media_content.status_code}, + ) + return False + attach_values.update( + { + "type": "binary", + "res_model": self._name, + "res_id": self.id, + "datas": base64.b64encode(media_content.content), + } + ) + return Command.create(attach_values) + + def _copy_medias_account(self, attachments, names): + """Copy the local attachments of the post into this publication. + + Used when the social media has not exposed the uploaded medias yet: + the binary is already in Odoo, so it is copied instead of being + downloaded back. Every copy takes the name of the media it became on + the social media, so :meth:`_get_medias_account` recognises it and + the next synchronization does not download a duplicate. + + :param attachments: the local attachments of the post. + :param names: the remote names, in the same order as ``attachments``. + :return: the commands creating the copies. + :rtype: list + """ + self.ensure_one() + stored = self._get_medias_account(list(names)) + return [ + Command.create( + { + "name": name, + "type": "binary", + "mimetype": attachment.mimetype, + "datas": attachment.datas, + "res_model": self._name, + "res_id": self.id, + } + ) + for attachment, name in zip(attachments, names, strict=False) + if name and name not in stored + ] 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..bf56ebe7d6 --- /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 Media 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/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..7951262e51 --- /dev/null +++ b/social_media_base/readme/DESCRIPTION.md @@ -0,0 +1,15 @@ +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 media. +- Basic business structure. +- Dashboard of published posts with 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..4c0dcef809 --- /dev/null +++ b/social_media_base/readme/USAGE.md @@ -0,0 +1,165 @@ +Posts on the dashboard. +--------------- + +- The dashboard lists the publications that exist on the social media, + together with their statistics. A scheduled publication is not online yet + and a failed one never got there, so neither of them is listed: they are + followed from the *Posts* menu, on the post they belong to, and the + scheduled ones also in the calendar. +- 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 media are marked as *Deleted on + \* and kept in the dashboard as history. +- Commenting a publication from the dashboard publishes the comment on the + social media, under the account of the publication, which is what the + composer announces. +- Deleting a post deletes the publications it created. A post whose + publications are still online cannot be deleted: remove them from the + dashboard first, which deletes them on the social media as well, or + archive the post to keep its history. + + ![DASHBOARD](../static/img/readme/DASHBOARD.png) + + ![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 media when the + view is opened, so a loader is displayed until they are available. +- The statistics can be grouped by day, by week or by month, and the period + can be narrowed with the two dates. Each connector module offers only the + granularities its own social media supports, so the list of the filter + depends on the media of the account. +- The date range has to cover at least one whole unit of the granularity + selected: the social media group their statistics by that unit and + refuse a shorter range. A range of a single day grouped by month, for + example, is rejected before the network is even called. + + ![CHARTS](../static/img/readme/CHARTS.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 publications and the + posts left without any active account. A post that still targets an active + account stays visible, and it is archived as soon as its last account is. +- Archiving a post archives its publications too. Archived posts are found + in the *Posts* menu with the *Archived* filter, and their form shows an + *Archived* ribbon and an *Unarchive* button. +- Nothing is removed from the social media. The archived account shows an + *Archived* ribbon and an *Unarchive account* button that restores + everything. +- A scheduled post whose date passed while the account was archived comes + back as *Draft*, with the reason in its chatter: unarchiving never hands + it to the scheduled action to be published on the spot. Reschedule it to + publish it. + +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. +- The records of the other applications that reference the account only lose + the link, they are **not** deleted. +- Nothing is deleted from the social media: the publications stay online. +- This cannot be undone. To keep the history, archive the account instead. + +Scheduled publishing. +--------------- + +- Setting *Send post* to *Schedule* proposes a date one hour from now and + leaves the post in the *Planned* state. The date can be changed. +- The scheduled action *Social Post: Send post schedule* sends the due posts + every 5 minutes, so a post is published up to 5 minutes after its date. +- While the post is being sent it stays in the *Publishing* state. +- The scheduled action never sends a post twice: as soon as the publication + is over, the post leaves the states it looks for. What happens when it does + not go through everywhere is described in *Partial failures* below. + +Partial failures. +--------------- + +- A post is sent to each of its accounts independently: if one account + fails, the publications that already succeeded are kept, with their + reference on the social media, and only the failed one is retried. +- The reason of the failure is shown on the failed publication, inside the + *Posts* tab of the post, and is also logged in the post chatter. +- If at least one account published, the post ends in the *Partially + Published* state. A message in the chatter names the accounts that failed + and notifies the user responsible for each of them. +- A *Partially Published* post is **never** retried by the scheduled action: + solve the problem and press *Post* again to send it to the accounts that + failed. The publications already online are not sent a second time. +- Its content is frozen from that moment on: message, accounts, images, + videos and schedule can no longer be changed, because they describe + something that already exists on a social media. To publish something + different, create a new post. +- If **every** account fails, nothing reached the social media: the post goes + back to *Draft*, where it can be corrected, and it is no longer retried + automatically. The corrected message is the one sent when *Post* is pressed + again. + +Expired credentials. +--------------- + +- The token of an account is renewed on its own before publishing, and again + by the scheduled action *Social: Checking social media updates*, so a post + planned days ago does not meet a token that ran out in the meantime. +- If the social media refuses the credentials anyway, the token is renewed and + the publication is sent a second time. That answer means nothing reached the + network, so nothing can be published twice. +- When the credentials cannot be renewed at all, because the authorization was + revoked or expired for good, the account is marked as needing an update: the + warning appears on the dashboard and its responsible user is told in the + chatter of the account. The publication is left as failed with the reason on + it, and pressing *Post* again works once the account is authorized. +- Whether the account can renew its own token depends on the social media, and + each connector module documents it. + +Statistics synchronization. +--------------- + +- The scheduled action *Social: Sync posts statistics* updates the post + statistics monthly. It also downloads the medias of the publications + created outside of Odoo, so those images appear on the dashboard only + after a synchronization. +- An initial synchronization is also triggered automatically right after an + account is linked. + +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 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 media, + not by their name: renaming the account on the social media 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*, *Archive account* and *Unarchive 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. 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..c7515fa619 --- /dev/null +++ b/social_media_base/security/ir.model.access.csv @@ -0,0 +1,11 @@ +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_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..6252ed1e93 --- /dev/null +++ b/social_media_base/security/social_account_security.xml @@ -0,0 +1,36 @@ + + + + + + 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..8b7d27efd2 --- /dev/null +++ b/social_media_base/security/social_media_base_groups.xml @@ -0,0 +1,37 @@ + + + + + + Social Media + Manage social media accounts and posts. + 20 + + + + User: Own Accounts + + The user is only able to see the social media accounts and posts he is responsible for. + + + + Administrator + + + 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. + + + + + + + + + + + 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..0ee31d4d79 --- /dev/null +++ b/social_media_base/security/social_post_account_security.xml @@ -0,0 +1,28 @@ + + + + + + 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..f1da291b67 --- /dev/null +++ b/social_media_base/security/social_post_security.xml @@ -0,0 +1,28 @@ + + + + + + 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..d218f1b9fa --- /dev/null +++ b/social_media_base/security/wizard_social_account_security.xml @@ -0,0 +1,12 @@ + + + + + + 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..ef094fef38 --- /dev/null +++ b/social_media_base/social_utils.py @@ -0,0 +1,189 @@ +# 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 import _ +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"): + 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(_("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..61213734cc --- /dev/null +++ b/social_media_base/static/description/index.html @@ -0,0 +1,654 @@ + + + + + +Social Media Base + + + +
+

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 +media.
  • +
  • Basic business structure.
  • +
  • Dashboard of published posts with 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

+
+

Posts on the dashboard.

+
    +
  • The dashboard lists the publications that exist on the social media, +together with their statistics. A scheduled publication is not online +yet and a failed one never got there, so neither of them is listed: +they are followed from the Posts menu, on the post they belong to, +and the scheduled ones also in the calendar.

    +
  • +
  • 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 media are marked as Deleted on +<media> and kept in the dashboard as history.

    +
  • +
  • Commenting a publication from the dashboard publishes the comment on +the social media, under the account of the publication, which is what +the composer announces.

    +
  • +
  • Deleting a post deletes the publications it created. A post whose +publications are still online cannot be deleted: remove them from the +dashboard first, which deletes them on the social media as well, or +archive the post to keep its history.

    +

    DASHBOARD

    +

    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 media +when the view is opened, so a loader is displayed until they are +available.

    +
  • +
  • The statistics can be grouped by day, by week or by month, and the +period can be narrowed with the two dates. Each connector module +offers only the granularities its own social media supports, so the +list of the filter depends on the media of the account.

    +
  • +
  • The date range has to cover at least one whole unit of the granularity +selected: the social media group their statistics by that unit and +refuse a shorter range. A range of a single day grouped by month, for +example, is rejected before the network is even called.

    +

    CHARTS

    +
  • +
+
+
+

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 publications and the +posts left without any active account. A post that still targets an +active account stays visible, and it is archived as soon as its last +account is.

    +
  • +
  • Archiving a post archives its publications too. Archived posts are +found in the Posts menu with the Archived filter, and their form +shows an Archived ribbon and an Unarchive button.

    +
  • +
  • Nothing is removed from the social media. The archived account shows +an Archived ribbon and an Unarchive account button that restores +everything.

    +
  • +
  • A scheduled post whose date passed while the account was archived +comes back as Draft, with the reason in its chatter: unarchiving +never hands it to the scheduled action to be published on the spot. +Reschedule it to publish it.

    +
  • +
+
+
+

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.
  • +
  • The records of the other applications that reference the account only +lose the link, they are not deleted.
  • +
  • Nothing is deleted from the social media: the publications stay +online.
  • +
  • This cannot be undone. To keep the history, archive the account +instead.
  • +
+
+
+

Scheduled publishing.

+
    +
  • Setting Send post to Schedule proposes a date one hour from now +and leaves the post in the Planned state. The date can be changed.
  • +
  • The scheduled action Social Post: Send post schedule sends the due +posts every 5 minutes, so a post is published up to 5 minutes after +its date.
  • +
  • While the post is being sent it stays in the Publishing state.
  • +
  • The scheduled action never sends a post twice: as soon as the +publication is over, the post leaves the states it looks for. What +happens when it does not go through everywhere is described in +Partial failures below.
  • +
+
+
+

Partial failures.

+
    +
  • A post is sent to each of its accounts independently: if one account +fails, the publications that already succeeded are kept, with their +reference on the social media, and only the failed one is retried.
  • +
  • The reason of the failure is shown on the failed publication, inside +the Posts tab of the post, and is also logged in the post chatter.
  • +
  • If at least one account published, the post ends in the Partially +Published state. A message in the chatter names the accounts that +failed and notifies the user responsible for each of them.
  • +
  • A Partially Published post is never retried by the scheduled +action: solve the problem and press Post again to send it to the +accounts that failed. The publications already online are not sent a +second time.
  • +
  • Its content is frozen from that moment on: message, accounts, images, +videos and schedule can no longer be changed, because they describe +something that already exists on a social media. To publish something +different, create a new post.
  • +
  • If every account fails, nothing reached the social media: the post +goes back to Draft, where it can be corrected, and it is no longer +retried automatically. The corrected message is the one sent when +Post is pressed again.
  • +
+
+
+

Expired credentials.

+
    +
  • The token of an account is renewed on its own before publishing, and +again by the scheduled action Social: Checking social media updates, +so a post planned days ago does not meet a token that ran out in the +meantime.
  • +
  • If the social media refuses the credentials anyway, the token is +renewed and the publication is sent a second time. That answer means +nothing reached the network, so nothing can be published twice.
  • +
  • When the credentials cannot be renewed at all, because the +authorization was revoked or expired for good, the account is marked +as needing an update: the warning appears on the dashboard and its +responsible user is told in the chatter of the account. The +publication is left as failed with the reason on it, and pressing +Post again works once the account is authorized.
  • +
  • Whether the account can renew its own token depends on the social +media, and each connector module documents it.
  • +
+
+
+

Statistics synchronization.

+
    +
  • The scheduled action Social: Sync posts statistics updates the post +statistics monthly. It also downloads the medias of the publications +created outside of Odoo, so those images appear on the dashboard only +after a synchronization.
  • +
  • An initial synchronization is also triggered automatically right after +an account is linked.
  • +
+
+
+

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 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 +media, not by their name: renaming the account on the social media +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, Archive account and Unarchive 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.
  • +
+
+
+
+

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/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/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..bf83db3b00 --- /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, + syncing: false, + }); + onWillStart(async () => { + this.state.needUpdate = + this.props.socialAccounts.filter((item) => item.need_update).length > 0; + this.state.syncing = this.props.socialAccounts.some( + (item) => item.pending_initial_sync + ); + }); + useBus(this.env.bus, "SOCIAL:NEED-UPDATE", async ({detail: data}) => { + this.state.needUpdate = data.needUpdate; + }); + useBus(this.env.bus, "SOCIAL:SYNCING", async ({detail: data}) => { + this.state.syncing = data.syncing; + }); + } + + 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..3d5713a7fd --- /dev/null +++ b/social_media_base/static/src/components/social_account/social_account.xml @@ -0,0 +1,84 @@ + + + + + +
+ + + + +
+
+
+ Social Media +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
diff --git a/social_media_base/static/src/components/social_chart/social_chart.esm.js b/social_media_base/static/src/components/social_chart/social_chart.esm.js new file mode 100644 index 0000000000..070e2dfa47 --- /dev/null +++ b/social_media_base/static/src/components/social_chart/social_chart.esm.js @@ -0,0 +1,51 @@ +/** @odoo-module **/ +import {Component, onMounted, useState} from "@odoo/owl"; +import {SocialChartAccount} from "@social_media_base/components/social_chart_account/social_chart_account.esm"; +import {SocialMediaMixin} from "../../js/app/social_media_mixin.esm"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +/** Client action showing the statistics chart of every connected account. */ +export class SocialChart extends SocialMediaMixin(Component) { + static template = "social_media_base.SocialChart"; + static props = ["*"]; + static components = { + SocialChartAccount, + }; + + /** The statistics are loaded once mounted, so the loader is rendered. */ + setup() { + super.setup(); + this.ormService = useService("orm"); + this.social_state = useState({ + statistics: [], + loaderChart: true, + }); + onMounted(async () => { + await this._loadAccountStatistics(); + }); + this.notif_view = "chart"; + this.notificationService = useService("notification"); + this.busService = this.env.services.bus_service; + this.enableSocialNotifications(); + } + + get socialAccountStatistics() { + return this.social_state.statistics; + } + + async _loadAccountStatistics() { + this.social_state.loaderChart = true; + try { + this.social_state.statistics = await this.ormService.call( + "social.account", + "get_chart_account_statistics", + [[]] + ); + } finally { + this.social_state.loaderChart = false; + } + } +} + +registry.category("actions").add("social_media_chart", SocialChart); diff --git a/social_media_base/static/src/components/social_chart/social_chart.scss b/social_media_base/static/src/components/social_chart/social_chart.scss new file mode 100644 index 0000000000..977d37569e --- /dev/null +++ b/social_media_base/static/src/components/social_chart/social_chart.scss @@ -0,0 +1,7 @@ +div.social-chart { + min-height: 100% !important; + height: 100% !important; + overflow: auto; + margin-right: 0 !important; + margin-left: 0 !important; +} diff --git a/social_media_base/static/src/components/social_chart/social_chart.xml b/social_media_base/static/src/components/social_chart/social_chart.xml new file mode 100644 index 0000000000..2f0706e5d4 --- /dev/null +++ b/social_media_base/static/src/components/social_chart/social_chart.xml @@ -0,0 +1,34 @@ + + + + + + +
+
+
+
+ No data +

No data to display

+

Once you have recorded information, you can view it here as a graph.

+
+
+
+
diff --git a/social_media_base/static/src/components/social_chart_account/social_chart_account.esm.js b/social_media_base/static/src/components/social_chart_account/social_chart_account.esm.js new file mode 100644 index 0000000000..beff00a1c2 --- /dev/null +++ b/social_media_base/static/src/components/social_chart_account/social_chart_account.esm.js @@ -0,0 +1,105 @@ +/** @odoo-module **/ + +import {Component, onMounted, useRef, useState} from "@odoo/owl"; +import {ControlPanel} from "@web/search/control_panel/control_panel"; +import {SocialFilter} from "../social_filter/social_filter.esm"; +import {_t} from "@web/core/l10n/translation"; +import {useService} from "@web/core/utils/hooks"; + +const {DateTime} = luxon; + +export class SocialChartAccount extends Component { + static template = "social_media_base.SocialChartAccount"; + static props = { + socialChartAccount: {type: Object, required: true}, + }; + static components = { + ControlPanel, + SocialFilter, + }; + + setup() { + super.setup(); + this.ormService = useService("orm"); + this.notificationService = useService("notification"); + this.chartCtx = useRef("chartAccount"); + this.chart = null; + onMounted(this.loadChart); + this.state = useState({ + impressionCount: 0, + commentCount: 0, + reactionCount: 0, + }); + } + + get chartAccount() { + return this.props.socialChartAccount; + } + + updateTotals(updateChart) { + this.state.impressionCount = updateChart.impressionCount; + this.state.reactionCount = updateChart.reactionCount; + this.state.commentCount = updateChart.commentCount; + } + + loadChart(labels, datasets) { + if (this.chart) this.chart.destroy(); + this.updateTotals(this.chartAccount); + this.chart = new window.Chart(this.chartCtx.el, { + type: "line", + data: { + labels: labels ? labels : this.chartAccount.labels, + datasets: datasets ? datasets : this.chartAccount.datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: () => this.chartAccount.chartLabel, + }, + }, + }, + }); + } + + isRangeLongEnough(startDate, endDate, granularity) { + if (!startDate || !endDate || !granularity) return true; + const start = DateTime.fromISO(startDate); + const end = DateTime.fromISO(endDate); + if (!start.isValid || !end.isValid) return true; + const units = {DAY: {days: 1}, WEEK: {weeks: 1}, MONTH: {months: 1}}; + const unit = units[granularity]; + return unit ? end >= start.plus(unit) : true; + } + + async onFilterChart({id, startDate, endDate, chartFilterType}) { + let updateChart = []; + const granularity = chartFilterType ? chartFilterType.toUpperCase() : null; + if (!this.isRangeLongEnough(startDate, endDate, granularity)) { + this.notificationService.add( + _t( + "The date range has to cover at least one %s.", + granularity.toLowerCase() + ), + {type: "warning"} + ); + return; + } + if (startDate || endDate || chartFilterType) + updateChart = await this.ormService.call( + "social.account", + "get_chart_account_statistics", + [id, startDate, endDate, granularity] + ); + if (updateChart.length > 0) { + this.loadChart(updateChart[0].labels, updateChart[0].datasets); + if (updateChart.length > 0) this.updateTotals(updateChart[0]); + } + } + + clearFilterChart() { + this.loadChart(); + } +} diff --git a/social_media_base/static/src/components/social_chart_account/social_chart_account.scss b/social_media_base/static/src/components/social_chart_account/social_chart_account.scss new file mode 100644 index 0000000000..f54f11995f --- /dev/null +++ b/social_media_base/static/src/components/social_chart_account/social_chart_account.scss @@ -0,0 +1,18 @@ +div.social-chart { + .social-chart-header { + border: 1px solid #dee2e6; + border-radius: 0.5rem !important; + box-sizing: border-box; + div { + box-sizing: border-box; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; + } + } + .social-chart-body { + border: 1px solid #dee2e6; + border-radius: 0.5rem !important; + box-sizing: border-box; + } +} diff --git a/social_media_base/static/src/components/social_chart_account/social_chart_account.xml b/social_media_base/static/src/components/social_chart_account/social_chart_account.xml new file mode 100644 index 0000000000..711b433e9b --- /dev/null +++ b/social_media_base/static/src/components/social_chart_account/social_chart_account.xml @@ -0,0 +1,67 @@ + + + + + +
+ + + + + +
+ + +
+
+
+ +
diff --git a/social_media_base/static/src/components/social_comment/social_comment.esm.js b/social_media_base/static/src/components/social_comment/social_comment.esm.js new file mode 100644 index 0000000000..b739473732 --- /dev/null +++ b/social_media_base/static/src/components/social_comment/social_comment.esm.js @@ -0,0 +1,91 @@ +/** @odoo-module **/ + +import {Component} from "@odoo/owl"; +import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog"; +import {Dropdown} from "@web/core/dropdown/dropdown"; +import {DropdownItem} from "@web/core/dropdown/dropdown_item"; +import {_t} from "@web/core/l10n/translation"; +import {useService} from "@web/core/utils/hooks"; + +export class SocialComment extends Component { + static template = "social_media_base.SocialComment"; + static components = { + Dropdown, + DropdownItem, + }; + static props = { + socialComment: {type: Object, required: true}, + post: {type: Object, required: true}, + }; + + setup() { + super.setup(); + this.socialService = useService("social_service"); + this.notificationService = useService("notification"); + this.effectService = useService("effect"); + this.dialog = useService("dialog"); + } + + async _onDeleteComment() { + return {}; + } + + async onDeleteComment() { + this.dialog.add(ConfirmationDialog, { + title: _t("Delete comment"), + body: _t("Are you sure you want to delete this comment?"), + confirm: () => this.deleteComment(), + confirmLabel: _t("Delete"), + cancel: () => undefined, + cancelLabel: _t("Cancel"), + }); + } + + async deleteComment() { + const result = await this._onDeleteComment(); + const message = + result.message === undefined ? _t("Comment deleted") : result.message; + const type_notif = result.success === true ? "success" : "danger"; + this.notificationService.add(message, { + type: type_notif, + sticky: type_notif === "danger", + }); + this.env.bus.trigger("SOCIAL:RELOAD_COMMENTS"); + if (result.success === true) { + this.env.bus.trigger("SOCIAL:RELOAD_ORGANIZATION", { + account_id: this.props.post.account_id.raw_value, + post_id: this.props.post.remote_ref.raw_value, + }); + } + } + + mediaNotLikeEnable() { + return []; + } + + async onLikeComment() { + const response = await this.socialService.likeComment( + this.props.post.id.raw_value, + this.props.socialComment.id, + this.props.post.account_remote_ref.raw_value + ); + if (response.success) { + this.effectService.add({ + type: "rainbow_man", + message: _t("You have liked the post."), + imgUrl: "/social_media_base/static/src/img/like.png", + fadeout: "fast", + }); + } else { + this.notificationService.add(_t(response.message), {type: "info"}); + } + } + + _onReplyComment() { + return {}; + } + + onReplyComment() { + this.env.bus.trigger("SOCIAL:RELOAD_COMMENTS"); + } +} diff --git a/social_media_base/static/src/components/social_comment/social_comment.scss b/social_media_base/static/src/components/social_comment/social_comment.scss new file mode 100644 index 0000000000..b9cd0eea93 --- /dev/null +++ b/social_media_base/static/src/components/social_comment/social_comment.scss @@ -0,0 +1,20 @@ +div.dialog-social-comment { + @include media-breakpoint-down(sm) { + padding: 0 !important; + } + + div.post-comments { + select:focus { + box-shadow: none !important; + } + background-color: rgba(0, 0, 0, 0.03) !important; + @include border-radius(2rem); + } + + div.o-dropdown { + button.dropdown-toggle { + border: 0 !important; + background-color: transparent !important; + } + } +} diff --git a/social_media_base/static/src/components/social_comment/social_comment.xml b/social_media_base/static/src/components/social_comment/social_comment.xml new file mode 100644 index 0000000000..b322057eeb --- /dev/null +++ b/social_media_base/static/src/components/social_comment/social_comment.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + +
+
+ + +