from typing import List, Sequence from .dates_days_between import days_between from .finance_tax_vat_return_types import VatReturn, VatTransaction from .math_round_div import round_div from .money_add import add_money from .money_amount import Money, money _ZERO_VAT_SALES = ("exempt", "outside-scope", "domestic-reverse-charge", "services-reverse-charge", "ni-eu-goods") _ZERO_VAT_PURCHASES = ("exempt", "outside-scope") _TREATMENTS = ( "taxable", "exempt", "outside-scope", "domestic-reverse-charge", "services-reverse-charge", "postponed-import-vat", "ni-eu-goods", ) def _whole_pounds(amount: Money) -> Money: """Drop the pence, towards zero: boxes 6 to 9 are whole pounds.""" return money(round_div(amount.minor, 100, "down") * 100, amount.currency) def vat_return(transactions: Sequence[VatTransaction], period_start: str, period_end: str) -> VatReturn: """The nine boxes of a UK VAT return for a period. Each transaction feeds the boxes its direction and treatment call for (VAT Notice 700/12 sections 3 and 4.6); boxes 6 to 9 drop the pence after totalling. """ if days_between(period_start, period_end) < 0: raise ValueError("VAT return period ends %s, before it starts %s" % (period_end, period_start)) box: List[Money] = [money(0, "GBP") for _ in range(10)] count = 0 for index, t in enumerate(transactions): n = index + 1 if t.direction not in ("sale", "purchase"): raise ValueError('transaction %d: unknown direction "%s"' % (n, t.direction)) if t.treatment not in _TREATMENTS: raise ValueError('transaction %d: unknown VAT treatment "%s"' % (n, t.treatment)) if t.net.currency != "GBP" or t.vat.currency != "GBP": raise ValueError("transaction %d: amounts must be in GBP" % (n,)) sale = t.direction == "sale" if sale and t.treatment == "postponed-import-vat": raise ValueError("transaction %d: postponed-import-vat applies to purchases only" % (n,)) if t.vat.minor != 0 and t.treatment in (_ZERO_VAT_SALES if sale else _ZERO_VAT_PURCHASES): raise ValueError("transaction %d: %s %ss must have zero VAT" % (n, t.treatment, t.direction)) if days_between(period_start, t.date) < 0 or days_between(t.date, period_end) < 0: continue count += 1 def add(b: int, amount: Money) -> None: box[b] = add_money(box[b], amount) if sale: if t.treatment == "taxable": add(1, t.vat) if t.treatment != "outside-scope": add(6, t.net) if t.treatment == "ni-eu-goods": add(8, t.net) elif t.treatment == "taxable": add(4, t.vat) add(7, t.net) elif t.treatment == "exempt": add(7, t.net) elif t.treatment in ("domestic-reverse-charge", "services-reverse-charge", "postponed-import-vat"): if t.treatment == "services-reverse-charge": add(6, t.net) add(1, t.vat) add(4, t.vat) add(7, t.net) elif t.treatment == "ni-eu-goods": add(2, t.vat) add(4, t.vat) add(7, t.net) add(9, t.net) box3 = add_money(box[1], box[2]) difference = box3.minor - box[4].minor return VatReturn( box1=box[1], box2=box[2], box3=box3, box4=box[4], box5=money(abs(difference), "GBP"), box6=_whole_pounds(box[6]), box7=_whole_pounds(box[7]), box8=_whole_pounds(box[8]), box9=_whole_pounds(box[9]), repayment=difference < 0, transactions=count, )