Skip to content

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_RoomPricing.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.GetTpPricingthe 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:

DiscountPercentage_ExclManual = $Variable_TotalDiscountPercentage

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 ratesPricing.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_PCG and Pricing.Sub_PriceBookingline_ByPeriod — the per-period pricing between the options flow and line creation.
  • TaxPricing.CalculateDetailedBookingLineTax, covered in pricing calculation.
  • Mandatory extrasPricing.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.