Functional Weave
Code in Rust

construction.cis-deduction@1.0.2

impl/python.py

3,014 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re

from .construction_cis_deduction_data import CIS_RATES, CIS_RATES_HISTORY  ← this capability’s own data, compiled from data/cis-rates.json into the same file by fune build
from .construction_cis_deduction_types import CisDeduction, CisStatus
from .math_round_div import RoundingMode  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_add import subtract_money  ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import Money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate  ← from money.apply-rate ^1.0.0 · built alongside by fune

ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _rate_on(status: str, on_date: str) -> int:
    best = None
    earliest = None
    for rule in CIS_RATES:
        if rule.status != status:
            continue
        if earliest is None or rule.valid_from < earliest:
            earliest = rule.valid_from
        if on_date < rule.valid_from:
            continue
        if rule.valid_to is not None and on_date > rule.valid_to:
            continue
        if best is None or rule.valid_from > best.valid_from:
            best = rule
    if best is not None:
        return best.basis_points
    # A history=current build has dropped the old rows; say so rather than
    # answering an old payment at today's rate.
    if CIS_RATES_HISTORY != "full" and earliest is not None and on_date < earliest:
        raise ValueError(
            "no CIS rate for %s on %s: this build was installed with history=%s, "
            "so it only carries rates from %s. Reinstall with history=full for older payments."
            % (status, on_date, CIS_RATES_HISTORY, earliest)
        )
    raise ValueError("no CIS rate for %s on %s" % (status, on_date))


def cis_deduction(payment: Money, materials: Money, status: CisStatus, on_date: str, mode: RoundingMode) -> CisDeduction:
    """The CIS deduction a contractor withholds from a subcontractor's payment.

    The rate applies only to the labour: VAT is left out by taking the payment
    net of VAT, and materials (with consumables, non-travel fuel, plant hire and
    prefabricated materials) are taken off before the rate is applied. Applying
    20% to the whole invoice is the classic over-deduction.
    """
    if status not in ("gross", "standard", "higher"):
        raise ValueError('unknown CIS status "%s"' % (status,))
    if not isinstance(on_date, str) or not ISO_DATE.fullmatch(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    if payment.minor < 0:
        raise ValueError("payment must not be negative, received %d" % (payment.minor,))
    if materials.minor < 0:
        raise ValueError("materials must not be negative, received %d" % (materials.minor,))
    labour = subtract_money(payment, materials)
    if labour.minor < 0:
        raise ValueError("materials (%d) cannot exceed the payment (%d)" % (materials.minor, payment.minor))
    basis_points = _rate_on(status, on_date)
    deduction = apply_rate(labour, basis_points, mode)
    return CisDeduction(
        payment=payment,
        materials=materials,
        labour=labour,
        basis_points=basis_points,
        deduction=deduction,
        net_payment=subtract_money(payment, deduction),
    )