from dataclasses import replace from .money_amount import Money, money from .payroll_gross_to_net import gross_to_net 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)