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.
221 lines
7.4 KiB
Python
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)
|