This page explains how data reaches the analytics module. Nine modules each keep a file analytics_facts.py that declares one or more fact sets. The analytics module emits DescribeAnalyticsFacts on the EventBus, collects the answers in src/cluster_erp/modules/analytics/registry.py, validates them, and builds the catalogue every analysis reads. No module imports the analytics module, and the analytics module imports no module (src/cluster_erp/core/contracts.py:2055-2057).
In short¶
- A fact set (
AnalyticsFactSet) names its module, a key, a label, its grain, the physical source table, its dimensions and measures, a read permission, and a live filter (src/cluster_erp/core/contracts.py:2126-2154). - Each contributing module subscribes a handler to
DescribeAnalyticsFactsthat returns its fact sets (src/cluster_erp/modules/fleet/module.py:338-346). registry.collectemits the event, type-checks and validates every answer, refuses duplicates, and returns aFactCatalogue(src/cluster_erp/modules/analytics/registry.py:242-283).- A caller sees only fact sets whose
read_permissionthey hold (src/cluster_erp/modules/analytics/registry.py:100-109).
How it works¶
Collection path¶
sequenceDiagram participant S as AnalyticsService participant R as registry.collect participant B as EventBus participant M as module analytics_facts.py S->>R: catalogue(permission_codes, locale) R->>B: emit DescribeAnalyticsFacts B->>M: describe_analytics_facts(event) M-->>B: AnalyticsFactSet or list B-->>R: results of all handlers R->>R: type check, validate_fact, duplicate key check R-->>S: FactCatalogue S->>S: visible_to(permission_codes)
In prose:
AnalyticsService.cataloguecallsregistry.collectwith the caller's locale (src/cluster_erp/modules/analytics/service.py:55-62).collectemitsDescribeAnalyticsFacts(source_module="analytics", locale=...). The locale only selects labels; keys stay language independent (src/cluster_erp/modules/analytics/registry.py:253,src/cluster_erp/core/contracts.py:2060-2073).- The EventBus calls every subscribed handler and returns their results. A handler that raises is logged and contributes nothing (
src/cluster_erp/core/event_bus.py:83-96). collectaccepts one fact set or a list per handler. Anything else fails withANALYTICS_INVALID_CONTRIBUTION(src/cluster_erp/modules/analytics/registry.py:254-266).validate_factchecks each set; a second set with the same key fails instead of overwriting (src/cluster_erp/modules/analytics/registry.py:267-277).visible_tokeeps the sets whoseread_permissionis among the caller's permission codes (src/cluster_erp/modules/analytics/registry.py:100-109).
What a contribution declares¶
| Part | Rule checked by the registry | Source |
|---|---|---|
key, label, grain |
key matches a simple pattern; label and grain are required | src/cluster_erp/modules/analytics/registry.py:183-190 |
source |
a table or schema-qualified relation name matching a strict pattern | src/cluster_erp/modules/analytics/registry.py:45, src/cluster_erp/modules/analytics/registry.py:191-192 |
read_permission |
required | src/cluster_erp/modules/analytics/registry.py:193-194 |
live_filter |
a column and is_null or is_false; the default is deleted_at with is_null |
src/cluster_erp/modules/analytics/registry.py:198-201, src/cluster_erp/core/contracts.py:2154 |
| Dimensions | data_type among text, date, number, boolean, reference; a reference names its target; a date declares grains; rollup only on references |
src/cluster_erp/modules/analytics/registry.py:121-147 |
| Measures | aggregation among sum, avg, min, max, count, count_distinct, median; additivity among additive, semi_additive, non_additive; a semi_additive measure names an existing axis |
src/cluster_erp/modules/analytics/registry.py:50-51, src/cluster_erp/modules/analytics/registry.py:150-180, src/cluster_erp/modules/analytics/registry.py:222-230 |
| Money | a non-empty currency_field marks a monetary measure and must also be exposed as a dimension |
src/cluster_erp/core/contracts.py:2110-2113, src/cluster_erp/modules/analytics/registry.py:231-240 |
Contributions by module¶
Found with grep -rl analytics_facts src/cluster_erp/modules: nine modules, 13 fact sets.
| Module | Fact keys | Source table | Read permission | Monetary measures (currency_field) |
Source |
|---|---|---|---|---|---|
fleet |
fleet_fuel, fleet_work_order, fleet_penalty, fleet_compliance |
fleet_fuel_entries, fleet_work_orders, fleet_penalties, fleet_compliance_items |
fleet_asset.read |
none declared | src/cluster_erp/modules/fleet/analytics_facts.py:35-310 |
inventory |
goods_movement |
inventory_goods_movements |
goods_movement.read |
none; the module stores no money | src/cluster_erp/modules/inventory/analytics_facts.py:29-109 |
parties |
party |
parties_parties |
party.read |
none; count only | src/cluster_erp/modules/parties/analytics_facts.py:17-77 |
procurement |
purchase_order, purchase_request |
procurement_purchase_orders, procurement_purchase_requests |
purchase_order.read, purchase_request.read |
4 | src/cluster_erp/modules/procurement/analytics_facts.py:19-191 |
production |
production_order |
production_production_orders |
production_order.read |
none | src/cluster_erp/modules/production/analytics_facts.py:18-110 |
products_and_services |
product |
products_products |
product.read |
none | src/cluster_erp/modules/products_and_services/analytics_facts.py:21-94 |
purchasing_quotations |
purchasing_quotation |
purq_quotations |
purchasing_quotation.read |
2 | src/cluster_erp/modules/purchasing_quotations/analytics_facts.py:17-117 |
quotations |
sales_quote |
quot_quotes |
quote.read |
3 | src/cluster_erp/modules/quotations/analytics_facts.py:23-115 |
sales_orders |
sales_order |
so_sales_orders |
sales_order.read |
3 | src/cluster_erp/modules/sales_orders/analytics_facts.py:20-134 |
Each module subscribes in its register_events, for example src/cluster_erp/modules/inventory/module.py:119-124, src/cluster_erp/modules/parties/module.py:134-139, and src/cluster_erp/modules/sales_orders/module.py:80-93. procurement subscribes two handlers, one per fact set (src/cluster_erp/modules/procurement/module.py:212-219). Every set uses the default live filter on deleted_at (grep live_filter in the nine files).
Some modules state in their docstrings what they leave out and why. inventory leaves stock levels out (src/cluster_erp/modules/inventory/analytics_facts.py:1-8), and fleet leaves cost per kilometre and CO2 out because a ratio of two aggregates is not additive (src/cluster_erp/modules/fleet/analytics_facts.py:1-11).
Why it is built this way¶
- The registry docstring states that the declaration is also the security allow-list: a user query may only reference a key, and a physical name never comes from user input (
src/cluster_erp/modules/analytics/registry.py:1-9). - It fails loudly because a silently skipped contribution would mean that a module's data never entered the analysis, discovered only through a wrong number (
src/cluster_erp/modules/analytics/registry.py:11-16). - The contract docstring calls
live_filtermandatory because a set without it also counts deactivated records (src/cluster_erp/core/contracts.py:2139-2142). - The module docstring states that a future module can contribute with no change in the analytics module (
src/cluster_erp/modules/analytics/module.py:18-22).
Rules and boundaries¶
- A reference dimension must name its target so that labels resolve instead of showing raw ids (
src/cluster_erp/modules/analytics/registry.py:131-135). rollupmarks a low-cardinality reference dimension for the summary cells (src/cluster_erp/core/contracts.py:2092-2097); see reports, summaries, and cross.- The analytics catalogue is rebuilt from the bus on each catalogue call (
src/cluster_erp/modules/analytics/service.py:55-62).
Interaction with other parts¶
| Contract | Direction | Purpose | Source |
|---|---|---|---|
DescribeAnalyticsFacts |
analytics emits, nine modules answer | collect the declarations | src/cluster_erp/core/contracts.py:2060-2073 |
AnalyticsFactSet, AnalyticsDimension, AnalyticsMeasure |
modules build | the declaration shapes | src/cluster_erp/core/contracts.py:2076-2154 |
The full contract list is on the contracts reference; the bus is described on event bus.
Limits and open points¶
- The fleet fact sets sum
total_costandamountbut declare nocurrency_field, so the mixed-currency gate does not apply to them (src/cluster_erp/modules/fleet/analytics_facts.py:98-104,src/cluster_erp/modules/fleet/analytics_facts.py:240-246). - A handler that raises an exception is logged by the bus and its fact sets are missing from the catalogue without an error to the caller (
src/cluster_erp/core/event_bus.py:94-96).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| fact set | AnalyticsFactSet |
One module's declaration of a single analysable subject with its grain, dimensions, and measures. | Olgu kümesi | src/cluster_erp/core/contracts.py:2126 |
| grain | grain |
What one row of a fact set is. | Tanecik | src/cluster_erp/core/contracts.py:2129-2131 |
| additivity | additivity |
Whether a measure may be summed across all, some, or no dimensions. | Toplanabilirlik | src/cluster_erp/core/contracts.py:2101-2110 |
| live filter | live_filter |
The column test that excludes deactivated rows from a fact set. | Canlı süzgeç | src/cluster_erp/core/contracts.py:2154 |
Related pages¶
- Analytics module
- Recipes and query builder
- Fleet TCO, telematics, and analytics
- Inventory module
- Event bus
Verified against commit 92f8051 on 2026-11-13.