Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .dates_days_between import days_between ← from dates.days-between ^1.0.0 · built alongside by fune
from .energy_bill_calculate_types import EnergyBill, EnergyBillInput
from .energy_climate_change_levy import climate_change_levy ← from energy.climate-change-levy ^1.0.0 · built alongside by fune
from .energy_gas_kwh import gas_kwh ← from energy.gas-kwh ^1.0.0 · built alongside by fune
from .energy_meter_advance import meter_advance ← from energy.meter-advance ^1.0.0 · built alongside by fune
from .finance_tax_add_vat import add_vat ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode, round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
# watt-hours x thousandths of a penny per kWh is millionths of a penny.
DIVISOR = 1_000_000
MAX_SAFE = 9007199254740991
def _check_rate(what: str, value: object) -> None:
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise ValueError("%s must be a whole number of thousandths of a penny, 0 or more, received %s" % (what, value))
def _exact(product: int) -> int:
if product > MAX_SAFE:
raise ValueError("energy charge too large to calculate exactly")
return product
def calculate_energy_bill(input: EnergyBillInput, mode: RoundingMode) -> EnergyBill:
"""A GB electricity or gas bill from two reads: energy at the unit rate,
standing charge by the day, Climate Change Levy unless the supply is for
qualifying use, and VAT on the lot at the caller's category.
"""
if input.fuel not in ("electricity", "gas"):
raise ValueError('unknown fuel "%s": expected electricity or gas' % (input.fuel,))
_check_rate("unitRate", input.unit_rate)
_check_rate("standingCharge", input.standing_charge)
units = meter_advance(input.opening_read.value, input.closing_read.value, input.meter_digits)
days = days_between(input.opening_read.date, input.closing_read.date)
if days < 0:
raise ValueError(
"the closing read must not be dated before the opening read, received %s and %s"
% (input.opening_read.date, input.closing_read.date)
)
if input.fuel == "gas":
if input.gas_unit is None or input.calorific_value is None:
raise ValueError("a gas bill needs gasUnit and calorificValue")
watt_hours = gas_kwh(units, input.gas_unit, input.calorific_value).watt_hours
else:
if input.gas_unit is not None or input.calorific_value is not None:
raise ValueError("gasUnit and calorificValue are for gas bills only")
watt_hours = units * 1000
energy = round_div(_exact(watt_hours * input.unit_rate), DIVISOR, mode)
standing = round_div(_exact(days * input.standing_charge), 1000, mode)
ccl = climate_change_levy(input.fuel, watt_hours, input.closing_read.date, input.qualifying_use, mode).levy.minor
vat = add_vat(money(energy + standing + ccl, "GBP"), "GB", input.vat_category, input.closing_read.date)
return EnergyBill(
units=units,
watt_hours=watt_hours,
days=days,
energy_charge=money(energy, "GBP"),
standing_charge=money(standing, "GBP"),
climate_change_levy=money(ccl, "GBP"),
net=vat.net,
vat=vat.tax,
vat_basis_points=vat.basis_points,
total=vat.gross,
)