Files
default_thai_company/default_thai_company/tests/test_tax_withholding.py
T
kurogeek ed4062590a feat: show expected withholding tax on Sales Invoice
Custom fields Tax Withholding Category (fetched from Customer),
Withholding Tax and Net Payable After Withholding Tax on Sales Invoice;
computed in the Thai set_tax_withholding override from the category
rate and single threshold, informational only (totals/GL unchanged),
printed under Rounded Total by the standard layout. Payment Entry now
prefers each invoice's category over the customer's.
2026-09-11 10:20:03 +00:00

221 lines
7.4 KiB
Python

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")
CUSTOM_FIELDS = frappe.get_app_path("default_thai_company", "fixtures", "custom_field.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)
import_file_by_path(CUSTOM_FIELDS, force=True, data_import=True)
frappe.clear_cache(doctype="Sales Invoice")
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, category=None):
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%",
}
],
}
)
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_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, paid=10200)
self.assertEqual([(d.account, d.amount) for d in pe.deductions], [(self.receivable, 500.0)])
self.assertEqual(pe.difference_amount, 0)
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)