Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions spp_metrics_core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# OpenSPP Metrics Core

Shared foundation for all metrics (statistics, simulation metrics, etc.) in OpenSPP.

## Overview

`spp_metrics_core` provides the base model and categorization system that eliminates
duplication of genuinely shared fields across different metric types. All domain modules
(statistics, simulations, dashboards) inherit from the base model and add their own
computation-specific fields.

## Architecture

```
spp.metric.base (AbstractModel)
├── spp.statistic (extends with publication flags)
├── spp.simulation.metric (extends with scenario-specific fields)
└── [Your custom metric models]
```

## Models

### spp.metric.base

Abstract model providing genuinely shared fields for all metric types.

Concrete models define their own computation-specific fields (metric_type, format,
expressions, etc.) since these vary incompatibly between metric types.

**Identity**

- `name` - Technical identifier (e.g., 'children_under_5')
- `label` - Human-readable display label (translated)
- `description` - Detailed description (translated)

**Presentation**

- `unit` - Unit of measurement (e.g., 'people', 'USD', '%')
- `decimal_places` - Decimal precision for display

**Categorization**

- `category_id` - Many2one to `spp.metric.category`

**Metadata**

- `sequence` - Display order within category
- `active` - Inactive metrics are hidden

**What's NOT in the base** (defined by concrete models):

- `metric_type` / `format` - Each concrete model defines its own selections
- `cel_expression` / `variable_id` - Computation approaches vary by type
- `aggregation` - Only relevant for certain metric types

### spp.metric.category

Shared categorization for all metric types:

- `name` - Category name (e.g., "Population")
- `code` - Technical code (e.g., "population")
- `description` - Category description
- `sequence` - Display order
- `parent_id` - Optional parent category for hierarchical organization

## Default Categories

- **Population** - Population counts and demographics
- **Coverage** - Program coverage and reach metrics
- **Targeting** - Targeting accuracy and fairness metrics
- **Distribution** - Distribution and entitlement metrics

## Defining Metrics

Since `spp.metric.base` is an **AbstractModel**, it does not store data directly. Domain
modules define concrete metrics by inheriting from the base:

- `spp_statistic` - Defines published statistics
- `spp_simulation` - Defines simulation metrics
- Custom modules - Define domain-specific metrics

See the [Usage](#usage) section below for examples.

## Usage

### Creating Custom Metrics

Inherit from `spp.metric.base` to create domain-specific metrics:

```python
class CustomMetric(models.Model):
_name = "custom.metric"
_inherit = ["spp.metric.base"]
_description = "Custom Metric Type"

# Shared fields inherited from base:
# - name, label, description
# - unit, decimal_places
# - category_id, sequence, active

# Define your computation-specific fields
metric_type = fields.Selection([...]) # Your type selections
computation_field = fields.Text() # Your computation approach

# Add domain-specific fields
custom_field = fields.Boolean()
custom_config = fields.Text()
```

### Using Categories

Reference categories in your metrics:

```xml
<record id="my_custom_metric" model="custom.metric">
<field name="name">my_metric</field>
<field name="label">My Custom Metric</field>
<field name="category_id" ref="spp_metrics_core.category_population" />
</record>
```

### Creating Custom Categories

Add domain-specific categories:

```xml
<record id="category_health" model="spp.metric.category">
<field name="name">Health</field>
<field name="code">health</field>
<field name="description">Health-related metrics</field>
<field name="sequence">50</field>
</record>
```

## Migration

### From spp_statistic.category

The migration automatically renames `spp.statistic.category` to `spp.metric.category`
while preserving all data and external references.

**Before**:

```python
category = env['spp.statistic.category'].search([...])
```

**After**:

```python
category = env['spp.metric.category'].search([...])
```

See [Migration Guide](../../docs/migration/statistics-refactoring.md) for details.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The relative links to documentation in this file (here and on line 177) might be broken depending on the repository structure or how the documentation is viewed. It would be beneficial to verify that these links resolve correctly to avoid confusion for developers.


## Benefits

1. **No Duplication**: Genuinely shared fields defined once, reused everywhere
2. **Model-Specific Freedom**: Each concrete model defines its own computation fields
without conflicts
3. **Consistent UI**: Common fields (name, label, category) display the same way
4. **Shared Categories**: One categorization system for all metrics
5. **Future-Proof**: New metric types easily add their own computation approaches

## Dependencies

- `base` - Odoo core

## Used By

- `spp_metrics_services` - Aggregation and computation services
- `spp_statistic` - Published statistics
- `spp_simulation` - Simulation metrics
- Domain modules with custom metrics

## Architecture Documentation

See [Statistics System Architecture](../../docs/architecture/statistics-systems.md) for
the complete system design.

## License

LGPL-3
3 changes: 3 additions & 0 deletions spp_metrics_core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.

from . import models
26 changes: 26 additions & 0 deletions spp_metrics_core/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
{
"name": "OpenSPP Metrics Core",
"summary": "Unified metric foundation for statistics and simulations",
"category": "OpenSPP",
"version": "19.0.2.0.0",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
"license": "LGPL-3",
"development_status": "Alpha",
"maintainers": ["jeremi", "gonzalesedwin1123"],
"depends": [
"base",
],
"data": [
"security/ir.model.access.csv",
"data/metric_categories.xml",
],
"assets": {},
"demo": [],
"images": [],
"application": False,
"installable": True,
"auto_install": False,
}
35 changes: 35 additions & 0 deletions spp_metrics_core/data/metric_categories.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Part of OpenSPP. See LICENSE file for full copyright and licensing details.
-->
<odoo>
<!-- Default Metric Categories -->

<record id="category_population" model="spp.metric.category">
<field name="name">Population</field>
<field name="code">population</field>
<field name="description">Population counts and demographics</field>
<field name="sequence">10</field>
</record>

<record id="category_coverage" model="spp.metric.category">
<field name="name">Coverage</field>
<field name="code">coverage</field>
<field name="description">Program coverage and reach metrics</field>
<field name="sequence">20</field>
</record>

<record id="category_targeting" model="spp.metric.category">
<field name="name">Targeting</field>
<field name="code">targeting</field>
<field name="description">Targeting accuracy and fairness metrics</field>
<field name="sequence">30</field>
</record>

<record id="category_distribution" model="spp.metric.category">
<field name="name">Distribution</field>
<field name="code">distribution</field>
<field name="description">Distribution and entitlement metrics</field>
<field name="sequence">40</field>
</record>
</odoo>
65 changes: 65 additions & 0 deletions spp_metrics_core/migrations/19.0.1.0.0/pre-migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Migrate spp.statistic.category to spp.metric.category."""

import logging

_logger = logging.getLogger(__name__)


def migrate(cr, version):
"""Migrate spp.statistic.category to spp.metric.category.

Renames the table and sequence if they exist. This allows existing
statistic categories to be used as metric categories without data loss.
"""
_logger.info("Starting migration: spp.statistic.category -> spp.metric.category")

# Check if old table exists
cr.execute(
"""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'spp_statistic_category'
)
"""
)
table_exists = cr.fetchone()[0]

if table_exists:
_logger.info("Found spp_statistic_category table, renaming to spp_metric_category")

# Rename table
cr.execute(
"""
ALTER TABLE spp_statistic_category
RENAME TO spp_metric_category
"""
)

# Check if old sequence exists
cr.execute(
"""
SELECT EXISTS (
SELECT FROM pg_class
WHERE relname = 'spp_statistic_category_id_seq'
AND relkind = 'S'
)
"""
)
seq_exists = cr.fetchone()[0]

if seq_exists:
_logger.info("Found spp_statistic_category_id_seq sequence, renaming to spp_metric_category_id_seq")

# Rename sequence
cr.execute(
"""
ALTER SEQUENCE spp_statistic_category_id_seq
RENAME TO spp_metric_category_id_seq
"""
)

_logger.info("Successfully migrated spp.statistic.category to spp.metric.category")
else:
_logger.info("No spp_statistic_category table found, skipping migration")
4 changes: 4 additions & 0 deletions spp_metrics_core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.

from . import metric_base
from . import metric_category
Loading
Loading