Supplier price breaks
Price breaks or volume discounts, represent situations where the margin unit price of goods is varying, depending on the quantity considered for the purchase. Usually, unit prices are decreasing when purchase quantities increase, in order to give the client an incentive to buy more. When suppliers offer price breaks, there is an economic incentive in having purchase quantities correspondingly adjusted to take advantage of those price breaks. Envision offers extensive support for supplier price breaks. In this section, we detail how to model price breaks from the purchasing optimization perspective.
Table of contents
Tabular price breaks
The most frequent way to represent price breaks for a list of products is to have a table with 3 columns:
Id
the identifier of the product.Quantity
the minimal quantity (inclusive) to get the price.Price
the purchase unit price of the product.
In order to keep the table readable, when looking at a given product, the lines are typically sorted by quantities in increasing order. This ordering helps making sense of the magnitude of the discounts.
However, once this tabular representation is adopted, there are two distinct way to interpret the price breaks:
- the merchant flavor of the price break: once the target quantity is reached, the lowered price is applied to all units. Ex: spoons are sold at 1€ per unit, with a price break at 100 where the unit price drops at 0.80€. Purchasing 50 spoons cost 50€. Purchasing 110 spoons costs 88€.
- the fiscal flavor of the price break: once the target quantity is reached, the lowered price is applied to units beyond the target quantity. Ex: spoons are sold at 1€ per unit, with a price break at 100 where the unit price drops at 0.80€. Purchasing 50 spoons cost 50€. Purchasing 110 spoons costs 108€.
The main benefit of the merchant flavor is simplicity. In order to compute the final price, one only needs to look up the applicable unit price, and a single multiplication gives the final price. However, the merchant flavor also yields bizarre pricing effects. In the first example above, purchasing 99 spoons costs 99€, which happens to be more expensive than the 110 spoons priced at 88€. This flavor is frequently found among B2C merchants selling to a diverse crowd.
On the other hand, the main benefit of the fiscal flavor is consistency. There is no way to “game” the pricing, the total price is strictly increasing with the quantity. However, as a major drawback, without an Excel sheet or some kind of dedicated price calculator, it is not possible to reliably compute the final price for a given quantity. This flavor is frequently found among B2B merchants selling to professionals.
In both cases, the price break function - which represents the marginal unit price for any given quantity - can be represented as a zedfunc, a native specialized datatype in Envision that is precisely intended to support these situations.
pricebrk.m(…) and pricebrk.f(…)
The purpose of the pricebrk.m()
(resp. pricebrk.f()
) function is to transform a merchant (resp. fiscal) price break from its tabular form to its corresponding zedfunc. The Envision syntax is:
T.ZM = pricebrk.m(T.StartPrice, B.Quantity, B.Price)
T.ZF = pricebrk.f(T.StartPrice, B.Quantity, B.Price)
Where T
is expected to be the table containing the list of products, while B
is expected to be an extension of the table T
that contains the price breaks - if any. The StartPrice
is used as the per unit price until a price break is encountered.
In the case of the merchant flavor, the resulting zedfunc ZM
may evaluate to negative values if a situation similar to the example above is encountered.
Ordering space vs. Stocking space
The price break zedfunc is computed from an ordering perspective, reflecting the marginal purchase price for every purchased quantity. However, from an inventory optimization perspective, we are usually considering the economic reward associated with the acquisition of one more unit of stock, thus taking into account the stock already available. The inventory optimization happens in the stocking space.
Translating the price break zedfunc into the stocking space is straight forward with the shift()
function as illustrated:
T.ZX = shift(T.ZM, (T.StockOnHand + T.StockOnOrder))
Where StockOnHand
and StockOnHand
are expected to be two quantities that are used to reflect the total quantity that must be considered prior to purchasing more of the product.
Stock rewards and price breaks
In a previous section, we have seen how the stock reward function can be associated to economic variables that represent the gross margin, the stock out penalty and the carrying cost. In our previous discussion, those economic variables were plain numbers. However, when price breaks are involved, those economic variables are also varying along with the quantity being ordered. These variations are straightforward to model through zedfuncs, as obtained from pricebrk.m()
or pricebrk.f()
:
// T.Demand is a ranvar, probabilistic forecast, precomputed
// T.Leadtime is a ranvar, probabilistic forecast, precomputed
// 'B' is the marginal buy price - in the ordering space
T.B = pricebrk.m(T.StartBuyPrice, Breaks.Quantity, Break.Price)
// shifting 'B' - now expressed in the storage space
T.B = shift(T.B, (T.StockOnHand + T.StockOnOrder))
// M, S and C are zedfuncs
T.M = T.SellPrice - T.B
T.S = -0.5 * (T.SellPrice - T.B)
T.C = -0.3 * T.B * mean(T.Leadtime) / 365
T.RM = stockrwd.m(T.Demand, 0.3) * T.M // margin (quantity dependent)
T.RS = stockrwd.s(T.Demand) * S // stock-out penalty
T.RC = stockrwd.c(T.Demand, 0.3) * T.C // purchase
// 'R' is the marginal economic reward for every stock position
T.R = T.RM + T.RS + T.RC
At lines 11-13, the economic variables are turned into zedfuncs. When a number is added (or multiplied) to a zedfunc, the result of the operation is a zedfunc. Thus, M
, S
and C
are zedfuncs as being the results of operations that involve B
a zedfunc.
Yet, while we are operating on zedfunc, it’s notable that the economic calculation remain unchanged. Under the hood, point-wise operations are performed over zedfuncs rather than numbers, but this complexity is completely taken care of by the algebra of zedfuncs itself.
Finally, at line 15, the final stock reward is composed just as we did in the previous section.