import json import frappe from frappe.modules.import_file import import_file_by_path from frappe.modules.utils import sync_customizations_for_doctype from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from default_thai_company.tax_withholding import ( ASSET_ACCOUNT, INPUT_VAT_ACCOUNT, LIABILITY_ACCOUNT, OUTPUT_VAT_ACCOUNT, get_payment_entry, thai_companies, ) COMPANY = "_Test WHT Company" ABBR = "_TWC" CUSTOMER = "_Test WHT Customer" ITEM = "_Test WHT Service" FIXTURE = frappe.get_app_path("default_thai_company", "fixtures", "tax_withholding_category.json") CUSTOM_DIR = frappe.get_app_path("default_thai_company", "default_thai_company", "custom") class TestTaxWithholding(FrappeTestCase): @classmethod def setUpClass(cls): super().setUpClass() # Fixture categories must exist before the company is created so the # Company hook has something to link; force=True re-imports. import_file_by_path(FIXTURE, force=True, data_import=True) for fname in ("sales_invoice.json", "payment_entry.json"): with open(f"{CUSTOM_DIR}/{fname}") as f: sync_customizations_for_doctype(json.load(f), CUSTOM_DIR, fname) frappe.get_doc( { "doctype": "Company", "company_name": COMPANY, "abbr": ABBR, "country": "Thailand", "default_currency": "THB", "chart_of_accounts": "Standard", } ).insert() cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}" cls.receivable = f"{ASSET_ACCOUNT} - {ABBR}" cls.vat = frappe.get_doc("Account", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}") frappe.get_doc( { "doctype": "Customer", "customer_name": CUSTOMER, "customer_type": "Company", "customer_group": frappe.db.get_value("Customer Group", {"is_group": 0}), "territory": frappe.db.get_value("Territory", {"is_group": 0}), "tax_withholding_category": "WHT 3% - Service", } ).insert() frappe.get_doc( { "doctype": "Item", "item_code": ITEM, "item_group": frappe.db.get_value("Item Group", {"is_group": 0}), "is_stock_item": 0, "stock_uom": "Nos", } ).insert() def make_invoice(self, rate=10000, category=None, inclusive=False): si = frappe.get_doc( { "doctype": "Sales Invoice", "company": COMPANY, "customer": CUSTOMER, "posting_date": nowdate(), "due_date": nowdate(), "tax_withholding_category": category, "items": [{"item_code": ITEM, "qty": 1, "rate": rate}], "taxes": [ { "charge_type": "On Net Total", "account_head": self.vat.name, "rate": 7, "description": "VAT 7%", "included_in_print_rate": int(inclusive), } ], } ) si.set_missing_values() return si.submit() def make_receipt(self, invoice, allocated, apply=1): """Paid amount is the gross allocation; withholding reduces what reaches the bank.""" return frappe.get_doc( { "doctype": "Payment Entry", "company": COMPANY, "payment_type": "Receive", "party_type": "Customer", "party": CUSTOMER, "posting_date": nowdate(), "paid_from": f"Debtors - {ABBR}", "paid_to": f"Cash - {ABBR}", "paid_amount": allocated, "received_amount": allocated, "apply_tax_withholding_amount": apply, "references": [ { "reference_doctype": "Sales Invoice", "reference_name": invoice.name, "allocated_amount": allocated, } ], } ).insert() def withheld(self, pe): return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes] def category_account(self, category, company=COMPANY): return frappe.db.get_value( "Tax Withholding Account", {"parent": category, "parenttype": "Tax Withholding Category", "company": company}, "account", ) def test_company_creation_adds_accounts_and_links_categories(self): self.assertIn(COMPANY, thai_companies()) for account, expected in ( (self.payable, ("Liability", f"Duties and Taxes - {ABBR}", "Tax")), (self.receivable, ("Asset", f"Tax Assets - {ABBR}", "Tax")), (f"{OUTPUT_VAT_ACCOUNT} - {ABBR}", ("Liability", f"Duties and Taxes - {ABBR}", "Tax")), (f"{INPUT_VAT_ACCOUNT} - {ABBR}", ("Asset", f"Tax Assets - {ABBR}", "Tax")), ): self.assertEqual( frappe.db.get_value("Account", account, ["root_type", "parent_account", "account_type"]), expected, account, ) categories = frappe.get_all( "Tax Withholding Category", filters={"name": ("like", "WHT %")}, pluck="name" ) self.assertEqual(len(categories), 22) for name in categories: self.assertEqual(self.category_account(name), self.payable, name) def test_company_creation_adds_vat_schemes(self): for doctype, account in ( ("Sales Taxes and Charges Template", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}"), ("Purchase Taxes and Charges Template", f"{INPUT_VAT_ACCOUNT} - {ABBR}"), ): schemes = {} for name in frappe.get_all( doctype, filters={"company": COMPANY, "title": ("like", "Thailand VAT%")}, pluck="name" ): doc = frappe.get_doc(doctype, name) (row,) = doc.taxes schemes[doc.title] = ( doc.is_default, row.account_head, row.charge_type, row.rate, row.included_in_print_rate, ) self.assertEqual( schemes, { "Thailand VAT 7%": (1, account, "On Net Total", 7.0, 0), "Thailand VAT 7% (Included)": (0, account, "On Net Total", 7.0, 1), "Thailand VAT 0%": (0, account, "On Net Total", 0.0, 0), }, doctype, ) def test_company_creation_adds_shipping_rule(self): rule = frappe.get_doc("Shipping Rule", f"Shipping Charges - {ABBR}") self.assertEqual( (rule.company, rule.shipping_rule_type, rule.calculate_based_on, rule.account, rule.cost_center), (COMPANY, "Selling", "Fixed", f"Shipping Charges - {ABBR}", f"Main - {ABBR}"), ) self.assertEqual(frappe.db.get_value("Account", rule.account, "root_type"), "Income") def test_shipping_amount_entered_on_transaction_survives_recalculation(self): rule = f"Shipping Charges - {ABBR}" so = frappe.get_doc( { "doctype": "Sales Order", "company": COMPANY, "customer": CUSTOMER, "transaction_date": nowdate(), "delivery_date": nowdate(), "shipping_rule": rule, "items": [{"item_code": ITEM, "qty": 1, "rate": 1000}], } ) so.set_missing_values() so.apply_shipping_rule() (charge,) = so.taxes self.assertEqual((charge.description, charge.tax_amount), (rule, 0)) charge.tax_amount = 150 so.insert() so.apply_shipping_rule() so.save() self.assertEqual([t.tax_amount for t in so.taxes], [150]) self.assertEqual(so.grand_total, 1150) def test_fixture_reimport_keeps_site_account_and_relinks(self): alt = frappe.get_doc( { "doctype": "Account", "company": COMPANY, "account_name": "Alternative WHT Payable", "parent_account": f"Duties and Taxes - {ABBR}", "account_type": "Tax", } ).insert() rent = frappe.get_doc("Tax Withholding Category", "WHT 5% - Rent") for row in rent.accounts: if row.company == COMPANY: row.account = alt.name rent.save() import_file_by_path(FIXTURE, force=True, data_import=True) self.assertEqual(self.category_account("WHT 5% - Rent"), alt.name) self.assertEqual(self.category_account("WHT 3% - Service"), self.payable) rates = frappe.get_doc("Tax Withholding Category", "WHT 5% - Rent").rates self.assertEqual([(r.tax_withholding_rate, r.single_threshold) for r in rates], [(5.0, 1000.0)]) def test_sales_invoice_shows_withholding_without_grossing_up(self): si = self.make_invoice() self.assertEqual((si.net_total, si.grand_total), (10000.0, 10700.0)) self.assertEqual([t.account_head for t in si.taxes], [self.vat.name]) self.assertEqual(si.tax_withholding_category, "WHT 3% - Service") # fetched from Customer self.assertEqual((si.withholding_tax_amount, si.amount_after_withholding), (300.0, 10400.0)) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 10700.0) def test_sales_invoice_below_threshold_shows_nothing(self): si = self.make_invoice(rate=800) self.assertEqual((si.withholding_tax_amount, si.amount_after_withholding), (0.0, 0.0)) def test_receipt_uses_invoice_category_over_customer_category(self): si = self.make_invoice(category="WHT 5% - Rent") self.assertEqual(si.withholding_tax_amount, 500.0) pe = self.make_receipt(si, allocated=10700) self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 500.0)]) self.assertEqual(pe.received_amount_after_tax, 10200.0) def test_receipt_deducts_withholding_and_settles_invoice(self): si = self.make_invoice() pe = self.make_receipt(si, allocated=10700) self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service") self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)]) self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0)) self.assertEqual((pe.unallocated_amount, pe.difference_amount), (0, 0)) pe.submit() gl = {} for g in frappe.get_all( "GL Entry", filters={"voucher_no": pe.name}, fields=["account", "debit", "credit"] ): gl.setdefault(g.account, [0, 0]) gl[g.account][0] += g.debit gl[g.account][1] += g.credit self.assertEqual(gl[f"Cash - {ABBR}"], [10400.0, 0.0]) self.assertEqual(gl[self.receivable], [300.0, 0.0]) self.assertEqual(gl[f"Debtors - {ABBR}"], [0.0, 10700.0]) self.assertEqual(len(gl), 3) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 0) def test_inclusive_vat_invoice_withholds_on_pre_vat_amount(self): si = self.make_invoice(inclusive=True) # 10,000 incl. 7% VAT -> net 9,345.79 self.assertEqual((si.net_total, si.grand_total), (9345.79, 10000.0)) self.assertEqual(si.withholding_tax_amount, 280.37) pe = self.make_receipt(si, allocated=10000) self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 280.37)]) self.assertEqual(pe.received_amount_after_tax, 9719.63) def test_partial_allocation_prorates_withholding(self): si = self.make_invoice() pe = self.make_receipt(si, allocated=5350) self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)]) self.assertEqual(pe.received_amount_after_tax, 5200.0) def test_below_single_threshold_has_no_deduction(self): si = self.make_invoice(rate=800) # net 800 < 1,000 threshold pe = self.make_receipt(si, allocated=856, apply=0) self.assertEqual(pe.apply_tax_withholding_amount, 0) # invoice carries no withholding self.assertEqual(pe.taxes, []) self.assertEqual(pe.received_amount_after_tax, 856.0) def test_create_payment_from_invoice_applies_withholding(self): si = self.make_invoice() pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}") self.assertEqual(pe.apply_tax_withholding_amount, 1) self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service") self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)]) self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0)) pe.insert() self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)]) def test_new_receipt_referencing_withheld_invoice_applies_on_save(self): si = self.make_invoice() pe = self.make_receipt(si, allocated=10700, apply=0) self.assertEqual(pe.apply_tax_withholding_amount, 1) self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)]) def test_unticking_after_save_removes_withholding(self): si = self.make_invoice() pe = self.make_receipt(si, allocated=10700) pe.apply_tax_withholding_amount = 0 pe.save() self.assertEqual(pe.apply_tax_withholding_amount, 0) # saved docs keep the user's choice self.assertEqual(pe.taxes, []) self.assertEqual(pe.received_amount_after_tax, 10700.0) def test_reallocation_on_form_recomputes_withholding(self): si = self.make_invoice() pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}") pe.paid_amount = pe.received_amount = 5350 pe.allocate_amount_to_references( paid_amount=5350, paid_amount_change=True, allocate_payment_amount=True ) self.assertEqual(pe.references[0].allocated_amount, 5350.0) self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])