From e701e060766e842d35e3636d8d1089b6e409ed1e Mon Sep 17 00:00:00 2001 From: alvaro-domatix Date: Wed, 27 May 2026 16:47:22 +0000 Subject: [PATCH 1/3] [IMP] subscription_oca: compute MRR and ARR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two stored computed monetary fields on `sale.subscription`: * `recurring_monthly` — sum of the subscription lines normalised to a monthly amount (Monthly Recurring Revenue). * `recurring_yearly` — twelve times the monthly figure (Annual Recurring Revenue). Each line also exposes its own `recurring_monthly`, derived from `price_subtotal` and the template recurrence. A line that bills every N `days`, `weeks`, `months` or `years` is converted to months using 30.4375 average days per month for sub-monthly cadences and a direct multiplier otherwise, then divided by the `recurring_interval`. This makes subscriptions of different cadences directly comparable and sum-able, so reports can answer the basic "how much do I bill per month?" question without external spreadsheets. The fields are visible as a new MRR/ARR stat button on the subscription form, as optional columns with sum aggregation on the list view, and as a "With recurring revenue" filter on the search view. Migration: pure ORM recompute on `-u subscription_oca`. Expect roughly two seconds per ten thousand subscription lines. --- subscription_oca/README.rst | 41 +-- subscription_oca/__manifest__.py | 2 +- subscription_oca/models/sale_subscription.py | 48 ++++ .../models/sale_subscription_line.py | 59 +++++ subscription_oca/readme/USAGE.md | 12 + .../static/description/index.html | 13 + subscription_oca/tests/__init__.py | 2 +- .../tests/test_subscription_mrr.py | 233 ++++++++++++++++++ .../views/sale_subscription_views.xml | 67 +++++ 9 files changed, 461 insertions(+), 16 deletions(-) create mode 100644 subscription_oca/tests/test_subscription_mrr.py diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst index bbc12fa30c..9387a3f72d 100644 --- a/subscription_oca/README.rst +++ b/subscription_oca/README.rst @@ -71,12 +71,25 @@ To create subscriptions with the sale of a product: *Subscribable product* and *Subscription template* 3. Create a sales order with the product and confirm it. +Recurring revenue (MRR / ARR): + +- Each subscription exposes its *Monthly recurring revenue* (MRR) and + *Annual recurring revenue* (ARR). The line subtotals (net of + discount, excluding taxes) are normalised to a monthly amount + according to the template recurrence and then **converted to the + company currency** using the rate at the subscription start date. + This keeps totals comparable when subscriptions use pricelists in + different currencies. +- Use the *Active recurring revenue* filter to restrict the figures to + in-progress subscriptions, which reflects live recurring revenue + rather than the theoretical value of draft or closed subscriptions. + Known issues / Roadmap ====================== -- Refactor all the onchanges that have business logic to computed - write-able fields when possible. Keep onchanges only for UI purposes. -- Add tests. +- Refactor all the onchanges that have business logic to computed + write-able fields when possible. Keep onchanges only for UI purposes. +- Add tests. Bug Tracker =========== @@ -100,22 +113,22 @@ Authors Contributors ------------ -- Carlos Martínez -- Carolina Ferrer -- `Ooops404 `__: +- Carlos Martínez +- Carolina Ferrer +- `Ooops404 `__: - - Ilyas + - Ilyas -- `Sygel `__: +- `Sygel `__: - - Harald Panten - - Valentin Vinagre - - Alberto Martínez + - Harald Panten + - Valentin Vinagre + - Alberto Martínez -- Dennis Sluijk -- `IKU Solutions `__: +- Dennis Sluijk +- `IKU Solutions `__: - - Yan Chirino + - Yan Chirino Maintainers ----------- diff --git a/subscription_oca/__manifest__.py b/subscription_oca/__manifest__.py index 58335fe6eb..0af22ce55f 100644 --- a/subscription_oca/__manifest__.py +++ b/subscription_oca/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Subscription management", "summary": "Generate recurring invoices.", - "version": "19.0.1.0.0", + "version": "19.0.1.1.0", "development_status": "Beta", "category": "Subscription Management", "website": "https://github.com/OCA/contract", diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py index e44e9da11f..072b0ae002 100644 --- a/subscription_oca/models/sale_subscription.py +++ b/subscription_oca/models/sale_subscription.py @@ -95,6 +95,28 @@ class SaleSubscription(models.Model): ) amount_tax = fields.Monetary(compute="_compute_total", store=True) amount_total = fields.Monetary(compute="_compute_total", store=True) + recurring_monthly = fields.Monetary( + string="Monthly recurring revenue", + compute="_compute_mrr", + store=True, + currency_field="company_currency_id", + help="Sum of the subscription lines normalised to a monthly amount, " + "expressed in the company currency.", + ) + recurring_yearly = fields.Monetary( + string="Annual recurring revenue", + compute="_compute_mrr", + store=True, + currency_field="company_currency_id", + help="Twelve times the monthly recurring revenue.", + ) + company_currency_id = fields.Many2one( + "res.currency", + string="Company Currency", + related="company_id.currency_id", + store=True, + readonly=True, + ) tag_ids = fields.Many2many(comodel_name="sale.subscription.tag", string="Tags") image = fields.Binary("Image", related="user_id.image_512", store=True) journal_id = fields.Many2one(comodel_name="account.journal", string="Journal") @@ -179,6 +201,13 @@ def _compute_total(self): } ) + @api.depends("sale_subscription_line_ids.recurring_monthly") + def _compute_mrr(self): + for record in self: + monthly = sum(record.sale_subscription_line_ids.mapped("recurring_monthly")) + record.recurring_monthly = monthly + record.recurring_yearly = monthly * 12.0 + @api.depends("template_id", "code") def _compute_name(self): for record in self: @@ -250,6 +279,25 @@ def action_start_subscription(self): ) self.stage_id = in_progress_stage + def action_view_recurring_revenue(self): + self.ensure_one() + return { + "name": self.env._("Recurring revenue"), + "type": "ir.actions.act_window", + "res_model": "sale.subscription.line", + "view_mode": "list", + "views": [ + ( + self.env.ref( + "subscription_oca.sale_subscription_line_recurring_revenue_list" + ).id, + "list", + ) + ], + "domain": [("sale_subscription_id", "=", self.id)], + "context": {"create": False, "edit": False, "delete": False}, + } + def action_close_subscription(self): return { "view_type": "form", diff --git a/subscription_oca/models/sale_subscription_line.py b/subscription_oca/models/sale_subscription_line.py index 31cf985270..0fa060c63f 100644 --- a/subscription_oca/models/sale_subscription_line.py +++ b/subscription_oca/models/sale_subscription_line.py @@ -3,6 +3,15 @@ from odoo import Command, api, fields, models from odoo.tools.misc import get_lang +# Average length of a calendar month: 365.25 days / 12 months. +_AVG_DAYS_PER_MONTH = 365.25 / 12 # 30.4375 +_PERIOD_LENGTH_IN_MONTHS = { + "days": 1 / _AVG_DAYS_PER_MONTH, + "weeks": 7 / _AVG_DAYS_PER_MONTH, + "months": 1.0, + "years": 12.0, +} + class SaleSubscriptionLine(models.Model): _name = "sale.subscription.line" @@ -20,6 +29,13 @@ class SaleSubscriptionLine(models.Model): store=True, readonly=True, ) + company_currency_id = fields.Many2one( + "res.currency", + string="Company Currency", + related="sale_subscription_id.company_id.currency_id", + store=True, + readonly=True, + ) name = fields.Char( string="Description", compute="_compute_name", store=True, readonly=False ) @@ -49,6 +65,14 @@ class SaleSubscriptionLine(models.Model): amount_tax_line_amount = fields.Float( string="Taxes Amount", compute="_compute_subtotal", store=True ) + recurring_monthly = fields.Monetary( + string="Monthly recurring revenue", + compute="_compute_recurring_monthly", + store=True, + currency_field="company_currency_id", + help="Subtotal of this line normalised to a monthly amount and " + "converted to the company currency, based on the template recurrence.", + ) sale_subscription_id = fields.Many2one( comodel_name="sale.subscription", string="Subscription" ) @@ -80,6 +104,41 @@ def _compute_subtotal(self): } ) + @api.depends( + "price_subtotal", + "currency_id", + "company_currency_id", + "sale_subscription_id.company_id", + "sale_subscription_id.date_start", + "sale_subscription_id.template_id.recurring_rule_type", + "sale_subscription_id.template_id.recurring_interval", + ) + def _compute_recurring_monthly(self): + for record in self: + template = record.sale_subscription_id.template_id + if not template: + record.recurring_monthly = 0.0 + continue + interval = max(template.recurring_interval or 1, 1) + period_months = ( + _PERIOD_LENGTH_IN_MONTHS.get(template.recurring_rule_type, 1.0) + * interval + ) + monthly = record.price_subtotal / period_months if period_months else 0.0 + record.recurring_monthly = record._convert_to_company_currency(monthly) + + def _convert_to_company_currency(self, amount): + """Normalise a line amount to the company currency so aggregated + MRR/ARR figures stay comparable across pricelist currencies.""" + self.ensure_one() + source = self.currency_id + target = self.company_currency_id + if not amount or not source or not target or source == target: + return amount + company = self.sale_subscription_id.company_id or self.env.company + date = self.sale_subscription_id.date_start or fields.Date.context_today(self) + return source._convert(amount, target, company, date) + @api.depends("product_id") def _compute_name(self): for record in self: diff --git a/subscription_oca/readme/USAGE.md b/subscription_oca/readme/USAGE.md index 707414ff28..0679b7d47d 100644 --- a/subscription_oca/readme/USAGE.md +++ b/subscription_oca/readme/USAGE.md @@ -24,3 +24,15 @@ To create subscriptions with the sale of a product: 2. Create the product and in the sales tab, complete the fields *Subscribable product* and *Subscription template* 3. Create a sales order with the product and confirm it. + +Recurring revenue (MRR / ARR): + +- Each subscription exposes its *Monthly recurring revenue* (MRR) and + *Annual recurring revenue* (ARR). The line subtotals (net of discount, + excluding taxes) are normalised to a monthly amount according to the + template recurrence and then **converted to the company currency** using + the rate at the subscription start date. This keeps totals comparable when + subscriptions use pricelists in different currencies. +- Use the *Active recurring revenue* filter to restrict the figures to + in-progress subscriptions, which reflects live recurring revenue rather + than the theoretical value of draft or closed subscriptions. diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html index 17514b570e..368d74b892 100644 --- a/subscription_oca/static/description/index.html +++ b/subscription_oca/static/description/index.html @@ -421,6 +421,19 @@

Usage

Subscribable product and Subscription template
  • Create a sales order with the product and confirm it.
  • +

    Recurring revenue (MRR / ARR):

    +
      +
    • Each subscription exposes its Monthly recurring revenue (MRR) and +Annual recurring revenue (ARR). The line subtotals (net of +discount, excluding taxes) are normalised to a monthly amount +according to the template recurrence and then converted to the +company currency using the rate at the subscription start date. +This keeps totals comparable when subscriptions use pricelists in +different currencies.
    • +
    • Use the Active recurring revenue filter to restrict the figures to +in-progress subscriptions, which reflects live recurring revenue +rather than the theoretical value of draft or closed subscriptions.
    • +

    Known issues / Roadmap

    diff --git a/subscription_oca/tests/__init__.py b/subscription_oca/tests/__init__.py index f445239d7f..ce9533b727 100644 --- a/subscription_oca/tests/__init__.py +++ b/subscription_oca/tests/__init__.py @@ -1,3 +1,3 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from . import test_subscription_oca +from . import test_subscription_mrr, test_subscription_oca diff --git a/subscription_oca/tests/test_subscription_mrr.py b/subscription_oca/tests/test_subscription_mrr.py new file mode 100644 index 0000000000..0ee05bbe17 --- /dev/null +++ b/subscription_oca/tests/test_subscription_mrr.py @@ -0,0 +1,233 @@ +# Copyright 2026 Domatix - Alvaro +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.base.tests.common import BaseCommon +from odoo.addons.product.tests.common import ProductCommon + + +class TestSubscriptionMRR(ProductCommon, BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.pricelist = cls.env["product.pricelist"].create({"name": "MRR pricelist"}) + cls.partner = cls.env["res.partner"].create( + { + "name": "MRR test partner", + "property_product_pricelist": cls.pricelist.id, + } + ) + cls.product = cls._create_product( + name="MRR product", + lst_price=120.0, + subscribable=True, + uom_id=cls.uom_unit.id, + taxes_id=[(6, 0, [])], + ) + cls.stage_draft = cls.env["sale.subscription.stage"].search( + [("type", "=", "draft")], limit=1 + ) + + @classmethod + def _create_template(cls, rule_type, interval=1): + return cls.env["sale.subscription.template"].create( + { + "name": f"Tmpl {rule_type} {interval}", + "code": f"T{rule_type[0].upper()}{interval}", + "recurring_rule_type": rule_type, + "recurring_interval": interval, + } + ) + + def _create_subscription(self, template, price_unit=120.0, qty=1.0): + subscription = self.env["sale.subscription"].create( + { + "partner_id": self.partner.id, + "template_id": template.id, + "pricelist_id": self.pricelist.id, + "stage_id": self.stage_draft.id, + } + ) + self.env["sale.subscription.line"].create( + { + "sale_subscription_id": subscription.id, + "product_id": self.product.id, + "product_uom_qty": qty, + "price_unit": price_unit, + "tax_ids": [(6, 0, [])], + } + ) + return subscription + + def test_mrr_monthly_template_equals_subtotal(self): + tmpl = self._create_template("months", 1) + sub = self._create_subscription(tmpl, price_unit=120.0) + self.assertAlmostEqual(sub.recurring_monthly, sub.recurring_total, 2) + + def test_mrr_yearly_template_is_one_twelfth(self): + tmpl = self._create_template("years", 1) + sub = self._create_subscription(tmpl, price_unit=1200.0) + self.assertAlmostEqual(sub.recurring_monthly, 100.0, 2) + + def test_mrr_weekly_is_fixed_value(self): + # 50/week over an average month of 30.4375/7 weeks -> 217.41 (computed + # by hand, independent of the implementation constant). + tmpl = self._create_template("weeks", 1) + sub = self._create_subscription(tmpl, price_unit=50.0) + self.assertAlmostEqual(sub.recurring_monthly, 217.41, 2) + + def test_mrr_daily_is_fixed_value(self): + # 1/day -> roughly the number of days in an average month (30.44). + tmpl = self._create_template("days", 1) + sub = self._create_subscription(tmpl, price_unit=1.0) + self.assertAlmostEqual(sub.recurring_monthly, 30.44, 2) + + def test_mrr_respects_recurring_interval(self): + # 3-month interval at 300 -> MRR == 100 + tmpl = self._create_template("months", 3) + sub = self._create_subscription(tmpl, price_unit=300.0) + self.assertAlmostEqual(sub.recurring_monthly, 100.0, 2) + + def test_mrr_aggregates_over_lines(self): + tmpl = self._create_template("months", 1) + sub = self._create_subscription(tmpl, price_unit=120.0) + self.env["sale.subscription.line"].create( + { + "sale_subscription_id": sub.id, + "product_id": self.product.id, + "product_uom_qty": 1.0, + "price_unit": 80.0, + "tax_ids": [(6, 0, [])], + } + ) + self.assertAlmostEqual(sub.recurring_monthly, 200.0, 2) + + def test_arr_is_absolute_value(self): + tmpl = self._create_template("months", 1) + sub = self._create_subscription(tmpl, price_unit=150.0) + self.assertAlmostEqual(sub.recurring_yearly, 1800.0, 2) + + def test_mrr_recomputes_after_qty_change(self): + tmpl = self._create_template("months", 1) + sub = self._create_subscription(tmpl, price_unit=100.0, qty=1.0) + line = sub.sale_subscription_line_ids + self.assertAlmostEqual(sub.recurring_monthly, 100.0, 2) + line.product_uom_qty = 3.0 + self.assertAlmostEqual(sub.recurring_monthly, 300.0, 2) + + def test_mrr_recomputes_after_template_recurrence_change(self): + tmpl_monthly = self._create_template("months", 1) + tmpl_yearly = self._create_template("years", 1) + sub = self._create_subscription(tmpl_monthly, price_unit=1200.0) + self.assertAlmostEqual(sub.recurring_monthly, 1200.0, 2) + sub.template_id = tmpl_yearly + self.assertAlmostEqual(sub.recurring_monthly, 100.0, 2) + + def test_mrr_is_zero_without_lines(self): + tmpl = self._create_template("months", 1) + sub = self.env["sale.subscription"].create( + { + "partner_id": self.partner.id, + "template_id": tmpl.id, + "pricelist_id": self.pricelist.id, + "stage_id": self.stage_draft.id, + } + ) + self.assertEqual(sub.recurring_monthly, 0.0) + self.assertEqual(sub.recurring_yearly, 0.0) + + def test_mrr_recomputes_after_line_unlink(self): + tmpl = self._create_template("months", 1) + sub = self._create_subscription(tmpl, price_unit=120.0) + extra = self.env["sale.subscription.line"].create( + { + "sale_subscription_id": sub.id, + "product_id": self.product.id, + "product_uom_qty": 1.0, + "price_unit": 80.0, + "tax_ids": [(6, 0, [])], + } + ) + self.assertAlmostEqual(sub.recurring_monthly, 200.0, 2) + extra.unlink() + self.assertAlmostEqual(sub.recurring_monthly, 120.0, 2) + + def test_mrr_uses_subtotal_net_of_discount(self): + # MRR must be computed on the discounted subtotal, not price_unit * qty. + tmpl = self._create_template("months", 1) + sub = self.env["sale.subscription"].create( + { + "partner_id": self.partner.id, + "template_id": tmpl.id, + "pricelist_id": self.pricelist.id, + "stage_id": self.stage_draft.id, + } + ) + self.env["sale.subscription.line"].create( + { + "sale_subscription_id": sub.id, + "product_id": self.product.id, + "product_uom_qty": 1.0, + "price_unit": 200.0, + "discount": 50.0, + "tax_ids": [(6, 0, [])], + } + ) + self.assertAlmostEqual(sub.recurring_monthly, 100.0, 2) + + def test_mrr_converted_to_company_currency(self): + # A subscription priced in a foreign currency must report MRR in the + # company currency, so aggregated totals stay comparable. + company_currency = self.env.company.currency_id + foreign = self.env["res.currency"].create( + { + "name": "MRRC", + "symbol": "M", + "rounding": 0.01, + } + ) + self.env["res.currency.rate"].create( + { + "name": "2020-01-01", + "currency_id": foreign.id, + "company_id": self.env.company.id, + # 2 foreign units == 1 company unit + "rate": 2.0, + } + ) + foreign_pricelist = self.env["product.pricelist"].create( + {"name": "Foreign pricelist", "currency_id": foreign.id} + ) + tmpl = self._create_template("months", 1) + sub = self.env["sale.subscription"].create( + { + "partner_id": self.partner.id, + "template_id": tmpl.id, + "pricelist_id": foreign_pricelist.id, + "stage_id": self.stage_draft.id, + } + ) + self.env["sale.subscription.line"].create( + { + "sale_subscription_id": sub.id, + "product_id": self.product.id, + "product_uom_qty": 1.0, + "price_unit": 200.0, + "tax_ids": [(6, 0, [])], + } + ) + self.assertEqual(sub.currency_id, foreign) + self.assertEqual(sub.company_currency_id, company_currency) + # 200 foreign / month -> 100 company / month + self.assertAlmostEqual(sub.recurring_monthly, 100.0, 2) + + def test_action_view_recurring_revenue(self): + tmpl = self._create_template("months", 1) + subscription = self._create_subscription(tmpl) + action = subscription.action_view_recurring_revenue() + self.assertEqual(action["res_model"], "sale.subscription.line") + self.assertEqual( + action["domain"], [("sale_subscription_id", "=", subscription.id)] + ) + lines = self.env["sale.subscription.line"].search(action["domain"]) + self.assertEqual(lines, subscription.sale_subscription_line_ids) diff --git a/subscription_oca/views/sale_subscription_views.xml b/subscription_oca/views/sale_subscription_views.xml index e78b61ce42..360eaa75b0 100644 --- a/subscription_oca/views/sale_subscription_views.xml +++ b/subscription_oca/views/sale_subscription_views.xml @@ -57,6 +57,43 @@ string="Invoices" /> + +
    + + sale.subscription.line.recurring.revenue.list + sale.subscription.line + + + + + + + + + + + + + + + sale.subscription.list sale.subscription @@ -208,6 +263,8 @@ + + @@ -345,6 +402,16 @@ name="pendingsubs" domain="[('to_renew','=', True)]" /> + + Date: Wed, 10 Jun 2026 10:38:13 +0000 Subject: [PATCH 2/3] [ADD] subscription_oca: subscription analysis reporting Add a Reporting menu to analyse subscriptions with pivot and graph views, in the spirit of the Enterprise subscription reports but based only on community views: - New sale.subscription.report SQL view with one row per subscription line, exposing the customer, template, product, category, stage, close reason, salesperson, team, dates and the monthly/annual recurring revenue in company currency. - Three actions under Subscriptions > Reporting: Subscriptions Analysis, MRR Breakdown (running subscriptions by template and product) and Churn Analysis (closed subscriptions and lost revenue by close reason). - Read access for salesmen and a multi-company record rule on the report model. --- subscription_oca/README.rst | 15 + subscription_oca/__init__.py | 1 + subscription_oca/__manifest__.py | 4 +- subscription_oca/readme/USAGE.md | 13 + subscription_oca/report/__init__.py | 2 + .../report/sale_subscription_report.py | 121 +++++++ .../report/sale_subscription_report_views.xml | 311 ++++++++++++++++++ subscription_oca/security/ir.model.access.csv | 1 + .../sale_subscription_report_security.xml | 10 + .../static/description/index.html | 15 + subscription_oca/tests/__init__.py | 1 + .../tests/test_subscription_report.py | 98 ++++++ 12 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 subscription_oca/report/__init__.py create mode 100644 subscription_oca/report/sale_subscription_report.py create mode 100644 subscription_oca/report/sale_subscription_report_views.xml create mode 100644 subscription_oca/security/sale_subscription_report_security.xml create mode 100644 subscription_oca/tests/test_subscription_report.py diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst index 9387a3f72d..16a09ab192 100644 --- a/subscription_oca/README.rst +++ b/subscription_oca/README.rst @@ -84,6 +84,21 @@ Recurring revenue (MRR / ARR): in-progress subscriptions, which reflects live recurring revenue rather than the theoretical value of draft or closed subscriptions. +Reporting: + +- Go to *Subscriptions > Reporting* to analyse your recurring revenue + with pivot and graph views: + + - *Subscriptions Analysis*: recurring revenue per line, groupable by + customer, template, product, salesperson or start month. + - *MRR Breakdown*: monthly recurring revenue of the running + subscriptions by template and product. + - *Churn Analysis*: closed subscriptions and the revenue lost, + grouped by close reason. + +- All amounts are expressed in the company currency, so figures remain + comparable when subscriptions use pricelists in other currencies. + Known issues / Roadmap ====================== diff --git a/subscription_oca/__init__.py b/subscription_oca/__init__.py index 9b4296142f..7660e7bf61 100644 --- a/subscription_oca/__init__.py +++ b/subscription_oca/__init__.py @@ -1,2 +1,3 @@ from . import models +from . import report from . import wizard diff --git a/subscription_oca/__manifest__.py b/subscription_oca/__manifest__.py index 0af22ce55f..00867e6d2c 100644 --- a/subscription_oca/__manifest__.py +++ b/subscription_oca/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Subscription management", "summary": "Generate recurring invoices.", - "version": "19.0.1.1.0", + "version": "19.0.1.2.0", "development_status": "Beta", "category": "Subscription Management", "website": "https://github.com/OCA/contract", @@ -23,6 +23,8 @@ "data/sale_subscription_data.xml", "wizard/close_subscription_wizard.xml", "security/ir.model.access.csv", + "security/sale_subscription_report_security.xml", + "report/sale_subscription_report_views.xml", ], "installable": True, "application": True, diff --git a/subscription_oca/readme/USAGE.md b/subscription_oca/readme/USAGE.md index 0679b7d47d..eefb77f92b 100644 --- a/subscription_oca/readme/USAGE.md +++ b/subscription_oca/readme/USAGE.md @@ -36,3 +36,16 @@ Recurring revenue (MRR / ARR): - Use the *Active recurring revenue* filter to restrict the figures to in-progress subscriptions, which reflects live recurring revenue rather than the theoretical value of draft or closed subscriptions. + +Reporting: + +- Go to *Subscriptions > Reporting* to analyse your recurring revenue + with pivot and graph views: + - *Subscriptions Analysis*: recurring revenue per line, groupable + by customer, template, product, salesperson or start month. + - *MRR Breakdown*: monthly recurring revenue of the running + subscriptions by template and product. + - *Churn Analysis*: closed subscriptions and the revenue lost, + grouped by close reason. +- All amounts are expressed in the company currency, so figures remain + comparable when subscriptions use pricelists in other currencies. diff --git a/subscription_oca/report/__init__.py b/subscription_oca/report/__init__.py new file mode 100644 index 0000000000..b66ded3e6b --- /dev/null +++ b/subscription_oca/report/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import sale_subscription_report diff --git a/subscription_oca/report/sale_subscription_report.py b/subscription_oca/report/sale_subscription_report.py new file mode 100644 index 0000000000..ac220744e1 --- /dev/null +++ b/subscription_oca/report/sale_subscription_report.py @@ -0,0 +1,121 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models, tools + + +class SaleSubscriptionReport(models.Model): + _name = "sale.subscription.report" + _description = "Subscription Analysis" + _auto = False + _rec_name = "subscription_id" + _order = "date_start desc" + + subscription_id = fields.Many2one( + comodel_name="sale.subscription", string="Subscription", readonly=True + ) + partner_id = fields.Many2one( + comodel_name="res.partner", string="Customer", readonly=True + ) + template_id = fields.Many2one( + comodel_name="sale.subscription.template", + string="Subscription template", + readonly=True, + ) + stage_id = fields.Many2one( + comodel_name="sale.subscription.stage", string="Stage", readonly=True + ) + stage_type = fields.Selection( + [ + ("draft", "Draft"), + ("pre", "Ready to start"), + ("in_progress", "In progress"), + ("post", "Closed"), + ], + string="Stage type", + readonly=True, + ) + close_reason_id = fields.Many2one( + comodel_name="sale.subscription.close.reason", + string="Close Reason", + readonly=True, + ) + user_id = fields.Many2one( + comodel_name="res.users", string="Commercial agent", readonly=True + ) + crm_team_id = fields.Many2one( + comodel_name="crm.team", string="Sale team", readonly=True + ) + company_id = fields.Many2one( + comodel_name="res.company", string="Company", readonly=True + ) + company_currency_id = fields.Many2one( + comodel_name="res.currency", string="Company Currency", readonly=True + ) + product_id = fields.Many2one( + comodel_name="product.product", string="Product", readonly=True + ) + categ_id = fields.Many2one( + comodel_name="product.category", string="Product Category", readonly=True + ) + in_progress = fields.Boolean(string="In progress", readonly=True) + to_renew = fields.Boolean(string="To renew", readonly=True) + date_start = fields.Date(string="Start date", readonly=True) + date_end = fields.Date(string="Finish date", readonly=True) + recurring_next_date = fields.Date(string="Next invoice date", readonly=True) + product_uom_qty = fields.Float(string="Quantity", readonly=True) + recurring_monthly = fields.Monetary( + string="Monthly recurring revenue", + currency_field="company_currency_id", + readonly=True, + ) + recurring_yearly = fields.Monetary( + string="Annual recurring revenue", + currency_field="company_currency_id", + readonly=True, + ) + + def _select(self): + return """ + SELECT + line.id AS id, + sub.id AS subscription_id, + sub.partner_id AS partner_id, + sub.template_id AS template_id, + sub.stage_id AS stage_id, + stage.type AS stage_type, + sub.close_reason_id AS close_reason_id, + sub.user_id AS user_id, + sub.crm_team_id AS crm_team_id, + sub.company_id AS company_id, + sub.company_currency_id AS company_currency_id, + line.product_id AS product_id, + template.categ_id AS categ_id, + sub.in_progress AS in_progress, + sub.to_renew AS to_renew, + sub.date_start AS date_start, + sub.date AS date_end, + sub.recurring_next_date AS recurring_next_date, + line.product_uom_qty AS product_uom_qty, + line.recurring_monthly AS recurring_monthly, + line.recurring_monthly * 12.0 AS recurring_yearly + """ + + def _from(self): + return """ + FROM sale_subscription_line line + JOIN sale_subscription sub + ON sub.id = line.sale_subscription_id + LEFT JOIN sale_subscription_stage stage + ON stage.id = sub.stage_id + LEFT JOIN product_product product + ON product.id = line.product_id + LEFT JOIN product_template template + ON template.id = product.product_tmpl_id + WHERE sub.active = TRUE + """ + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute( + f"CREATE OR REPLACE VIEW {self._table} AS ({self._select()} {self._from()})" + ) diff --git a/subscription_oca/report/sale_subscription_report_views.xml b/subscription_oca/report/sale_subscription_report_views.xml new file mode 100644 index 0000000000..c720355255 --- /dev/null +++ b/subscription_oca/report/sale_subscription_report_views.xml @@ -0,0 +1,311 @@ + + + + sale.subscription.report.search + sale.subscription.report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sale.subscription.report.pivot + sale.subscription.report + + + + + + + + + + + sale.subscription.report.graph + sale.subscription.report + + + + + + + + + + sale.subscription.report.graph.timeline + sale.subscription.report + + + + + + + + + + sale.subscription.report.list + sale.subscription.report + + + + + + + + + + + + + + + + + + + + + + + + + Subscriptions Analysis + sale.subscription.report + graph,pivot,list + + {"search_default_in_progress": 1} + +

    + No data to analyse yet +

    +

    + This analysis aggregates the recurring revenue of your + subscriptions per line, so you can group it by customer, + template, product or salesperson. +

    +
    +
    + + + MRR Breakdown + sale.subscription.report + pivot,graph,list + + + {"search_default_in_progress": 1, + "pivot_measures": ["recurring_monthly"], + "pivot_row_groupby": ["template_id", "product_id"], + "graph_groupbys": ["template_id"], + "graph_measure": "recurring_monthly"} + + +

    + No recurring revenue yet +

    +

    + Breakdown of the monthly recurring revenue of the running + subscriptions by template and product. +

    +
    +
    + + + MRR Timeline + sale.subscription.report + graph,pivot + + + {"search_default_in_progress": 1, + "search_default_groupby_date_start": 1} + + +

    + No recurring revenue yet +

    +

    + Evolution of the monthly recurring revenue over time, accumulated + by the month the subscriptions started. +

    +
    +
    + + + + graph + + + + + + + pivot + + + + + + Churn Analysis + sale.subscription.report + pivot,graph,list + + [("stage_type", "=", "post")] + + {"search_default_groupby_close_reason_id": 1, + "pivot_measures": ["recurring_monthly", "__count"], + "pivot_row_groupby": ["close_reason_id"]} + + +

    + No closed subscriptions yet +

    +

    + Closed subscriptions and the recurring revenue lost, grouped + by close reason. +

    +
    +
    + + + + + + + + + + +
    diff --git a/subscription_oca/security/ir.model.access.csv b/subscription_oca/security/ir.model.access.csv index cd0f7dba90..54a995861e 100644 --- a/subscription_oca/security/ir.model.access.csv +++ b/subscription_oca/security/ir.model.access.csv @@ -6,3 +6,4 @@ access_custom_sale_subscription_stage,sale.subscription.stage,model_sale_subscri access_custom_sale_subscription_line,sale.subscription.line,model_sale_subscription_line,sales_team.group_sale_salesman,1,1,1,1 access_custom_sale_subscription_tag,sale.subscription.tag,model_sale_subscription_tag,sales_team.group_sale_salesman,1,1,1,1 access_close_subscription,Close subscription access,model_close_reason_wizard,sales_team.group_sale_salesman,1,1,1,1 +access_sale_subscription_report,sale.subscription.report,model_sale_subscription_report,sales_team.group_sale_salesman,1,0,0,0 diff --git a/subscription_oca/security/sale_subscription_report_security.xml b/subscription_oca/security/sale_subscription_report_security.xml new file mode 100644 index 0000000000..ce32b41ab0 --- /dev/null +++ b/subscription_oca/security/sale_subscription_report_security.xml @@ -0,0 +1,10 @@ + + + + Subscription analysis: multi-company + + + ["|", ("company_id", "=", False), ("company_id", "in", company_ids)] + + + diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html index 368d74b892..f1a27ac8c7 100644 --- a/subscription_oca/static/description/index.html +++ b/subscription_oca/static/description/index.html @@ -434,6 +434,21 @@

    Usage

    in-progress subscriptions, which reflects live recurring revenue rather than the theoretical value of draft or closed subscriptions. +

    Reporting:

    +
      +
    • Go to Subscriptions > Reporting to analyse your recurring revenue +with pivot and graph views:
        +
      • Subscriptions Analysis: recurring revenue per line, groupable by +customer, template, product, salesperson or start month.
      • +
      • MRR Breakdown: monthly recurring revenue of the running +subscriptions by template and product.
      • +
      • Churn Analysis: closed subscriptions and the revenue lost, +grouped by close reason.
      • +
      +
    • +
    • All amounts are expressed in the company currency, so figures remain +comparable when subscriptions use pricelists in other currencies.
    • +

    Known issues / Roadmap

    diff --git a/subscription_oca/tests/__init__.py b/subscription_oca/tests/__init__.py index ce9533b727..cb4046fde4 100644 --- a/subscription_oca/tests/__init__.py +++ b/subscription_oca/tests/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import test_subscription_mrr, test_subscription_oca +from . import test_subscription_report diff --git a/subscription_oca/tests/test_subscription_report.py b/subscription_oca/tests/test_subscription_report.py new file mode 100644 index 0000000000..f10707fd61 --- /dev/null +++ b/subscription_oca/tests/test_subscription_report.py @@ -0,0 +1,98 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.base.tests.common import BaseCommon +from odoo.addons.product.tests.common import ProductCommon + + +class TestSubscriptionReport(ProductCommon, BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.pricelist = cls.env["product.pricelist"].create( + {"name": "Report pricelist"} + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "Report partner", + "property_product_pricelist": cls.pricelist.id, + } + ) + cls.product = cls._create_product( + name="Report product", + lst_price=120.0, + subscribable=True, + uom_id=cls.uom_unit.id, + taxes_id=[(6, 0, [])], + ) + cls.template = cls.env["sale.subscription.template"].create( + { + "name": "Report template", + "code": "REP-MTH", + "recurring_rule_type": "months", + "recurring_interval": 1, + } + ) + cls.subscription = cls.env["sale.subscription"].create( + { + "partner_id": cls.partner.id, + "template_id": cls.template.id, + "pricelist_id": cls.pricelist.id, + "date_start": fields.Date.today(), + } + ) + cls.line = cls.env["sale.subscription.line"].create( + { + "sale_subscription_id": cls.subscription.id, + "product_id": cls.product.id, + "product_uom_qty": 2.0, + "price_unit": 120.0, + "tax_ids": [(6, 0, [])], + } + ) + + def _report_rows(self): + self.env.flush_all() + return self.env["sale.subscription.report"].search( + [("subscription_id", "=", self.subscription.id)] + ) + + def test_report_has_one_row_per_line(self): + rows = self._report_rows() + self.assertEqual(len(rows), len(self.subscription.sale_subscription_line_ids)) + + def test_report_row_matches_line_revenue(self): + row = self._report_rows() + self.assertAlmostEqual(row.recurring_monthly, self.line.recurring_monthly, 2) + self.assertAlmostEqual( + row.recurring_yearly, self.line.recurring_monthly * 12.0, 2 + ) + self.assertEqual(row.partner_id, self.partner) + self.assertEqual(row.template_id, self.template) + self.assertEqual(row.product_id, self.product) + self.assertEqual(row.stage_type, self.subscription.stage_id.type) + + def test_report_mrr_aggregation_matches_subscription(self): + self.env["sale.subscription.line"].create( + { + "sale_subscription_id": self.subscription.id, + "product_id": self.product.id, + "product_uom_qty": 1.0, + "price_unit": 60.0, + "tax_ids": [(6, 0, [])], + } + ) + rows = self._report_rows() + self.assertAlmostEqual( + sum(rows.mapped("recurring_monthly")), + self.subscription.recurring_monthly, + 2, + ) + + def test_report_excludes_archived_subscription(self): + self.assertTrue(self._report_rows()) + self.subscription.active = False + self.assertFalse(self._report_rows()) From 18147fef0ec1b2680248c79f2f7fe969c5078f98 Mon Sep 17 00:00:00 2001 From: alvaro-domatix Date: Mon, 22 Jun 2026 10:35:12 +0000 Subject: [PATCH 3/3] [ADD] spreadsheet_dashboard_subscription_oca: spreadsheet dashboards for subscriptions --- .../README.rst | 111 +++ .../__init__.py | 3 + .../__manifest__.py | 19 + .../files/mrr_evolution_dashboard.osheet.json | 703 +++++++++++++++++ .../files/retention_dashboard.osheet.json | 525 +++++++++++++ .../files/salesperson_dashboard.osheet.json | 687 +++++++++++++++++ .../files/subscriptions_dashboard.osheet.json | 713 ++++++++++++++++++ .../data/spreadsheet_dashboards.xml | 114 +++ .../i18n/es.po | 429 +++++++++++ .../models/__init__.py | 4 + .../models/sale_subscription_mrr_report.py | 91 +++ .../sale_subscription_retention_report.py | 96 +++ .../pyproject.toml | 3 + .../readme/DESCRIPTION.md | 22 + .../readme/USAGE.md | 6 + .../report/sql_report_views.xml | 249 ++++++ .../security/ir.model.access.csv | 3 + .../static/description/index.html | 457 +++++++++++ 18 files changed, 4235 insertions(+) create mode 100644 spreadsheet_dashboard_subscription_oca/README.rst create mode 100644 spreadsheet_dashboard_subscription_oca/__init__.py create mode 100644 spreadsheet_dashboard_subscription_oca/__manifest__.py create mode 100644 spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json create mode 100644 spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json create mode 100644 spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json create mode 100644 spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json create mode 100644 spreadsheet_dashboard_subscription_oca/data/spreadsheet_dashboards.xml create mode 100644 spreadsheet_dashboard_subscription_oca/i18n/es.po create mode 100644 spreadsheet_dashboard_subscription_oca/models/__init__.py create mode 100644 spreadsheet_dashboard_subscription_oca/models/sale_subscription_mrr_report.py create mode 100644 spreadsheet_dashboard_subscription_oca/models/sale_subscription_retention_report.py create mode 100644 spreadsheet_dashboard_subscription_oca/pyproject.toml create mode 100644 spreadsheet_dashboard_subscription_oca/readme/DESCRIPTION.md create mode 100644 spreadsheet_dashboard_subscription_oca/readme/USAGE.md create mode 100644 spreadsheet_dashboard_subscription_oca/report/sql_report_views.xml create mode 100644 spreadsheet_dashboard_subscription_oca/security/ir.model.access.csv create mode 100644 spreadsheet_dashboard_subscription_oca/static/description/index.html diff --git a/spreadsheet_dashboard_subscription_oca/README.rst b/spreadsheet_dashboard_subscription_oca/README.rst new file mode 100644 index 0000000000..501b391ab8 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/README.rst @@ -0,0 +1,111 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================================= +Subscription OCA Spreadsheet Dashboards +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f1ffb09d1dc8f9a4a2d46af8273e5e4dd79b3e6e6f6fae841fa49a7434cd02c0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/19.0/spreadsheet_dashboard_subscription_oca + :alt: OCA/contract +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/contract-19-0/contract-19-0-spreadsheet_dashboard_subscription_oca + :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/contract&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds spreadsheet dashboards for subscriptions, built on top +of the community ``spreadsheet_dashboard`` engine and the analysis +models provided by ``subscription_oca``. No enterprise dependency is +required. + +It ships four ready-to-use dashboards under the **Dashboards** +application, in a dedicated *Subscriptions* group: + +- **Subscriptions**: monthly/annual recurring revenue, active + subscriptions and average MRR, with breakdowns by template, product + category, sales team, salesperson, start month and stage. +- **Salesperson**: recurring revenue and number of subscriptions broken + down by salesperson and sales team. +- **MRR Evolution**: net/new/churned MRR and the cumulated MRR over + time, with a monthly new-vs-churn breakdown. MRR change events are + derived from the current state of the subscriptions (a positive event + at the start date and a negative one at the closing date); mid-life + expansion/contraction is not tracked, which would require a dedicated + MRR event log. +- **Retention**: cohort sizes and recurring revenue per start month, + plus a retention/survival curve built from the start and closing + dates. + +All dashboards expose global filters (salesperson, sales team, customer, +template) and refresh automatically from the subscription analysis +models. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Open the **Dashboards** application. The four dashboards are available +in the *Subscriptions* group. Use the filters at the top (salesperson, +sales team, customer, template) to narrow down the figures. The data is +aggregated from the ``sale.subscription.report``, +``sale.subscription.mrr.report`` and +``sale.subscription.retention.report`` analysis models and refreshes +automatically. + +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 +------- + +* Domatix + +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. + +This module is part of the `OCA/contract `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/spreadsheet_dashboard_subscription_oca/__init__.py b/spreadsheet_dashboard_subscription_oca/__init__.py new file mode 100644 index 0000000000..a161af0977 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models diff --git a/spreadsheet_dashboard_subscription_oca/__manifest__.py b/spreadsheet_dashboard_subscription_oca/__manifest__.py new file mode 100644 index 0000000000..268b968fec --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Subscription OCA Spreadsheet Dashboards", + "version": "19.0.1.0.0", + "license": "AGPL-3", + "author": "Domatix, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/contract", + "category": "Productivity/Dashboard", + "summary": "Spreadsheet dashboards for subscriptions (MRR, salesperson)", + "depends": ["spreadsheet_dashboard", "subscription_oca"], + "data": [ + "security/ir.model.access.csv", + "report/sql_report_views.xml", + "data/spreadsheet_dashboards.xml", + ], + "auto_install": True, + "development_status": "Beta", +} diff --git a/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json b/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json new file mode 100644 index 0000000000..d9c3a7ac77 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json @@ -0,0 +1,703 @@ +{ + "version": "18.5.10", + "sheets": [ + { + "id": "dashboard", + "name": "Dashboard", + "colNumber": 22, + "rowNumber": 80, + "rows": {}, + "cols": { + "0": { + "size": 50 + }, + "1": { + "size": 50 + }, + "2": { + "size": 50 + }, + "3": { + "size": 50 + }, + "4": { + "size": 50 + }, + "5": { + "size": 50 + }, + "6": { + "size": 50 + }, + "7": { + "size": 50 + }, + "8": { + "size": 50 + }, + "9": { + "size": 50 + }, + "10": { + "size": 50 + }, + "11": { + "size": 50 + }, + "12": { + "size": 50 + }, + "13": { + "size": 50 + }, + "14": { + "size": 50 + }, + "15": { + "size": 50 + }, + "16": { + "size": 50 + }, + "17": { + "size": 50 + }, + "18": { + "size": 50 + }, + "19": { + "size": 50 + }, + "20": { + "size": 50 + }, + "21": { + "size": 50 + } + }, + "merges": [], + "cells": {}, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [ + { + "id": "cccccccc-0000-0000-0000-000000000001", + "width": 345, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Net MRR", + "color": "#1f2937", + "bold": true + }, + "background": "#EFF6FF", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C1", + "chartId": "cccccccc-0000-0000-0000-000000000001" + }, + "offset": { + "x": 16, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000002", + "width": 345, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "New MRR (all time)", + "color": "#1f2937", + "bold": true + }, + "background": "#F0FDF4", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C2", + "chartId": "cccccccc-0000-0000-0000-000000000002" + }, + "offset": { + "x": 377, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000003", + "width": 345, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Churned MRR (all time)", + "color": "#1f2937", + "bold": true + }, + "background": "#FEF2F2", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C3", + "chartId": "cccccccc-0000-0000-0000-000000000003" + }, + "offset": { + "x": 738, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000010", + "width": 1068, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Net MRR Over Time" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["event_date:month"], + "measure": "mrr_change", + "order": null, + "resModel": "sale.subscription.mrr.report", + "mode": "line", + "cumulatedStart": true + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [], + "groupBy": ["event_date:month"], + "orderBy": [] + }, + "type": "odoo_line", + "dataSets": [{}], + "chartId": "cccccccc-0000-0000-0000-000000000010", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_mrr_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "cumulative": true, + "cumulatedStart": true + }, + "offset": { + "x": 16, + "y": 172 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000011", + "width": 1068, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR Change by Month & Type" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["event_date:month", "event_type"], + "measure": "mrr_change", + "order": null, + "resModel": "sale.subscription.mrr.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [], + "groupBy": ["event_date:month", "event_type"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "cccccccc-0000-0000-0000-000000000011", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_mrr_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "stacked": true + }, + "offset": { + "x": 16, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000012", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR Change by Type" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["event_type"], + "measure": "mrr_change", + "order": null, + "resModel": "sale.subscription.mrr.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [], + "groupBy": ["event_type"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "cccccccc-0000-0000-0000-000000000012", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_mrr_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 16, + "y": 932 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000013", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "New MRR by Template" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["template_id"], + "measure": "mrr_change", + "order": null, + "resModel": "sale.subscription.mrr.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["event_type", "=", "new"]], + "groupBy": ["template_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "cccccccc-0000-0000-0000-000000000013", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_mrr_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 558, + "y": 932 + }, + "col": 0, + "row": 0 + }, + { + "id": "cccccccc-0000-0000-0000-000000000014", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "New MRR by Salesperson" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["user_id"], + "measure": "mrr_change", + "order": null, + "resModel": "sale.subscription.mrr.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["event_type", "=", "new"]], + "groupBy": ["user_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "cccccccc-0000-0000-0000-000000000014", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_mrr_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 16, + "y": 1312 + }, + "col": 0, + "row": 0 + } + ], + "tables": [], + "areGridLinesVisible": false, + "isVisible": true, + "headerGroups": [], + "comments": {} + }, + { + "id": "data", + "name": "Data", + "colNumber": 8, + "rowNumber": 30, + "rows": {}, + "cols": {}, + "merges": [], + "cells": { + "A1": "=_t(\"MRR KPIs\")", + "B1": "=PIVOT.VALUE(1,\"mrr_change:sum\")", + "B2": "=PIVOT.VALUE(2,\"mrr_change:sum\")", + "B3": "=PIVOT.VALUE(3,\"mrr_change:sum\")", + "C1": "=IFERROR(FORMAT.LARGE.NUMBER(B1))", + "C2": "=IFERROR(FORMAT.LARGE.NUMBER(B2))", + "C3": "=IFERROR(FORMAT.LARGE.NUMBER(B3))" + }, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [], + "tables": [], + "areGridLinesVisible": true, + "isVisible": false, + "headerGroups": [], + "comments": {} + } + ], + "styles": {}, + "formats": {}, + "borders": {}, + "revisionId": "START_REVISION", + "uniqueFigureIds": true, + "settings": { + "locale": { + "name": "English (US)", + "code": "en_US", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "dateFormat": "m/d/yyyy", + "timeFormat": "hh:mm:ss a", + "formulaArgSeparator": ",", + "weekStart": 7 + } + }, + "pivots": { + "1": { + "type": "ODOO", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "context": {}, + "domain": [], + "id": "1", + "measures": [ + { + "id": "mrr_change:sum", + "fieldName": "mrr_change", + "aggregator": "sum" + }, + { + "id": "__count", + "fieldName": "__count" + } + ], + "model": "sale.subscription.mrr.report", + "name": "KPIs 1", + "sortedColumn": null, + "formulaId": "1", + "columns": [], + "rows": [] + }, + "2": { + "type": "ODOO", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "context": {}, + "domain": [["event_type", "=", "new"]], + "id": "2", + "measures": [ + { + "id": "mrr_change:sum", + "fieldName": "mrr_change", + "aggregator": "sum" + } + ], + "model": "sale.subscription.mrr.report", + "name": "KPIs 2", + "sortedColumn": null, + "formulaId": "2", + "columns": [], + "rows": [] + }, + "3": { + "type": "ODOO", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "event_date", + "type": "datetime", + "offset": 0 + } + }, + "context": {}, + "domain": [["event_type", "=", "churn"]], + "id": "3", + "measures": [ + { + "id": "mrr_change:sum", + "fieldName": "mrr_change", + "aggregator": "sum" + } + ], + "model": "sale.subscription.mrr.report", + "name": "KPIs 3", + "sortedColumn": null, + "formulaId": "3", + "columns": [], + "rows": [] + } + }, + "pivotNextId": 4, + "customTableStyles": {}, + "globalFilters": [ + { + "id": "55555555-5555-5555-5555-555555555555", + "type": "date", + "label": "Period", + "defaultValue": "last_90_days" + }, + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "relation", + "label": "Salesperson", + "defaultValueDisplayNames": [], + "modelName": "res.users" + }, + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "relation", + "label": "Sales Team", + "defaultValueDisplayNames": [], + "modelName": "crm.team" + }, + { + "id": "33333333-3333-3333-3333-333333333333", + "type": "relation", + "label": "Customer", + "defaultValueDisplayNames": [], + "modelName": "res.partner" + }, + { + "id": "44444444-4444-4444-4444-444444444444", + "type": "relation", + "label": "Template", + "defaultValueDisplayNames": [], + "modelName": "sale.subscription.template" + } + ], + "lists": {}, + "listNextId": 1, + "chartOdooMenusReferences": {} +} diff --git a/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json b/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json new file mode 100644 index 0000000000..4c860e23c3 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json @@ -0,0 +1,525 @@ +{ + "version": "18.5.10", + "sheets": [ + { + "id": "dashboard", + "name": "Dashboard", + "colNumber": 22, + "rowNumber": 80, + "rows": {}, + "cols": { + "0": { + "size": 50 + }, + "1": { + "size": 50 + }, + "2": { + "size": 50 + }, + "3": { + "size": 50 + }, + "4": { + "size": 50 + }, + "5": { + "size": 50 + }, + "6": { + "size": 50 + }, + "7": { + "size": 50 + }, + "8": { + "size": 50 + }, + "9": { + "size": 50 + }, + "10": { + "size": 50 + }, + "11": { + "size": 50 + }, + "12": { + "size": 50 + }, + "13": { + "size": 50 + }, + "14": { + "size": 50 + }, + "15": { + "size": 50 + }, + "16": { + "size": 50 + }, + "17": { + "size": 50 + }, + "18": { + "size": 50 + }, + "19": { + "size": 50 + }, + "20": { + "size": 50 + }, + "21": { + "size": 50 + } + }, + "merges": [], + "cells": {}, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [ + { + "id": "dddddddd-0000-0000-0000-000000000001", + "width": 526, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Subscriptions (cohorts)", + "color": "#1f2937", + "bold": true + }, + "background": "#EFF6FF", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C1", + "chartId": "dddddddd-0000-0000-0000-000000000001" + }, + "offset": { + "x": 16, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "dddddddd-0000-0000-0000-000000000002", + "width": 526, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "MRR at Start", + "color": "#1f2937", + "bold": true + }, + "background": "#FFF7ED", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C2", + "chartId": "dddddddd-0000-0000-0000-000000000002" + }, + "offset": { + "x": 558, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "dddddddd-0000-0000-0000-000000000010", + "width": 1068, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Retention Curve (active subscriptions by month since start)" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["period_index"], + "measure": "__count", + "order": null, + "resModel": "sale.subscription.retention.report", + "mode": "line" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [], + "groupBy": ["period_index"], + "orderBy": [] + }, + "type": "odoo_line", + "dataSets": [{}], + "chartId": "dddddddd-0000-0000-0000-000000000010", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_retention_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "cohort_date", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 16, + "y": 172 + }, + "col": 0, + "row": 0 + }, + { + "id": "dddddddd-0000-0000-0000-000000000011", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Subscriptions by Cohort (start month)" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["cohort_date:month"], + "measure": "__count", + "order": null, + "resModel": "sale.subscription.retention.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["period_index", "=", 0]], + "groupBy": ["cohort_date:month"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "dddddddd-0000-0000-0000-000000000011", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_retention_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "cohort_date", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 16, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "dddddddd-0000-0000-0000-000000000012", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Cohort (start month)" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["cohort_date:month"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.retention.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["period_index", "=", 0]], + "groupBy": ["cohort_date:month"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "dddddddd-0000-0000-0000-000000000012", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_retention_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "cohort_date", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 558, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "dddddddd-0000-0000-0000-000000000013", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Cohort MRR by Template" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["template_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.retention.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["period_index", "=", 0]], + "groupBy": ["template_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "dddddddd-0000-0000-0000-000000000013", + "actionXmlId": "spreadsheet_dashboard_subscription_oca.action_sale_subscription_retention_report", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "cohort_date", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 16, + "y": 932 + }, + "col": 0, + "row": 0 + } + ], + "tables": [], + "areGridLinesVisible": false, + "isVisible": true, + "headerGroups": [], + "comments": {} + }, + { + "id": "data", + "name": "Data", + "colNumber": 8, + "rowNumber": 30, + "rows": {}, + "cols": {}, + "merges": [], + "cells": { + "A1": "=_t(\"Retention KPIs\")", + "B1": "=PIVOT.VALUE(1,\"__count\")", + "B2": "=PIVOT.VALUE(1,\"recurring_monthly:sum\")", + "C1": "=IFERROR(FORMAT.LARGE.NUMBER(B1))", + "C2": "=IFERROR(FORMAT.LARGE.NUMBER(B2))" + }, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [], + "tables": [], + "areGridLinesVisible": true, + "isVisible": false, + "headerGroups": [], + "comments": {} + } + ], + "styles": {}, + "formats": {}, + "borders": {}, + "revisionId": "START_REVISION", + "uniqueFigureIds": true, + "settings": { + "locale": { + "name": "English (US)", + "code": "en_US", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "dateFormat": "m/d/yyyy", + "timeFormat": "hh:mm:ss a", + "formulaArgSeparator": ",", + "weekStart": 7 + } + }, + "pivots": { + "1": { + "type": "ODOO", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "cohort_date", + "type": "datetime", + "offset": 0 + } + }, + "context": {}, + "domain": [["period_index", "=", 0]], + "id": "1", + "measures": [ + { + "id": "recurring_monthly:sum", + "fieldName": "recurring_monthly", + "aggregator": "sum" + }, + { + "id": "__count", + "fieldName": "__count" + } + ], + "model": "sale.subscription.retention.report", + "name": "KPIs 1", + "sortedColumn": null, + "formulaId": "1", + "columns": [], + "rows": [] + } + }, + "pivotNextId": 2, + "customTableStyles": {}, + "globalFilters": [ + { + "id": "55555555-5555-5555-5555-555555555555", + "type": "date", + "label": "Period", + "defaultValue": "last_90_days" + }, + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "relation", + "label": "Salesperson", + "defaultValueDisplayNames": [], + "modelName": "res.users" + }, + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "relation", + "label": "Sales Team", + "defaultValueDisplayNames": [], + "modelName": "crm.team" + }, + { + "id": "33333333-3333-3333-3333-333333333333", + "type": "relation", + "label": "Customer", + "defaultValueDisplayNames": [], + "modelName": "res.partner" + }, + { + "id": "44444444-4444-4444-4444-444444444444", + "type": "relation", + "label": "Template", + "defaultValueDisplayNames": [], + "modelName": "sale.subscription.template" + } + ], + "lists": {}, + "listNextId": 1, + "chartOdooMenusReferences": {} +} diff --git a/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json b/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json new file mode 100644 index 0000000000..9c82a462bd --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json @@ -0,0 +1,687 @@ +{ + "version": "18.5.10", + "sheets": [ + { + "id": "dashboard", + "name": "Dashboard", + "colNumber": 22, + "rowNumber": 80, + "rows": {}, + "cols": { + "0": { + "size": 50 + }, + "1": { + "size": 50 + }, + "2": { + "size": 50 + }, + "3": { + "size": 50 + }, + "4": { + "size": 50 + }, + "5": { + "size": 50 + }, + "6": { + "size": 50 + }, + "7": { + "size": 50 + }, + "8": { + "size": 50 + }, + "9": { + "size": 50 + }, + "10": { + "size": 50 + }, + "11": { + "size": 50 + }, + "12": { + "size": 50 + }, + "13": { + "size": 50 + }, + "14": { + "size": 50 + }, + "15": { + "size": 50 + }, + "16": { + "size": 50 + }, + "17": { + "size": 50 + }, + "18": { + "size": 50 + }, + "19": { + "size": 50 + }, + "20": { + "size": 50 + }, + "21": { + "size": 50 + } + }, + "merges": [], + "cells": {}, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [ + { + "id": "bbbbbbbb-0000-0000-0000-000000000001", + "width": 345, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Monthly Recurring Revenue", + "color": "#1f2937", + "bold": true + }, + "background": "#FFF7ED", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "Data!C2", + "humanize": false, + "keyValue": "Data!C1", + "chartId": "bbbbbbbb-0000-0000-0000-000000000001", + "baselineDescr": { + "text": "ARR" + } + }, + "offset": { + "x": 16, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000002", + "width": 345, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Active Subscriptions", + "color": "#1f2937", + "bold": true + }, + "background": "#F0FDF4", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C3", + "chartId": "bbbbbbbb-0000-0000-0000-000000000002" + }, + "offset": { + "x": 377, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000003", + "width": 345, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Avg MRR / Subscription", + "color": "#1f2937", + "bold": true + }, + "background": "#FAF5FF", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C4", + "chartId": "bbbbbbbb-0000-0000-0000-000000000003" + }, + "offset": { + "x": 738, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000010", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Salesperson" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["user_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["user_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "bbbbbbbb-0000-0000-0000-000000000010", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 16, + "y": 172 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000011", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR share by Salesperson" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["user_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["user_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "bbbbbbbb-0000-0000-0000-000000000011", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 558, + "y": 172 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000012", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Subscriptions by Salesperson" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["user_id"], + "measure": "__count", + "order": null, + "resModel": "sale.subscription.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["user_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "bbbbbbbb-0000-0000-0000-000000000012", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 16, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000013", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Sales Team" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["crm_team_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["crm_team_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "bbbbbbbb-0000-0000-0000-000000000013", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 558, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000014", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Subscriptions by Sales Team" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["crm_team_id"], + "measure": "__count", + "order": null, + "resModel": "sale.subscription.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["crm_team_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "bbbbbbbb-0000-0000-0000-000000000014", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 16, + "y": 932 + }, + "col": 0, + "row": 0 + }, + { + "id": "bbbbbbbb-0000-0000-0000-000000000015", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Salesperson & Template" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["user_id", "template_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["user_id", "template_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "bbbbbbbb-0000-0000-0000-000000000015", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "stacked": true + }, + "offset": { + "x": 558, + "y": 932 + }, + "col": 0, + "row": 0 + } + ], + "tables": [], + "areGridLinesVisible": false, + "isVisible": true, + "headerGroups": [], + "comments": {} + }, + { + "id": "data", + "name": "Data", + "colNumber": 8, + "rowNumber": 30, + "rows": {}, + "cols": {}, + "merges": [], + "cells": { + "A1": "=_t(\"Salesperson KPIs\")", + "B1": "=PIVOT.VALUE(1,\"recurring_monthly:sum\")", + "B2": "=PIVOT.VALUE(1,\"recurring_yearly:sum\")", + "B3": "=PIVOT.VALUE(1,\"__count\")", + "B4": "=IFERROR(B1/B3,0)", + "C1": "=IFERROR(FORMAT.LARGE.NUMBER(B1))", + "C2": "=IFERROR(FORMAT.LARGE.NUMBER(B2))", + "C3": "=IFERROR(FORMAT.LARGE.NUMBER(B3))", + "C4": "=IFERROR(FORMAT.LARGE.NUMBER(B4))" + }, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [], + "tables": [], + "areGridLinesVisible": true, + "isVisible": false, + "headerGroups": [], + "comments": {} + } + ], + "styles": {}, + "formats": {}, + "borders": {}, + "revisionId": "START_REVISION", + "uniqueFigureIds": true, + "settings": { + "locale": { + "name": "English (US)", + "code": "en_US", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "dateFormat": "m/d/yyyy", + "timeFormat": "hh:mm:ss a", + "formulaArgSeparator": ",", + "weekStart": 7 + } + }, + "pivots": { + "1": { + "type": "ODOO", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "context": {}, + "domain": [["in_progress", "=", true]], + "id": "1", + "measures": [ + { + "id": "recurring_monthly:sum", + "fieldName": "recurring_monthly", + "aggregator": "sum" + }, + { + "id": "recurring_yearly:sum", + "fieldName": "recurring_yearly", + "aggregator": "sum" + }, + { + "id": "__count", + "fieldName": "__count" + } + ], + "model": "sale.subscription.report", + "name": "KPIs 1", + "sortedColumn": null, + "formulaId": "1", + "columns": [], + "rows": [] + } + }, + "pivotNextId": 2, + "customTableStyles": {}, + "globalFilters": [ + { + "id": "55555555-5555-5555-5555-555555555555", + "type": "date", + "label": "Period", + "defaultValue": "last_90_days" + }, + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "relation", + "label": "Salesperson", + "defaultValueDisplayNames": [], + "modelName": "res.users" + }, + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "relation", + "label": "Sales Team", + "defaultValueDisplayNames": [], + "modelName": "crm.team" + }, + { + "id": "33333333-3333-3333-3333-333333333333", + "type": "relation", + "label": "Customer", + "defaultValueDisplayNames": [], + "modelName": "res.partner" + }, + { + "id": "44444444-4444-4444-4444-444444444444", + "type": "relation", + "label": "Template", + "defaultValueDisplayNames": [], + "modelName": "sale.subscription.template" + } + ], + "lists": {}, + "listNextId": 1, + "chartOdooMenusReferences": {} +} diff --git a/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json b/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json new file mode 100644 index 0000000000..f9eb55deef --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json @@ -0,0 +1,713 @@ +{ + "version": "18.5.10", + "sheets": [ + { + "id": "dashboard", + "name": "Dashboard", + "colNumber": 22, + "rowNumber": 80, + "rows": {}, + "cols": { + "0": { + "size": 50 + }, + "1": { + "size": 50 + }, + "2": { + "size": 50 + }, + "3": { + "size": 50 + }, + "4": { + "size": 50 + }, + "5": { + "size": 50 + }, + "6": { + "size": 50 + }, + "7": { + "size": 50 + }, + "8": { + "size": 50 + }, + "9": { + "size": 50 + }, + "10": { + "size": 50 + }, + "11": { + "size": 50 + }, + "12": { + "size": 50 + }, + "13": { + "size": 50 + }, + "14": { + "size": 50 + }, + "15": { + "size": 50 + }, + "16": { + "size": 50 + }, + "17": { + "size": 50 + }, + "18": { + "size": 50 + }, + "19": { + "size": 50 + }, + "20": { + "size": 50 + }, + "21": { + "size": 50 + } + }, + "merges": [], + "cells": {}, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [ + { + "id": "aaaaaaaa-0000-0000-0000-000000000001", + "width": 255, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Monthly Recurring Revenue", + "color": "#1f2937", + "bold": true + }, + "background": "#FFF7ED", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "Data!C2", + "humanize": false, + "keyValue": "Data!C1", + "chartId": "aaaaaaaa-0000-0000-0000-000000000001", + "baselineDescr": { + "text": "ARR" + } + }, + "offset": { + "x": 16, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000002", + "width": 255, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Annual Recurring Revenue", + "color": "#1f2937", + "bold": true + }, + "background": "#EFF6FF", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C2", + "chartId": "aaaaaaaa-0000-0000-0000-000000000002" + }, + "offset": { + "x": 287, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000003", + "width": 255, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Active Subscriptions", + "color": "#1f2937", + "bold": true + }, + "background": "#F0FDF4", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C3", + "chartId": "aaaaaaaa-0000-0000-0000-000000000003" + }, + "offset": { + "x": 558, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000004", + "width": 255, + "height": 132, + "tag": "chart", + "data": { + "type": "scorecard", + "title": { + "text": "Avg MRR / Subscription", + "color": "#1f2937", + "bold": true + }, + "background": "#FAF5FF", + "baselineColorUp": "#00A04A", + "baselineColorDown": "#DC6965", + "baselineMode": "text", + "baseline": "", + "humanize": false, + "keyValue": "Data!C4", + "chartId": "aaaaaaaa-0000-0000-0000-000000000004" + }, + "offset": { + "x": 829, + "y": 16 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000010", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Template" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["template_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["template_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "aaaaaaaa-0000-0000-0000-000000000010", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 16, + "y": 172 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000011", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Start Month" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["date_start:month"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "line" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["date_start:month"], + "orderBy": [] + }, + "type": "odoo_line", + "dataSets": [{}], + "chartId": "aaaaaaaa-0000-0000-0000-000000000011", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 558, + "y": 172 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000012", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Product Category" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["categ_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["categ_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "aaaaaaaa-0000-0000-0000-000000000012", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 16, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000013", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Sales Team" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["crm_team_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["crm_team_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "aaaaaaaa-0000-0000-0000-000000000013", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 558, + "y": 552 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000014", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "MRR by Salesperson" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["user_id"], + "measure": "recurring_monthly", + "order": null, + "resModel": "sale.subscription.report", + "mode": "pie" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["user_id"], + "orderBy": [] + }, + "type": "odoo_pie", + "dataSets": [{}], + "chartId": "aaaaaaaa-0000-0000-0000-000000000014", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + } + }, + "offset": { + "x": 16, + "y": 932 + }, + "col": 0, + "row": 0 + }, + { + "id": "aaaaaaaa-0000-0000-0000-000000000015", + "width": 526, + "height": 360, + "tag": "chart", + "data": { + "title": { + "text": "Subscriptions by Stage" + }, + "background": "#FFFFFF", + "legendPosition": "top", + "metaData": { + "groupBy": ["stage_id"], + "measure": "__count", + "order": null, + "resModel": "sale.subscription.report", + "mode": "bar" + }, + "searchParams": { + "comparison": null, + "context": {}, + "domain": [["in_progress", "=", true]], + "groupBy": ["stage_id"], + "orderBy": [] + }, + "type": "odoo_bar", + "dataSets": [{}], + "chartId": "aaaaaaaa-0000-0000-0000-000000000015", + "actionXmlId": "subscription_oca.sale_subscription_report_action", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "stacked": false + }, + "offset": { + "x": 558, + "y": 932 + }, + "col": 0, + "row": 0 + } + ], + "tables": [], + "areGridLinesVisible": false, + "isVisible": true, + "headerGroups": [], + "comments": {} + }, + { + "id": "data", + "name": "Data", + "colNumber": 8, + "rowNumber": 30, + "rows": {}, + "cols": {}, + "merges": [], + "cells": { + "A1": "=_t(\"Subscription KPIs\")", + "B1": "=PIVOT.VALUE(1,\"recurring_monthly:sum\")", + "B2": "=PIVOT.VALUE(1,\"recurring_yearly:sum\")", + "B3": "=PIVOT.VALUE(1,\"__count\")", + "B4": "=IFERROR(B1/B3,0)", + "C1": "=IFERROR(FORMAT.LARGE.NUMBER(B1))", + "C2": "=IFERROR(FORMAT.LARGE.NUMBER(B2))", + "C3": "=IFERROR(FORMAT.LARGE.NUMBER(B3))", + "C4": "=IFERROR(FORMAT.LARGE.NUMBER(B4))" + }, + "styles": {}, + "formats": {}, + "borders": {}, + "conditionalFormats": [], + "dataValidationRules": [], + "figures": [], + "tables": [], + "areGridLinesVisible": true, + "isVisible": false, + "headerGroups": [], + "comments": {} + } + ], + "styles": {}, + "formats": {}, + "borders": {}, + "revisionId": "START_REVISION", + "uniqueFigureIds": true, + "settings": { + "locale": { + "name": "English (US)", + "code": "en_US", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "dateFormat": "m/d/yyyy", + "timeFormat": "hh:mm:ss a", + "formulaArgSeparator": ",", + "weekStart": 7 + } + }, + "pivots": { + "1": { + "type": "ODOO", + "fieldMatching": { + "11111111-1111-1111-1111-111111111111": { + "chain": "user_id", + "type": "many2one" + }, + "22222222-2222-2222-2222-222222222222": { + "chain": "crm_team_id", + "type": "many2one" + }, + "33333333-3333-3333-3333-333333333333": { + "chain": "partner_id", + "type": "many2one" + }, + "44444444-4444-4444-4444-444444444444": { + "chain": "template_id", + "type": "many2one" + }, + "55555555-5555-5555-5555-555555555555": { + "chain": "date_start", + "type": "datetime", + "offset": 0 + } + }, + "context": {}, + "domain": [["in_progress", "=", true]], + "id": "1", + "measures": [ + { + "id": "recurring_monthly:sum", + "fieldName": "recurring_monthly", + "aggregator": "sum" + }, + { + "id": "recurring_yearly:sum", + "fieldName": "recurring_yearly", + "aggregator": "sum" + }, + { + "id": "__count", + "fieldName": "__count" + } + ], + "model": "sale.subscription.report", + "name": "KPIs 1", + "sortedColumn": null, + "formulaId": "1", + "columns": [], + "rows": [] + } + }, + "pivotNextId": 2, + "customTableStyles": {}, + "globalFilters": [ + { + "id": "55555555-5555-5555-5555-555555555555", + "type": "date", + "label": "Period", + "defaultValue": "last_90_days" + }, + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "relation", + "label": "Salesperson", + "defaultValueDisplayNames": [], + "modelName": "res.users" + }, + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "relation", + "label": "Sales Team", + "defaultValueDisplayNames": [], + "modelName": "crm.team" + }, + { + "id": "33333333-3333-3333-3333-333333333333", + "type": "relation", + "label": "Customer", + "defaultValueDisplayNames": [], + "modelName": "res.partner" + }, + { + "id": "44444444-4444-4444-4444-444444444444", + "type": "relation", + "label": "Template", + "defaultValueDisplayNames": [], + "modelName": "sale.subscription.template" + } + ], + "lists": {}, + "listNextId": 1, + "chartOdooMenusReferences": {} +} diff --git a/spreadsheet_dashboard_subscription_oca/data/spreadsheet_dashboards.xml b/spreadsheet_dashboard_subscription_oca/data/spreadsheet_dashboards.xml new file mode 100644 index 0000000000..4bad0df6a6 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/data/spreadsheet_dashboards.xml @@ -0,0 +1,114 @@ + + + + Subscriptions + 150 + + + + Subscriptions + + + + + 10 + True + + + + Salesperson + + + + + 20 + True + + + + MRR Evolution + + + + + 30 + True + + + + Retention + + + + + 40 + True + + diff --git a/spreadsheet_dashboard_subscription_oca/i18n/es.po b/spreadsheet_dashboard_subscription_oca/i18n/es.po new file mode 100644 index 0000000000..7fc246f41e --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/i18n/es.po @@ -0,0 +1,429 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spreadsheet_dashboard_subscription_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-22 17:34+0000\n" +"PO-Revision-Date: 2026-06-22 17:34+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: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "ARR" +msgstr "ARR" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Active Subscriptions" +msgstr "Suscripciones activas" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Annual Recurring Revenue" +msgstr "Ingresos recurrentes anuales" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Avg MRR / Subscription" +msgstr "MRR medio / suscripción" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields.selection,name:spreadsheet_dashboard_subscription_oca.selection__sale_subscription_mrr_report__event_type__churn +msgid "Churn" +msgstr "Baja" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "Churned MRR (all time)" +msgstr "MRR perdido (histórico)" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__cohort_date +msgid "Cohort (start month)" +msgstr "Cohorte (mes de inicio)" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "Cohort MRR by Template" +msgstr "MRR de cohorte por plantilla" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__user_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__user_id +msgid "Commercial agent" +msgstr "Comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__company_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__company_id +msgid "Company" +msgstr "Compañía" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__company_currency_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__company_currency_id +msgid "Company Currency" +msgstr "Divisa de la compañía" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__partner_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__partner_id +msgid "Customer" +msgstr "Cliente" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__display_name +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__event_date +msgid "Event date" +msgstr "Fecha del evento" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__event_type +msgid "Event type" +msgstr "Tipo de evento" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__id +msgid "ID" +msgstr "ID" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "MRR Change by Month & Type" +msgstr "Variación de MRR por mes y tipo" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "MRR Change by Type" +msgstr "Variación de MRR por tipo" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:spreadsheet.dashboard,name:spreadsheet_dashboard_subscription_oca.spreadsheet_dashboard_subscription_oca_mrr_evolution +msgid "MRR Evolution" +msgstr "Evolución del MRR" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "MRR KPIs" +msgstr "KPIs de MRR" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "MRR at Start" +msgstr "MRR inicial" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "MRR by Cohort (start month)" +msgstr "MRR por cohorte (mes de inicio)" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "MRR by Product Category" +msgstr "MRR por categoría de producto" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "MRR by Sales Team" +msgstr "MRR por equipo de ventas" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "MRR by Salesperson" +msgstr "MRR por comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +msgid "MRR by Salesperson & Template" +msgstr "MRR por comercial y plantilla" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "MRR by Start Month" +msgstr "MRR por mes de inicio" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "MRR by Template" +msgstr "MRR por plantilla" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__mrr_change +msgid "MRR change" +msgstr "Variación de MRR" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +msgid "MRR share by Salesperson" +msgstr "Reparto de MRR por comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Monthly Recurring Revenue" +msgstr "Ingresos recurrentes mensuales" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__recurring_monthly +msgid "Monthly recurring revenue" +msgstr "Ingresos recurrentes mensuales" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__period_index +msgid "Months since start" +msgstr "Meses desde el inicio" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "Net MRR" +msgstr "MRR neto" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "Net MRR Over Time" +msgstr "MRR neto a lo largo del tiempo" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields.selection,name:spreadsheet_dashboard_subscription_oca.selection__sale_subscription_mrr_report__event_type__new +msgid "New" +msgstr "Alta" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "New MRR (all time)" +msgstr "MRR nuevo (histórico)" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "New MRR by Salesperson" +msgstr "MRR nuevo por comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +msgid "New MRR by Template" +msgstr "MRR nuevo por plantilla" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:spreadsheet.dashboard,name:spreadsheet_dashboard_subscription_oca.spreadsheet_dashboard_subscription_oca_retention +msgid "Retention" +msgstr "Retención" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "Retention Curve (active subscriptions by month since start)" +msgstr "Curva de retención (suscripciones activas por mes desde el inicio)" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "Retention KPIs" +msgstr "KPIs de retención" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__crm_team_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__crm_team_id +msgid "Sale team" +msgstr "Equipo de ventas" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Sales Team" +msgstr "Equipo de ventas" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: model:spreadsheet.dashboard,name:spreadsheet_dashboard_subscription_oca.spreadsheet_dashboard_subscription_oca_salesperson +msgid "Salesperson" +msgstr "Comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +msgid "Salesperson KPIs" +msgstr "KPIs de comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__subscription_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__subscription_id +msgid "Subscription" +msgstr "Suscripción" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Subscription KPIs" +msgstr "KPIs de suscripciones" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model,name:spreadsheet_dashboard_subscription_oca.model_sale_subscription_mrr_report +msgid "Subscription MRR Evolution" +msgstr "Evolución del MRR de suscripciones" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model,name:spreadsheet_dashboard_subscription_oca.model_sale_subscription_retention_report +msgid "Subscription Retention" +msgstr "Retención de suscripciones" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_mrr_report__template_id +#: model:ir.model.fields,field_description:spreadsheet_dashboard_subscription_oca.field_sale_subscription_retention_report__template_id +msgid "Subscription template" +msgstr "Plantilla de suscripción" + +#. module: spreadsheet_dashboard_subscription_oca +#: model:spreadsheet.dashboard,name:spreadsheet_dashboard_subscription_oca.spreadsheet_dashboard_subscription_oca_subscriptions +#: model:spreadsheet.dashboard.group,name:spreadsheet_dashboard_subscription_oca.spreadsheet_dashboard_group_subscription_oca +msgid "Subscriptions" +msgstr "Suscripciones" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "Subscriptions (cohorts)" +msgstr "Suscripciones (cohortes)" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +msgid "Subscriptions by Cohort (start month)" +msgstr "Suscripciones por cohorte (mes de inicio)" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +msgid "Subscriptions by Sales Team" +msgstr "Suscripciones por equipo de ventas" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +msgid "Subscriptions by Salesperson" +msgstr "Suscripciones por comercial" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Subscriptions by Stage" +msgstr "Suscripciones por etapa" + +#. module: spreadsheet_dashboard_subscription_oca +#. odoo-javascript +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/Domatix/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/mrr_evolution_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/retention_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/salesperson_dashboard.osheet.json:0 +#: code:addons/spreadsheet_dashboard_subscription_oca/data/files/subscriptions_dashboard.osheet.json:0 +msgid "Template" +msgstr "Plantilla" diff --git a/spreadsheet_dashboard_subscription_oca/models/__init__.py b/spreadsheet_dashboard_subscription_oca/models/__init__.py new file mode 100644 index 0000000000..115ccbd74e --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import sale_subscription_mrr_report +from . import sale_subscription_retention_report diff --git a/spreadsheet_dashboard_subscription_oca/models/sale_subscription_mrr_report.py b/spreadsheet_dashboard_subscription_oca/models/sale_subscription_mrr_report.py new file mode 100644 index 0000000000..39103a2e3e --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/models/sale_subscription_mrr_report.py @@ -0,0 +1,91 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models, tools + + +class SaleSubscriptionMrrReport(models.Model): + """MRR change events derived from the current state of the subscriptions. + + There is no stored MRR event log in ``subscription_oca``. This SQL view + reconstructs an approximate one from what we do have: a positive ``new`` + event at the start date of every subscription and a negative ``churn`` + event at the closing date of the closed ones (the end date when set, + otherwise the last write date as a fallback). Cumulating ``mrr_change`` + over time therefore yields the net active MRR. Mid-life expansion and + contraction are not tracked (that would require a real event log). + """ + + _name = "sale.subscription.mrr.report" + _description = "Subscription MRR Evolution" + _auto = False + _rec_name = "subscription_id" + _order = "event_date desc" + + subscription_id = fields.Many2one( + comodel_name="sale.subscription", string="Subscription", readonly=True + ) + partner_id = fields.Many2one( + comodel_name="res.partner", string="Customer", readonly=True + ) + template_id = fields.Many2one( + comodel_name="sale.subscription.template", + string="Subscription template", + readonly=True, + ) + user_id = fields.Many2one( + comodel_name="res.users", string="Commercial agent", readonly=True + ) + crm_team_id = fields.Many2one( + comodel_name="crm.team", string="Sale team", readonly=True + ) + company_id = fields.Many2one( + comodel_name="res.company", string="Company", readonly=True + ) + company_currency_id = fields.Many2one( + comodel_name="res.currency", string="Company Currency", readonly=True + ) + event_date = fields.Date(string="Event date", readonly=True) + event_type = fields.Selection( + [("new", "New"), ("churn", "Churn")], + string="Event type", + readonly=True, + ) + mrr_change = fields.Monetary( + string="MRR change", + currency_field="company_currency_id", + readonly=True, + ) + + def _query(self): + return """ + SELECT + (sub.id * 2 + ev.n) AS id, + sub.id AS subscription_id, + sub.partner_id AS partner_id, + sub.template_id AS template_id, + sub.user_id AS user_id, + sub.crm_team_id AS crm_team_id, + sub.company_id AS company_id, + sub.company_currency_id AS company_currency_id, + ev.event_date AS event_date, + ev.event_type AS event_type, + ev.mrr_change AS mrr_change + FROM sale_subscription sub + LEFT JOIN sale_subscription_stage stage + ON stage.id = sub.stage_id + CROSS JOIN LATERAL ( + VALUES + (0, sub.date_start, 'new', sub.recurring_monthly), + (1, COALESCE(sub.date, sub.write_date::date), 'churn', + -sub.recurring_monthly) + ) AS ev(n, event_date, event_type, mrr_change) + WHERE sub.active = TRUE + AND ev.event_date IS NOT NULL + AND (ev.event_type = 'new' OR stage.type = 'post') + """ + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute( + f"CREATE OR REPLACE VIEW {self._table} AS ({self._query()})" + ) diff --git a/spreadsheet_dashboard_subscription_oca/models/sale_subscription_retention_report.py b/spreadsheet_dashboard_subscription_oca/models/sale_subscription_retention_report.py new file mode 100644 index 0000000000..90d19341dd --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/models/sale_subscription_retention_report.py @@ -0,0 +1,96 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models, tools + + +class SaleSubscriptionRetentionReport(models.Model): + """Retention cohorts derived from the current state of the subscriptions. + + Each subscription is expanded into one row per elapsed month between its + start date and either its closing date (for churned ones) or today (for + the ones still running). Grouping by ``cohort_date`` (the start month) and + ``period_index`` (months since the start) gives a retention/survival matrix + without needing a stored event log. + """ + + _name = "sale.subscription.retention.report" + _description = "Subscription Retention" + _auto = False + _rec_name = "subscription_id" + _order = "cohort_date desc, period_index" + + subscription_id = fields.Many2one( + comodel_name="sale.subscription", string="Subscription", readonly=True + ) + partner_id = fields.Many2one( + comodel_name="res.partner", string="Customer", readonly=True + ) + template_id = fields.Many2one( + comodel_name="sale.subscription.template", + string="Subscription template", + readonly=True, + ) + user_id = fields.Many2one( + comodel_name="res.users", string="Commercial agent", readonly=True + ) + crm_team_id = fields.Many2one( + comodel_name="crm.team", string="Sale team", readonly=True + ) + company_id = fields.Many2one( + comodel_name="res.company", string="Company", readonly=True + ) + company_currency_id = fields.Many2one( + comodel_name="res.currency", string="Company Currency", readonly=True + ) + cohort_date = fields.Date(string="Cohort (start month)", readonly=True) + period_index = fields.Integer(string="Months since start", readonly=True) + recurring_monthly = fields.Monetary( + string="Monthly recurring revenue", + currency_field="company_currency_id", + readonly=True, + ) + + def _query(self): + return """ + SELECT + (sub.id * 1000 + gs.idx) AS id, + sub.id AS subscription_id, + sub.partner_id AS partner_id, + sub.template_id AS template_id, + sub.user_id AS user_id, + sub.crm_team_id AS crm_team_id, + sub.company_id AS company_id, + sub.company_currency_id AS company_currency_id, + date_trunc('month', sub.date_start)::date AS cohort_date, + gs.idx AS period_index, + sub.recurring_monthly AS recurring_monthly + FROM sale_subscription sub + LEFT JOIN sale_subscription_stage stage + ON stage.id = sub.stage_id + CROSS JOIN LATERAL ( + SELECT CASE + WHEN stage.type = 'post' + THEN COALESCE(sub.date, sub.write_date::date) + ELSE CURRENT_DATE + END AS d + ) AS ref + CROSS JOIN LATERAL generate_series( + 0, + LEAST( + GREATEST(0, ( + EXTRACT(YEAR FROM age(ref.d, sub.date_start)) * 12 + + EXTRACT(MONTH FROM age(ref.d, sub.date_start)) + )::int), + 60 + ) + ) AS gs(idx) + WHERE sub.active = TRUE + AND sub.date_start IS NOT NULL + AND sub.date_start <= CURRENT_DATE + """ + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute( + f"CREATE OR REPLACE VIEW {self._table} AS ({self._query()})" + ) diff --git a/spreadsheet_dashboard_subscription_oca/pyproject.toml b/spreadsheet_dashboard_subscription_oca/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spreadsheet_dashboard_subscription_oca/readme/DESCRIPTION.md b/spreadsheet_dashboard_subscription_oca/readme/DESCRIPTION.md new file mode 100644 index 0000000000..3dd1d979d0 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/readme/DESCRIPTION.md @@ -0,0 +1,22 @@ +This module adds spreadsheet dashboards for subscriptions, built on top of the +community ``spreadsheet_dashboard`` engine and the analysis models provided by +``subscription_oca``. No enterprise dependency is required. + +It ships four ready-to-use dashboards under the **Dashboards** application, in a +dedicated *Subscriptions* group: + +* **Subscriptions**: monthly/annual recurring revenue, active subscriptions and + average MRR, with breakdowns by template, product category, sales team, + salesperson, start month and stage. +* **Salesperson**: recurring revenue and number of subscriptions broken down by + salesperson and sales team. +* **MRR Evolution**: net/new/churned MRR and the cumulated MRR over time, with a + monthly new-vs-churn breakdown. MRR change events are derived from the current + state of the subscriptions (a positive event at the start date and a negative + one at the closing date); mid-life expansion/contraction is not tracked, which + would require a dedicated MRR event log. +* **Retention**: cohort sizes and recurring revenue per start month, plus a + retention/survival curve built from the start and closing dates. + +All dashboards expose global filters (salesperson, sales team, customer, +template) and refresh automatically from the subscription analysis models. diff --git a/spreadsheet_dashboard_subscription_oca/readme/USAGE.md b/spreadsheet_dashboard_subscription_oca/readme/USAGE.md new file mode 100644 index 0000000000..b8746d1551 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/readme/USAGE.md @@ -0,0 +1,6 @@ +Open the **Dashboards** application. The four dashboards are available in the +*Subscriptions* group. Use the filters at the top (salesperson, sales team, +customer, template) to narrow down the figures. The data is aggregated from the +``sale.subscription.report``, ``sale.subscription.mrr.report`` and +``sale.subscription.retention.report`` analysis models and refreshes +automatically. diff --git a/spreadsheet_dashboard_subscription_oca/report/sql_report_views.xml b/spreadsheet_dashboard_subscription_oca/report/sql_report_views.xml new file mode 100644 index 0000000000..5be5f5b15a --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/report/sql_report_views.xml @@ -0,0 +1,249 @@ + + + + + sale.subscription.mrr.report.search + sale.subscription.mrr.report + + + + + + + + + + + + + + + + + + + + + + + sale.subscription.mrr.report.graph + sale.subscription.mrr.report + + + + + + + + + + sale.subscription.mrr.report.pivot + sale.subscription.mrr.report + + + + + + + + + + + sale.subscription.mrr.report.list + sale.subscription.mrr.report + + + + + + + + + + + + + + + + + + MRR Evolution + sale.subscription.mrr.report + graph,pivot,list + + {"search_default_groupby_event_date": 1} + +

    + No MRR events yet +

    +

    + Reconstruction of the monthly recurring revenue changes over + time: a "new" event when a subscription starts and a "churn" + event when one is closed. Cumulate it to see the net active MRR. +

    +
    +
    + + + + sale.subscription.retention.report.search + sale.subscription.retention.report + + + + + + + + + + + + + + + + + + + sale.subscription.retention.report.graph + sale.subscription.retention.report + + + + + + + + + + sale.subscription.retention.report.pivot + sale.subscription.retention.report + + + + + + + + + + + sale.subscription.retention.report.list + sale.subscription.retention.report + + + + + + + + + + + + + + + + + + Retention Analysis + sale.subscription.retention.report + pivot,graph,list + + {"search_default_groupby_cohort_date": 1} + +

    + No cohorts yet +

    +

    + Retention cohorts built from the current subscriptions. Each + cohort groups the subscriptions that started the same month and + tracks the still-active recurring revenue over the following + months. +

    +
    +
    + + + + + +
    diff --git a/spreadsheet_dashboard_subscription_oca/security/ir.model.access.csv b/spreadsheet_dashboard_subscription_oca/security/ir.model.access.csv new file mode 100644 index 0000000000..1c9192e8db --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_subscription_mrr_report,sale.subscription.mrr.report,model_sale_subscription_mrr_report,sales_team.group_sale_salesman,1,0,0,0 +access_sale_subscription_retention_report,sale.subscription.retention.report,model_sale_subscription_retention_report,sales_team.group_sale_salesman,1,0,0,0 diff --git a/spreadsheet_dashboard_subscription_oca/static/description/index.html b/spreadsheet_dashboard_subscription_oca/static/description/index.html new file mode 100644 index 0000000000..6de5fa05d9 --- /dev/null +++ b/spreadsheet_dashboard_subscription_oca/static/description/index.html @@ -0,0 +1,457 @@ + + + + + +README.rst + + + +
    + + + +Odoo Community Association + +
    +

    Subscription OCA Spreadsheet Dashboards

    + +

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

    +

    This module adds spreadsheet dashboards for subscriptions, built on top +of the community spreadsheet_dashboard engine and the analysis +models provided by subscription_oca. No enterprise dependency is +required.

    +

    It ships four ready-to-use dashboards under the Dashboards +application, in a dedicated Subscriptions group:

    +
      +
    • Subscriptions: monthly/annual recurring revenue, active +subscriptions and average MRR, with breakdowns by template, product +category, sales team, salesperson, start month and stage.
    • +
    • Salesperson: recurring revenue and number of subscriptions broken +down by salesperson and sales team.
    • +
    • MRR Evolution: net/new/churned MRR and the cumulated MRR over +time, with a monthly new-vs-churn breakdown. MRR change events are +derived from the current state of the subscriptions (a positive event +at the start date and a negative one at the closing date); mid-life +expansion/contraction is not tracked, which would require a dedicated +MRR event log.
    • +
    • Retention: cohort sizes and recurring revenue per start month, +plus a retention/survival curve built from the start and closing +dates.
    • +
    +

    All dashboards expose global filters (salesperson, sales team, customer, +template) and refresh automatically from the subscription analysis +models.

    +

    Table of contents

    + +
    +

    Usage

    +

    Open the Dashboards application. The four dashboards are available +in the Subscriptions group. Use the filters at the top (salesperson, +sales team, customer, template) to narrow down the figures. The data is +aggregated from the sale.subscription.report, +sale.subscription.mrr.report and +sale.subscription.retention.report analysis models and refreshes +automatically.

    +
    +
    +

    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

    +
      +
    • Domatix
    • +
    +
    +
    +

    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.

    +

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

    +

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

    +
    +
    +
    +
    + +