feat: Thai VAT schemes as Sales/Purchase Taxes and Charges Templates

Per Thai company: Thailand VAT 7%, Thailand VAT 7% (Included) for
VAT-inclusive pricing, Thailand VAT 0% for zero-rated supplies. Sales
schemes post to Output VAT, purchase schemes to Input VAT (category
Total, so recoverable VAT stays out of valuation). The 7% scheme is the
default when the company has none. Created on Company save, after
install, and by a patch for existing companies.
This commit is contained in:
2026-09-15 09:35:15 +00:00
parent 381aab2c83
commit 04735ca813
6 changed files with 124 additions and 3 deletions
+6 -2
View File
@@ -103,7 +103,8 @@ fixtures = [
# ------------
# before_install = "default_thai_company.install.before_install"
# after_install = "default_thai_company.install.after_install"
# Fixtures are synced before this runs; VAT schemes for companies that already exist.
after_install = "default_thai_company.vat.setup_companies"
# Uninstallation
# ------------
@@ -167,7 +168,10 @@ doc_events = {
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
},
"Company": {
"on_update": "default_thai_company.tax_withholding.setup_company",
"on_update": [
"default_thai_company.tax_withholding.setup_company",
"default_thai_company.vat.setup_company",
],
},
"Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
+1
View File
@@ -5,3 +5,4 @@
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated
default_thai_company.patches.create_vat_accounts
default_thai_company.patches.create_vat_templates
@@ -0,0 +1,5 @@
from default_thai_company.vat import setup_companies
def execute():
setup_companies()
+6 -1
View File
@@ -118,9 +118,14 @@ def prepare_fixture_accounts(doc, method=None):
link_company(doc, company, ensure_company_accounts(company))
def company_ready(doc):
"""A Thai company whose chart of accounts exists."""
return doc.country == "Thailand" and frappe.db.exists("Account", {"company": doc.name})
def setup_company(doc, method=None):
"""Company.on_update: create the tax accounts and link every WHT category."""
if doc.country != "Thailand" or not frappe.db.exists("Account", {"company": doc.name}):
if not company_ready(doc):
return
account = ensure_company_accounts(doc.name)
@@ -149,6 +149,34 @@ class TestTaxWithholding(FrappeTestCase):
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_fixture_reimport_keeps_site_account_and_relinks(self):
alt = frappe.get_doc(
{
+78
View File
@@ -0,0 +1,78 @@
import erpnext
import frappe
from default_thai_company.tax_withholding import (
INPUT_VAT_ACCOUNT,
OUTPUT_VAT_ACCOUNT,
company_ready,
get_or_create_account,
thai_companies,
)
RATE = 7.0
# title, rate, included_in_print_rate. Exempt supplies take no template.
SCHEMES = (
("Thailand VAT 7%", RATE, 0),
("Thailand VAT 7% (Included)", RATE, 1), # VAT-inclusive prices, the retail norm
("Thailand VAT 0%", 0.0, 0), # zero-rated: exports, international transport
)
DEFAULT_SCHEME = "Thailand VAT 7%"
# doctype, account, root type, extra row fields. Input VAT is recoverable, so on
# purchases it is "Total" (added to the bill, kept out of item valuation).
TEMPLATES = (
("Sales Taxes and Charges Template", OUTPUT_VAT_ACCOUNT, "Liability", {}),
(
"Purchase Taxes and Charges Template",
INPUT_VAT_ACCOUNT,
"Asset",
{"category": "Total", "add_deduct_tax": "Add"},
),
)
def ensure_vat_templates(company):
"""Create the VAT schemes for `company`, skipping titles that exist. The 7%
scheme becomes the default when the company has no default template."""
cost_center = erpnext.get_default_cost_center(company)
for doctype, account_name, root_type, extra in TEMPLATES:
account = get_or_create_account(company, account_name, root_type)
has_default = frappe.db.exists(doctype, {"company": company, "is_default": 1})
for title, rate, included in SCHEMES:
if frappe.db.exists(doctype, {"company": company, "title": title}):
continue
is_default = title == DEFAULT_SCHEME and not has_default
frappe.get_doc(
{
"doctype": doctype,
"title": title,
"company": company,
"is_default": int(is_default),
"taxes": [
{
"charge_type": "On Net Total",
"account_head": account,
"rate": rate,
"description": f"VAT {rate:g}%",
"included_in_print_rate": included,
"cost_center": cost_center,
**extra,
}
],
}
).insert(ignore_permissions=True)
has_default = has_default or is_default
def setup_company(doc, method=None):
"""Company.on_update: VAT schemes for a Thai company."""
if company_ready(doc):
ensure_vat_templates(doc.name)
def setup_companies():
"""after_install and the create_vat_templates patch: companies that exist
before this code did never pass through `setup_company`."""
for company in thai_companies():
ensure_vat_templates(company)