from typing import List, Sequence from .finance_invoice_calculate_types import InvoiceLine, CalculatedLine, TaxGroup, Invoice from .finance_invoice_line_total import line_total from .finance_tax_add_vat import add_vat from .money_add import add_money from .money_amount import Money, money from .money_sum import sum_money def calculate_invoice(lines: Sequence[InvoiceLine], jurisdiction: str, invoice_date: str) -> Invoice: """Calculate a complete invoice. Note what this function does not contain: no multiplication, no percentages, no rounding, no rate table. Each of those is a capability that is already pinned by its own vectors. What is left here is the part that is genuinely about invoices - the order of operations, and how tax is grouped. VAT is calculated per line and then grouped by rate, which keeps each line's tax reconcilable against that line. """ if len(lines) == 0: raise ValueError("an invoice needs at least one line") currency = lines[0].unit_price.currency calculated: List[CalculatedLine] = [] for line in lines: net = line_total(line.unit_price, line.quantity, line.discount_basis_points) vat = add_vat(net, jurisdiction, line.tax_category, invoice_date) calculated.append( CalculatedLine( description=line.description, net=vat.net, tax=vat.tax, gross=vat.gross, basis_points=vat.basis_points, ) ) subtotal = sum_money([line.net for line in calculated], currency) tax_total = sum_money([line.tax for line in calculated], currency) # Grouped by rate and ordered by rate, so the summary block on a printed # invoice is stable between runs and between languages. groups = {} for line in calculated: taxable, tax = groups.get(line.basis_points, (money(0, currency), money(0, currency))) groups[line.basis_points] = (add_money(taxable, line.net), add_money(tax, line.tax)) tax_breakdown = [ TaxGroup(basis_points=basis_points, taxable=taxable, tax=tax) for basis_points, (taxable, tax) in sorted(groups.items()) ] return Invoice( currency=currency, lines=calculated, subtotal=subtotal, tax_total=tax_total, total=add_money(subtotal, tax_total), tax_breakdown=tax_breakdown, )