6 Commits
Author SHA1 Message Date
kurogeek 4785878025 feat: default Location tree and Asset Categories
Location fixture: "All Locations" group with Head Office, Branch Office and
Warehouse. Fixture import re-inserts tree nodes with fresh lft/rgt, which
strands locations users add beneath them, so after_migrate rebuilds the
Location tree.

Asset Category fixture: Land (non-depreciable), Buildings (20 years),
Plant and Machinery, Vehicles, Furniture and Fixtures, Office Equipment,
Computers and Electronics, Software (5 years) and Intangible Assets
(10 years), per Royal Decree 145 rates; monthly straight-line, daily
pro-rata. The accounts table is company-specific and therefore not
shipped: an Asset Category before_import hook links every Thai company
(same contract as the WHT categories), reusing the Standard chart's
fixed asset accounts and creating Land, Vehicles and Intangible Assets
under Fixed Assets. Company.on_update links new companies. Depreciation
accounts are left to the Company defaults.

get_or_create_account now takes account_type and parent_groups.
2026-09-17 05:13:00 +00:00
kurogeek 2a44aed574 feat: Thailand Personal Income Tax slab fixture
Submitted Income Tax Slab "Thailand Personal Income Tax" (THB, effective
2017-01-01) with the eight progressive brackets 0% to 35%, using the HRMS
from/to convention (150,001-300,000 etc.). standard_tax_exemption_amount
is 160,000: the Sec. 42 bis expense deduction (50%, max 100,000) plus the
personal allowance (60,000), which every employee gets without filing a
declaration. The "Expense Deduction" category and "Personal allowance"
sub-category are dropped from the exemption fixtures so they cannot be
counted twice; Personal and Family Allowances cap becomes 270,000.
2026-09-17 01:10:43 +00:00
kurogeek 6a969841e5 feat: Thai personal income tax exemption categories for HRMS
Fixtures for Employee Tax Exemption Category / Sub Category covering the
Thai PIT allowances (personal and family, insurance, retirement savings,
Thai ESG/ESGX, social security, home loan interest, donations, income
exemptions, stimulus measures) plus the Sec. 42 bis expense deduction.

Category caps follow HRMS validation: a sub-category max must not exceed
its category max and the declaration caps the category total, so groups
with a real combined limit carry it, groups without one use the sum of
their sub caps, and uncapped per-person/percentage items live in 0-cap
categories. Social Security uses the 2026 ceiling (10,500/yr); SSF is
kept inactive. sync_fixtures skips these files on sites without hrms.
2026-09-16 11:22:28 +00:00
kurogeek d074657511 feat: add Default Standard Quotation print format 2026-09-16 07:46:59 +00:00
kurogeek 04735ca813 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.
2026-09-15 09:35:15 +00:00
kurogeek 381aab2c83 feat: Output VAT and Input VAT accounts for every Thai company
Thai VAT is filed on P.P.30 as output tax (ภาษีขาย, collected on sales)
net of input tax (ภาษีซื้อ, paid on purchases); ERPNext's wizard makes a
single VAT account. Both join the per-company tax accounts created on
Company save and fixture import; a patch adds them to existing companies.
Account names are translated for the chart of accounts tree.
2026-09-15 09:19:43 +00:00
16 changed files with 1148 additions and 34 deletions
+91
View File
@@ -0,0 +1,91 @@
import frappe
from frappe.utils.nestedset import rebuild_tree
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Asset Category -> fixed asset account. Names follow ERPNext's Standard chart so
# setup-wizard companies reuse their accounts; the rest are created under the
# same "Fixed Assets" group. Depreciation accounts are left to the Company
# defaults (Asset falls back to them), so one change there covers every category.
FIXED_ASSET_ACCOUNTS = {
"Land": "Land",
"Buildings": "Buildings",
"Plant and Machinery": "Plants and Machineries",
"Vehicles": "Vehicles",
"Furniture and Fixtures": "Furnitures and Fixtures",
"Office Equipment": "Office Equipments",
"Computers and Electronics": "Electronic Equipments",
"Software": "Softwares",
"Intangible Assets": "Intangible Assets",
}
FIXED_ASSET_GROUPS = ("Fixed Assets",)
def fixed_asset_account(company, category):
return get_or_create_account(
company, FIXED_ASSET_ACCOUNTS[category], "Asset", "Fixed Asset", FIXED_ASSET_GROUPS
)
def link_company(category, company):
"""Append the company's fixed asset account row to the category if missing."""
if any(row.company_name == company for row in category.accounts):
return False
category.append(
"accounts",
{"company_name": company, "fixed_asset_account": fixed_asset_account(company, category.name)},
)
return True
def prepare_fixture_accounts(doc, method=None):
"""Asset Category fixtures ship without `accounts` (company-specific).
Same contract as the Tax Withholding Category fixtures: fixture import
re-inserts the doc on every migrate, so carry over the rows already on this
site, then link every Thai company. Mandatory is relaxed for sites without a
Thai company yet; `setup_company` links them later.
"""
if doc.name not in FIXED_ASSET_ACCOUNTS:
return
doc.flags.ignore_mandatory = True
if not doc.accounts and frappe.db.exists(doc.doctype, doc.name):
for row in frappe.get_all(
"Asset Category Account",
filters={"parent": doc.name, "parenttype": doc.doctype},
fields=[
"company_name",
"fixed_asset_account",
"accumulated_depreciation_account",
"depreciation_expense_account",
"capital_work_in_progress_account",
],
order_by="idx",
):
doc.append("accounts", row)
for company in thai_companies():
link_company(doc, company)
def setup_company(doc, method=None):
"""Company.on_update: link a Thai company on every default Asset Category."""
if not company_ready(doc):
return
for name in FIXED_ASSET_ACCOUNTS:
if not frappe.db.exists("Asset Category", name):
continue
category = frappe.get_doc("Asset Category", name)
if link_company(category, doc.name):
category.save(ignore_permissions=True)
def rebuild_locations():
"""after_migrate: fixture import re-inserts the Location tree nodes (fresh
lft/rgt), which strands locations users added beneath them; rebuild."""
if frappe.db.exists("Location", "All Locations"):
rebuild_tree("Location")
@@ -0,0 +1,237 @@
[
{
"accounts": [],
"asset_category_name": "Land",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [],
"modified": "2026-09-16 09:00:00.000000",
"name": "Land",
"non_depreciable_category": 1
},
{
"accounts": [],
"asset_category_name": "Buildings",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Buildings",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 240
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Buildings",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Plant and Machinery",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Plant and Machinery",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Plant and Machinery",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Vehicles",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Vehicles",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Vehicles",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Furniture and Fixtures",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Furniture and Fixtures",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Furniture and Fixtures",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Office Equipment",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Office Equipment",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Office Equipment",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Computers and Electronics",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Computers and Electronics",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Computers and Electronics",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Software",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Software",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Software",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Intangible Assets",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Intangible Assets",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 120
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Intangible Assets",
"non_depreciable_category": 0
}
]
@@ -0,0 +1,106 @@
[
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 270000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Personal and Family Allowances"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child and Dependant Allowances"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Life and Health Insurance"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 25000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Family Insurance"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Retirement Savings and Investments"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESG Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 600000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 10500.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Housing"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Donations"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Political Party Donation"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Income Exemptions"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 250000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Government Stimulus Measures"
}
]
@@ -0,0 +1,299 @@
[
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse allowance (spouse without income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 90000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Adopted child allowance (30,000 per child, max 3)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Parent allowance (30,000 per parent, max 2)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse's parent allowance (30,000 per parent, max 2)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child allowance (30,000 per child)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child allowance - 2nd child onward born 2018 or later (60,000 per child)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Disabled or incapacitated dependant care (60,000 per person)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Prenatal care and childbirth expenses (60,000 per pregnancy)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Life and Health Insurance",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Life insurance premium (policy term 10 years or more)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Life and Health Insurance",
"is_active": 1,
"max_amount": 25000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Health insurance premium (self)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Family Insurance",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse life insurance premium (spouse without income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Family Insurance",
"is_active": 1,
"max_amount": 15000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Parents' health insurance premium"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Provident fund contribution (15% of wages)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Government Pension Fund contribution (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Private Teacher Aid Fund contribution (15% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Retirement Mutual Fund - RMF (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Pension life insurance premium (15% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 30000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "National Savings Fund contribution"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 0,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Super Savings Fund - SSF (30% of income, tax years 2020-2024)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESG Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESG fund (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESGX Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX fund - new units (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESGX Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX fund - LTF switch (500,000 spread over tax years 2025-2029)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 10500.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 33 employee"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 5184.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 39 voluntary"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 3600.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 40 informal worker"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Housing",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Home loan interest"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Donations",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "General donations (10% of income after allowances)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Donations",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Education, sports, hospital and social development donations (2x, 10% of income after allowances)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Political Party Donation",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Political party donation"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Income Exemptions",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Taxpayer aged 65 or over - income exemption"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Income Exemptions",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Disabled taxpayer under 65 - income exemption"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Government Stimulus Measures",
"is_active": 1,
"max_amount": 50000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Easy E-Receipt (e-Tax Invoice / e-Receipt purchases)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Government Stimulus Measures",
"is_active": 1,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Solar rooftop installation (tax years 2025-2027)"
}
]
@@ -0,0 +1,113 @@
[
{
"allow_tax_exemption": 1,
"currency": "THB",
"disabled": 0,
"docstatus": 1,
"doctype": "Income Tax Slab",
"effective_from": "2017-01-01",
"modified": "2026-09-16 09:00:00.000000",
"name": "Thailand Personal Income Tax",
"other_taxes_and_charges": [],
"slabs": [
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 0.0,
"idx": 1,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 0.0,
"to_amount": 150000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 150001.0,
"idx": 2,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 5.0,
"to_amount": 300000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 300001.0,
"idx": 3,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 10.0,
"to_amount": 500000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 500001.0,
"idx": 4,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 15.0,
"to_amount": 750000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 750001.0,
"idx": 5,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 20.0,
"to_amount": 1000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 1000001.0,
"idx": 6,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 25.0,
"to_amount": 2000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 2000001.0,
"idx": 7,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 30.0,
"to_amount": 5000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 5000001.0,
"idx": 8,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 35.0,
"to_amount": 0.0
}
],
"standard_tax_exemption_amount": 160000.0,
"tax_relief_limit": 0.0
}
]
@@ -0,0 +1,42 @@
[
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 1,
"location_name": "All Locations",
"modified": "2026-09-16 09:00:00.000000",
"name": "All Locations",
"parent_location": null
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Head Office",
"modified": "2026-09-16 09:00:00.000000",
"name": "Head Office",
"parent_location": "All Locations"
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Branch Office",
"modified": "2026-09-16 09:00:00.000000",
"name": "Branch Office",
"parent_location": "All Locations"
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Warehouse",
"modified": "2026-09-16 09:00:00.000000",
"name": "Warehouse",
"parent_location": "All Locations"
}
]
File diff suppressed because one or more lines are too long
+49 -3
View File
@@ -93,17 +93,56 @@ fixtures = [
{
"doctype": "Print Format",
"filters": [
["name", "in", ["Default Standard Sales Invoice", "Default Standard Tax Invoice/Receipt"]]
[
"name",
"in",
[
"Default Standard Sales Invoice",
"Default Standard Tax Invoice/Receipt",
"Default Standard Quotation",
],
]
],
},
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
{
"doctype": "Location",
"filters": [["name", "in", ["All Locations", "Head Office", "Branch Office", "Warehouse"]]],
},
{
"doctype": "Asset Category",
"filters": [
[
"name",
"in",
[
"Land",
"Buildings",
"Plant and Machinery",
"Vehicles",
"Furniture and Fixtures",
"Office Equipment",
"Computers and Electronics",
"Software",
"Intangible Assets",
],
]
],
},
# HRMS doctypes; skipped by sync_fixtures on sites without hrms
{"doctype": "Employee Tax Exemption Category"},
{"doctype": "Employee Tax Exemption Sub Category"},
{"doctype": "Income Tax Slab", "filters": [["name", "=", "Thailand Personal Income Tax"]]},
]
# Installation
# ------------
# 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"
# Fixture import re-inserts the Location tree; restore lft/rgt for user-added nodes.
after_migrate = "default_thai_company.assets.rebuild_locations"
# Uninstallation
# ------------
@@ -166,8 +205,15 @@ doc_events = {
"Tax Withholding Category": {
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
},
"Asset Category": {
"before_import": "default_thai_company.assets.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",
"default_thai_company.assets.setup_company",
],
},
"Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
+2
View File
@@ -4,3 +4,5 @@
[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,8 @@
from default_thai_company.tax_withholding import ensure_company_accounts, thai_companies
def execute():
"""Output VAT / Input VAT were added to COMPANY_ACCOUNTS; Company.on_update
only runs on save, so give existing Thai companies the pair."""
for company in thai_companies():
ensure_company_accounts(company)
@@ -0,0 +1,5 @@
from default_thai_company.vat import setup_companies
def execute():
setup_companies()
+35 -12
View File
@@ -12,10 +12,23 @@ from frappe.utils import cint, flt
from default_thai_company.utils import money_in_words
# Liability: tax we withhold from suppliers and remit on P.N.D.3/53 (linked on every category).
# Asset: tax customers withhold from us, creditable against corporate income tax.
# Tax accounts every Thai company gets (Company.on_update, fixture import, and the
# create_vat_accounts patch for companies that predate the VAT pair).
# Withholding: liability = tax we withhold from suppliers, remitted on P.N.D.3/53
# (linked on every category); asset = tax customers withhold from us, creditable
# against corporate income tax.
# VAT: output (ภาษีขาย) collected on sales is payable, input (ภาษีซื้อ) paid on
# purchases is recoverable; the P.P.30 return nets the two, so they are kept apart.
LIABILITY_ACCOUNT = "Withholding Tax Payable"
ASSET_ACCOUNT = "Withholding Tax Receivable"
OUTPUT_VAT_ACCOUNT = "Output VAT"
INPUT_VAT_ACCOUNT = "Input VAT"
COMPANY_ACCOUNTS = (
(LIABILITY_ACCOUNT, "Liability"),
(ASSET_ACCOUNT, "Asset"),
(OUTPUT_VAT_ACCOUNT, "Liability"),
(INPUT_VAT_ACCOUNT, "Asset"),
)
# Preferred parent groups by root type; first match wins, else the root account.
PARENT_GROUPS = {
@@ -30,7 +43,9 @@ def thai_companies():
return frappe.get_all("Company", filters={"country": "Thailand"}, pluck="name")
def get_or_create_account(company, account_name, root_type):
def get_or_create_account(company, account_name, root_type, account_type="Tax", parent_groups=None):
"""Leaf account `account_name` for `company`, created under the first existing
group in `parent_groups` (default: PARENT_GROUPS[root_type]), else the root."""
name = frappe.db.get_value(
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
)
@@ -42,17 +57,19 @@ def get_or_create_account(company, account_name, root_type):
"doctype": "Account",
"company": company,
"account_name": account_name,
"parent_account": find_parent_group(company, root_type),
"parent_account": find_parent_group(
company, root_type, parent_groups or PARENT_GROUPS[root_type]
),
"root_type": root_type,
"account_type": "Tax",
"account_type": account_type,
"is_group": 0,
}
).insert(ignore_permissions=True)
return account.name
def find_parent_group(company, root_type):
for account_name in PARENT_GROUPS[root_type]:
def find_parent_group(company, root_type, group_names):
for account_name in group_names:
parent = frappe.db.get_value(
"Account",
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
@@ -67,9 +84,10 @@ def find_parent_group(company, root_type):
def ensure_company_accounts(company):
"""Create both withholding accounts for `company`; return the liability account name."""
get_or_create_account(company, ASSET_ACCOUNT, "Asset")
return get_or_create_account(company, LIABILITY_ACCOUNT, "Liability")
"""Create every account in COMPANY_ACCOUNTS for `company`; return the
withholding liability account name (the one Tax Withholding Categories link)."""
accounts = {name: get_or_create_account(company, name, root_type) for name, root_type in COMPANY_ACCOUNTS}
return accounts[LIABILITY_ACCOUNT]
def link_company(category, company, account):
@@ -104,9 +122,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 withholding accounts and link every WHT category."""
if doc.country != "Thailand" or not frappe.db.exists("Account", {"company": doc.name}):
"""Company.on_update: create the tax accounts and link every WHT category."""
if not company_ready(doc):
return
account = ensure_company_accounts(doc.name)
@@ -8,7 +8,9 @@ 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,
)
@@ -44,16 +46,7 @@ class TestTaxWithholding(FrappeTestCase):
).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()
cls.vat = frappe.get_doc("Account", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}")
frappe.get_doc(
{
@@ -137,14 +130,17 @@ class TestTaxWithholding(FrappeTestCase):
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}"),
)
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"
@@ -153,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(
{
+6
View File
@@ -11,6 +11,8 @@ Signatures,ลายเซ็น
On behalf of,ในนาม
Receiver,ผู้รับ
Issuer,ผู้ออก
Accepted By,ผู้อนุมัติสั่งซื้อ
Quoted By,ผู้เสนอราคา
Tax Invoice,ใบกำกับภาษี
Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
@@ -53,3 +55,7 @@ Grand Total,ยอดรวมทั้งสิ้น
Rounded Total,ยอดรวมปัดเศษ
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
Page {0} of {1},หน้า {0} จาก {1}
Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย
Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย
Output VAT,ภาษีขาย
Input VAT,ภาษีซื้อ
1 Net Payable After Withholding Tax ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
11 On behalf of ในนาม
12 Receiver ผู้รับ
13 Issuer ผู้ออก
14 Accepted By ผู้อนุมัติสั่งซื้อ
15 Quoted By ผู้เสนอราคา
16 Tax Invoice ใบกำกับภาษี
17 Tax Invoice/Receipt ใบกำกับภาษี/ใบเสร็จรับเงิน
18 Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53) ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
55 Rounded Total ยอดรวมปัดเศษ
56 Terms and Conditions Details รายละเอียดข้อตกลงและเงื่อนไข
57 Page {0} of {1} หน้า {0} จาก {1}
58 Withholding Tax Payable ภาษีหัก ณ ที่จ่ายค้างจ่าย
59 Withholding Tax Receivable ภาษีถูกหัก ณ ที่จ่าย
60 Output VAT ภาษีขาย
61 Input VAT ภาษีซื้อ
+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)