Detailed booking lines: cost and sell, line by line¶
The pricing documents so far describe which rules qualify. This one describes what the other documents left out: how a unit price from Tourplan becomes a cost price and a sell price on a detailed booking line — the quantity, day and pax multipliers, the order the discounts apply in, and the twenty-six attributes that get written at the end.
Scope: the non-accommodation path and the standard-options (accommodation) path. Dynamic pricing, Siteminder bar rates and the mandatory-extras variants are named where the code branches to them and are not traced.
1. Two paths, and a branch that reads backwards¶
Pricing.Sub_PriceBookingLine_Wilderness (57 nodes) is the dispatcher. Everything below
hangs off one condition at [8]:
[8] SPLIT if $BookingLine/BookingLineType = Booking.BookingLineType.Accomodation
and $BookingLine/SoleUse = false
and $Supplier/ExclusiveUse = false
or $BookingLine/BookingLineType = Booking.BookingLineType.Exploration
| Case | Goes to | Node |
|---|---|---|
| false | Pricing.Sub_CreatePricingNon_Accom |
[9] |
| true | guest-allocation check → Pricing.Sub_GetOptionsForBookingline |
[25] → [31] |
The names do not match the branches
A sole-use accommodation line, or any line whose supplier is marked
ExclusiveUse, evaluates that condition to false and is therefore priced by
Pricing.Sub_CreatePricingNon_Accom — the flow whose name says non-accommodation. An
Exploration line goes the other way, into the accommodation path.
So "non-accommodation" in this code means "not priced per room configuration", not "not accommodation" (inferred from the branch — worth confirming with the team, because the name will mislead every reader).
Before the split the dispatcher calls Booking.CheckSupplier [4] and
Pricing.ArchivePrices [5], so the previous prices are archived before anything is
recalculated. On the accommodation side, three checks sit between the split and the options
flow: Pricing.CheckAllGuestsAllocated [25], a Siteminder fork [27] →
Pricing.Price_Siteminder, and a bar-rates/dynamic-pricing fork [28], [39]–[48]
→ Pricing.DPSwitch and Pricing.CheckDynamicPricing. Only when none of those apply does
Pricing.Sub_GetOptionsForBookingline run.
2. Path A — Pricing.Sub_CreatePricingNon_Accom (42 nodes)¶
Resets, finds the option, then decides whether to price the party as a group or per cost group.
| Node | What happens |
|---|---|
| [5] | Pricing.ResetBL returns $PCG_UnpricedList — the cost groups still to price |
| [6] | cost groups filtered: ExtraLuggagePG = false |
| [9]–[10] | Pricing.GetReturnOption; no option code → warning, return false at [37]–[39] |
| [11]–[12] | country lookup, then Pricing.GetDMCOption — a DMC option can replace the supplier's own |
| [13] | writes BookingLine.OverrideOption and ReconfRequiredOnly from the resolved option |
| [14] | Pricing.GetChargePerOption → $ChargePer, the multiplier basis for section 6 |
| [15] | Pricing.SadcMix — mixed-residency parties |
| [17] | SADC mix, or sub-type TP_Doc_Pack, Courier, BushBuddy |
| [18] | the group/per-group decision |
| [20] | group → Pricing.PricingNonAccom_PCGList, and BookingLine.PricedByGroup = true at [19] |
| [31] | otherwise → Pricing.PricingNonAccom_PCG |
The group decision, verbatim:
[18] SPLIT if ($ChargePer = BookingMasterData.ChargePer.G or $Option/pxb1 != 9999)
and $CountPCG > 1
or $BookingLine/SoleUse
or $BookingLine/BookingLineType = Booking.BookingLineType.Rental_car
or $BookingLine/OverrideOption and $CountPCG > 1
pxb1 is a Tourplan pax-break field; != 9999 means the option has real pax breaks
(inferred). Either failure path writes BookingLine.Priced = false and logs a
Critical message naming the flow that failed — [26]–[29] and [33]–[36].
3. Path B — Pricing.Sub_GetOptionsForBookingline (93 nodes)¶
The accommodation path, driven by room configuration rather than by a single option.
| Node | What happens |
|---|---|
| [3]–[6] | country, travel location, AvailableRoomConfig, travel days; every day's Tmp_SingleCounter reset at [8] |
| [13]–[14] | HasMultiPG_Room → Pricing.PriceAccom_MultiPG (one room shared by several cost groups) |
| [17] | HasSinglePG_Room fork |
| [19] | loop over party cost groups — everything below is per cost group |
| [23] | Pricing.GetAlternateOptionPricing — a pricing rule can substitute a different option |
| [24]–[25] | non-standard room options first: options attached to a RequiredRoom |
| [27] | Pricing.GetDMCOption again, per option |
| [33] | Pricing.Sub_CreateAgeGroupsPerOption_PCG → $AgeGroupings; empty means price the whole group |
| [37], [39] | Pricing.GetTpPricing — the Tourplan rates, first for the option, then for the alternate |
| [38], [40]–[41] | no rates and not a holding pattern → Critical "No pricing found for supplier", $Priced_BL = false |
| [44] | Pricing.GetFactor → the rack factor used later for RackEstimate |
| [45] | Pricing.GetTriplesPolicy |
| [46] | Pricing.Sub_PriceBookingline_PCG — per cost group, per option |
| [48]–[50] | Pricing.CheckDays — all days covered, else keep looping |
| [53]–[60] | no non-standard rooms → Pricing.GetOption (the standard Wilderness option) and the RequiredRoomList_OldMethod route |
Both paths converge on the same two flows.
4. The line itself — Pricing.Sub_CreateDetailedBookingLines_PCG (37 nodes)¶
_PCG creates one detailed booking line per guest grouping; _PCGList (32 nodes) creates
one for the group as a whole. Otherwise they are the same shape.
[4] is a guard: sell and cost both zero, no OverrideOption, no dynamic pricing → skip
creation entirely.
[5] copies the PriceByDate_tmp into a committed Booking.PriceByDate. [7]–[8]
handle a leap-year edge case: 29-02 against 01-03 keeps the booking line's own first and
last day.
[9] Pricing.CalcRackFactor → $CheckFactor, stored as factor.
[10] creates the Booking.DetailedBookingLine. The fields that matter for the
arithmetic:
| Attribute | Set from |
|---|---|
Quantity |
$Qty |
Pax |
$Pax |
Days |
if $Days = 0 then 1 else $Days |
GrossUnitSP, GrossUnitSP_Original |
$Price_Sell — the Tourplan unit sell |
GrossUnitCP |
$Price_Cost — the Tourplan unit cost |
ChargePer |
if PriceType = Staff then P else $ChargePer |
IsDiscountable |
if PriceType = Extra then false else $Option/Discountable |
PriceConfirmation |
Subject_to_Confirmation when the price is manual or an auto-increase was applied, else Confirmed |
factor |
$CheckFactor from [9] |
Then [11] discounts, [17] the calculation, and afterwards [25]
Pricing.CalculateDetailedBookingLineTax and [26] Pricing.CheckPriceChanges. A tax failure logs
Critical and returns false — [29].
[12]–[15] are an ownership-specific detour: for suppliers A1 B1 C1 G1 I1 K2 K3 K4
Q1 J3, if an active ManAdj_SubCategory marked WSAutoIncreaseFix exists,
Pricing.ApplyAutoIncrease_FixAdj runs first.
[18]–[19]: if the calculation set DeleteMe, the line is deleted and the flow
returns true — a priced-to-nothing line leaves no trace.
5. Discounts and rules — Pricing.Sub_GetDiscountAndPricingRulesPerDBL (170 nodes)¶
The largest flow in the chain. It decides which pricing rules apply to this line and converts them into one number. What it calls:
| Flow | Role |
|---|---|
Pricing.GetExclusivePricingRules_PCG_BL |
rules that cannot combine |
Pricing.ChooseExclusiveDiscount_DBL |
picks the winner among them |
Pricing.GetNonExclusivePricingRules_PCG_BL, Pricing.GetNonExclusiveDiscount |
rules that stack |
Pricing.GetAltOptionPricingRules_PCG_BL |
rules that swap in a different option or package |
Pricing.GetSellPriceForCategory, Pricing.GetCostPriceForCategory, GetPrice |
prices for the substituted option |
Pricing.CheckMaxRooms |
room-count ceilings on a rule |
The one output the calculation consumes:
Everything else it writes is bookkeeping or option substitution: PricingRulesApplied,
PricingRulesAppliedName, DiscountPerc_AlternateOption, NetDiscount, and — when an
alternate-option package wins — GrossUnitSP, GrossUnitCP, GrossUnitSP_Original,
DisplayName, PriceCode, OSR_CODE, StayType, Currency and the
DetailedBookingLine_Option association, all replaced from the substitute.
Manual discounts are not here
This flow handles automated rules — supplier specials. Manual adjustments arrive later,
in the calculation itself, from Pricing.GetManualAdjustment_PCG. See
manual adjustments.
6. The calculation — Pricing.Sub_AddPriceToDetailedBookingLine_PCG (60 nodes)¶
6.1 The multiplier: Pricing.CalcPrice¶
Every price on the line goes through this, and it is thirteen nodes long:
PriceByDate.PerDayCharge |
ChargePer |
Returns |
|---|---|---|
| true | G (group) |
Unit × Quantity if Days = 0, else Unit × Days × Quantity |
| true | anything else | Unit × Pax × Quantity if Days = 0, else Unit × Pax × Days × Quantity |
| false | G |
Unit × Quantity |
| false | anything else | Unit × Pax × Quantity |
ChargePer values: P Pax, R Room, G Group, Camp, S SCU, F FCU/SCU, and the
complimentary variants _0 Comp Pax, _1 Comp Pax Single, _2 Comp Group, _3 Comp
Group Single.
Only G escapes the pax multiplier
R (Room) multiplies by Pax exactly like P (Pax) does. If a room rate is stored
per room, the pax count must already be 1 on that line for the total to be right — the
formula itself does not distinguish them (inferred from Pricing.CalcPrice; a room-rate
line with Pax > 1 would multiply).
6.2 Sell price, in four stages¶
| Node | Variable | Expression |
|---|---|---|
| [7] | SellingPriceNonDiscounted |
CalcPrice(GrossUnitSP_Original) |
| [8] | SellingPriceDiscounted_BeforeStayPay |
CalcPrice(GrossUnitSP − DiscountPercentage_ExclManual/100 × GrossUnitSP) |
| [9] | NewUnitSP |
Sub_GetPriceAfterStayPays(GrossUnitSP, …) — stay-pay deals rewrite the unit price |
| [10] | SellingPriceDiscounted_AfterStayPayBeforeManual |
CalcPrice(NewUnitSP − DiscountPercentage_ExclManual/100 × NewUnitSP) |
| [11] | ManualAdjustment |
Pricing.GetManualAdjustment_PCG(…) |
| [12]–[13] | SellingPriceDiscounted |
manual override if one exists, else the automated figure |
So the order is: rack → automated rule discount → stay-pay → manual override, and a manual override replaces the unit price rather than discounting the result:
[12] $DiscountedUnit_SP = if $ManualAdjustment = empty
then ($NewUnitSP - ($DetailedBookingLine/DiscountPercentage_ExclManual div 100) * $NewUnitSP)
else $ManualAdjustment/ManualOverrideGrossUnitSP
6.3 Cost price, and the discount it may not receive¶
| Node | Variable | Expression |
|---|---|---|
| [16] | NewUnitCP |
Sub_GetPriceAfterStayPays(GrossUnitCP, …) |
| [17] | CostPrice_BeforeDisc |
CalcPrice(NewUnitCP) |
| [18] | CostDiscPerc |
if $IsAP and DoNotApplyDiscountToAP_Cost or DoNotAppyDiscount_Cost then 0 else DiscountPercentage_ExclManual |
| [19] | DiscCostPrice_BeforeManualDisc |
CalcPrice(NewUnitCP − CostDiscPerc/100 × NewUnitCP) |
| [20]–[21] | DiscCostPrice |
manual cost override when BookingLine.Override_Manual, else the above |
$IsAP is set at [5]: Supplier.OwnershipID in M1, J1, J2. For those suppliers a
discount can be applied to the sell price while the cost stays whole — which is where
margin comes from, and the reason the two figures diverge.
Note the asymmetry: a manual sell override applies whenever an adjustment exists
[12], but a manual cost override additionally requires BookingLine.Override_Manual
[20].
6.4 What gets written¶
All of it in one CHANGE at [23] — 26 attributes:
| Group | Attributes |
|---|---|
| Sell | SellingPriceBeforeDisc, SellingPriceBeforeManualDisc, SellingPriceAfterDiscounts, SellingPrice, SellingPriceDiscount |
| Cost | CostPriceBeforeDiscounts, CostPriceBeforeManualDisc, CostPriceAfterDiscounts, CostPrice |
| Margin | GrossProfit = sell − cost, GrossProfitPerc = rounded to 2, RackEstimate = factor × SellingPrice |
| Discounts | DiscountPercentage, DiscountPercentage_Str, StayPayDiscount, ManualDiscountSP, ManualDiscountCP |
| Unit prices kept for audit | GrossUnitSP_AfterManualDisc, GrossUnitCP_AfterManualDisc, GrossUnitSP_tmp, GrossUnitCP_tmp |
| Description | Description, Description_A, Description_B from Pricing.GetChargingUnitDescription and Pricing.GetDbl_Description_Breakdown_New |
| Flag | OverRidePrice — true whenever a manual adjustment exists |
SellingPrice and CostPrice are the discounted figures; the …BeforeDisc and
…BeforeManualDisc columns exist so the stages remain auditable after the fact.
6.5 Accumulation, and the three ways a line ends early¶
| Node | Condition | Result |
|---|---|---|
| [25] | everything zero, no override, no dynamic pricing, no pricing rule | DeleteMe = true [58] — the line is discarded by the caller |
| [26] | Booking.BookingLite |
returns without accumulating |
| [36] | DiscCostPrice > SellingPrice and not allowed by Option.CostGreaterThanSell, not an itinerary label, no do-not-discount flags |
Warning "Cost is greater than the sell price for this booking line", and [46] checks TravelLocation.CostPricingOnly |
Accumulation itself: [34] Pricing.AccumulatePrice_PartyCostGroup when a guest list exists,
[47] Pricing.AccumulatePrice_Accommodation otherwise, and [54]–[55] the single-cost-group
case which calls both the per-group and the list accumulators. PartyCostGroup_BL.Currency
is back-filled at [28] if it was empty.
7. The group variant — Pricing.Sub_AddPriceToDetailedBookingLine_PCGList (39 nodes)¶
Same writes, same order, one difference that matters at [11]:
$SP_Disc = if $DetailedBookingLine/ChargePer = BookingMasterData.ChargePer.G
then ($NewUnitSP - ($DetailedBookingLine/DiscountPercentage_ExclManual …
The group flow branches on ChargePer = G when computing the discounted unit, and
accumulates only through Pricing.AccumulatePrice_PartyCostGroupList [34]. It does not write
SellingPriceBeforeManualDisc, and there is no Pricing.AccumulatePrice_Accommodation call.
8. Every live flow that writes the sell price¶
| flow | folder | step | sets_to | runnable_by |
|---|---|---|---|---|
| API.PriceDMCLine_AddSellPrice | MF | 24 | $AdjustedSell | |
| Booking.ApplyCancelFees | BookingWizard/Step5 - Confirmation/MF | 8 | $SellFee | |
| Booking.CreateSingleCurrencyCharge | BookingWizard/Step4 - Quote/MF | 28 | $Charge | |
| Booking.IVK_DetailedBookingLineRR_Copy | Dynamic Pricing | 5 | $DetailedBookingLine/SellingPrice | Booking.Employee |
| Booking.IVK_DetailedBookingLine_Copy | Dynamic Pricing | 4 | $DetailedBookingLine/SellingPrice | Booking.Employee |
| Booking.OCh_BL_CancelFees | BookingWizard/Step5 - Confirmation/MF | 6 | $SellFee | Booking.Debug, Booking.Employee |
| Booking.OCh_DBL_Days | Dynamic Pricing | 6 | $DetailedBookingLine/GrossUnitSP * $Multiple | Booking.Employee |
| Booking.SetDonationAmount | BookingWizard/Step3 - Time line/MF | 6 | $BookingLine/DonationAmount*$DetailedBookingLine/Pax | |
| Booking.SetDonationAmount | BookingWizard/Step3 - Time line/MF | 11 | $BookingLine/DonationAmount*$DetailedBookingLine/Pax | |
| Booking.SetDonationAmount | BookingWizard/Step3 - Time line/MF | 13 | $BookingLine/DonationAmount*$DetailedBookingLine/Pax | |
| Booking.SetDonationAmount | BookingWizard/Step3 - Time line/MF | 15 | $BookingLine/DonationAmount*$DetailedBookingLine/Pax | |
| Booking.SetDonationAmount | BookingWizard/Step3 - Time line/MF | 17 | $BookingLine/DonationAmount*$DetailedBookingLine/Pax | |
| Dashboard.OCh_CalcNetSellingPrice | Consultant/MF | 16 | $DetailedBookingLine/GrossUnitSP * $Multiple | Dashboard.Administrator, Dashboard.Employee, Dashboard.Manager |
| Dashboard.OCh_CalcNetSellingPrice_Dbl | Consultant/MF | 17 | $DetailedBookingLine/GrossUnitSP * $Multiple | Dashboard.Administrator, Dashboard.Employee, Dashboard.Manager |
| DataManagement.ResetDetailedBookingLines | MFTourplanNew | 4 | 0 | |
| Pricing.Sub_AddPriceToDetailedBookingLine_PCG | MF_Trans | 23 | $SellingPriceDiscounted | |
| Pricing.Sub_AddPriceToDetailedBookingLine_PCGList | MF_Trans | 22 | $SellingPriceDiscounted | |
| Pricing.UpdateFuelPrice | MF_Trans | 23 | $Charge | |
| Pricing.UpdateSingleCcy_Charge | MF_Trans | 22 | $Charge | |
| Supplier_Portal.OCh_CalcNetCostPrice_DMC | MF | 6 | $SellPrice | Supplier_Portal.Administrator, Supplier_Portal.SupplierUser |
expr is verbatim; a conditional write is one row, not a decision table.
9. Decision points in the calculation, for QA¶
| step | condition | outcomes |
|---|---|---|
| 25 | SPLIT if $SellingPriceNonDiscounted=0 and $DiscCostPrice=0 and $SellingPriceDiscounted=0 and $BookingLine/OverrideOption = false and $BookingLine/HasDynamicPricing=false and ($DetailedBookingLine/Booking.DetailedBookingLine_PricingRule=empty or $DetailedBookingLine/Booking.DetailedBookingLine_PricingRule!=empty and $DetailedBookingLine/DiscountPercentage=0) and $Has100PercDisc=false | false | true |
| 26 | SPLIT if $Booking/BookingLite | false | true |
| 27 | SPLIT if $PartyCostGroup_BL/Currency = empty | true | false |
| 30 | SPLIT if $PartyCostGroup_BL/Pax_Total=0 and $PartyCostGroup_BL/Pax_Staff=0 | false | true |
| 32 | SPLIT if $SellingPriceDiscounted=0 | false | true |
| 33 | SPLIT if $GuestList=empty | true | false |
| 36 | SPLIT if $DiscCostPrice>$DetailedBookingLine/SellingPrice and $Option_Actual/CostGreaterThanSell=false and contains($DetailedBookingLine/PriceLabel, 'Itinerary') = false and $DetailedBookingLine/DoNotApplyDiscountToAP_Cost=false and $DetailedBookingLine/DoNotAppyDiscount_Cost=false | false | true |
| 41 | SPLIT if $TravelLocation=empty | true | false |
| 46 | SPLIT if $TravelLocation/CostPricingOnly | true | false |
| 49 | SPLIT if $PartyCostGroup_BLList=empty | false | true |
| 52 | SPLIT if $Count=1 | true | false |
one row per branch condition - a QA checklist, not a test case list.
10. What this document does not cover¶
| excluded | live_documents | reason | source |
|---|---|---|---|
| .OCh_ | 316 | Seven live flows recalculate price fields when someone edits a figure on screen: Booking.OCh_ApplyMargin_MU, Booking.OCh_BL_CancelFees, Booking.OCh_DBL_Days, Dashboard.OCh_CalcNetCostPrice, Dashboard.OCh_CalcNetSellingPrice, Dashboard.OCh_CalcNetSellingPrice_Dbl and Supplier_Portal.OCh_CalcNetCostPrice_DMC. All are triggered from pages - Booking.EditPrice, Booking.BookingLine_AmendCancelFees, Dashboard.DetailedBookingLine_NewEdit, Supplier_Portal.DMC_Booking and Supplier_Portal.Supplier_Booking - and none is an entity event, so none runs during an automated pricing run. Excluded here as the editing surface rather than the calculation. Two of them deserve a look on their own terms, which is why this reason names them rather than hiding behind a pattern: Dashboard.OCh_CalcNetSellingPrice writes PPPriced, a pricing state flag, from an edit screen; and Supplier_Portal.OCh_CalcNetCostPrice_DMC writes SellingPrice and GrossProfit from the supplier and DMC portals, so an external party's screen adjusts sell-side figures. Whether either recalculates the same way Pricing.Sub_AddPriceToDetailedBookingLine_PCG does is unverified. | docs/deep-dives/pricing-detailed-lines.md#10 |
| Pricing/MF_Config/* | 167 | Rule configuration and maintenance screens. The documents describe how a rule is qualified and applied, not how an administrator edits one. 167 flows. | docs/deep-dives/pricing-rules.md#9 |
| Pricing/Forms/* | 84 | The pricing UI: 82 pages, untraced by decision. | docs/deep-dives/pricing-rules.md#9 |
| Pricing.Sub_Check*_List | 10 | The 23 per-type qualification flows - one per rule type, each encoding its own eligibility conditions. Explicitly excluded and named as excluded. | docs/deep-dives/pricing-rules.md#9 |
| .IVK_DetailedBookingLineCopy* | 2 | Booking.IVK_DetailedBookingLine_Copy and Booking.IVK_DetailedBookingLineRR_Copy, both in the Dynamic Pricing folder, Booking.Employee role. Verified: each creates a new detailed booking line copying every field from the source - quantity, pax, days, dates, currency, description and the price fields - then calls Dashboard.SumBookingLine. They carry figures across rather than computing them, so they are not part of the calculation. | docs/deep-dives/pricing-detailed-lines.md#10 |
| Pricing.CheckResidentsPricing* | 1 | The Residents / SADC path, stated as untraced and unverified. | docs/deep-dives/pricing-rules.md#9 |
this is the "does not cover" section, generated from journeys/.toml plus the model rather than from memory of where the author stopped reading*.
Beyond those decisions, named here because the code branches to them above and they are not traced in this document:
- Dynamic pricing and bar rates —
Pricing.DPSwitch,Pricing.CheckDynamicPricing,Pricing.Price_Siteminder, reached from [27]–[50] of the dispatcher. Pricing.Sub_GetPriceAfterStayPays— stay-pay deals rewrite the unit price before discounts; the mechanism is not traced, only its position in the order.Pricing.PriceAccom_MultiPG— one room shared across several cost groups, [14] of the options flow.Pricing.Sub_PriceBookingline_PCGandPricing.Sub_PriceBookingline_ByPeriod— the per-period pricing between the options flow and line creation.- Tax —
Pricing.CalculateDetailedBookingLineTax, covered in pricing calculation. - Mandatory extras —
Pricing.Price_MandExtras,Sub_PriceSelectedExtras*, which reach line creation by their own routes. - Rule values. Every percentage and threshold is a database row; the model gives the shape, only the running system has the numbers.