Compare commits
15
Commits
two-language
...
wht-cert
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baff8de0d3 | ||
|
|
2ee381f369 | ||
|
|
a1ca8439fc | ||
|
|
42857b1377 | ||
|
|
270e44c080 | ||
|
|
68f0817125 | ||
|
|
4a19c60ba2 | ||
|
|
71eda0bec9 | ||
|
|
8cba58ba02 | ||
|
|
4785878025 | ||
|
|
2a44aed574 | ||
|
|
6a969841e5 | ||
|
|
d074657511 | ||
|
|
04735ca813 | ||
|
|
381aab2c83 |
@@ -20,6 +20,17 @@ file overrides the upstream Thai for the standard labels the print formats show.
|
|||||||
print formats embed Sarabun (`public/fonts`, OFL) so PDFs render Thai on servers
|
print formats embed Sarabun (`public/fonts`, OFL) so PDFs render Thai on servers
|
||||||
without a Thai system font. Switch a user or the print language to `th` to use it.
|
without a Thai system font. Switch a user or the print language to `th` to use it.
|
||||||
|
|
||||||
|
### Withholding tax certificate
|
||||||
|
|
||||||
|
A paid Purchase Invoice with *Apply Tax Withholding Amount* gets a **Withholding Tax Certificate**
|
||||||
|
button that downloads the Revenue Department's Sec. 50 bis form (หนังสือรับรองการหักภาษี ณ ที่จ่าย,
|
||||||
|
two copies) as a PDF: the *Withholding Tax Certificate* print format lays the invoice's data
|
||||||
|
over the official form (`public/images`). The row the payment is reported on comes from
|
||||||
|
*Type of Income Paid* on the Tax Withholding Category (set for the shipped categories; pick it
|
||||||
|
for your own), and the ภ.ง.ด. return is ticked from the Supplier's type: ภ.ง.ด.53 for juristic
|
||||||
|
payees, ภ.ง.ด.1ก / 2 / 3 by income type for individuals. เล่มที่ / เลขที่ (certificate book
|
||||||
|
numbers) and ลำดับที่ (the line in the return) are left for the accountant.
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
|
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
|
||||||
|
|||||||
@@ -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,73 @@
|
|||||||
|
{
|
||||||
|
"custom_fields": [
|
||||||
|
{
|
||||||
|
"_assign": null,
|
||||||
|
"_comments": null,
|
||||||
|
"_liked_by": null,
|
||||||
|
"_user_tags": null,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"creation": "2026-09-22 09:00:00.000000",
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": "Row of the withholding tax certificate (50 Tawi) the payment is reported on; Other prints the category name in row 6.",
|
||||||
|
"docstatus": 0,
|
||||||
|
"dt": "Tax Withholding Category",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "income_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"idx": 2,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "category_name",
|
||||||
|
"is_system_generated": 0,
|
||||||
|
"is_virtual": 0,
|
||||||
|
"label": "Type of Income Paid",
|
||||||
|
"length": 0,
|
||||||
|
"link_filters": null,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2026-09-22 09:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": null,
|
||||||
|
"name": "Tax Withholding Category-income_type",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": "\nSalary and Wages - Sec. 40(1)\nFees and Commissions - Sec. 40(2)\nRoyalties - Sec. 40(3)\nInterest - Sec. 40(4)(a)\nDividends - Sec. 40(4)(b)\nSec. 3 Tera (Services, Rent, Contract Work etc.)\nOther",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permlevel": 0,
|
||||||
|
"placeholder": null,
|
||||||
|
"precision": null,
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"show_dashboard": 0,
|
||||||
|
"sort_options": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom_perms": [],
|
||||||
|
"doctype": "Tax Withholding Category",
|
||||||
|
"links": [],
|
||||||
|
"property_setters": [],
|
||||||
|
"sync_on_migrate": 1
|
||||||
|
}
|
||||||
@@ -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
@@ -5,7 +5,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Fees and Commissions - Sec. 40(2)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 3% - Commission",
|
"name": "WHT 3% - Commission",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Royalties - Sec. 40(3)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 3% - Royalty",
|
"name": "WHT 3% - Royalty",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -57,7 +59,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Interest - Sec. 40(4)(a)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 1% - Interest (Company)",
|
"name": "WHT 1% - Interest (Company)",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -83,7 +86,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Interest - Sec. 40(4)(a)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 15% - Interest (Individual)",
|
"name": "WHT 15% - Interest (Individual)",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -109,7 +113,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Dividends - Sec. 40(4)(b)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 10% - Dividend",
|
"name": "WHT 10% - Dividend",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -135,7 +140,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 5% - Rent",
|
"name": "WHT 5% - Rent",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -161,7 +167,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 1% - Ship Rental",
|
"name": "WHT 1% - Ship Rental",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -187,7 +194,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 3% - Professional Fee",
|
"name": "WHT 3% - Professional Fee",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -213,7 +221,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 3% - Contract Work",
|
"name": "WHT 3% - Contract Work",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -239,7 +248,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 3% - Service",
|
"name": "WHT 3% - Service",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -265,7 +275,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 3% - Sales Promotion",
|
"name": "WHT 3% - Sales Promotion",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -291,7 +302,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 2% - Advertising",
|
"name": "WHT 2% - Advertising",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -317,7 +329,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 1% - Transportation",
|
"name": "WHT 1% - Transportation",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -343,7 +356,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 1% - Non-Life Insurance",
|
"name": "WHT 1% - Non-Life Insurance",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -369,7 +383,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 5% - Prize",
|
"name": "WHT 5% - Prize",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -395,7 +410,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 5% - Public Entertainer",
|
"name": "WHT 5% - Public Entertainer",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -421,7 +437,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 0.75% - Agricultural Produce",
|
"name": "WHT 0.75% - Agricultural Produce",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -447,7 +464,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 1% - Aquatic Animals",
|
"name": "WHT 1% - Aquatic Animals",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -473,7 +491,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Other",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 1% - Immovable Property (Company)",
|
"name": "WHT 1% - Immovable Property (Company)",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -499,7 +518,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Other",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 15% - Non-Resident Individual",
|
"name": "WHT 15% - Non-Resident Individual",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -525,7 +545,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Other",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 15% - Foreign Company",
|
"name": "WHT 15% - Foreign Company",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
@@ -551,7 +572,8 @@
|
|||||||
"consider_party_ledger_amount": 0,
|
"consider_party_ledger_amount": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"modified": "2026-09-11 09:00:00.000000",
|
"income_type": "Other",
|
||||||
|
"modified": "2026-09-22 12:00:00.000000",
|
||||||
"name": "WHT 10% - Foreign Company Dividend",
|
"name": "WHT 10% - Foreign Company Dividend",
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ required_apps = ["erpnext"]
|
|||||||
# page_js = {"page" : "public/js/file.js"}
|
# page_js = {"page" : "public/js/file.js"}
|
||||||
|
|
||||||
# include js in doctype views
|
# include js in doctype views
|
||||||
doctype_js = {"Payment Entry": "public/js/payment_entry.js"}
|
doctype_js = {
|
||||||
|
"Payment Entry": "public/js/payment_entry.js",
|
||||||
|
"Purchase Invoice": "public/js/purchase_invoice.js",
|
||||||
|
}
|
||||||
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
|
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
|
||||||
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
|
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
|
||||||
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
|
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
|
||||||
@@ -79,6 +82,7 @@ jinja = {
|
|||||||
"default_thai_company.utils.get_letter_head_company",
|
"default_thai_company.utils.get_letter_head_company",
|
||||||
"default_thai_company.utils.get_company_bank_account",
|
"default_thai_company.utils.get_company_bank_account",
|
||||||
"default_thai_company.utils.get_in_words",
|
"default_thai_company.utils.get_in_words",
|
||||||
|
"default_thai_company.tax_withholding.get_withholding_certificate",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,17 +97,65 @@ fixtures = [
|
|||||||
{
|
{
|
||||||
"doctype": "Print Format",
|
"doctype": "Print Format",
|
||||||
"filters": [
|
"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",
|
||||||
|
"Withholding Tax Certificate",
|
||||||
|
],
|
||||||
|
]
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
|
{"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
|
# Installation
|
||||||
# ------------
|
# ------------
|
||||||
|
|
||||||
# before_install = "default_thai_company.install.before_install"
|
# Fixtures are synced before Custom Fields on install and migrate; the Tax Withholding
|
||||||
# after_install = "default_thai_company.install.after_install"
|
# Category fixtures carry values for the app's Custom Fields, so create those first.
|
||||||
|
before_install = "default_thai_company.tax_withholding.sync_category_customizations"
|
||||||
|
before_migrate = "default_thai_company.tax_withholding.sync_category_customizations"
|
||||||
|
# Fixtures are synced before this runs; VAT schemes and Shipping Rules for companies
|
||||||
|
# that already exist.
|
||||||
|
after_install = [
|
||||||
|
"default_thai_company.vat.setup_companies",
|
||||||
|
"default_thai_company.shipping.setup_companies",
|
||||||
|
"default_thai_company.payroll.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
|
# Uninstallation
|
||||||
# ------------
|
# ------------
|
||||||
@@ -117,7 +169,8 @@ fixtures = [
|
|||||||
# Name of the app being installed is passed as an argument
|
# Name of the app being installed is passed as an argument
|
||||||
|
|
||||||
# before_app_install = "default_thai_company.utils.before_app_install"
|
# before_app_install = "default_thai_company.utils.before_app_install"
|
||||||
# after_app_install = "default_thai_company.utils.after_app_install"
|
# Payroll Period for companies created before hrms was installed.
|
||||||
|
after_app_install = "default_thai_company.payroll.after_app_install"
|
||||||
|
|
||||||
# Integration Cleanup
|
# Integration Cleanup
|
||||||
# -------------------
|
# -------------------
|
||||||
@@ -152,6 +205,7 @@ fixtures = [
|
|||||||
override_doctype_class = {
|
override_doctype_class = {
|
||||||
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
|
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
|
||||||
"Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
|
"Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
|
||||||
|
"Shipping Rule": "default_thai_company.shipping.ThaiShippingRule",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Document Events
|
# Document Events
|
||||||
@@ -166,8 +220,17 @@ doc_events = {
|
|||||||
"Tax Withholding Category": {
|
"Tax Withholding Category": {
|
||||||
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
|
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
|
||||||
},
|
},
|
||||||
|
"Asset Category": {
|
||||||
|
"before_import": "default_thai_company.assets.prepare_fixture_accounts",
|
||||||
|
},
|
||||||
"Company": {
|
"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",
|
||||||
|
"default_thai_company.shipping.setup_company",
|
||||||
|
"default_thai_company.payroll.setup_company",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"Payment Entry": {
|
"Payment Entry": {
|
||||||
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
|
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
|
||||||
@@ -177,23 +240,10 @@ doc_events = {
|
|||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
# ---------------
|
# ---------------
|
||||||
|
|
||||||
# scheduler_events = {
|
# Payroll Period is per calendar year; roll the default over on 1 January.
|
||||||
# "all": [
|
scheduler_events = {
|
||||||
# "default_thai_company.tasks.all"
|
"daily": ["default_thai_company.payroll.setup_companies"],
|
||||||
# ],
|
}
|
||||||
# "daily": [
|
|
||||||
# "default_thai_company.tasks.daily"
|
|
||||||
# ],
|
|
||||||
# "hourly": [
|
|
||||||
# "default_thai_company.tasks.hourly"
|
|
||||||
# ],
|
|
||||||
# "weekly": [
|
|
||||||
# "default_thai_company.tasks.weekly"
|
|
||||||
# ],
|
|
||||||
# "monthly": [
|
|
||||||
# "default_thai_company.tasks.monthly"
|
|
||||||
# ],
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
# -------
|
# -------
|
||||||
|
|||||||
@@ -4,3 +4,7 @@
|
|||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
# Patches added in this section will be executed after doctypes are migrated
|
# 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
|
||||||
|
default_thai_company.patches.create_shipping_rules
|
||||||
|
default_thai_company.patches.create_payroll_periods
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from default_thai_company.payroll import setup_companies
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
setup_companies()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from default_thai_company.shipping import setup_companies
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
setup_companies()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.utils import getdate
|
||||||
|
|
||||||
|
from default_thai_company.tax_withholding import thai_companies
|
||||||
|
|
||||||
|
# Thai personal income tax (P.N.D.1/91) is assessed per calendar year whatever
|
||||||
|
# the company's fiscal year, so the Payroll Period is January to December.
|
||||||
|
# Payroll Period is named by prompt (site-wide unique) but bound to one company,
|
||||||
|
# so each Thai company gets "<year> - <abbr>", like the Shipping Rule.
|
||||||
|
|
||||||
|
|
||||||
|
def hrms_installed():
|
||||||
|
return "hrms" in frappe.get_installed_apps()
|
||||||
|
|
||||||
|
|
||||||
|
def payroll_period_name(company, year):
|
||||||
|
abbr = frappe.get_cached_value("Company", company, "abbr")
|
||||||
|
return f"{year} - {abbr}"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_payroll_period(company, year):
|
||||||
|
"""Calendar-year Payroll Period for `company`. No-op when any period of the
|
||||||
|
company touches the year: an existing one covers it, or the company runs its
|
||||||
|
own scheme and a second period would only fail the overlap check."""
|
||||||
|
start, end = f"{year}-01-01", f"{year}-12-31"
|
||||||
|
if frappe.db.exists(
|
||||||
|
"Payroll Period", {"company": company, "start_date": ("<=", end), "end_date": (">=", start)}
|
||||||
|
):
|
||||||
|
return
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Payroll Period",
|
||||||
|
"name": payroll_period_name(company, year),
|
||||||
|
"company": company,
|
||||||
|
"start_date": start,
|
||||||
|
"end_date": end,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_company(doc, method=None):
|
||||||
|
"""Company.on_update: this year's Payroll Period for a Thai company."""
|
||||||
|
if doc.country == "Thailand" and hrms_installed():
|
||||||
|
ensure_payroll_period(doc.name, getdate().year)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_companies():
|
||||||
|
"""after_install, the daily scheduler, and the create_payroll_periods patch:
|
||||||
|
this year's Payroll Period for every Thai company. The scheduler rolls the
|
||||||
|
default over on 1 January."""
|
||||||
|
if not hrms_installed():
|
||||||
|
return
|
||||||
|
year = getdate().year
|
||||||
|
for company in thai_companies():
|
||||||
|
ensure_payroll_period(company, year)
|
||||||
|
|
||||||
|
|
||||||
|
def after_app_install(app_name):
|
||||||
|
"""Payroll Period only exists once hrms is installed; catch companies that
|
||||||
|
were created before it."""
|
||||||
|
if app_name == "hrms":
|
||||||
|
setup_companies()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
frappe.ui.form.on("Purchase Invoice", {
|
||||||
|
refresh(frm) {
|
||||||
|
// The certificate is issued once the tax has been withheld, i.e. on payment.
|
||||||
|
if (!frm.doc.apply_tds || frm.doc.docstatus !== 1 || frm.doc.status !== "Paid") return;
|
||||||
|
// The Withholding Tax Certificate print format renders the Revenue Department's
|
||||||
|
// Sec. 50 bis form (fixtures/print_format.json) as a PDF.
|
||||||
|
frm.add_custom_button(__("Withholding Tax Certificate"), () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
doctype: frm.doctype,
|
||||||
|
name: frm.docname,
|
||||||
|
format: "Withholding Tax Certificate",
|
||||||
|
no_letterhead: 1,
|
||||||
|
_lang: "th",
|
||||||
|
});
|
||||||
|
const w = window.open(
|
||||||
|
frappe.urllib.get_full_url(
|
||||||
|
`/api/method/frappe.utils.print_format.download_pdf?${params}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!w) frappe.msgprint(__("Please enable pop-ups"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import erpnext
|
||||||
|
import frappe
|
||||||
|
from erpnext.accounts.doctype.shipping_rule.shipping_rule import ShippingRule
|
||||||
|
|
||||||
|
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
|
||||||
|
|
||||||
|
# Shipping Rule is named by its label (site-wide unique) but bound to one company,
|
||||||
|
# so each Thai company gets "<LABEL> - <abbr>", the way ERPNext names accounts and
|
||||||
|
# tax templates. The label doubles as the charge description on the transaction.
|
||||||
|
LABEL = "Shipping Charges"
|
||||||
|
|
||||||
|
# Shipping billed to customers is revenue; the courier's bill stays an expense.
|
||||||
|
SHIPPING_ACCOUNT = "Shipping Charges"
|
||||||
|
SHIPPING_ACCOUNT_GROUPS = ("Direct Income", "Income")
|
||||||
|
|
||||||
|
|
||||||
|
class ThaiShippingRule(ShippingRule):
|
||||||
|
def add_shipping_rule_to_tax_table(self, doc, shipping_amount):
|
||||||
|
"""A Fixed rule without an amount only seeds the charge row; the amount is
|
||||||
|
entered on the transaction. ERPNext re-applies the rule on every
|
||||||
|
recalculation, which would otherwise reset the row to 0."""
|
||||||
|
manual = self.calculate_based_on == "Fixed" and not self.shipping_amount
|
||||||
|
entered = [(row, row.tax_amount) for row in doc.get("taxes")] if manual else ()
|
||||||
|
super().add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||||
|
for row, amount in entered:
|
||||||
|
row.tax_amount = amount
|
||||||
|
|
||||||
|
|
||||||
|
def shipping_rule_name(company):
|
||||||
|
abbr = frappe.get_cached_value("Company", company, "abbr")
|
||||||
|
return f"{LABEL} - {abbr}"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_shipping_rule(company):
|
||||||
|
"""Selling Shipping Rule for `company` with the amount entered per
|
||||||
|
transaction (see ThaiShippingRule). No-op when the rule exists."""
|
||||||
|
name = shipping_rule_name(company)
|
||||||
|
if frappe.db.exists("Shipping Rule", name):
|
||||||
|
return
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Shipping Rule",
|
||||||
|
"label": name,
|
||||||
|
"company": company,
|
||||||
|
"shipping_rule_type": "Selling",
|
||||||
|
"calculate_based_on": "Fixed",
|
||||||
|
"shipping_amount": 0,
|
||||||
|
"account": get_or_create_account(
|
||||||
|
company, SHIPPING_ACCOUNT, "Income", "Income Account", SHIPPING_ACCOUNT_GROUPS
|
||||||
|
),
|
||||||
|
"cost_center": erpnext.get_default_cost_center(company),
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_company(doc, method=None):
|
||||||
|
"""Company.on_update: default Shipping Rule for a Thai company."""
|
||||||
|
if company_ready(doc):
|
||||||
|
ensure_shipping_rule(doc.name)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_companies():
|
||||||
|
"""after_install and the create_shipping_rules patch: companies that exist
|
||||||
|
before this code did never pass through `setup_company`."""
|
||||||
|
for company in thai_companies():
|
||||||
|
ensure_shipping_rule(company)
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
import frappe
|
import frappe
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
|
||||||
@@ -8,14 +11,29 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
|||||||
normal_round,
|
normal_round,
|
||||||
)
|
)
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, flt
|
from frappe.contacts.doctype.address.address import get_default_address
|
||||||
|
from frappe.modules.utils import sync_customizations_for_doctype
|
||||||
|
from frappe.utils import cint, flt, fmt_money, getdate
|
||||||
|
|
||||||
from default_thai_company.utils import money_in_words
|
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).
|
# Tax accounts every Thai company gets (Company.on_update, fixture import, and the
|
||||||
# Asset: tax customers withhold from us, creditable against corporate income tax.
|
# 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"
|
LIABILITY_ACCOUNT = "Withholding Tax Payable"
|
||||||
ASSET_ACCOUNT = "Withholding Tax Receivable"
|
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.
|
# Preferred parent groups by root type; first match wins, else the root account.
|
||||||
PARENT_GROUPS = {
|
PARENT_GROUPS = {
|
||||||
@@ -30,7 +48,9 @@ def thai_companies():
|
|||||||
return frappe.get_all("Company", filters={"country": "Thailand"}, pluck="name")
|
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(
|
name = frappe.db.get_value(
|
||||||
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
|
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
|
||||||
)
|
)
|
||||||
@@ -42,17 +62,19 @@ def get_or_create_account(company, account_name, root_type):
|
|||||||
"doctype": "Account",
|
"doctype": "Account",
|
||||||
"company": company,
|
"company": company,
|
||||||
"account_name": account_name,
|
"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,
|
"root_type": root_type,
|
||||||
"account_type": "Tax",
|
"account_type": account_type,
|
||||||
"is_group": 0,
|
"is_group": 0,
|
||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
return account.name
|
return account.name
|
||||||
|
|
||||||
|
|
||||||
def find_parent_group(company, root_type):
|
def find_parent_group(company, root_type, group_names):
|
||||||
for account_name in PARENT_GROUPS[root_type]:
|
for account_name in group_names:
|
||||||
parent = frappe.db.get_value(
|
parent = frappe.db.get_value(
|
||||||
"Account",
|
"Account",
|
||||||
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
|
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
|
||||||
@@ -67,9 +89,10 @@ def find_parent_group(company, root_type):
|
|||||||
|
|
||||||
|
|
||||||
def ensure_company_accounts(company):
|
def ensure_company_accounts(company):
|
||||||
"""Create both withholding accounts for `company`; return the liability account name."""
|
"""Create every account in COMPANY_ACCOUNTS for `company`; return the
|
||||||
get_or_create_account(company, ASSET_ACCOUNT, "Asset")
|
withholding liability account name (the one Tax Withholding Categories link)."""
|
||||||
return get_or_create_account(company, LIABILITY_ACCOUNT, "Liability")
|
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):
|
def link_company(category, company, account):
|
||||||
@@ -104,9 +127,25 @@ def prepare_fixture_accounts(doc, method=None):
|
|||||||
link_company(doc, company, ensure_company_accounts(company))
|
link_company(doc, company, ensure_company_accounts(company))
|
||||||
|
|
||||||
|
|
||||||
|
def sync_category_customizations():
|
||||||
|
"""before_install / before_migrate: create the Tax Withholding Category Custom
|
||||||
|
Fields (custom/tax_withholding_category.json) ahead of the fixture import.
|
||||||
|
Frappe syncs fixtures before customizations, and a fixture value for a field
|
||||||
|
that does not exist yet is dropped."""
|
||||||
|
folder = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
|
||||||
|
filename = "tax_withholding_category.json"
|
||||||
|
with open(os.path.join(folder, filename)) as f:
|
||||||
|
sync_customizations_for_doctype(json.load(f), folder, filename)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def setup_company(doc, method=None):
|
||||||
"""Company.on_update: create withholding accounts and link every WHT category."""
|
"""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
|
return
|
||||||
|
|
||||||
account = ensure_company_accounts(doc.name)
|
account = ensure_company_accounts(doc.name)
|
||||||
@@ -351,3 +390,128 @@ def get_customer_withholding(doc):
|
|||||||
else:
|
else:
|
||||||
description = _("Withholding tax deducted by customer")
|
description = _("Withholding tax deducted by customer")
|
||||||
return flt(total, precision), description
|
return flt(total, precision), description
|
||||||
|
|
||||||
|
|
||||||
|
# Withholding tax certificate (หนังสือรับรองการหักภาษี ณ ที่จ่าย, Sec. 50 bis): the income
|
||||||
|
# row the payment is reported on, from Tax Withholding Category.income_type (custom field).
|
||||||
|
# Rows: 1 = Sec. 40(1), 2 = 40(2), 3 = 40(3), 4a = 40(4)(a), 4b = 40(4)(b) dividends
|
||||||
|
# paid from profits taxed at the standard 20% rate (form row 4(b)(1)(1.3)), 5 = payments
|
||||||
|
# withheld under Revenue Department orders issued under Sec. 3 tera, 6 = other (specified).
|
||||||
|
CERTIFICATE_ROWS = {
|
||||||
|
"Salary and Wages - Sec. 40(1)": "1",
|
||||||
|
"Fees and Commissions - Sec. 40(2)": "2",
|
||||||
|
"Royalties - Sec. 40(3)": "3",
|
||||||
|
"Interest - Sec. 40(4)(a)": "4a",
|
||||||
|
"Dividends - Sec. 40(4)(b)": "4b",
|
||||||
|
"Sec. 3 Tera (Services, Rent, Contract Work etc.)": "5",
|
||||||
|
}
|
||||||
|
|
||||||
|
THAI_MONTHS = (
|
||||||
|
"มกราคม",
|
||||||
|
"กุมภาพันธ์",
|
||||||
|
"มีนาคม",
|
||||||
|
"เมษายน",
|
||||||
|
"พฤษภาคม",
|
||||||
|
"มิถุนายน",
|
||||||
|
"กรกฎาคม",
|
||||||
|
"สิงหาคม",
|
||||||
|
"กันยายน",
|
||||||
|
"ตุลาคม",
|
||||||
|
"พฤศจิกายน",
|
||||||
|
"ธันวาคม",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pnd_form(row, supplier_type):
|
||||||
|
"""P.N.D. return the certificate row is filed on, as numbered on the form:
|
||||||
|
1 = ภ.ง.ด.1ก, 2 = ภ.ง.ด.2, 3 = ภ.ง.ด.3, 53 = ภ.ง.ด.53.
|
||||||
|
|
||||||
|
Juristic payees (Company; Partnership, taken as registered) file on
|
||||||
|
ภ.ง.ด.53 whatever the income; individuals by income type: salary on
|
||||||
|
ภ.ง.ด.1ก, Sec. 40(3)/(4) on ภ.ง.ด.2, the rest on ภ.ง.ด.3.
|
||||||
|
"""
|
||||||
|
if supplier_type != "Individual":
|
||||||
|
return "53"
|
||||||
|
if row == "1":
|
||||||
|
return "1"
|
||||||
|
if row in ("3", "4a", "4b"):
|
||||||
|
return "2"
|
||||||
|
return "3"
|
||||||
|
|
||||||
|
|
||||||
|
def one_line_address(address_name):
|
||||||
|
"""Address in the order of the Thailand Address Template, on one line."""
|
||||||
|
if not address_name:
|
||||||
|
return None
|
||||||
|
address = frappe.get_cached_doc("Address", address_name)
|
||||||
|
parts = [
|
||||||
|
address.address_line1,
|
||||||
|
address.address_line2,
|
||||||
|
address.county,
|
||||||
|
address.city,
|
||||||
|
address.state,
|
||||||
|
address.pincode,
|
||||||
|
]
|
||||||
|
if address.country and address.country != "Thailand":
|
||||||
|
parts.append(address.country)
|
||||||
|
return " ".join(part.strip() for part in parts if part and part.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def tax_id_digits(tax_id):
|
||||||
|
"""The 13 digits of a Thai tax ID for the form's boxes, or None when it is not one."""
|
||||||
|
digits = "".join(ch for ch in (tax_id or "") if ch.isdigit())
|
||||||
|
return digits if len(digits) == 13 else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_withholding_certificate(doc):
|
||||||
|
"""Certificate data for a Purchase Invoice with Apply Tax Withholding Amount.
|
||||||
|
|
||||||
|
Amounts are in company currency; the form is Thai, so dates are Buddhist Era
|
||||||
|
and the tax in words is Thai whatever the print language.
|
||||||
|
"""
|
||||||
|
company = frappe.get_cached_doc("Company", doc.company)
|
||||||
|
currency = erpnext.get_company_currency(doc.company)
|
||||||
|
precision = doc.precision("base_grand_total")
|
||||||
|
|
||||||
|
category = (
|
||||||
|
frappe.get_cached_doc("Tax Withholding Category", doc.tax_withholding_category)
|
||||||
|
if doc.tax_withholding_category
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
row = CERTIFICATE_ROWS.get(category.income_type if category else None, "6")
|
||||||
|
supplier_type = frappe.get_cached_value("Supplier", doc.supplier, "supplier_type")
|
||||||
|
|
||||||
|
amount = flt(doc.base_tax_withholding_net_total, precision)
|
||||||
|
tax = flt(
|
||||||
|
sum(abs(flt(t.base_tax_amount)) for t in doc.get("taxes") if cint(t.is_tax_withholding_account)),
|
||||||
|
precision,
|
||||||
|
)
|
||||||
|
posting_date = getdate(doc.posting_date)
|
||||||
|
|
||||||
|
return frappe._dict(
|
||||||
|
payer=frappe._dict(
|
||||||
|
name=company.company_name,
|
||||||
|
tax_id=company.tax_id,
|
||||||
|
tax_id_digits=tax_id_digits(company.tax_id),
|
||||||
|
address=one_line_address(get_default_address("Company", doc.company)),
|
||||||
|
),
|
||||||
|
payee=frappe._dict(
|
||||||
|
name=doc.supplier_name,
|
||||||
|
tax_id=doc.tax_id,
|
||||||
|
tax_id_digits=tax_id_digits(doc.tax_id),
|
||||||
|
address=one_line_address(doc.supplier_address or get_default_address("Supplier", doc.supplier)),
|
||||||
|
),
|
||||||
|
row=row,
|
||||||
|
# row 6 prints what was paid for; translations/th.csv carries the fixture categories
|
||||||
|
row_note=_(category.category_name) if row == "6" and category else None,
|
||||||
|
pnd=pnd_form(row, supplier_type),
|
||||||
|
date=f"{posting_date.day:02d}/{posting_date.month:02d}/{posting_date.year + 543}",
|
||||||
|
amount=fmt_money(amount, precision),
|
||||||
|
tax=fmt_money(tax, precision),
|
||||||
|
tax_in_words=money_in_words(tax, currency, lang="th"),
|
||||||
|
issued=frappe._dict(
|
||||||
|
day=posting_date.day,
|
||||||
|
month=THAI_MONTHS[posting_date.month - 1],
|
||||||
|
year=posting_date.year + 543,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{#- ERPNext's templates/print_formats/includes/taxes.html, printing the amounts that are posted.
|
||||||
|
|
||||||
|
ERPNext takes an Additional Discount off the items' net amounts (item.distributed_discount_amount;
|
||||||
|
a discount on Grand Total is split between net and taxes) and books each charge at
|
||||||
|
tax_amount_after_discount_amount, while upstream prints the discount as entered and the
|
||||||
|
pre-discount tax_amount, so the lines do not add up to Grand Total.
|
||||||
|
|
||||||
|
Inclusive tax with a discount on Grand Total: the Total line is the tax-inclusive item total
|
||||||
|
and the discount is printed as entered; Total After Discount is then broken down into
|
||||||
|
Net Total and the charges. Otherwise the Total line is the net before the discount, so the
|
||||||
|
discount is its net share, followed by Net Total and the charges. A discount that is not
|
||||||
|
distributed (cash / non-trade) comes off the grand total only and stays after the charges,
|
||||||
|
as upstream. -#}
|
||||||
|
{%- macro amount_row(label, value) -%}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
|
||||||
|
<label>{{ label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-7 text-right">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endmacro -%}
|
||||||
|
{%- macro currency(value) -%}
|
||||||
|
{{ frappe.format_value(value, {"fieldtype": "Currency", "options": "currency"}, doc) }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
{%- set net_discount = doc.get("items")|map(attribute="distributed_discount_amount")|select|sum -%}
|
||||||
|
{%- set gross_discount = net_discount and doc.apply_discount_on == "Grand Total" and doc.flags.show_inclusive_tax_in_print -%}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6"></div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
{%- if gross_discount -%}
|
||||||
|
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ doc.get_formatted("discount_amount", doc)) }}
|
||||||
|
{{ amount_row(_("Total After Discount"), currency(doc.total - doc.discount_amount)) }}
|
||||||
|
{{ amount_row(_(doc.meta.get_label("net_total")), doc.get_formatted("net_total", doc)) }}
|
||||||
|
{%- elif net_discount -%}
|
||||||
|
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ currency(net_discount)) }}
|
||||||
|
{{ amount_row(_(doc.meta.get_label("net_total")), doc.get_formatted("net_total", doc)) }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- for charge in data -%}
|
||||||
|
{%- if (charge.tax_amount or print_settings.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
|
||||||
|
{{ amount_row(charge.get_formatted("description"), charge.get_formatted("tax_amount_after_discount_amount", doc)) }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- if doc.discount_amount and not net_discount -%}
|
||||||
|
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ doc.get_formatted("discount_amount", doc)) }}
|
||||||
|
{%- endif -%}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{#- ERPNext's templates/print_formats/includes/total.html, paired with the taxes include:
|
||||||
|
with inclusive tax and a discount on Grand Total the Total line is the tax-inclusive item
|
||||||
|
total (the discount is printed as entered below it); with inclusive tax otherwise it is
|
||||||
|
the net before the Additional Discount, since net_total already has the discount's net
|
||||||
|
share (item.distributed_discount_amount) taken off. -#}
|
||||||
|
{%- set net_discount = doc.get("items")|map(attribute="distributed_discount_amount")|select|sum -%}
|
||||||
|
{%- set gross_discount = net_discount and doc.apply_discount_on == "Grand Total" and doc.flags.show_inclusive_tax_in_print -%}
|
||||||
|
<div class="row {% if df.bold %}important{% endif %} data-field">
|
||||||
|
{% if doc.flags.show_inclusive_tax_in_print and not gross_discount %}
|
||||||
|
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
|
||||||
|
<label>{{ _("Total (Without Tax)") }}</label></div>
|
||||||
|
<div class="col-xs-7 text-right value">
|
||||||
|
{{ frappe.format_value(doc.net_total + net_discount, {"fieldtype": "Currency", "options": "currency"}, doc) }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
|
||||||
|
<label>{{ _(df.label) }}</label></div>
|
||||||
|
<div class="col-xs-7 text-right value">
|
||||||
|
{{ doc.get_formatted("total", doc) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
{#- หนังสือรับรองการหักภาษี ณ ที่จ่าย (Sec. 50 bis certificate) for a Purchase Invoice.
|
||||||
|
|
||||||
|
The Revenue Department's fillable form (approve_wh3_081156.pdf, A4 = 595 x 842 pt) is the page
|
||||||
|
background; every value sits in the rectangle of the corresponding AcroForm field, given here as
|
||||||
|
the PDF's (x0, y0, x1, y1) with the origin at the bottom-left so the numbers can be checked against
|
||||||
|
the form. Two identical copies are printed: copy 1 goes with the payee's tax return, copy 2 is the
|
||||||
|
payee's record. -#}
|
||||||
|
{%- set c = get_withholding_certificate(doc) -%}
|
||||||
|
{%- set rows = {
|
||||||
|
"1": {"date": (327, 533, 403, 546), "pay": (411, 533, 490, 547), "tax": (496, 534, 560, 547)},
|
||||||
|
"2": {"date": (328, 519, 403, 533), "pay": (410, 520, 489, 534), "tax": (496, 519, 560, 532)},
|
||||||
|
"3": {"date": (328, 504, 403, 518), "pay": (411, 504, 490, 517), "tax": (496, 504, 560, 517)},
|
||||||
|
"4a": {"date": (328, 490, 403, 505), "pay": (412, 490, 491, 504), "tax": (496, 490, 560, 503)},
|
||||||
|
"4b": {"date": (328, 402, 404, 416), "pay": (410, 403, 489, 417), "tax": (497, 403, 562, 416)},
|
||||||
|
"5": {"date": (327, 216, 403, 230), "pay": (409, 216, 489, 230), "tax": (496, 215, 561, 230)},
|
||||||
|
"6": {"date": (327, 199, 403, 213), "pay": (409, 199, 489, 213), "tax": (496, 198, 561, 213)},
|
||||||
|
} -%}
|
||||||
|
{%- set pnd_boxes = {
|
||||||
|
"1": (209, 603, 222, 615),
|
||||||
|
"2": (395, 602, 407, 615),
|
||||||
|
"3": (471, 602, 484, 615),
|
||||||
|
"53": (395, 584, 407, 597),
|
||||||
|
} -%}
|
||||||
|
{#- Left edges of the printed 13-digit ID boxes (12 pt wide, grouped 1-4-5-2-1), measured on the
|
||||||
|
form; the AcroForm comb field does not line up with them. -#}
|
||||||
|
{%- set id_cells = (375, 393, 405, 417, 429, 447.5, 459.5, 471.5, 483.5, 495.5, 513.5, 525.5, 545) -%}
|
||||||
|
|
||||||
|
{%- macro box(rect, text, align="left", size=10) -%}
|
||||||
|
<div class="f" style="left: {{ rect[0] }}pt; top: {{ 842 - rect[3] }}pt; width: {{ rect[2] - rect[0] }}pt; height: {{ rect[3] - rect[1] }}pt; line-height: {{ rect[3] - rect[1] }}pt; text-align: {{ align }}; font-size: {{ size }}pt;">{{ text }}</div>
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro tick(rect) -%}
|
||||||
|
<div class="tick" style="left: {{ rect[0] }}pt; top: {{ 842 - rect[3] }}pt; width: {{ rect[2] - rect[0] }}pt; height: {{ rect[3] - rect[1] }}pt;"></div>
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro tax_id(rect, party) -%}
|
||||||
|
{%- if party.tax_id_digits -%}
|
||||||
|
{%- for digit in party.tax_id_digits -%}
|
||||||
|
{%- set x = id_cells[loop.index0] -%}
|
||||||
|
{{ box((x, rect[1], x + 12, rect[3]), digit, "center") }}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- elif party.tax_id -%}
|
||||||
|
{{ box(rect, party.tax_id, "center") }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro amount_row(key) -%}
|
||||||
|
{{ box(rows[key].date, c.date, "center") }}
|
||||||
|
{{ box(rows[key].pay, c.amount, "right") }}
|
||||||
|
{{ box(rows[key].tax, c.tax, "right") }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- for _ in range(2) -%}
|
||||||
|
<div class="wht-page">
|
||||||
|
<img class="wht-form" src="/assets/default_thai_company/images/withholding_tax_certificate.png" alt="">
|
||||||
|
|
||||||
|
{#- Invoice reference in the top-right margin; เล่มที่ / เลขที่ are the certificate book's numbers. -#}
|
||||||
|
{{ box((400, 821, 560, 835), doc.name, "right", 8) }}
|
||||||
|
|
||||||
|
{#- ผู้มีหน้าที่หักภาษี ณ ที่จ่าย: the company -#}
|
||||||
|
{{ tax_id((375, 744, 558, 759), c.payer) }}
|
||||||
|
{{ box((54, 729, 316, 745), c.payer.name) }}
|
||||||
|
{{ box((61, 706, 550, 722), c.payer.address or "", "left", 9) }}
|
||||||
|
|
||||||
|
{#- ผู้ถูกหักภาษี ณ ที่จ่าย: the supplier -#}
|
||||||
|
{{ tax_id((375, 676, 558, 690), c.payee) }}
|
||||||
|
{{ box((53, 658, 315, 671), c.payee.name) }}
|
||||||
|
{{ box((59, 627, 550, 643), c.payee.address or "", "left", 9) }}
|
||||||
|
|
||||||
|
{{ tick(pnd_boxes[c.pnd]) }}
|
||||||
|
|
||||||
|
{%- if c.row == "6" %}
|
||||||
|
{{ box((96, 197, 325, 214), c.row_note or "", "left", 8) }}
|
||||||
|
{%- endif %}
|
||||||
|
{{ amount_row(c.row) }}
|
||||||
|
{{ box((409, 180, 488, 196), c.amount, "right") }}
|
||||||
|
{{ box((496, 180, 560, 196), c.tax, "right") }}
|
||||||
|
{{ box((185, 158, 558, 177), c.tax_in_words) }}
|
||||||
|
|
||||||
|
{#- ผู้จ่ายเงิน (1) หัก ณ ที่จ่าย -#}
|
||||||
|
{{ tick((82, 119, 94, 131)) }}
|
||||||
|
|
||||||
|
{{ box((342, 72, 366, 87), c.issued.day, "center") }}
|
||||||
|
{{ box((364, 71, 428, 87), c.issued.month, "center") }}
|
||||||
|
{{ box((429, 72, 470, 87), c.issued.year, "center") }}
|
||||||
|
</div>
|
||||||
|
{%- endfor -%}
|
||||||
@@ -8,14 +8,19 @@ from frappe.utils import nowdate
|
|||||||
|
|
||||||
from default_thai_company.tax_withholding import (
|
from default_thai_company.tax_withholding import (
|
||||||
ASSET_ACCOUNT,
|
ASSET_ACCOUNT,
|
||||||
|
INPUT_VAT_ACCOUNT,
|
||||||
LIABILITY_ACCOUNT,
|
LIABILITY_ACCOUNT,
|
||||||
|
OUTPUT_VAT_ACCOUNT,
|
||||||
get_payment_entry,
|
get_payment_entry,
|
||||||
|
get_withholding_certificate,
|
||||||
thai_companies,
|
thai_companies,
|
||||||
)
|
)
|
||||||
|
|
||||||
COMPANY = "_Test WHT Company"
|
COMPANY = "_Test WHT Company"
|
||||||
ABBR = "_TWC"
|
ABBR = "_TWC"
|
||||||
CUSTOMER = "_Test WHT Customer"
|
CUSTOMER = "_Test WHT Customer"
|
||||||
|
SUPPLIER = "_Test WHT Supplier"
|
||||||
|
INDIVIDUAL = "_Test WHT Individual"
|
||||||
ITEM = "_Test WHT Service"
|
ITEM = "_Test WHT Service"
|
||||||
FIXTURE = frappe.get_app_path("default_thai_company", "fixtures", "tax_withholding_category.json")
|
FIXTURE = frappe.get_app_path("default_thai_company", "fixtures", "tax_withholding_category.json")
|
||||||
CUSTOM_DIR = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
|
CUSTOM_DIR = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
|
||||||
@@ -25,12 +30,13 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
# Fixture categories must exist before the company is created so the
|
# Custom Fields first: the category fixtures carry income_type. Fixture
|
||||||
# Company hook has something to link; force=True re-imports.
|
# categories must exist before the company is created so the Company hook
|
||||||
import_file_by_path(FIXTURE, force=True, data_import=True)
|
# has something to link; force=True re-imports.
|
||||||
for fname in ("sales_invoice.json", "payment_entry.json"):
|
for fname in ("tax_withholding_category.json", "sales_invoice.json", "payment_entry.json"):
|
||||||
with open(f"{CUSTOM_DIR}/{fname}") as f:
|
with open(f"{CUSTOM_DIR}/{fname}") as f:
|
||||||
sync_customizations_for_doctype(json.load(f), CUSTOM_DIR, fname)
|
sync_customizations_for_doctype(json.load(f), CUSTOM_DIR, fname)
|
||||||
|
import_file_by_path(FIXTURE, force=True, data_import=True)
|
||||||
|
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -40,20 +46,12 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
"country": "Thailand",
|
"country": "Thailand",
|
||||||
"default_currency": "THB",
|
"default_currency": "THB",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
|
"tax_id": "0105551234567",
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}"
|
cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}"
|
||||||
cls.receivable = f"{ASSET_ACCOUNT} - {ABBR}"
|
cls.receivable = f"{ASSET_ACCOUNT} - {ABBR}"
|
||||||
|
cls.vat = frappe.get_doc("Account", f"{OUTPUT_VAT_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(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -76,6 +74,35 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
|
supplier_group = frappe.db.get_value("Supplier Group", {"is_group": 0})
|
||||||
|
for name, supplier_type, tax_id in (
|
||||||
|
(SUPPLIER, "Company", "0-1234-56789-01-2"),
|
||||||
|
(INDIVIDUAL, "Individual", "1234567890123"),
|
||||||
|
):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Supplier",
|
||||||
|
"supplier_name": name,
|
||||||
|
"supplier_type": supplier_type,
|
||||||
|
"supplier_group": supplier_group,
|
||||||
|
"tax_id": tax_id,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Address",
|
||||||
|
"address_title": SUPPLIER,
|
||||||
|
"address_type": "Billing",
|
||||||
|
"address_line1": "99/9 Moo 5",
|
||||||
|
"address_line2": "Soi Sukhumvit 24",
|
||||||
|
"city": "Khlong Toei",
|
||||||
|
"state": "Bangkok",
|
||||||
|
"pincode": "10110",
|
||||||
|
"country": "Thailand",
|
||||||
|
"links": [{"link_doctype": "Supplier", "link_name": SUPPLIER}],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
def make_invoice(self, rate=10000, category=None, inclusive=False):
|
def make_invoice(self, rate=10000, category=None, inclusive=False):
|
||||||
si = frappe.get_doc(
|
si = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -128,6 +155,31 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
def withheld(self, pe):
|
def withheld(self, pe):
|
||||||
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
|
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
|
||||||
|
|
||||||
|
def make_purchase_invoice(self, supplier, category, rate=100000):
|
||||||
|
pi = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Purchase Invoice",
|
||||||
|
"company": COMPANY,
|
||||||
|
"supplier": supplier,
|
||||||
|
"set_posting_time": 1,
|
||||||
|
"posting_date": "2026-09-16",
|
||||||
|
"due_date": "2026-09-16",
|
||||||
|
"apply_tds": 1,
|
||||||
|
"tax_withholding_category": category,
|
||||||
|
"items": [{"item_code": ITEM, "qty": 1, "rate": rate}],
|
||||||
|
"taxes": [
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": f"{INPUT_VAT_ACCOUNT} - {ABBR}",
|
||||||
|
"rate": 7,
|
||||||
|
"description": "VAT 7%",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pi.set_missing_values()
|
||||||
|
return pi.insert()
|
||||||
|
|
||||||
def category_account(self, category, company=COMPANY):
|
def category_account(self, category, company=COMPANY):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Tax Withholding Account",
|
"Tax Withholding Account",
|
||||||
@@ -137,14 +189,17 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
|
|
||||||
def test_company_creation_adds_accounts_and_links_categories(self):
|
def test_company_creation_adds_accounts_and_links_categories(self):
|
||||||
self.assertIn(COMPANY, thai_companies())
|
self.assertIn(COMPANY, thai_companies())
|
||||||
self.assertEqual(
|
for account, expected in (
|
||||||
frappe.db.get_value("Account", self.payable, ["root_type", "parent_account"]),
|
(self.payable, ("Liability", f"Duties and Taxes - {ABBR}", "Tax")),
|
||||||
("Liability", f"Duties and Taxes - {ABBR}"),
|
(self.receivable, ("Asset", f"Tax Assets - {ABBR}", "Tax")),
|
||||||
)
|
(f"{OUTPUT_VAT_ACCOUNT} - {ABBR}", ("Liability", f"Duties and Taxes - {ABBR}", "Tax")),
|
||||||
self.assertEqual(
|
(f"{INPUT_VAT_ACCOUNT} - {ABBR}", ("Asset", f"Tax Assets - {ABBR}", "Tax")),
|
||||||
frappe.db.get_value("Account", self.receivable, ["root_type", "parent_account"]),
|
):
|
||||||
("Asset", f"Tax Assets - {ABBR}"),
|
self.assertEqual(
|
||||||
)
|
frappe.db.get_value("Account", account, ["root_type", "parent_account", "account_type"]),
|
||||||
|
expected,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
categories = frappe.get_all(
|
categories = frappe.get_all(
|
||||||
"Tax Withholding Category", filters={"name": ("like", "WHT %")}, pluck="name"
|
"Tax Withholding Category", filters={"name": ("like", "WHT %")}, pluck="name"
|
||||||
@@ -153,6 +208,67 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
for name in categories:
|
for name in categories:
|
||||||
self.assertEqual(self.category_account(name), self.payable, name)
|
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_company_creation_adds_shipping_rule(self):
|
||||||
|
rule = frappe.get_doc("Shipping Rule", f"Shipping Charges - {ABBR}")
|
||||||
|
self.assertEqual(
|
||||||
|
(rule.company, rule.shipping_rule_type, rule.calculate_based_on, rule.account, rule.cost_center),
|
||||||
|
(COMPANY, "Selling", "Fixed", f"Shipping Charges - {ABBR}", f"Main - {ABBR}"),
|
||||||
|
)
|
||||||
|
self.assertEqual(frappe.db.get_value("Account", rule.account, "root_type"), "Income")
|
||||||
|
|
||||||
|
def test_shipping_amount_entered_on_transaction_survives_recalculation(self):
|
||||||
|
rule = f"Shipping Charges - {ABBR}"
|
||||||
|
so = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Sales Order",
|
||||||
|
"company": COMPANY,
|
||||||
|
"customer": CUSTOMER,
|
||||||
|
"transaction_date": nowdate(),
|
||||||
|
"delivery_date": nowdate(),
|
||||||
|
"shipping_rule": rule,
|
||||||
|
"items": [{"item_code": ITEM, "qty": 1, "rate": 1000}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
so.set_missing_values()
|
||||||
|
so.apply_shipping_rule()
|
||||||
|
(charge,) = so.taxes
|
||||||
|
self.assertEqual((charge.description, charge.tax_amount), (rule, 0))
|
||||||
|
|
||||||
|
charge.tax_amount = 150
|
||||||
|
so.insert()
|
||||||
|
so.apply_shipping_rule()
|
||||||
|
so.save()
|
||||||
|
self.assertEqual([t.tax_amount for t in so.taxes], [150])
|
||||||
|
self.assertEqual(so.grand_total, 1150)
|
||||||
|
|
||||||
def test_fixture_reimport_keeps_site_account_and_relinks(self):
|
def test_fixture_reimport_keeps_site_account_and_relinks(self):
|
||||||
alt = frappe.get_doc(
|
alt = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -273,3 +389,34 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
|
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
|
||||||
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
|
||||||
|
|
||||||
|
def test_certificate_reports_withholding_on_the_category_row(self):
|
||||||
|
pi = self.make_purchase_invoice(SUPPLIER, "WHT 3% - Professional Fee")
|
||||||
|
self.assertEqual(pi.grand_total, 104000.0) # 100,000 + 7% VAT - 3% withheld
|
||||||
|
|
||||||
|
c = get_withholding_certificate(pi)
|
||||||
|
self.assertEqual((c.row, c.row_note, c.pnd), ("5", None, "53"))
|
||||||
|
self.assertEqual((c.amount, c.tax, c.tax_in_words), ("100,000.00", "3,000.00", "สามพันบาทถ้วน"))
|
||||||
|
self.assertEqual(
|
||||||
|
(c.date, dict(c.issued)), ("16/09/2569", {"day": 16, "month": "กันยายน", "year": 2569})
|
||||||
|
)
|
||||||
|
self.assertEqual((c.payer.name, c.payer.tax_id_digits), (COMPANY, "0105551234567"))
|
||||||
|
self.assertEqual(
|
||||||
|
(c.payee.name, c.payee.tax_id_digits, c.payee.address),
|
||||||
|
(SUPPLIER, "0123456789012", "99/9 Moo 5 Soi Sukhumvit 24 Khlong Toei Bangkok 10110"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_certificate_return_follows_payee_type_and_income(self):
|
||||||
|
c = get_withholding_certificate(
|
||||||
|
self.make_purchase_invoice(INDIVIDUAL, "WHT 15% - Interest (Individual)")
|
||||||
|
)
|
||||||
|
self.assertEqual((c.row, c.pnd, c.tax), ("4a", "2", "15,000.00"))
|
||||||
|
self.assertIsNone(c.payee.address)
|
||||||
|
|
||||||
|
c = get_withholding_certificate(
|
||||||
|
self.make_purchase_invoice(INDIVIDUAL, "WHT 15% - Non-Resident Individual")
|
||||||
|
)
|
||||||
|
self.assertEqual((c.row, c.pnd), ("6", "3"))
|
||||||
|
self.assertEqual(
|
||||||
|
c.row_note, "Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3)"
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ Signatures,ลายเซ็น
|
|||||||
On behalf of,ในนาม
|
On behalf of,ในนาม
|
||||||
Receiver,ผู้รับ
|
Receiver,ผู้รับ
|
||||||
Issuer,ผู้ออก
|
Issuer,ผู้ออก
|
||||||
|
Accepted By,ผู้อนุมัติสั่งซื้อ
|
||||||
|
Quoted By,ผู้เสนอราคา
|
||||||
Tax Invoice,ใบกำกับภาษี
|
Tax Invoice,ใบกำกับภาษี
|
||||||
Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
|
Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
|
||||||
"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
|
"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
|
||||||
@@ -48,8 +50,21 @@ Amount,จำนวนเงิน
|
|||||||
Total,รวม
|
Total,รวม
|
||||||
Total (Without Tax),รวมก่อนภาษี
|
Total (Without Tax),รวมก่อนภาษี
|
||||||
Net Total,ยอดรวมสุทธิ
|
Net Total,ยอดรวมสุทธิ
|
||||||
|
Total After Discount,ยอดรวมหลังหักส่วนลด
|
||||||
Total Taxes and Charges,รวมภาษีและค่าธรรมเนียม
|
Total Taxes and Charges,รวมภาษีและค่าธรรมเนียม
|
||||||
Grand Total,ยอดรวมทั้งสิ้น
|
Grand Total,ยอดรวมทั้งสิ้น
|
||||||
Rounded Total,ยอดรวมปัดเศษ
|
Rounded Total,ยอดรวมปัดเศษ
|
||||||
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
|
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
|
||||||
Page {0} of {1},หน้า {0} จาก {1}
|
Page {0} of {1},หน้า {0} จาก {1}
|
||||||
|
Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย
|
||||||
|
Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย
|
||||||
|
Output VAT,ภาษีขาย
|
||||||
|
Input VAT,ภาษีซื้อ
|
||||||
|
Withholding Tax Certificate,หนังสือรับรองการหักภาษี ณ ที่จ่าย
|
||||||
|
Type of Income Paid,ประเภทเงินได้พึงประเมินที่จ่าย
|
||||||
|
Salary and Wages - Sec. 40(1),เงินเดือน ค่าจ้าง ฯลฯ ตามมาตรา 40(1)
|
||||||
|
Fees and Commissions - Sec. 40(2),ค่าธรรมเนียม ค่านายหน้า ฯลฯ ตามมาตรา 40(2)
|
||||||
|
Royalties - Sec. 40(3),ค่าแห่งลิขสิทธิ์ ฯลฯ ตามมาตรา 40(3)
|
||||||
|
Interest - Sec. 40(4)(a),ดอกเบี้ย ฯลฯ ตามมาตรา 40(4)(ก)
|
||||||
|
Dividends - Sec. 40(4)(b),เงินปันผล เงินส่วนแบ่งกำไร ฯลฯ ตามมาตรา 40(4)(ข)
|
||||||
|
"Sec. 3 Tera (Services, Rent, Contract Work etc.)",ตามคำสั่งกรมสรรพากรที่ออกตามมาตรา 3 เตรส (ค่าบริการ ค่าเช่า ค่าจ้างทำของ ฯลฯ)
|
||||||
|
|||||||
|
@@ -5,14 +5,14 @@ from frappe.contacts.doctype.address.address import get_default_address, render_
|
|||||||
from num2words import num2words
|
from num2words import num2words
|
||||||
|
|
||||||
|
|
||||||
def money_in_words(amount, currency):
|
def money_in_words(amount, currency, lang=None):
|
||||||
"""Amount in words for the active language.
|
"""Amount in words for `lang` (default: the active language).
|
||||||
|
|
||||||
frappe.utils.money_in_words renders "<currency> <words> only." in every
|
frappe.utils.money_in_words renders "<currency> <words> only." in every
|
||||||
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
|
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
|
||||||
Currencies num2words cannot spell in Thai keep frappe's wording.
|
Currencies num2words cannot spell in Thai keep frappe's wording.
|
||||||
"""
|
"""
|
||||||
if frappe.local.lang == "th":
|
if (lang or frappe.local.lang) == "th":
|
||||||
try:
|
try:
|
||||||
return num2words(amount, lang="th", to="currency", currency=currency)
|
return num2words(amount, lang="th", to="currency", currency=currency)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user