import re from .construction_cis_deduction_data import CIS_RATES, CIS_RATES_HISTORY from .construction_cis_deduction_types import CisDeduction, CisStatus from .math_round_div import RoundingMode from .money_add import subtract_money from .money_amount import Money from .money_apply_rate import apply_rate 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), )