RateCard entities and calculations

Overview

JobOrder and Placement RateCards are a powerful tool that makes it easy for Bullhorn users to set up pricing scales and controls for the contingency workforce. This guide explains the different types of RateCard entities and the calculations run when creating or updating them.

RateCard entities

RateCards are a complex Bullhorn feature that consists of a parent entity and several sub-entities. When working together, all of these entities form a complete JobOrder or Placement RateCard. Despite their similar setup and naming conventions, JobOrder and Placement RateCards are not composed of any common entities. For each entity below, there is a JobOrder variant and a Placement variant:

  • JobOrder/PlacementRateCard: This is the parent entity that has one or more versions. This entity contains a reference to the JobOrder ID or Placement ID with which it is associated.
  • JobOrder/PlacementRateCardVersion: Since RateCards is an effective-dated feature, each RateCard has one or more versions. The Version entity has a reference to the JobOrder/PlacementRateCard entity as well as the effective date.
  • JobOrder/PlacementRateCardLineGroup: A group that is associated with a version. Each group is also tied to an EarnCodeGroup, which has either one or three associated EarnCodes.
    • If the EarnCodeGroup tied to the LineGroup accrues overtime, the LineGroup has three lines: Standard, Overtime, and Double Time.
    • If the EarnCodeGroup tied to the LineGroup does not accrue overtime, the LineGroup has one line: Standard.
    • LineGroups can also be defined as containing or not containing the base rate. This is used by Bullhorn Time and Expense to identify the set of default rates used for time entry.
  • JobOrder/PlacementRateCardLine: Each line is associated with a parent LineGroup as well as an EarnCode from the associated EarnCodeGroup. The user can define the pay and bill rates, currency, and alias. There are also custom fields available for this entity.

EarnCode configuration and RateCards

EarnCodes are a vital part of any JobOrder or Placement RateCard. Each RateCardLine has an associated EarnCode and each RateCardLineGroup has an associated EarnCodeGroup. The EarnCodes on each RateCardLine should always be members of the EarnCodeGroup on the RateCardLineGroup.

The EarnCodeGroup sets a number of important values that impact the RateCard. One of these values indicates whether EarnCodes support pay fields, bill fields, or both. This directly impacts which fields data can be entered into and which fields are calculated. Another important value that is set on the EarnCodeGroup indicates whether rates are required for the EarnCodes in the group. Most notably, the EarnCodeGroup is responsible for determining whether overtime should be accrued. This determines if the standard EarnCode is joined by an overtime EarnCode and a double time EarnCode. If an EarnCodeGroup accrues overtime, all three EarnCodes must be used together on a RateCard. The standard EarnCode cannot appear without the overtime and double time EarnCodes and vice versa.

Shared RateCard attributes

  • RateCards provide version control that allows rates to become available or fall off from the RateCard at a predetermined date.
  • Adding, editing, deleting, and viewing of RateCards are all controlled by administrators, which provides a high level of customization on a per-user basis.
  • RateCards can also be customized by administrators to automatically populate certain fields to a default configuration in the user interface such as the default EarnCode, and pay and bill multipliers for overtime and double time rates.
  • Overtime and double time rates are automatically calculated based on the standard rate and pay and bill multipliers. This is explained in more detail below.
  • Markup percent and markup value are also automatically calculated based on pay and bill rates.
  • When making changes in the user interface, calculations are performed instantly, showing exactly how changes to one field impact the others.
  • There are many other supporting features available, such as tracking edit history, reporting data in Canvas, Data Mirror 8 support, and the ability to execute field and page interactions.

JobOrder RateCard specifics

JobOrderRateCards is a feature that only works if PlacementRateCards and the PlacementRateCard status field are enabled. While these RateCards offer most of the features of their Placement counterparts, they do not include a status field and are always treated as Incomplete RateCards. This means that Pay and Bill rates are not required regardless of the EarnCodeGroup’s setting. When a Placement is created from a JobOrder with a RateCard, the Placement is created with a PlacementRateCard that matches the JobOrderRateCard.

Placement RateCard status

The most unique feature exclusive to PlacementRateCards is the status field. When enabled, the PlacementRateCard status field can show the RateCard is in a number of different states, the most notable being Incomplete and Active. When a RateCard is Incomplete, the pay and bill rates are not marked as required regardless of the settings on the EarnCodeGroup. If a RateCard has a status of Active, not only do the Pay and Bill rates respect the EarnCodeGroup’s setting, but the user is able to create different versions of the RateCard. If the PlacementRateCard status field is not enabled, the PlacementRateCard is treated as if it is set to Active.

While the example here describes two specific statuses, you can configure others. You can also apply the behavior described above in relation to the Active status to other statuses through the Should Run Validation on Save setting.

How RateCards are calculated

Whether through the user interface or an API request, each time a RateCard is saved or edited, a series of calculations are performed on the fields in each RateCardLine. Note that these calculations are only performed when the fields are left empty. When making an API request, fields with data provided by the user are not overwritten by calculations unless otherwise noted. When adding or editing a RateCard from the user interface, RateCard calculations are performed instantly so the user is able to see them before saving the RateCard.

The only fields that can be calculated are those listed below, which are all on the JobOrder/PlacementRateCardLine. For this guide, REG refers to a line with a standard EarnCode, OT refers to a line with an overtime EarnCode, and DT refers to a line with a double time EarnCode.

Field Name Calculation
Pay Multiplier REG: Will always be calculated to 1, regardless of what the user inputs.
OT: OT Pay Rate / REG Pay Rate
DT: DT Pay Rate / REG Pay Rate
Pay Rate REG: Bill Rate / (1 + Markup Percent)
OT: REG Pay Rate * OT Pay Multiplier
DT: REG Pay Rate * DT Pay Multiplier
Bill Multiplier REG: Will always be calculated to 1, regardless of what the user inputs.
OT: OT Bill Rate / REG Bill Rate
DT: DT Bill Rate / REG Bill Rate
Bill Rate REG: Pay Rate / (1 + Markup Percent)
OT: REG Bill Rate * OT Bill Multiplier
DT: REG Bill Rate * DT Bill Multiplier
Markup Percent REG: (REG Bill Rate - REG Pay Rate) / REG Pay Rate
OT: (OT Bill Rate - OT Pay Rate) / OT Pay Rate
DT: (DT Bill Rate - DT Pay Rate) / DT Pay Rate
Markup Value REG: REG Bill Rate - REG Pay Rate
OT: OT Bill Rate - OT Pay Rate
DT: DT Bill Rate - DT Pay Rate

In some cases, there may not be enough data to calculate the fields that are left empty. What happens in this scenario is dependant on a number of different factors and is explained in the table below:

Rate Card Type Adding or Editing Rates Required on Earn Code Group Placement Rate Card Status Enabled Status has Should Run Validations Result
Job Order Adding or Editing Either N/A N/A Fields will be saved as empty.
Placement Adding or Editing Not Required Enabled or Not Enabled Yes or No Fields will be saved as empty.
Placement Adding or Editing Required Not Enabled N/A Request will error and Rate Card will not save.
Placement Adding or Editing Required Enabled No Fields will be saved as empty.
Placement Adding Required Enabled Yes Status will be set to Incomplete and fields will be saved as empty.
Placement Editing Required Enabled Yes Request will error and Rate Card will not save.

Creating and Updating RateCards

Despite being composed of four separate entities, the JobOrder and Placement RateCard entities depend on each other and therefore must all be created at the same time as part of one PUT request.

Here is an example of a request used to create a JobOrderRateCard where none of the fields are calculated:

PUT https://rest.bullhornstaffing.com/rest-services/{corpToken}/entity/JobOrderRateCard

payload: 
{
    "jobOrderRateCardLineGroups": [
        {
            "isBase": true,
            "earnCodeGroup": {
                "id": 1
            },
            "jobOrderRateCardLines": [
                {
                    "earnCode": {
                        "id": 1
                    },
                    "alias": "This is a Regular Line",
                    "payMultiplier": 1,
                    "payRate": "1",
                    "billMultiplier": 1,
                    "billRate": "2",
                    "markupPercent": 1,
                    "markupValue": 1
                },
                {
                    "earnCode": {
                        "id": 2
                    },
                    "alias": "This is an Overtime Line",
                    "payMultiplier": 1.5,
                    "payRate": 1.5,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0.3333,
                    "markupValue": 0.5
                },
                {
                    "earnCode": {
                        "id": 3
                    },
                    "alias": "This is a Double Time Line",
                    "payMultiplier": 2,
                    "payRate": 2,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0,
                    "markupValue": 0
                }
            ]
        }
    ],
    "effectiveDate": "2022-02-02",
    "jobPosting": {
        "id": 1
    }
}

Here is an example of a request to create a PlacementRateCard where none of the fields are calculated:

PUT https://rest.bullhornstaffing.com/rest-services/{corpToken}/entity/PlacementRateCard

payload:
{
    "placementRateCardLineGroups": [
        {
            "isBase": true,
            "earnCodeGroup": {
                "id": 1
            },
            "placementRateCardLines": [
                {
                    "earnCode": {
                        "id": 1
                    },
                    "alias": "This is a Regular Line",
                    "payMultiplier": 1,
                    "payRate": "1",
                    "billMultiplier": 1,
                    "billRate": "2",
                    "markupPercent": 1,
                    "markupValue": 1
                },
                {
                    "earnCode": {
                        "id": 2
                    },
                    "alias": "This is an Overtime Line",
                    "payMultiplier": 1.5,
                    "payRate": 1.5,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0.3333,
                    "markupValue": 0.5
                },
                {
                    "earnCode": {
                        "id": 3
                    },
                    "alias": "This is a Double Time Line",
                    "payMultiplier": 2,
                    "payRate": 2,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0,
                    "markupValue": 0
                }
            ]
        }
    ],
    "placementRateCardStatusLookup":{"id":1},
    "effectiveDate": "2022-02-02",
    "placement": {
        "id": 1
    }
}

Both requests have a very similar setup with the only differences being the entity names and the inclusion of a PlacementRateCard status on the PlacementRateCard request. This line should be removed when a user without PlacementRateCard status enabled makes the request.

When updating a RateCard, it is important to include the IDs of each of the sub-entities that will be updated as well. Entities without listed IDs are treated as new additions the the RateCard rather than updates to existing data.

Here is an example of a request to update a JobOrderRate Card:

POST https://rest.bullhornstaffing.com/rest-services/{corpToken}/entity/JobOrderRateCard/1

payload:
{
    "jobOrderRateCardLineGroups": [
        {
            "isBase": true,
            "earnCodeGroup": {
                "id": 1
            },
            "id": 2,
            "jobOrderRateCardLines": [
                {
                    "earnCode": {
                        "id": 1
                    },
                    "alias": "This is a Regular Line",
                    "payMultiplier": 1,
                    "payRate": 1,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 1,
                    "markupValue": 1,
                    "id": 3
                },
                {
                    "earnCode": {
                        "id": 2
                    },
                    "alias": "This is an Overtime Line",
                    "payMultiplier": 1.5,
                    "payRate": 1.5,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0.3333,
                    "markupValue": 0.5,
                    "id": 4
                },
                {
                    "earnCode": {
                        "id": 3
                    },
                    "alias": "This is a Double Time Line",
                    "payMultiplier": 2,
                    "payRate": 2,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0,
                    "markupValue": 0,
                    "id": 5
                }
            ]
        },
        {
            "isBase": false,
            "earnCodeGroup": {
                "id": 7
            },
            "jobOrderRateCardLines": [
                {
                    "earnCode": {
                        "id": 9
                    },
                    "alias": "This is an additional line that is brand new.",
                    "payMultiplier": 1,
                    "payRate": "3",
                    "billMultiplier": 1,
                    "billRate": "4",
                    "markupPercent": 0.3333,
                    "markupValue": 1
                }
            ]
        }
    ],
    "jobPosting": {
        "id": 6
    },
    "versionID": 7
}

The body above demonstrates adding a new JobOrderRateCardLineGroup to an existing JobOrderRateCard. Note that for the request to succeed, all of the IDs must be properly nested. For example, the JobOrderRateCardLine IDs that appear under a JobOrderRateCardLineGroup must match the Lines that were originally in that group. It is not possible to swap preexisting sub and parent entities.

PlacementRateCard updates must also follow a similar pattern, with the main difference again being the unique entity names:

POST https://rest.bullhornstaffing.com/rest-services/{corpToken}/entity/PlacementRateCard/2

payload:
{
    "placementRateCardLineGroups": [
        {
            "isBase": true,
            "earnCodeGroup": {
                "id": 1
            },
            "id": 1,
            "placementRateCardLines": [
                {
                    "earnCode": {
                        "id": 1
                    },
                    "alias": "This is a Regular Line.",
                    "payMultiplier": 1,
                    "payRate": 1,
                    "billMultiplier": 1,
                    "billRate": "2",
                    "markupPercent": 1,
                    "markupValue": 1,
                    "id": 2
                },
                {
                    "earnCode": {
                        "id": 2
                    },
                    "alias": "This is a Overtime Line.",
                    "payMultiplier": 1,
                    "payRate": 1,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 1,
                    "markupValue": 1,
                    "id": 3
                },
                {
                    "earnCode": {
                        "id": 3
                    },
                    "alias": "This is a Double Time Line",
                    "payMultiplier": 2,
                    "payRate": 2,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0,
                    "markupValue": 0,
                    "id": 4
                }
            ]
        },
        {
            "isBase": false,
            "earnCodeGroup": {
                "id": 7
            },
            "placementRateCardLines": [
                {
                    "earnCode": {
                        "id": 9
                    },
                    "alias": "This is another unique new line",
                    "payMultiplier": 1,
                    "payRate": "2",
                    "billMultiplier": 1,
                    "billRate": "3",
                    "markupPercent": 0.5,
                    "markupValue": 1
                }
            ]
        }
    ],
    "placementRateCardStatusLookup": {
        "id": 2
    },
    "placement": {
        "id": 5
    },
    "versionID": 6
}

While the previous requests show how to add or edit a RateCard with all the fields defined, it is also possible to make a request with fewer fields included and have the other fields be calculated automatically. In this case, the fields to be calculated should be excluded from the request body. For example, a JobOrderRateCard body where the overtime bill rate should be calculated might look something like this:

{
    "jobOrderRateCardLineGroups": [
        {
            "isBase": true,
            "earnCodeGroup": {
                "id": 1
            },
            "jobOrderRateCardLines": [
                {
                    "earnCode": {
                        "id": 1
                    },
                    "alias": "This is a Regular Line",
                    "payMultiplier": 1,
                    "billMultiplier": 1,
                    "billRate": "2",
                    "markupPercent": 1,
                    "markupValue": 1
                },
                {
                    "earnCode": {
                        "id": 2
                    },
                    "alias": "This is an Overtime Line",
                    "payMultiplier": 1.5,
                    "payRate": "",
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0.3333,
                    "markupValue": 0.5
                },
                {
                    "earnCode": {
                        "id": 3
                    },
                    "alias": "This is a Double Time Line",
                    "payMultiplier": 2,
                    "payRate": null,
                    "billMultiplier": 1,
                    "billRate": 2,
                    "markupPercent": 0,
                    "markupValue": 0
                }
            ]
        }
    ],
    "effectiveDate": "2022-02-02",
    "jobPosting": {
        "id": 1
    }
}

This request body demonstrates three different ways to indicate that a value should be calculated, all using the payRate as an example. In the regular line, that payRate is missing completely. In the overtime line, the payRate is set to "". In the double time line, the payRate is set to null. All three of these methods are acceptable ways of leaving an empty field to be calculated automatically.

There are a few limitations to how and when fields can be calculated. If a field can be calculated, it will be. The only times that a field will not be calculated is if there is already data for it, or there is not enough information to calculate it. Cases where there is not enough information to calculate are handled differently based on whether the PlacementRateCard status field is enabled. If the PlacementRateCard status field is not enabled, a PUT or POST request that does not include enough data to calculated the missing fields fails. If the PlacementRateCard status field is enabled, a POST request that does not include enough data to calculate the missing fields always defaults to the Incomplete status and saves properly regardless of what status the PlacementRateCard was originally saved with. A POST request on a Placement Rate Card without enough data to calculate missing fields fails if the Rate Card is saved with a status that requires validations, such as Active. JobOrderRateCards do not have a status field and are always treated as Incomplete so they do not fail due to missing data for fields that can be calculated.