From 4767d4bb46b851e8b1b9d5143b39860902739228 Mon Sep 17 00:00:00 2001 From: kurogeek Date: Fri, 11 Sep 2026 10:09:01 +0000 Subject: [PATCH] test: withholding tax accounts, fixture re-import, and customer receipts Integration tests on a throwaway Thai company: account creation and category linking on Company insert, site-configured account surviving fixture re-import, Sales Invoice left un-grossed, and Payment Entry deductions (full, prorated, below threshold, unchecked). Run with CI=1 bench --site run-tests --app default_thai_company. --- default_thai_company/tests/__init__.py | 0 .../tests/test_tax_withholding.py | 202 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 default_thai_company/tests/__init__.py create mode 100644 default_thai_company/tests/test_tax_withholding.py diff --git a/default_thai_company/tests/__init__.py b/default_thai_company/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/default_thai_company/tests/test_tax_withholding.py b/default_thai_company/tests/test_tax_withholding.py new file mode 100644 index 0000000..503fad8 --- /dev/null +++ b/default_thai_company/tests/test_tax_withholding.py @@ -0,0 +1,202 @@ +import frappe +from frappe.modules.import_file import import_file_by_path +from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate + +from default_thai_company.tax_withholding import ASSET_ACCOUNT, LIABILITY_ACCOUNT, 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") + + +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) + + 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( + { + "doctype": "Account", + "company": COMPANY, + "account_name": "Output VAT", + "parent_account": f"Duties and Taxes - {ABBR}", + "account_type": "Tax", + } + ).insert() + + 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): + si = frappe.get_doc( + { + "doctype": "Sales Invoice", + "company": COMPANY, + "customer": CUSTOMER, + "posting_date": nowdate(), + "due_date": nowdate(), + "items": [{"item_code": ITEM, "qty": 1, "rate": rate}], + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": self.vat.name, + "rate": 7, + "description": "VAT 7%", + } + ], + } + ) + si.set_missing_values() + return si.submit() + + def make_receipt(self, invoice, allocated, paid, apply=1): + 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": paid, + "received_amount": paid, + "apply_tax_withholding_amount": apply, + "references": [ + { + "reference_doctype": "Sales Invoice", + "reference_name": invoice.name, + "allocated_amount": allocated, + } + ], + } + ).insert() + + 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()) + self.assertEqual( + frappe.db.get_value("Account", self.payable, ["root_type", "parent_account"]), + ("Liability", f"Duties and Taxes - {ABBR}"), + ) + self.assertEqual( + frappe.db.get_value("Account", self.receivable, ["root_type", "parent_account"]), + ("Asset", f"Tax Assets - {ABBR}"), + ) + + 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_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_is_not_grossed_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]) + + def test_receipt_deducts_withholding_and_settles_invoice(self): + si = self.make_invoice() + pe = self.make_receipt(si, allocated=10700, paid=10400) + + self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service") + self.assertEqual([(d.account, d.amount) for d in pe.deductions], [(self.receivable, 300.0)]) + self.assertEqual(pe.difference_amount, 0) + + pe.submit() + gl = { + g.account: (g.debit, g.credit) + for g in frappe.get_all( + "GL Entry", filters={"voucher_no": pe.name}, fields=["account", "debit", "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(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 0) + + def test_partial_allocation_prorates_withholding(self): + si = self.make_invoice() + pe = self.make_receipt(si, allocated=5350, paid=5200) + self.assertEqual([(d.account, d.amount) for d in pe.deductions], [(self.receivable, 150.0)]) + self.assertEqual(pe.difference_amount, 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, paid=856) + self.assertEqual(pe.deductions, []) + self.assertEqual(pe.difference_amount, 0) + + def test_unchecked_receipt_is_untouched(self): + si = self.make_invoice() + pe = self.make_receipt(si, allocated=10700, paid=10700, apply=0) + self.assertEqual(pe.deductions, []) + self.assertEqual(pe.difference_amount, 0)