Skip to content

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 of PaySlip.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
  • 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:

  1. filter the data resource (e.g.: Timesheet, Milestone, Invoice, PaySlip)
  2. by the grouping resource (e.g.: Customer, Department, Employee, Project),
  3. and then apply the relevant .kpis() method.

Typical Example:

py
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.
py
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)

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)

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_hours is 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 manager queries.py will provide reliable KPIs for a QuerySet.

  • Defined in QuerySet in the resource's queries.py file
  • 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:

py
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:

py
Customer.objects.get(pk=..).kpis()
..

API

If implemented, you can fetch KPIs/metrics at the endpoint:

curl
GET /:resource/:resource_id/kpis/?start_date=2025-01-01&end_date=2026-01-01

INFO

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