Skip to content

Commit a6a6a13

Browse files
authored
Merge pull request #43 from OpenSPP/feat/statistics-system
feat: add metrics core and statistics system
2 parents 1aed869 + f1740c6 commit a6a6a13

31 files changed

Lines changed: 2029 additions & 0 deletions

spp_metrics_core/README.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# OpenSPP Metrics Core
2+
3+
Shared foundation for all metrics (statistics, simulation metrics, etc.) in OpenSPP.
4+
5+
## Overview
6+
7+
`spp_metrics_core` provides the base model and categorization system that eliminates
8+
duplication of genuinely shared fields across different metric types. All domain modules
9+
(statistics, simulations, dashboards) inherit from the base model and add their own
10+
computation-specific fields.
11+
12+
## Architecture
13+
14+
```
15+
spp.metric.base (AbstractModel)
16+
17+
├── spp.statistic (extends with publication flags)
18+
├── spp.simulation.metric (extends with scenario-specific fields)
19+
└── [Your custom metric models]
20+
```
21+
22+
## Models
23+
24+
### spp.metric.base
25+
26+
Abstract model providing genuinely shared fields for all metric types.
27+
28+
Concrete models define their own computation-specific fields (metric_type, format,
29+
expressions, etc.) since these vary incompatibly between metric types.
30+
31+
**Identity**
32+
33+
- `name` - Technical identifier (e.g., 'children_under_5')
34+
- `label` - Human-readable display label (translated)
35+
- `description` - Detailed description (translated)
36+
37+
**Presentation**
38+
39+
- `unit` - Unit of measurement (e.g., 'people', 'USD', '%')
40+
- `decimal_places` - Decimal precision for display
41+
42+
**Categorization**
43+
44+
- `category_id` - Many2one to `spp.metric.category`
45+
46+
**Metadata**
47+
48+
- `sequence` - Display order within category
49+
- `active` - Inactive metrics are hidden
50+
51+
**What's NOT in the base** (defined by concrete models):
52+
53+
- `metric_type` / `format` - Each concrete model defines its own selections
54+
- `cel_expression` / `variable_id` - Computation approaches vary by type
55+
- `aggregation` - Only relevant for certain metric types
56+
57+
### spp.metric.category
58+
59+
Shared categorization for all metric types:
60+
61+
- `name` - Category name (e.g., "Population")
62+
- `code` - Technical code (e.g., "population")
63+
- `description` - Category description
64+
- `sequence` - Display order
65+
- `parent_id` - Optional parent category for hierarchical organization
66+
67+
## Default Categories
68+
69+
- **Population** - Population counts and demographics
70+
- **Coverage** - Program coverage and reach metrics
71+
- **Targeting** - Targeting accuracy and fairness metrics
72+
- **Distribution** - Distribution and entitlement metrics
73+
74+
## Defining Metrics
75+
76+
Since `spp.metric.base` is an **AbstractModel**, it does not store data directly. Domain
77+
modules define concrete metrics by inheriting from the base:
78+
79+
- `spp_statistic` - Defines published statistics
80+
- `spp_simulation` - Defines simulation metrics
81+
- Custom modules - Define domain-specific metrics
82+
83+
See the [Usage](#usage) section below for examples.
84+
85+
## Usage
86+
87+
### Creating Custom Metrics
88+
89+
Inherit from `spp.metric.base` to create domain-specific metrics:
90+
91+
```python
92+
class CustomMetric(models.Model):
93+
_name = "custom.metric"
94+
_inherit = ["spp.metric.base"]
95+
_description = "Custom Metric Type"
96+
97+
# Shared fields inherited from base:
98+
# - name, label, description
99+
# - unit, decimal_places
100+
# - category_id, sequence, active
101+
102+
# Define your computation-specific fields
103+
metric_type = fields.Selection([...]) # Your type selections
104+
computation_field = fields.Text() # Your computation approach
105+
106+
# Add domain-specific fields
107+
custom_field = fields.Boolean()
108+
custom_config = fields.Text()
109+
```
110+
111+
### Using Categories
112+
113+
Reference categories in your metrics:
114+
115+
```xml
116+
<record id="my_custom_metric" model="custom.metric">
117+
<field name="name">my_metric</field>
118+
<field name="label">My Custom Metric</field>
119+
<field name="category_id" ref="spp_metrics_core.category_population" />
120+
</record>
121+
```
122+
123+
### Creating Custom Categories
124+
125+
Add domain-specific categories:
126+
127+
```xml
128+
<record id="category_health" model="spp.metric.category">
129+
<field name="name">Health</field>
130+
<field name="code">health</field>
131+
<field name="description">Health-related metrics</field>
132+
<field name="sequence">50</field>
133+
</record>
134+
```
135+
136+
## Migration
137+
138+
### From spp_statistic.category
139+
140+
The migration automatically renames `spp.statistic.category` to `spp.metric.category`
141+
while preserving all data and external references.
142+
143+
**Before**:
144+
145+
```python
146+
category = env['spp.statistic.category'].search([...])
147+
```
148+
149+
**After**:
150+
151+
```python
152+
category = env['spp.metric.category'].search([...])
153+
```
154+
155+
See [Migration Guide](../../docs/migration/statistics-refactoring.md) for details.
156+
157+
## Benefits
158+
159+
1. **No Duplication**: Genuinely shared fields defined once, reused everywhere
160+
2. **Model-Specific Freedom**: Each concrete model defines its own computation fields
161+
without conflicts
162+
3. **Consistent UI**: Common fields (name, label, category) display the same way
163+
4. **Shared Categories**: One categorization system for all metrics
164+
5. **Future-Proof**: New metric types easily add their own computation approaches
165+
166+
## Dependencies
167+
168+
- `base` - Odoo core
169+
170+
## Used By
171+
172+
- `spp_metrics_services` - Aggregation and computation services
173+
- `spp_statistic` - Published statistics
174+
- `spp_simulation` - Simulation metrics
175+
- Domain modules with custom metrics
176+
177+
## Architecture Documentation
178+
179+
See [Statistics System Architecture](../../docs/architecture/statistics-systems.md) for
180+
the complete system design.
181+
182+
## License
183+
184+
LGPL-3

spp_metrics_core/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
from . import models

spp_metrics_core/__manifest__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
{
3+
"name": "OpenSPP Metrics Core",
4+
"summary": "Unified metric foundation for statistics and simulations",
5+
"category": "OpenSPP",
6+
"version": "19.0.2.0.0",
7+
"sequence": 1,
8+
"author": "OpenSPP.org",
9+
"website": "https://github.com/OpenSPP/OpenSPP2",
10+
"license": "LGPL-3",
11+
"development_status": "Alpha",
12+
"maintainers": ["jeremi", "gonzalesedwin1123"],
13+
"depends": [
14+
"base",
15+
],
16+
"data": [
17+
"security/ir.model.access.csv",
18+
"data/metric_categories.xml",
19+
],
20+
"assets": {},
21+
"demo": [],
22+
"images": [],
23+
"application": False,
24+
"installable": True,
25+
"auto_install": False,
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!--
3+
Part of OpenSPP. See LICENSE file for full copyright and licensing details.
4+
-->
5+
<odoo>
6+
<!-- Default Metric Categories -->
7+
8+
<record id="category_population" model="spp.metric.category">
9+
<field name="name">Population</field>
10+
<field name="code">population</field>
11+
<field name="description">Population counts and demographics</field>
12+
<field name="sequence">10</field>
13+
</record>
14+
15+
<record id="category_coverage" model="spp.metric.category">
16+
<field name="name">Coverage</field>
17+
<field name="code">coverage</field>
18+
<field name="description">Program coverage and reach metrics</field>
19+
<field name="sequence">20</field>
20+
</record>
21+
22+
<record id="category_targeting" model="spp.metric.category">
23+
<field name="name">Targeting</field>
24+
<field name="code">targeting</field>
25+
<field name="description">Targeting accuracy and fairness metrics</field>
26+
<field name="sequence">30</field>
27+
</record>
28+
29+
<record id="category_distribution" model="spp.metric.category">
30+
<field name="name">Distribution</field>
31+
<field name="code">distribution</field>
32+
<field name="description">Distribution and entitlement metrics</field>
33+
<field name="sequence">40</field>
34+
</record>
35+
</odoo>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Migrate spp.statistic.category to spp.metric.category."""
3+
4+
import logging
5+
6+
_logger = logging.getLogger(__name__)
7+
8+
9+
def migrate(cr, version):
10+
"""Migrate spp.statistic.category to spp.metric.category.
11+
12+
Renames the table and sequence if they exist. This allows existing
13+
statistic categories to be used as metric categories without data loss.
14+
"""
15+
_logger.info("Starting migration: spp.statistic.category -> spp.metric.category")
16+
17+
# Check if old table exists
18+
cr.execute(
19+
"""
20+
SELECT EXISTS (
21+
SELECT FROM information_schema.tables
22+
WHERE table_schema = 'public'
23+
AND table_name = 'spp_statistic_category'
24+
)
25+
"""
26+
)
27+
table_exists = cr.fetchone()[0]
28+
29+
if table_exists:
30+
_logger.info("Found spp_statistic_category table, renaming to spp_metric_category")
31+
32+
# Rename table
33+
cr.execute(
34+
"""
35+
ALTER TABLE spp_statistic_category
36+
RENAME TO spp_metric_category
37+
"""
38+
)
39+
40+
# Check if old sequence exists
41+
cr.execute(
42+
"""
43+
SELECT EXISTS (
44+
SELECT FROM pg_class
45+
WHERE relname = 'spp_statistic_category_id_seq'
46+
AND relkind = 'S'
47+
)
48+
"""
49+
)
50+
seq_exists = cr.fetchone()[0]
51+
52+
if seq_exists:
53+
_logger.info("Found spp_statistic_category_id_seq sequence, renaming to spp_metric_category_id_seq")
54+
55+
# Rename sequence
56+
cr.execute(
57+
"""
58+
ALTER SEQUENCE spp_statistic_category_id_seq
59+
RENAME TO spp_metric_category_id_seq
60+
"""
61+
)
62+
63+
_logger.info("Successfully migrated spp.statistic.category to spp.metric.category")
64+
else:
65+
_logger.info("No spp_statistic_category table found, skipping migration")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
from . import metric_base
4+
from . import metric_category

0 commit comments

Comments
 (0)