Repository
OCA/social · module folder · Try on Runboat
Module version
1.0.1
Category
Social Network
Folder size
1.03 MB
License
AGPL-3
Application
No
Auto-installable
No
Website
https://github.com/OCA/social
Last tracking update
2026-09-11 23:03:47
Authors
Odoo Community Association (OCA), Binhex
Maintainers
Odoo Community Association (OCA), Binhex
Committers
OCA-git-bot, oca-ci, Edilio Escalona Almira
Odoo dependencies
odoo/odoo:
- utm
- web
- bus
Python dependencies
None
System dependencies
None
Required by
None
Description
This module provides the fundamental foundation for social media management.
It facilitates the integration of user accounts, posts and native graph and
pivot analysis over a daily time series. Designed to be flexible and scalable,
it allows developers and businesses to integrate and customize social features
according to their needs.

What it asks of a social media is always a fixed number of calls per account:
publishing, deleting, and the daily series of the page. Reading back what an
account already published — the publications themselves and their comments —
costs one call per page, so it grows with the history of the account and lives
in *Social Media Sync* instead. The figures of a publication are read back
here, but only for the ones published in the last 30 days: what that window
costs is decided by the days it looks back and not by the history of the
account. Without *Social Media Sync* the dashboard still works: the figures of
a card are added up from what is already stored, and the *Statistics* dialog
of a publication shows both the clicks Odoo counted on its own tracked link
and the figures the social media reported for it, read back over the last 30
days. The list, the search filters and the form of a publication draw those
figures only with *Social Media Sync* installed, because they span the whole
history of the account.

This module does not connect to any social media by itself: it brings the
models, the security, the scheduled actions and the common interface. To use
it, a connector module has to be installed as well (for instance
*Social Media Linkedin* or *Social Media X*), which is what registers the
social media, implements the OAuth authorization and publishes for real.

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.
- A *Campaigns* menu inside the application, so the Odoo marketing campaigns
  (`utm.campaign`) can be managed without installing another marketing
  application. A post carries the campaign it belongs to, and the campaign
  form opens a new post already attached to it.
- Campaign badge on the posts kanban and on the dashboard cards.
- The posts of a marketing campaign, shown on the campaign from the moment
  they are drafted, with their consolidated figures and a stat button on the
  campaign form, the same way *Email Marketing* and *SMS Marketing* plug into
  a campaign.
- A UTM medium per social media and a UTM source per publication, so the
  links of a post are tracked separately for each account it is published on.
- Links of a published message routed through the Odoo link tracker, so a
  click from the social media is counted in Odoo and attributed to the
  campaign and to the publication.
- Because of the above the module depends on `utm` and on `link_tracker`,
  which are installed with it. They are core modules that bring the marketing
  campaigns, the mediums, the sources and the short links, and they change
  nothing in the social media data.
- Statistics stored as one row per account and day, so a native graph and a
  pivot draw them with the search panel, the comparison and the export of
  Odoo. Only the social media reporting figures by day fill it. A cron
  rewrites the last days every two hours, because the social media revise
  figures already past, and the *Update* button of the dashboard rewrites the
  same window on demand.
- Figures of the dashboard card added up from what is already stored, so
  opening the dashboard costs no call at all, however many publications the
  account has.
- Account credentials (OAuth tokens) are only visible to administrator users.
- What each social media refuses to publish, shown on the post while it is
  written instead of when it is sent. Every connector declares its own rules
  once — how long the message may be, how many images and videos, which
  formats and which sizes — and the post form shows two blocks: what a social
  media will publish differently from what is written, and what it will not
  publish at all. Saving is never blocked, because a post is written before it
  is finished; the publication is where an objection stops something, and it
  reads the very same rules, so the form and the publication can never
  disagree. A publication refused this way fails on its own line: the other
  accounts of the post go out as usual.

Code Analysis

Views touched (20)
XML IDNameModelTypeStatus
social_account_statistics_view_graph social.account.statistics.graph social.account.statistics graph New
social_account_statistics_view_pivot social.account.statistics.pivot social.account.statistics pivot New
social_account_statistics_view_search social.account.statistics.search social.account.statistics search New
social_account_view_form social.account.form social.account form New
social_account_view_search social.account.view.search social.account search New
social_account_view_tree social.account.tree social.account tree New
social_media_post_preview social_media_post_preview ir.ui.view qweb New
social_media_view_kanban social.media.kanban social.media kanban New
social_post_account_view_form social.post.account.form social.post.account form New
social_post_account_view_form_statistics social.post.account.form.statistics social.post.account form New
social_post_account_view_kanban social.post.account.kanban social.post.account kanban New
social_post_account_view_search social.post.account.search social.post.account search New
social_post_account_view_tree social.post.account.tree social.post.account tree New
social_post_view_form social.post.form social.post form New
social_post_view_kanban social.post.kanban social.post kanban New
social_post_view_search social.post.view.search social.post search New
social_post_view_tree social.post.tree social.post tree New
utm_campaign_view_form utm.campaign.view.form.inherit.social.media.base utm.campaign form Inherits utm.utm_campaign_view_form
utm_campaign_view_kanban utm.campaign.view.kanban.inherit.social.media.base utm.campaign kanban Inherits utm.utm_campaign_view_kanban
wizard_social_account_view_form Associate Social Account wizard.social.account form New
HTTP endpoints (0)

No HTTP endpoints found for this module.

Models touched (15)

New fields (0)

No new fields.

Public methods (1)
  • session_info(self)
    Deliver once the notification kept by the social media callbacks. See ``social.media.base.mixin._notify_user_session``.

New fields (1)
  • social_post_account_id Many2one → social.post.account
    help='Publication the tracked link was published in.' index='btree_not_null' ondelete='set null' string='Social Media Publication' args: 'social.post.account'
Public methods (0)

No public methods.

New fields (1)
  • social_post_account_id Many2one → social.post.account
    help='Publication the clicked link was published in.' index='btree_not_null' ondelete='set null' related='link_id.social_post_account_id' store=True string='Social Media Publication' args: 'social.post.account'
Public methods (0)

No public methods.

New fields (20)
  • access_token Char
    groups='base.group_system'
  • active Boolean
    default=True
  • can_manage_account 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.'
  • company_id Many2one → res.company
    default=<expr> args: 'res.company'
  • engagement Float
    compute='_compute_engagement' default=0 digits=(16, 4) help='Interactions of the account over its impressions, as a ratio.' store=True
  • expire_access_token_date Date
    string='Expire Access Token'
  • image_1920 Image
    default=_default_image
  • last_update_account Datetime
  • media_id Many2one → social.media
    ondelete='restrict' args: 'social.media'
  • media_type Selection
    related='media_id.media_type'
  • name Char
  • need_update Boolean
    default=False help='The credentials of the account expired and it has to be authorized again. It means that and nothing else; an account with publications left to import is marked with posts_need_import, in the synchronization module.'
  • post_account_ids One2many → social.post.account
    args: 'social.post.account', 'account_id'
  • post_count Integer
    compute='_compute_post_count'
  • post_ids Many2many → social.post
    column1='social_account_id' column2='social_post_id' help='Posts that target this account. Inverse of the accounts of a post, it is what keeps the counter up to date.' readonly=True relation='social_account_social_post_rel' string='Posts' args: 'social.post'
  • refresh_access_token Char
    groups='base.group_system'
  • remote_ref Char
    copy=False help='Identifier of this account on the social media. It is set by the connector module of each social media.' index=True string='Remote Reference'
  • user_id Many2one → res.users
    default=<expr> help='User this account belongs to. Only the responsible user and the social media administrators can see it.' index=True required=True string='Responsible' tracking=True args: 'res.users'
  • username Char
  • utm_campaign_count Integer
    compute='_compute_utm_campaign_count'
Public methods (10)
  • action_open_posts(self)
    Open the posts this account is one of the targets of.
  • action_open_utm_campaigns(self)
    Open the marketing campaigns of the publications of this account.
  • 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
  • action_refresh_statistics(self)
    Ask the social media again for the figures of the last days. The graph view reads what the crons left, so this is what a user presses when he does not want to wait for the next pass. A social media that reports no figures by day answers nothing, and saying so is more useful than announcing an update that did not happen. The figures of the recent publications are read back in the same press, over the same window the daily cron reads: one set to explain and one ceiling of cost, wherever the user presses. The notification keeps speaking of the daily series, which is the only part of this a social media can be unable to answer.
  • action_update_account(self)
  • 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 social media side. ``check_remote_token`` is what connectors use to tell this deliberate check from the guard that runs before every call.
  • compute_dashboard_statistics(self)
    Recompute the figures the dashboard shows, without asking anybody. What the client calls when the kanban loads. It is pure aggregation over rows that are already stored, so opening the dashboard costs no call at all and can happen as often as the user wants. :return: whether anything was recomputed. :rtype: bool
  • refresh_dashboard_statistics(self)
    Ask the social media for the figures again, from the dashboard. What the client calls when somebody presses *Update*. Unlike :meth:`compute_dashboard_statistics` this one does spend calls — one per account, against the endpoint that fills the daily series — and that is the point: the user is saying he does not want to wait for the two-hour cron. It is the same thing :meth:`action_refresh_statistics` already does from the account form, over every account instead of one. The figures of the recent publications are read back too, over the same window the daily cron reads: the button asks for the same set the cron does, so there is a single ceiling of cost and a single thing to explain. Refreshing what the social media reports first and aggregating afterwards is not optional: the figures of the card come from those rows and from the publications, so the other order would recompute it from what was already on screen. There is no throttle. Opening the dashboard costs no call at all, so the only calls there are to spare are the ones the user asked for by pressing the button, and a button that answers with the same figures looks broken — which is exactly what it is there to fix. :return: whether anything was refreshed. :rtype: bool
  • 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.
  • write(self, vals)

New fields (12)
  • account_id Many2one → social.account
    index=True ondelete='cascade' required=True args: 'social.account'
  • click_count Integer
    default=0
  • comment_count Integer
    default=0
  • company_id Many2one
    index=True related='account_id.company_id' store=True
  • date Date
    index=True required=True
  • engagement Float
    default=0 digits=(16, 4) group_operator='avg' help='Engagement rate as the social media reports it. It is a ratio, so periods are averaged instead of added up.'
  • impression_count Integer
    default=0
  • like_count Integer
    default=0
  • media_id Many2one
    index=True related='account_id.media_id' store=True
  • media_type Selection
    related='account_id.media_id.media_type' store=True
  • share_count Integer
    default=0
  • user_id Many2one
    index=True related='account_id.user_id' store=True
Public methods (0)

No public methods.

New fields (5)
  • description Text
  • image Binary
  • media_type Selection
    readonly=True args: []
  • name Char
  • utm_medium_id Many2one → utm.medium
    help='Delivery method reported to the marketing campaigns for the links published on this social media. The connector module of each social media provides a default one.' ondelete='restrict' string='UTM Medium' args: 'utm.medium'
Public methods (1)
  • action_open_account(self)
    Open the wizard that associates an account of this social media. Pure hook: the base module knows no social media, so it returns nothing. Every connector module overrides it and returns the ``ir.actions.act_window`` of its own association wizard.

New fields (0)

No new fields.

Public methods (0)

No public methods.

New fields (26)
  • account_ids Many2many → social.account
    ondelete='restrict' required=True args: 'social.account'
  • active Boolean
    default=True
  • any_failed_post Boolean
    compute='_compute_any_failed_post'
  • campaign_id Many2one → utm.campaign
    index='btree_not_null' ondelete='set null' string='Campaign' args: 'utm.campaign'
  • content_locked Boolean
    compute='_compute_content_locked' help='The post reached at least one social media, so what it says can no longer be changed.'
  • count_post_clicks Integer
    compute='_compute_post_statistics' default=0 string='Clicks'
  • count_post_comments Integer
    compute='_compute_post_statistics' default=0 string='Comments'
  • count_post_engagement Float
    compute='_compute_post_statistics' default=0 string='Engagement'
  • count_post_impression Integer
    compute='_compute_post_statistics' default=0 string='Impression'
  • count_post_interactions Integer
    compute='_compute_post_statistics' default=0 string='Interactions'
  • count_post_likes Integer
    compute='_compute_post_statistics' default=0 string='Likes'
  • count_post_shares Integer
    compute='_compute_post_statistics' default=0 string='Shares'
  • hide_post Boolean
    compute='_compute_hide_post'
  • image_ids Many2many → ir.attachment
    column1='post_id' column2='image_id' ondelete='restrict' relation='social_post_image_rel' args: 'ir.attachment'
  • link_click_count Integer
    compute='_compute_link_click_count' help="Clicks Odoo registered on the tracked links of the publications of this post. Different from 'Clicks', which is the figure the social media report." string='Tracked Clicks'
  • message Text
    required=True tracking=True
  • message_error Text
    compute='_compute_post_check_messages' help='What no social media of the post will be able to publish as the post stands. It does not block saving, so the post can be finished later, but the publication is refused until it is fixed.'
  • message_info Text
    compute='_compute_post_check_messages' help='What a social media of the post is going to publish differently from what is written here. It does not stop anything: the post is published, changed by the social media.'
  • post_account_ids One2many → social.post.account
    args: 'social.post.account', 'post_id'
  • post_preview Html
    compute='_compute_post_preview' store=True
  • published_date Datetime
    tracking=True
  • send_post Selection
    default='now' required=True tracking=True args: [('now', 'Now'), ('schedule', 'Schedule')]
  • send_post_date Datetime
    compute='_compute_send_post_date' help="Date the post is sent to the social media. Switching to 'Schedule' proposes one hour from now, and it can be changed." readonly=False store=True string='Schedule date'
  • state Selection
    default='draft' tracking=True args: [('draft', 'Draft'), ('planned', 'Planned'), ('publishing', 'Publishing'), ('partially_published', 'Partially Published'), ('published', 'Published'), ('cancelled', 'Cancelled')]
  • user_id Many2one → res.users
    default=<expr> help='User this post belongs to. Only the responsible user and the social media administrators can see it.' index=True required=True string='Responsible' tracking=True args: 'res.users'
  • video_ids Many2many → ir.attachment
    column1='post_id' column2='video_id' ondelete='restrict' relation='social_post_video_rel' args: 'ir.attachment'
Public methods (7)
  • action_cancel(self)
  • action_create_post_account(self)
    Publish the post on all its social media accounts.
  • action_draft(self)
  • create(self, vals_list)
    @api.model_create_multi
  • default_get(self, fields_list)
    @api.model
  • 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 social media, or one already gone from it, is deleted with its post; a publication still online stops the deletion with an explanation. The medias go with it, and nothing here has to arrange that: :meth:`_anchor_media_attachments` attaches every media to the record that holds it, and ``unlink`` deletes the attachments of the records it deletes, so the images of the post leave with the post and what a publication downloaded leaves with the publication. The filestore frees the files on its next collection.
  • write(self, vals)

New fields (26)
  • account_id Many2one → social.account
    ondelete='restrict' required=True args: 'social.account'
  • account_remote_ref Char
    related='account_id.remote_ref' string='Account Remote Reference'
  • active Boolean
    default=True
  • author Char
    related='account_id.name' store=True
  • campaign_id Many2one → utm.campaign
    compute='_compute_campaign_id' help='Marketing campaign of the parent post. A publication imported from the social media has no parent post, so its campaign is written on the imported publication itself.' index='btree_not_null' ondelete='set null' readonly=False store=True string='Campaign' args: 'utm.campaign'
  • effective_date Datetime
    compute='_compute_effective_date' help='Publication date, or the date the post is scheduled for while it is not published yet.' store=True string='Date'
  • engagement Float
    default=0 digits=(16, 4)
  • failed_description Html
  • has_video Boolean
    default=False help='Indicates that the published post has at least one video attached.'
  • image_ids Many2many → ir.attachment
    column1='post_id' column2='image_id' ondelete='restrict' relation='social_post_account_image_rel' args: 'ir.attachment'
  • is_scheduled Boolean
    compute='_compute_is_scheduled' help='The post of this publication is scheduled and not published yet.' store=True string='Scheduled'
  • link_click_count Integer
    compute='_compute_link_click_count' help="Clicks Odoo registered on the tracked links of this publication. Different from 'Clicks', which is the figure the social media reports for the publication itself." string='Tracked Clicks'
  • media_id Many2one → social.media
    related='account_id.media_id' required=True args: 'social.media'
  • media_refs Json
    copy=False default=dict help='Reference each media of this publication has on the social media, keyed by the identifier of its attachment. A media without an entry here is one the social media does not know about.'
  • media_type Selection
    related='media_id.media_type'
  • medium_id Many2one → utm.medium
    compute='_compute_medium_id' help='Delivery method reported to the marketing campaign. It lives on the publication and not on the post because a post is spread over several social media.' index='btree_not_null' ondelete='set null' readonly=False store=True string='Medium' args: 'utm.medium'
  • message Text
    help='Text this publication sends to the social media, which is not the text of its post: every publication carries its own UTM source, so each link is rewritten into a tracker of its own and the same post reads differently on every account. Publications imported from the social media have no post to read a message from either.' required=True
  • post_account_url Char
  • post_id Many2one → social.post
    ondelete='restrict' args: 'social.post'
  • published_date Datetime
  • remote_ref Char
    copy=False help='Identifier of this publication on the social media. It is set by the connector module of each social media.' index=True string='Remote Reference'
  • source_id Many2one → utm.source
    copy=False help='Created when the publication is sent, so that every publication of the same post owns its own tracked links.' index='btree_not_null' ondelete='restrict' readonly=True string='Source' args: 'utm.source'
  • state Selection
    default='ready' help="'Deleted' means the publication no longer exists on the social media although it is kept in Odoo for history." args: [('ready', 'Ready'), ('posted', 'Posted'), ('failed', 'Failed'), ('deleted', 'Deleted')]
  • statistics_date Datetime
    copy=False help='When the figures of this publication were last read back from the social media. Empty means they were never read.' readonly=True string='Statistics Read On'
  • user_id Many2one
    index=True readonly=True related='account_id.user_id' store=True string='Responsible'
  • video_ids Many2many → ir.attachment
    column1='post_id' column2='video_id' ondelete='restrict' relation='social_post_account_video_rel' args: 'ir.attachment'
Public methods (6)
  • action_delete_post_account(self)
  • action_open_post_account_url(self)
    Ask the social media, then open this publication on it. The address is only known for a publication that made it to the social media, so the button showing it is hidden otherwise. The address alone is not proof that the publication is still online: it survives a deletion made on the social media until something asks. Asking costs one call, for the one publication the user is opening, which is what makes it worth making here.
  • action_open_statistics(self)
    Open the figures of the publication in a dialog. Called from the menu of the card of the dashboard, which is where the figures brought by the synchronization were missing. Nothing is asked to the social media: what is shown is what the last update stored. :return: the action opening the statistics view. :rtype: dict
  • check_post_exists(self)
    Ask the social media whether this publication is still online. Public entry point shared by the form button and by the dashboard, so both answer the same thing from the same code. :rtype: bool
  • create(self, vals_list)
    @api.model_create_multi
  • write(self, vals)

New fields (2)
  • image_urls Char
    compute='_compute_image_urls' store=True
  • video_urls Char
    compute='_compute_video_urls' help='URL of each video of this record, in the order they were added.' store=True
Public methods (0)

No public methods.

New fields (6)
  • click_count Integer
    default=0
  • comment_count Integer
    default=0
  • impression_count Integer
    default=0 help='Total number of views, which may include multiple views by the same user.'
  • interactions_count Integer
    compute='_compute_interactions_count' default=0 help='Interactions with the publication: clicks, likes, comments and shares.' store=True
  • like_count Integer
    default=0
  • share_count Integer
    default=0
Public methods (0)

No public methods.

New fields (1)
  • web_url Char
    compute='_compute_web_url' help='Address of this record on the social media.' string='Web URL'
Public methods (1)
  • action_open_url(self)
    Open this record on the social media, in a new tab.

New fields (11)
  • social_click_count Integer
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Clicks'
  • social_comment_count Integer
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Comments'
  • social_engagement Float
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Engagement'
  • social_impression_count Integer
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Impressions'
  • social_interactions_count Integer
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Interactions'
  • social_like_count Integer
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Likes'
  • social_link_click_count Integer
    compute='_compute_social_link_click_count' groups=_SOCIAL_GROUPS help='Clicks Odoo registered on the tracked links of the social media publications of this campaign. Part of the clicks of the campaign, not something to add to them.' string='Social Media Tracked Clicks'
  • social_post_account_ids One2many → social.post.account
    groups=_SOCIAL_GROUPS readonly=True string='Social Media Publications' args: 'social.post.account', 'campaign_id'
  • social_post_count Integer
    compute='_compute_social_post_count' groups=_SOCIAL_GROUPS string='Social Media Posts Count'
  • social_post_ids One2many → social.post
    groups=_SOCIAL_GROUPS readonly=True string='Social Media Posts' args: 'social.post', 'campaign_id'
  • social_share_count Integer
    compute='_compute_social_statistics' groups=_SOCIAL_GROUPS string='Social Media Shares'
Public methods (1)
  • action_view_social_posts(self)
    Open the social media posts linked to this campaign.

New fields (0)

No new fields.

Public methods (0)

No public methods.

New fields (7)
  • account_id Many2one → social.account
    args: 'social.account'
  • csrf_state_token Char
    help='Anti-CSRF state token used during the OAuth flow.' readonly=True
  • image Binary
    related='media_id.image'
  • media_id Many2one → social.media
    required=True args: 'social.media'
  • media_type Selection
    related='media_id.media_type' string='Media Type'
  • update_keys Boolean
    default=False help='Only enable this field if your credentials have changed'
  • update_token Boolean
    default=False help='Only enable this field if the access token has expired and has to be requested again'
Public methods (2)
  • action_associate_social_account(self)
    Link the account with the social media.
  • action_update_account(self)
    Refresh the account data according to the wizard selection.
Status
Open migration PR — not merged yet for this version
CI status
checks failing
Open since
282 days ago
Last activity
113 days ago
Repository
OCA/social
Pull request
[18.0][MIG] social_media_base: Migration to 18.0 (#1769)