KPIS and Metrics on Objects
Understanding the data
Revenue:
Resources: Milestone, Timesheet, Invoice
Negociated:
The amount that we could potentially charge
- Sum of
Milestone.fee_estimation - Use when:
- To determine if we are leaving any money on the table (negociated > planned)
- forecasting (what we expect to bill)
Planned/expected:
The billable hours planned
- Sum of
TimesheetEntry.hours_expected*number of hours - or
TimesheetEntry.objects.filter(..).kpis().get('total_hours_expected_billing') - Use when:
- Forecasting
- To compare against
negociated
Recorded:
The actual billable hours recorded by the employee
- Sum of
TimesheetEntry.hours_recorded*number of hours - or
TimesheetEntry.objects.filter(..).kpis().get('total_hours_recorded_billing') - Use when:
- To compare work actually done (recorded) vs planned)
Actual:
The actual amount invoiced - this is the canonical real tracker of revenue
- Sum of
Invoice.sub_total - Use when:
- The know the canonical actual revenue that was generated
- Lagging indicator (can't be used for forecasting)
Cost
Resources: PaySlip
actual cost:Sum ofPaySlip.calculated_total_earnings_tec- Use when:
- To get the total cost of an Employee
- Can be used for forecasting because PaySlips are projected outwards
Assigning cost
Assigning cost can be quite complicated. For example, if am employee is working for multiple customers,
Cost metrics
- Canonical source: PaySlip
When looking at historic data, the canonical place to find cost is PaySlips.
- Cost:
total_earnings - TEC:
calculated_total_earnings_tec
- Cost:
PaySlips are projected into the future, and can be used to forecast predicted expenses
Assignment
Both cost and revenue metrics can be assigned/sliced by a grouping resource.
The general process for slicing KPIs is to:
- filter the data resource (e.g.:
Timesheet,Milestone,Invoice,PaySlip) - by the grouping resource (e.g.:
Customer,Department,Employee,Project), - and then apply the relevant
.kpis()method.
Typical Example:
TimesheetEntry.objects.for_customer(customer_id).kpis(start_date, end_date)Grouping Resources
Grouping is supported on the following resources
- Customer
- Department
- Employee
- Project
Complex assignment
For most resources, slicing is quite simple - it's relatively easy to filter the base resource so as to get only those relevant to the grouping resource. However, some resources are more complicated to allocate -> these require complex assignemnt.
PaySlips
INFO
WIP: This section is under development
Payslips assignment can be determined by:
- planned/recorded hours as a % of total *
- Assignment.amount_assigned
For example: You may want to get total cost (total_earnings) assigned by customer according to hours_expected:
Utilization Metrics
Utilization is generally expressed as a percentage of available working hours in a given period.
- Working hours available: the employee’s working hours for the period (typically excludes weekends, public holidays, and leave).
- Units: percent (%) Planned utilization
- Meaning: How much of an employee’s available working time is planned to be worked.
working_hours_available = employee.get_number_of_working_days(
start_date,
end_date,
)INFO
Utilization metrics can be derived from a TimesheetEntry QuerySet's .kpis() function
Example:
Planned utilization
- Meaning: How much of an employee's available working time is expected/planned.
- How it's calculated:
planned_utilization = total_hours_expected / working_hours_available * 100 - Source:
- Timesheet KPIs:
total_hours_expected - Availability: employee working hours in range (
working_hours_available)
- Timesheet KPIs:
Recorded utilization
- Meaning: How much of an employee's available working time has actually been recorded (logged).
- How it's calculated:
recorded_utilization = total_hours_recorded / working_hours_available * 100 - Source:
- Timesheet KPIs:
total_hours_recorded - Availability: employee working hours in range (
working_hours_available)
- Timesheet KPIs:
Assigned utilization
- Meaning: Utilization based on work assigned to the employee via assignments (i.e. planned work that is attributable to an assignment/milestone/project, rather than "unallocated/bench").
- Typical calculation (when needed):
assigned_utilization = assigned_hours / working_hours_available * 100 - Notes:
assigned_hoursis usually derived from expected/planned hours that are linked to an Assignment.- This is used as a concept across planning and reporting, even when the only persisted utilization metrics are planned/recorded.
Invoiced utilization
- Meaning: Utilization based on hours that have been billed/invoiced.
- Typical calculation (when needed):
invoiced_utilization = total_hours_billed / working_hours_available * 100 - Related values:
total_hours_billed(hours)total_hours_billed_billing(value)
KPIS on Queries
Any object that provides a
kpis()function in it's query managerqueries.pywill provide reliable KPIs for a QuerySet.
- Defined in QuerySet in the resource's
queries.pyfile - Function name should be
kpis(describe:bool=False)
INFO
If describe is set, then additional keys are added with a description of how the value is derived.
for example, if you have a metric at the key foo, if describe=True, you could also expect a key foo_description
For example:
Milestone.objects
.for_project(projectId)
.starts_after(dt1).ends_before(dt2)
.kpis()
Invoice.objects
.for_customer(customerId)
.for_date_range(dt1, dt2)
.kpis()
...INFO
TODO: You should be able to pass tags to a kpi method to filter which kpis to show
API
If a resource's QuerySet implements kpis(), then you can get kpis for any queryset at:
GET /:resource/kpis/?filter_1=..&filter_2=..INFO
Any parameters prefixed with kpi_ will be passed as kwargs to the kpis() method. For example, you might send: kpi_describe=True to add metric descriptions to the output
KPIs on an instance
You can get key metrics (kpis) on an instance of a resource using a provided
.kpis(start_date, end_date)
Exmples:
Customer.objects.get(pk=..).kpis()
..API
If implemented, you can fetch KPIs/metrics at the endpoint:
GET /:resource/:resource_id/kpis/?start_date=2025-01-01&end_date=2026-01-01INFO
Warning: We need to be careful at this point about circular references For example, one object's kpis might call another object's kpis -> which would lead to an infinite loop.
Patterns to look into that may prevent this issue:
- Curcuit breaker
- One-way data-flow