Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from dataclasses import replace
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_gross_to_net import gross_to_net ← from payroll.gross-to-net ^1.0.0 · built alongside by fune
from .payroll_gross_to_net_types import Payslip, PayslipInput
# The search refuses to look above 10,000,000 pounds of gross pay for one period.
CEILING = 1_000_000_000
def net_to_gross(target_net: Money, input: PayslipInput) -> Payslip:
"""Gross up a net payment: the gross pay whose payslip nets at least the target.
Net pay is not a smooth function of gross: tax is on whole pounds, student
loans are rounded down to whole pounds, and pension and NI thresholds
switch on. So there is no formula to invert, and a search is the honest
method. It is a bisection on whole pence, with fixed bounds and midpoints,
so it takes the same steps and finds the same answer in every language: a
gross G whose net is at least the target while the net at G - 1p is below it.
"""
if target_net.currency != "GBP":
raise ValueError("payroll amounts must be in GBP, received %s for targetNet" % (target_net.currency,))
if target_net.minor <= 0:
raise ValueError("targetNet must be more than zero, received %s" % (target_net.minor,))
def slip(gross: int) -> Payslip:
return gross_to_net(replace(input, gross=money(gross, "GBP")))
# A refund can make net exceed gross, so zero gross may already be enough.
at_zero = slip(0)
if at_zero.net.minor >= target_net.minor:
return at_zero
low = 0
high = target_net.minor
while slip(high).net.minor < target_net.minor:
low = high
high = high * 2
if high > CEILING:
raise ValueError("no gross pay up to %d pence gives a net of %d pence" % (CEILING, target_net.minor))
while high - low > 1:
middle = (low + high) // 2
if slip(middle).net.minor >= target_net.minor:
high = middle
else:
low = middle
return slip(high)