use super::funejson::Value; use super::dates_days_between::days_between; use super::math_round_div::round_div; use super::money_add::add_money; use super::money_amount::{money, money_from_value, money_to_value, Money}; const ZERO_VAT_SALES: [&str; 5] = ["exempt", "outside-scope", "domestic-reverse-charge", "services-reverse-charge", "ni-eu-goods"]; const ZERO_VAT_PURCHASES: [&str; 2] = ["exempt", "outside-scope"]; const TREATMENTS: [&str; 7] = [ "taxable", "exempt", "outside-scope", "domestic-reverse-charge", "services-reverse-charge", "postponed-import-vat", "ni-eu-goods", ]; /// Drop the pence, towards zero: boxes 6 to 9 are whole pounds. fn whole_pounds(amount: &Money) -> Money { money(round_div(amount.minor, 100, "down") * 100, &amount.currency) } /// 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. /// /// # Panics /// Panics on a period that ends before it starts, an unknown direction or /// treatment, non-GBP amounts, or VAT on a treatment that carries none. pub fn vat_return(transactions: &[VatTransaction], period_start: &str, period_end: &str) -> VatReturn { if days_between(period_start, period_end) < 0 { panic!("VAT return period ends {}, before it starts {}", period_end, period_start); } let mut boxes: Vec = (0..10).map(|_| money(0, "GBP")).collect(); let mut count = 0; for (index, t) in transactions.iter().enumerate() { let n = index + 1; if t.direction != "sale" && t.direction != "purchase" { panic!("transaction {}: unknown direction \"{}\"", n, t.direction); } if !TREATMENTS.contains(&t.treatment.as_str()) { panic!("transaction {}: unknown VAT treatment \"{}\"", n, t.treatment); } if t.net.currency != "GBP" || t.vat.currency != "GBP" { panic!("transaction {}: amounts must be in GBP", n); } let sale = t.direction == "sale"; if sale && t.treatment == "postponed-import-vat" { panic!("transaction {}: postponed-import-vat applies to purchases only", n); } let zero_vat = if sale { ZERO_VAT_SALES.contains(&t.treatment.as_str()) } else { ZERO_VAT_PURCHASES.contains(&t.treatment.as_str()) }; if t.vat.minor != 0 && zero_vat { panic!("transaction {}: {} {}s must have zero VAT", n, t.treatment, t.direction); } if days_between(period_start, &t.date) < 0 || days_between(&t.date, period_end) < 0 { continue; } count += 1; let mut add = |b: usize, amount: &Money| { boxes[b] = add_money(&boxes[b], amount); }; let treatment = t.treatment.as_str(); if sale { if treatment == "taxable" { add(1, &t.vat); } if treatment != "outside-scope" { add(6, &t.net); } if treatment == "ni-eu-goods" { add(8, &t.net); } } else { match treatment { "taxable" => { add(4, &t.vat); add(7, &t.net); } "exempt" => add(7, &t.net), "domestic-reverse-charge" | "services-reverse-charge" | "postponed-import-vat" => { if treatment == "services-reverse-charge" { add(6, &t.net); } add(1, &t.vat); add(4, &t.vat); add(7, &t.net); } "ni-eu-goods" => { add(2, &t.vat); add(4, &t.vat); add(7, &t.net); add(9, &t.net); } _ => {} } } } let box3 = add_money(&boxes[1], &boxes[2]); let difference = box3.minor - boxes[4].minor; VatReturn { box1: boxes[1].clone(), box2: boxes[2].clone(), box3, box4: boxes[4].clone(), box5: money(difference.abs(), "GBP"), box6: whole_pounds(&boxes[6]), box7: whole_pounds(&boxes[7]), box8: whole_pounds(&boxes[8]), box9: whole_pounds(&boxes[9]), repayment: difference < 0, transactions: count, } } pub fn vat_transaction_from_value(v: &Value) -> VatTransaction { VatTransaction { date: v.get("date").as_str().to_string(), direction: v.get("direction").as_str().to_string(), treatment: v.get("treatment").as_str().to_string(), net: money_from_value(v.get("net")), vat: money_from_value(v.get("vat")), } } pub fn vat_return_to_value(r: &VatReturn) -> Value { Value::obj(vec![ ("box1", money_to_value(&r.box1)), ("box2", money_to_value(&r.box2)), ("box3", money_to_value(&r.box3)), ("box4", money_to_value(&r.box4)), ("box5", money_to_value(&r.box5)), ("box6", money_to_value(&r.box6)), ("box7", money_to_value(&r.box7)), ("box8", money_to_value(&r.box8)), ("box9", money_to_value(&r.box9)), ("repayment", Value::Bool(r.repayment)), ("transactions", Value::Int(r.transactions)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let transactions: Vec = args[0].as_arr().iter().map(vat_transaction_from_value).collect(); vat_return_to_value(&vat_return(&transactions, args[1].as_str(), args[2].as_str())) }