Author SHA1 Message Date
kurogeek 813a5402f4 fix: label tax id as "Tax ID" in the letter head 2026-09-10 02:52:25 +00:00
kurogeek 33a0fc2603 fix: company logo on the left of the letter head 2026-09-10 02:48:28 +00:00
kurogeek 84e23a6b71 feat: place company logo on the right of the letter head 2026-09-10 02:45:57 +00:00
kurogeek a6620d3743 fix: right-align letter head with the document
Text block flex-fills remaining width and right-aligns its text, so it sits at
the document's right edge with or without a company logo. Drop the fixed 500px
width; keep -webkit-box fallbacks for wkhtmltopdf.
2026-09-10 02:40:46 +00:00
kurogeek 9091caffab feat: company-driven letter head and Thai address template
- Letter Head fixture renders company details (logo, name, tax id, address,
  phone, email) from the printed document's company via a jinja helper
  instead of hardcoded values; falls back to the default company for
  doctypes without a company field.
- Register fixtures in hooks (Letter Head, Address Template); they were
  never imported before.
- Preserve the fixture's `source = HTML` on import: LetterHead.before_insert
  forces "Image" and fixture import is delete + insert.
- Thailand Address Template: two-line address, phone/fax/email lines kept.
2026-09-09 10:30:01 +00:00
kurogeek 3d520cbd92 mob next [ci-skip] [ci skip] [skip ci]
lastFile:default_thai_company/fixtures/letter_head.json
2026-09-09 16:29:25 +07:00
32 changed files with 17 additions and 3098 deletions
-8
View File
@@ -12,14 +12,6 @@ bench get-app $URL_OF_THIS_REPO --branch develop
bench install-app default_thai_company
```
### Thai language
Every string the app adds (custom field labels, messages, print format and letter head
text, Tax Withholding Category names) is translated in `translations/th.csv`; the same
file overrides the upstream Thai for the standard labels the print formats show. The
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.
### 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:
-91
View File
@@ -1,91 +0,0 @@
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")
@@ -1,32 +0,0 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Payment Entry",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-09-11 17:18:28.670630",
"default_value": null,
"doc_type": "Payment Entry",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "apply_tax_withholding_amount",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-09-11 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Payment Entry-apply_tax_withholding_amount-depends_on",
"owner": "Administrator",
"property": "depends_on",
"property_type": "Data",
"row_name": null,
"value": "eval:doc.party_type == 'Supplier' || (doc.party_type == 'Customer' && doc.payment_type == 'Receive')"
}
],
"sync_on_migrate": 1
}
@@ -1,225 +0,0 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2026-09-11 17:18:48.182536",
"default": null,
"depends_on": "eval:doc.tax_withholding_category",
"description": null,
"docstatus": 0,
"dt": "Sales Invoice",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "amount_after_withholding",
"fieldtype": "Currency",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 80,
"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": "withholding_tax_amount",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Net Payable After Withholding Tax",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-11 17:18:48.182536",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-amount_after_withholding",
"no_copy": 1,
"non_negative": 0,
"options": "currency",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"print_hide_if_no_value": 1,
"print_width": null,
"read_only": 1,
"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
},
{
"_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-11 17:18:44.710456",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Sales Invoice",
"fetch_from": "customer.tax_withholding_category",
"fetch_if_empty": 1,
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 57,
"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": "taxes_and_charges",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Tax Withholding Category",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-11 18:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-tax_withholding_category",
"no_copy": 0,
"non_negative": 0,
"options": "Tax Withholding Category",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 1,
"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
},
{
"_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-11 17:18:47.244523",
"default": null,
"depends_on": "eval:doc.tax_withholding_category",
"description": null,
"docstatus": 0,
"dt": "Sales Invoice",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "withholding_tax_amount",
"fieldtype": "Currency",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 79,
"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": "rounded_total",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Withholding Tax",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-11 18:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-withholding_tax_amount",
"no_copy": 1,
"non_negative": 0,
"options": "currency",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"print_hide_if_no_value": 1,
"print_width": null,
"read_only": 1,
"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": "Sales Invoice",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-09-11 17:18:28.581896",
"default_value": null,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype_or_field": "DocType",
"field_name": null,
"idx": 0,
"is_system_generated": 1,
"modified": "2026-09-10 13:55:13.996942",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-main-default_print_format",
"owner": "Administrator",
"property": "default_print_format",
"property_type": "Data",
"row_name": null,
"value": "Default Standard Sales Invoice"
}
],
"sync_on_migrate": 1
}
@@ -6,6 +6,6 @@
"is_default": 1,
"modified": "2026-09-09 17:23:33.559202",
"name": "Thailand",
"template": "{{ address_line1 }}{% if address_line2 %} {{ address_line2 }}{% endif %}<br>\n{% if county %}{{ county }} {% endif -%}{{ city }}{% if state %} {{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}{% if country != \"Thailand\" %} {{ country }}{% endif %}<br>\n{% if phone %}{{ _(\"Phone\") }}: {{ phone }}<br>{% endif -%}\n{% if fax %}{{ _(\"Fax\") }}: {{ fax }}<br>{% endif -%}\n{% if email_id %}{{ _(\"Email\") }}: {{ email_id }}<br>{% endif -%}\n"
"template": "{{ address_line1 }}{% if address_line2 %} {{ address_line2 }}{% endif %}<br>\n{% if county %}{{ county }} {% endif -%}{{ city }}{% if state %} {{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}{% if country != \"Thailand\" %} {{ country }}{% endif %}<br>\n{% if phone or fax or email_id %}<br>{% endif -%}\n{% if phone %}{{ _(\"Phone\") }}: {{ phone }}<br>{% endif -%}\n{% if fax %}{{ _(\"Fax\") }}: {{ fax }}<br>{% endif -%}\n{% if email_id %}{{ _(\"Email\") }}: {{ email_id }}<br>{% endif -%}\n"
}
]
@@ -1,237 +0,0 @@
[
{
"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
}
]
@@ -1,106 +0,0 @@
[
{
"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"
}
]
@@ -1,299 +0,0 @@
[
{
"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)"
}
]
@@ -1,113 +0,0 @@
[
{
"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
}
]
@@ -1,7 +1,7 @@
[
{
"align": "Left",
"content": "<style>\n .bpt-title {\n display: -webkit-box;\n display: flex;\n -webkit-box-align: center;\n align-items: center;\n width: 100%;\n }\n .bpt-title img {\n margin-right: 1em;\n }\n .bpt-title .bpt-company {\n -webkit-box-flex: 1;\n flex: 1;\n text-align: right;\n }\n</style>\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n<div class=\"bpt-title\">\n {% if company.company_logo %}\n <img alt=\"{{ company.company_name }}\" height=\"125\" src=\"{{ company.company_logo }}\">\n {% endif %}\n <div class=\"bpt-company\">\n <h1>{{ company.company_name }}</h1>\n <small>\n {% if company.tax_id %}<div>{{ _(\"Tax ID\") }}: {{ company.tax_id }}</div>{% endif %}\n {% if company.address %}<div>{{ company.address }}</div>{% endif %}\n {% if company.phone_no %}<div>{{ _(\"Phone\") }}: {{ company.phone_no }}</div>{% endif %}\n {% if company.email %}<div>{{ _(\"Email\") }}: {{ company.email }}</div>{% endif %}\n </small>\n </div>\n</div>\n{% endif %}\n",
"content": "<style>\n .bpt-title {\n display: -webkit-box;\n display: flex;\n -webkit-box-align: center;\n align-items: center;\n width: 100%;\n }\n .bpt-title img {\n margin-right: 1em;\n }\n .bpt-title .bpt-company {\n -webkit-box-flex: 1;\n flex: 1;\n text-align: right;\n }\n</style>\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n<div class=\"bpt-title\">\n {% if company.company_logo %}\n <img alt=\"{{ company.company_name }}\" height=\"125\" src=\"{{ company.company_logo }}\">\n {% endif %}\n <div class=\"bpt-company\">\n <h1>{{ company.company_name }}</h1>\n <small>\n {% if company.tax_id %}<div>Tax ID: {{ company.tax_id }}</div>{% endif %}\n {% if company.address %}<div>{{ company.address }}</div>{% endif %}\n {% if company.phone_no %}<div>Phone: {{ company.phone_no }}</div>{% endif %}\n {% if company.email %}<div>Email: {{ company.email }}</div>{% endif %}\n </small>\n </div>\n</div>\n{% endif %}\n",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
@@ -18,7 +18,7 @@
"image_width": 0.0,
"is_default": 1,
"letter_head_name": "Default Letter Head",
"modified": "2026-09-15 06:44:37.653888",
"modified": "2026-09-10 09:51:23.933752",
"name": "Default Letter Head",
"source": "HTML"
}
@@ -1,42 +0,0 @@
[
{
"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
@@ -1,574 +0,0 @@
[
{
"accounts": [],
"category_name": "Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Commission",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Commission",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Royalty",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Royalty",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Interest (Company)",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Interest (Company)",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 15% - Interest (Individual)",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 15% - Interest (Individual)",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 15.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 10% - Dividend",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 10% - Dividend",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 10.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 5% - Rent",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 5% - Rent",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 5.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Ship Rental",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Ship Rental",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Professional Fee",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Professional Fee",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Contract Work",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Contract Work",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Service",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Service",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Sales Promotion",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Sales Promotion",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 2% - Advertising",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 2% - Advertising",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 2.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Transportation",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Transportation",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Non-life insurance premiums - Sec. 40(8) (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Non-Life Insurance",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Non-Life Insurance",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 5% - Prize",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 5% - Prize",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 5.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 5% - Public Entertainer",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 5% - Public Entertainer",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 5.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rubber, cassava, jute, rice, maize, sugarcane, coffee beans, oil palm - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 0.75% - Agricultural Produce",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 0.75% - Agricultural Produce",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 0.75,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Aquatic Animals",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Aquatic Animals",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Immovable Property (Company)",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Immovable Property (Company)",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 15% - Non-Resident Individual",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 15% - Non-Resident Individual",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 15.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Sec. 40(2)-(6) income excl. dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 15% - Foreign Company",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 15% - Foreign Company",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 15.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 10% - Foreign Company Dividend",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 10% - Foreign Company Dividend",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 10.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
}
]
+12 -87
View File
@@ -43,7 +43,7 @@ required_apps = ["erpnext"]
# page_js = {"page" : "public/js/file.js"}
# include js in doctype views
doctype_js = {"Payment Entry": "public/js/payment_entry.js"}
# doctype_js = {"doctype" : "public/js/doctype.js"}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
@@ -75,78 +75,22 @@ doctype_js = {"Payment Entry": "public/js/payment_entry.js"}
# add methods and filters to jinja environment
jinja = {
"methods": [
"default_thai_company.utils.get_letter_head_company",
"default_thai_company.utils.get_company_bank_account",
"default_thai_company.utils.get_in_words",
],
"methods": ["default_thai_company.utils.get_letter_head_company"],
}
# Fixtures
# --------
# Custom Fields and Property Setters live in default_thai_company/custom/*.json
# (synced in place on install and migrate); fixtures are for whole documents.
fixtures = [
{"doctype": "Letter Head", "filters": [["name", "in", ["Default Letter Head"]]]},
{"doctype": "Address Template", "filters": [["name", "in", ["Thailand"]]]},
{
"doctype": "Print Format",
"filters": [
[
"name",
"in",
[
"Default Standard Sales Invoice",
"Default Standard Tax Invoice/Receipt",
"Default Standard Quotation",
],
]
],
},
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
{
"doctype": "Location",
"filters": [["name", "in", ["All Locations", "Head Office", "Branch Office", "Warehouse"]]],
},
{
"doctype": "Asset Category",
"filters": [
[
"name",
"in",
[
"Land",
"Buildings",
"Plant and Machinery",
"Vehicles",
"Furniture and Fixtures",
"Office Equipment",
"Computers and Electronics",
"Software",
"Intangible Assets",
],
]
],
},
# HRMS doctypes; skipped by sync_fixtures on sites without hrms
{"doctype": "Employee Tax Exemption Category"},
{"doctype": "Employee Tax Exemption Sub Category"},
{"doctype": "Income Tax Slab", "filters": [["name", "=", "Thailand Personal Income Tax"]]},
]
# Installation
# ------------
# before_install = "default_thai_company.install.before_install"
# 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",
]
# Fixture import re-inserts the Location tree; restore lft/rgt for user-added nodes.
after_migrate = "default_thai_company.assets.rebuild_locations"
# after_install = "default_thai_company.install.after_install"
# Uninstallation
# ------------
@@ -192,11 +136,9 @@ after_migrate = "default_thai_company.assets.rebuild_locations"
# ---------------
# Override standard doctype classes
override_doctype_class = {
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
"Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
"Shipping Rule": "default_thai_company.shipping.ThaiShippingRule",
}
# override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo"
# }
# Document Events
# ---------------
@@ -206,24 +148,7 @@ doc_events = {
"Letter Head": {
"before_import": "default_thai_company.utils.stash_letter_head_source",
"before_insert": "default_thai_company.utils.restore_letter_head_source",
},
"Tax Withholding Category": {
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
},
"Asset Category": {
"before_import": "default_thai_company.assets.prepare_fixture_accounts",
},
"Company": {
"on_update": [
"default_thai_company.tax_withholding.setup_company",
"default_thai_company.vat.setup_company",
"default_thai_company.assets.setup_company",
"default_thai_company.shipping.setup_company",
],
},
"Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
},
}
}
# Scheduled Tasks
@@ -254,11 +179,11 @@ doc_events = {
# Overriding Methods
# ------------------------------
override_whitelisted_methods = {
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry": "default_thai_company.tax_withholding.get_payment_entry",
}
#
# override_whitelisted_methods = {
# "frappe.desk.doctype.event.event.get_events": "default_thai_company.event.get_events"
# }
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps
-3
View File
@@ -4,6 +4,3 @@
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated
default_thai_company.patches.create_vat_accounts
default_thai_company.patches.create_vat_templates
default_thai_company.patches.create_shipping_rules
@@ -1,5 +0,0 @@
from default_thai_company.shipping import setup_companies
def execute():
setup_companies()
@@ -1,8 +0,0 @@
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)
@@ -1,5 +0,0 @@
from default_thai_company.vat import setup_companies
def execute():
setup_companies()
-93
View File
@@ -1,93 +0,0 @@
Copyright 2018 The Sarabun Project Authors (https://github.com/cadsondemak/Sarabun)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
@@ -1,29 +0,0 @@
frappe.ui.form.on("Payment Entry", {
apply_tax_withholding_amount(frm) {
if (frm.doc.party_type !== "Customer" || frm.doc.payment_type !== "Receive") return;
if (!frm.doc.apply_tax_withholding_amount) {
frm.events.recompute_customer_withholding(frm);
return;
}
// ERPNext's handler looks the category up on Supplier and clears it for a
// Customer; wait for that request to settle, then set the Customer's value.
frappe.db.get_value("Customer", frm.doc.party, "tax_withholding_category").then(({ message }) => {
frappe.after_ajax(() => {
frm.set_value("tax_withholding_category", message.tax_withholding_category);
frm.events.recompute_customer_withholding(frm);
});
});
},
tax_withholding_category(frm) {
if (frm.doc.party_type === "Customer" && frm.doc.payment_type === "Receive") {
frm.events.recompute_customer_withholding(frm);
}
},
recompute_customer_withholding(frm) {
// allocate_amount_to_references is overridden server-side to refresh the withholding row
if (!frm.doc.references || !frm.doc.references.length) return;
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, false);
},
});
-66
View File
@@ -1,66 +0,0 @@
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)
-376
View File
@@ -1,376 +0,0 @@
import erpnext
import frappe
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry as _get_payment_entry
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_tax_withholding_details,
normal_round,
)
from frappe import _
from frappe.utils import cint, flt
from default_thai_company.utils import money_in_words
# Tax accounts every Thai company gets (Company.on_update, fixture import, and the
# create_vat_accounts patch for companies that predate the VAT pair).
# Withholding: liability = tax we withhold from suppliers, remitted on P.N.D.3/53
# (linked on every category); asset = tax customers withhold from us, creditable
# against corporate income tax.
# VAT: output (ภาษีขาย) collected on sales is payable, input (ภาษีซื้อ) paid on
# purchases is recoverable; the P.P.30 return nets the two, so they are kept apart.
LIABILITY_ACCOUNT = "Withholding Tax Payable"
ASSET_ACCOUNT = "Withholding Tax Receivable"
OUTPUT_VAT_ACCOUNT = "Output VAT"
INPUT_VAT_ACCOUNT = "Input VAT"
COMPANY_ACCOUNTS = (
(LIABILITY_ACCOUNT, "Liability"),
(ASSET_ACCOUNT, "Asset"),
(OUTPUT_VAT_ACCOUNT, "Liability"),
(INPUT_VAT_ACCOUNT, "Asset"),
)
# Preferred parent groups by root type; first match wins, else the root account.
PARENT_GROUPS = {
"Liability": ("Duties and Taxes", "Current Liabilities"),
"Asset": ("Tax Assets", "Current Assets"),
}
CATEGORY_FILTERS = {"name": ("like", "WHT %")}
def thai_companies():
return frappe.get_all("Company", filters={"country": "Thailand"}, pluck="name")
def get_or_create_account(company, account_name, root_type, account_type="Tax", parent_groups=None):
"""Leaf account `account_name` for `company`, created under the first existing
group in `parent_groups` (default: PARENT_GROUPS[root_type]), else the root."""
name = frappe.db.get_value(
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
)
if name:
return name
account = frappe.get_doc(
{
"doctype": "Account",
"company": company,
"account_name": account_name,
"parent_account": find_parent_group(
company, root_type, parent_groups or PARENT_GROUPS[root_type]
),
"root_type": root_type,
"account_type": account_type,
"is_group": 0,
}
).insert(ignore_permissions=True)
return account.name
def find_parent_group(company, root_type, group_names):
for account_name in group_names:
parent = frappe.db.get_value(
"Account",
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
)
if parent:
return parent
return frappe.db.get_value(
"Account",
{"company": company, "root_type": root_type, "is_group": 1, "parent_account": ("is", "not set")},
)
def ensure_company_accounts(company):
"""Create every account in COMPANY_ACCOUNTS for `company`; return the
withholding liability account name (the one Tax Withholding Categories link)."""
accounts = {name: get_or_create_account(company, name, root_type) for name, root_type in COMPANY_ACCOUNTS}
return accounts[LIABILITY_ACCOUNT]
def link_company(category, company, account):
"""Append `account` for `company` to the category's accounts table if missing."""
if any(row.company == company for row in category.accounts):
return False
category.append("accounts", {"company": company, "account": account})
return True
def prepare_fixture_accounts(doc, method=None):
"""Tax Withholding Category fixtures ship without `accounts` (company-specific).
Fixture import re-inserts the doc on every migrate, so: carry over the rows
already configured on this site, then link every Thai company, creating the
withholding accounts on first use. Mandatory is relaxed for sites without a
Thai company yet (setup wizard not run); `setup_company` links them later.
"""
doc.flags.ignore_mandatory = True
if not doc.accounts and frappe.db.exists(doc.doctype, doc.name):
for row in frappe.get_all(
"Tax Withholding Account",
filters={"parent": doc.name, "parenttype": doc.doctype},
fields=["company", "account"],
order_by="idx",
):
doc.append("accounts", row)
for company in thai_companies():
if not any(row.company == company for row in doc.accounts):
link_company(doc, company, ensure_company_accounts(company))
def company_ready(doc):
"""A Thai company whose chart of accounts exists."""
return doc.country == "Thailand" and frappe.db.exists("Account", {"company": doc.name})
def setup_company(doc, method=None):
"""Company.on_update: create the tax accounts and link every WHT category."""
if not company_ready(doc):
return
account = ensure_company_accounts(doc.name)
for name in frappe.get_all("Tax Withholding Category", filters=CATEGORY_FILTERS, pluck="name"):
category = frappe.get_doc("Tax Withholding Category", name)
if link_company(category, doc.name, account):
category.save(ignore_permissions=True)
def is_thai_company(company):
return frappe.get_cached_value("Company", company, "country") == "Thailand"
def get_withholding_details(category, posting_date, company):
details = get_tax_withholding_details(category, posting_date, company)
if not details:
frappe.throw(
_("Tax Withholding Category {0} has no account for Company {1}").format(category, company)
)
return details
def withholding_on(details, taxable, base_taxable, precision):
"""Withholding for a taxable amount; the single threshold is checked in company currency."""
if details.threshold and flt(base_taxable) < flt(details.threshold):
return 0.0
amount = flt(taxable) * flt(details.rate) / 100
return normal_round(amount) if cint(details.round_off_tax_amount) else flt(amount, precision)
class ThaiSalesInvoice(SalesInvoice):
def set_tax_withholding(self):
"""Thai customers withhold at payment (see `set_customer_withholding`);
ERPNext's customer-side handling is Indian TCS, which adds tax on top of
the invoice. For Thai companies only show the expected withholding and
the net payable; totals and GL are untouched."""
if not is_thai_company(self.company):
return super().set_tax_withholding()
self.withholding_tax_amount = self.amount_after_withholding = 0
if not self.tax_withholding_category:
return
details = get_withholding_details(self.tax_withholding_category, self.posting_date, self.company)
self.withholding_tax_amount = withholding_on(
details, self.net_total, self.base_net_total, self.precision("withholding_tax_amount")
)
if self.withholding_tax_amount:
self.amount_after_withholding = flt(
(self.rounded_total or self.grand_total) - self.withholding_tax_amount,
self.precision("amount_after_withholding"),
)
def set_total_in_words(self):
"""SellingController's version, with Thai wording when the saving user's
language is Thai. Print formats re-derive the text per print language
(`get_in_words`)."""
base_amount = abs(
self.base_grand_total if self.is_rounded_total_disabled() else self.base_rounded_total
)
self.base_in_words = money_in_words(base_amount, self.company_currency)
amount = abs(self.grand_total if self.is_rounded_total_disabled() else self.rounded_total)
self.in_words = money_in_words(amount, self.currency)
class ThaiPaymentEntry(PaymentEntry):
def build_gl_map(self):
"""A "Deduct" tax row on a receipt posts bank Dr gross and bank Cr
withholding; ERPNext merges them into one entry but only nets it when
the bank entry itself carries `post_net_value`. Set it so the bank
ledger shows the amount that actually arrived."""
gl_entries = super().build_gl_map()
if self.payment_type == "Receive" and self.party_type == "Customer" and self.get("taxes"):
for entry in gl_entries:
if entry.account == self.paid_to:
entry.post_net_value = True
return gl_entries
@frappe.whitelist()
def allocate_amount_to_references(self, *args, **kwargs):
"""Called by the form after Get Outstanding Invoices and on Paid Amount
changes; keep the withholding row in step with the allocation."""
super().allocate_amount_to_references(*args, **kwargs)
if self.party_type == "Customer" and self.payment_type == "Receive" and self.source_exchange_rate:
auto_apply_customer_withholding(self)
set_customer_withholding(self)
@frappe.whitelist()
def get_payment_entry(
dt,
dn,
party_amount=None,
bank_account=None,
bank_amount=None,
party_type=None,
payment_type=None,
reference_date=None,
created_from_payment_request=False,
):
"""Create > Payment from a Sales Invoice: arrive with withholding applied.
Signature mirrors ERPNext's so `frappe.call` drops request-only args
(`cmd`, ...) instead of forwarding them.
"""
pe = _get_payment_entry(
dt,
dn,
party_amount=party_amount,
bank_account=bank_account,
bank_amount=bank_amount,
party_type=party_type,
payment_type=payment_type,
reference_date=reference_date,
created_from_payment_request=created_from_payment_request,
)
if auto_apply_customer_withholding(pe) and pe.source_exchange_rate:
set_customer_withholding(pe)
return pe
def auto_apply_customer_withholding(doc):
"""Tick Apply Tax Withholding Amount on a new customer receipt whose
references include a Sales Invoice that carries withholding. Only for
unsaved entries: after the first save the checkbox is the user's."""
if (
doc.party_type != "Customer"
or doc.payment_type != "Receive"
or doc.apply_tax_withholding_amount
or not doc.is_new()
):
return False
invoices = [r.reference_name for r in doc.references if r.reference_doctype == "Sales Invoice"]
if not invoices or not frappe.db.exists(
"Sales Invoice", {"name": ("in", invoices), "withholding_tax_amount": (">", 0)}
):
return False
doc.apply_tax_withholding_amount = 1
return True
def set_customer_withholding(doc, method=None):
"""Payment Entry.validate: tax withheld by the customer on a Receive entry
is posted like ERPNext's supplier TDS, as a "Deduct" row in the taxes
table against the withholding receivable account.
`paid_amount` is the gross amount settled against the invoices (what
"Get Outstanding Invoices" fills in); the bank receives paid minus the
withholding (`received_amount_after_tax`). Withholding is rate x pre-VAT
amount of each allocated reference, prorated by the allocation. Runs after
the controller's validate, then re-runs the tax computation so the row's
base amounts and totals are final within this save.
"""
if doc.party_type != "Customer" or doc.payment_type != "Receive":
return
if method == "validate":
auto_apply_customer_withholding(doc)
account = frappe.db.get_value(
"Account",
{"company": doc.company, "account_name": ASSET_ACCOUNT, "root_type": "Asset", "is_group": 0},
)
row = next((d for d in doc.taxes if d.account_head == account), None) if account else None
if not doc.apply_tax_withholding_amount:
if row:
doc.remove(row)
doc.apply_taxes()
doc.set_amounts_after_tax()
return
if not account:
frappe.throw(_("Account {0} not found for Company {1}").format(ASSET_ACCOUNT, doc.company))
if not doc.tax_withholding_category:
doc.tax_withholding_category = frappe.db.get_value("Customer", doc.party, "tax_withholding_category")
amount, description = get_customer_withholding(doc)
if not amount:
if row:
doc.remove(row)
else:
if not row:
row = doc.append(
"taxes", {"account_head": account, "charge_type": "Actual", "add_deduct_tax": "Deduct"}
)
row.tax_amount = amount
row.description = description
row.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
doc.apply_taxes()
doc.set_amounts_after_tax()
def get_customer_withholding(doc):
"""Total withheld across allocated references, in company currency.
A Sales Invoice's own category wins over the Payment Entry's; Sales Orders
use the Payment Entry's. Withholding is prorated by allocated / grand total
on the pre-VAT amount, per reference.
"""
total = 0.0
details_by_category = {}
precision = doc.precision("difference_amount")
for ref in doc.references:
if ref.reference_doctype not in ("Sales Invoice", "Sales Order") or not ref.allocated_amount:
continue
fields = ["net_total", "grand_total"]
if ref.reference_doctype == "Sales Invoice":
fields.append("tax_withholding_category")
values = frappe.db.get_value(ref.reference_doctype, ref.reference_name, fields, as_dict=True)
if not values.grand_total:
continue
category = values.get("tax_withholding_category") or doc.tax_withholding_category
if not category:
frappe.throw(
_("Please set Tax Withholding Category on {0} {1} or on this Payment Entry").format(
_(ref.reference_doctype), ref.reference_name
)
)
if category not in details_by_category:
details_by_category[category] = get_withholding_details(category, doc.posting_date, doc.company)
base_taxable = (
flt(ref.allocated_amount)
* flt(values.net_total)
/ flt(values.grand_total)
* flt(doc.source_exchange_rate)
)
total += withholding_on(details_by_category[category], base_taxable, base_taxable, precision)
if len(details_by_category) == 1:
# category_name; translations/th.csv carries the fixture categories
description = _(next(iter(details_by_category.values())).description)
else:
description = _("Withholding tax deducted by customer")
return flt(total, precision), description
@@ -1,50 +0,0 @@
{#- 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>
@@ -1,22 +0,0 @@
{#- 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>
@@ -1,332 +0,0 @@
import json
import frappe
from frappe.modules.import_file import import_file_by_path
from frappe.modules.utils import sync_customizations_for_doctype
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from default_thai_company.tax_withholding import (
ASSET_ACCOUNT,
INPUT_VAT_ACCOUNT,
LIABILITY_ACCOUNT,
OUTPUT_VAT_ACCOUNT,
get_payment_entry,
thai_companies,
)
COMPANY = "_Test WHT Company"
ABBR = "_TWC"
CUSTOMER = "_Test WHT Customer"
ITEM = "_Test WHT Service"
FIXTURE = frappe.get_app_path("default_thai_company", "fixtures", "tax_withholding_category.json")
CUSTOM_DIR = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
class TestTaxWithholding(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Fixture categories must exist before the company is created so the
# Company hook has something to link; force=True re-imports.
import_file_by_path(FIXTURE, force=True, data_import=True)
for fname in ("sales_invoice.json", "payment_entry.json"):
with open(f"{CUSTOM_DIR}/{fname}") as f:
sync_customizations_for_doctype(json.load(f), CUSTOM_DIR, fname)
frappe.get_doc(
{
"doctype": "Company",
"company_name": COMPANY,
"abbr": ABBR,
"country": "Thailand",
"default_currency": "THB",
"chart_of_accounts": "Standard",
}
).insert()
cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}"
cls.receivable = f"{ASSET_ACCOUNT} - {ABBR}"
cls.vat = frappe.get_doc("Account", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}")
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": CUSTOMER,
"customer_type": "Company",
"customer_group": frappe.db.get_value("Customer Group", {"is_group": 0}),
"territory": frappe.db.get_value("Territory", {"is_group": 0}),
"tax_withholding_category": "WHT 3% - Service",
}
).insert()
frappe.get_doc(
{
"doctype": "Item",
"item_code": ITEM,
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}),
"is_stock_item": 0,
"stock_uom": "Nos",
}
).insert()
def make_invoice(self, rate=10000, category=None, inclusive=False):
si = frappe.get_doc(
{
"doctype": "Sales Invoice",
"company": COMPANY,
"customer": CUSTOMER,
"posting_date": nowdate(),
"due_date": nowdate(),
"tax_withholding_category": category,
"items": [{"item_code": ITEM, "qty": 1, "rate": rate}],
"taxes": [
{
"charge_type": "On Net Total",
"account_head": self.vat.name,
"rate": 7,
"description": "VAT 7%",
"included_in_print_rate": int(inclusive),
}
],
}
)
si.set_missing_values()
return si.submit()
def make_receipt(self, invoice, allocated, apply=1):
"""Paid amount is the gross allocation; withholding reduces what reaches the bank."""
return frappe.get_doc(
{
"doctype": "Payment Entry",
"company": COMPANY,
"payment_type": "Receive",
"party_type": "Customer",
"party": CUSTOMER,
"posting_date": nowdate(),
"paid_from": f"Debtors - {ABBR}",
"paid_to": f"Cash - {ABBR}",
"paid_amount": allocated,
"received_amount": allocated,
"apply_tax_withholding_amount": apply,
"references": [
{
"reference_doctype": "Sales Invoice",
"reference_name": invoice.name,
"allocated_amount": allocated,
}
],
}
).insert()
def withheld(self, pe):
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
def category_account(self, category, company=COMPANY):
return frappe.db.get_value(
"Tax Withholding Account",
{"parent": category, "parenttype": "Tax Withholding Category", "company": company},
"account",
)
def test_company_creation_adds_accounts_and_links_categories(self):
self.assertIn(COMPANY, thai_companies())
for account, expected in (
(self.payable, ("Liability", f"Duties and Taxes - {ABBR}", "Tax")),
(self.receivable, ("Asset", f"Tax Assets - {ABBR}", "Tax")),
(f"{OUTPUT_VAT_ACCOUNT} - {ABBR}", ("Liability", f"Duties and Taxes - {ABBR}", "Tax")),
(f"{INPUT_VAT_ACCOUNT} - {ABBR}", ("Asset", f"Tax Assets - {ABBR}", "Tax")),
):
self.assertEqual(
frappe.db.get_value("Account", account, ["root_type", "parent_account", "account_type"]),
expected,
account,
)
categories = frappe.get_all(
"Tax Withholding Category", filters={"name": ("like", "WHT %")}, pluck="name"
)
self.assertEqual(len(categories), 22)
for name in categories:
self.assertEqual(self.category_account(name), self.payable, name)
def test_company_creation_adds_vat_schemes(self):
for doctype, account in (
("Sales Taxes and Charges Template", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}"),
("Purchase Taxes and Charges Template", f"{INPUT_VAT_ACCOUNT} - {ABBR}"),
):
schemes = {}
for name in frappe.get_all(
doctype, filters={"company": COMPANY, "title": ("like", "Thailand VAT%")}, pluck="name"
):
doc = frappe.get_doc(doctype, name)
(row,) = doc.taxes
schemes[doc.title] = (
doc.is_default,
row.account_head,
row.charge_type,
row.rate,
row.included_in_print_rate,
)
self.assertEqual(
schemes,
{
"Thailand VAT 7%": (1, account, "On Net Total", 7.0, 0),
"Thailand VAT 7% (Included)": (0, account, "On Net Total", 7.0, 1),
"Thailand VAT 0%": (0, account, "On Net Total", 0.0, 0),
},
doctype,
)
def test_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):
alt = frappe.get_doc(
{
"doctype": "Account",
"company": COMPANY,
"account_name": "Alternative WHT Payable",
"parent_account": f"Duties and Taxes - {ABBR}",
"account_type": "Tax",
}
).insert()
rent = frappe.get_doc("Tax Withholding Category", "WHT 5% - Rent")
for row in rent.accounts:
if row.company == COMPANY:
row.account = alt.name
rent.save()
import_file_by_path(FIXTURE, force=True, data_import=True)
self.assertEqual(self.category_account("WHT 5% - Rent"), alt.name)
self.assertEqual(self.category_account("WHT 3% - Service"), self.payable)
rates = frappe.get_doc("Tax Withholding Category", "WHT 5% - Rent").rates
self.assertEqual([(r.tax_withholding_rate, r.single_threshold) for r in rates], [(5.0, 1000.0)])
def test_sales_invoice_shows_withholding_without_grossing_up(self):
si = self.make_invoice()
self.assertEqual((si.net_total, si.grand_total), (10000.0, 10700.0))
self.assertEqual([t.account_head for t in si.taxes], [self.vat.name])
self.assertEqual(si.tax_withholding_category, "WHT 3% - Service") # fetched from Customer
self.assertEqual((si.withholding_tax_amount, si.amount_after_withholding), (300.0, 10400.0))
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 10700.0)
def test_sales_invoice_below_threshold_shows_nothing(self):
si = self.make_invoice(rate=800)
self.assertEqual((si.withholding_tax_amount, si.amount_after_withholding), (0.0, 0.0))
def test_receipt_uses_invoice_category_over_customer_category(self):
si = self.make_invoice(category="WHT 5% - Rent")
self.assertEqual(si.withholding_tax_amount, 500.0)
pe = self.make_receipt(si, allocated=10700)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 500.0)])
self.assertEqual(pe.received_amount_after_tax, 10200.0)
def test_receipt_deducts_withholding_and_settles_invoice(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=10700)
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0))
self.assertEqual((pe.unallocated_amount, pe.difference_amount), (0, 0))
pe.submit()
gl = {}
for g in frappe.get_all(
"GL Entry", filters={"voucher_no": pe.name}, fields=["account", "debit", "credit"]
):
gl.setdefault(g.account, [0, 0])
gl[g.account][0] += g.debit
gl[g.account][1] += g.credit
self.assertEqual(gl[f"Cash - {ABBR}"], [10400.0, 0.0])
self.assertEqual(gl[self.receivable], [300.0, 0.0])
self.assertEqual(gl[f"Debtors - {ABBR}"], [0.0, 10700.0])
self.assertEqual(len(gl), 3)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 0)
def test_inclusive_vat_invoice_withholds_on_pre_vat_amount(self):
si = self.make_invoice(inclusive=True) # 10,000 incl. 7% VAT -> net 9,345.79
self.assertEqual((si.net_total, si.grand_total), (9345.79, 10000.0))
self.assertEqual(si.withholding_tax_amount, 280.37)
pe = self.make_receipt(si, allocated=10000)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 280.37)])
self.assertEqual(pe.received_amount_after_tax, 9719.63)
def test_partial_allocation_prorates_withholding(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=5350)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
self.assertEqual(pe.received_amount_after_tax, 5200.0)
def test_below_single_threshold_has_no_deduction(self):
si = self.make_invoice(rate=800) # net 800 < 1,000 threshold
pe = self.make_receipt(si, allocated=856, apply=0)
self.assertEqual(pe.apply_tax_withholding_amount, 0) # invoice carries no withholding
self.assertEqual(pe.taxes, [])
self.assertEqual(pe.received_amount_after_tax, 856.0)
def test_create_payment_from_invoice_applies_withholding(self):
si = self.make_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
self.assertEqual(pe.apply_tax_withholding_amount, 1)
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0))
pe.insert()
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
def test_new_receipt_referencing_withheld_invoice_applies_on_save(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=10700, apply=0)
self.assertEqual(pe.apply_tax_withholding_amount, 1)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
def test_unticking_after_save_removes_withholding(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=10700)
pe.apply_tax_withholding_amount = 0
pe.save()
self.assertEqual(pe.apply_tax_withholding_amount, 0) # saved docs keep the user's choice
self.assertEqual(pe.taxes, [])
self.assertEqual(pe.received_amount_after_tax, 10700.0)
def test_reallocation_on_form_recomputes_withholding(self):
si = self.make_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
pe.paid_amount = pe.received_amount = 5350
pe.allocate_amount_to_references(
paid_amount=5350, paid_amount_change=True, allocate_payment_amount=True
)
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
-62
View File
@@ -1,62 +0,0 @@
Net Payable After Withholding Tax,ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
Withholding Tax,ภาษีหัก ณ ที่จ่าย
Tax Withholding Category,ประเภทภาษีหัก ณ ที่จ่าย
Withholding tax deducted by customer,ภาษีหัก ณ ที่จ่ายที่ลูกค้าหักไว้
Tax Withholding Category {0} has no account for Company {1},ประเภทภาษีหัก ณ ที่จ่าย {0} ไม่มีบัญชีสำหรับบริษัท {1}
Account {0} not found for Company {1},ไม่พบบัญชี {0} สำหรับบริษัท {1}
Please set Tax Withholding Category on {0} {1} or on this Payment Entry,กรุณาระบุประเภทภาษีหัก ณ ที่จ่ายใน {0} {1} หรือในรายการชำระเงินนี้
Tax ID,เลขประจำตัวผู้เสียภาษี
Tax Id,เลขประจำตัวผู้เสียภาษี
Signatures,ลายเซ็น
On behalf of,ในนาม
Receiver,ผู้รับ
Issuer,ผู้ออก
Accepted By,ผู้อนุมัติสั่งซื้อ
Quoted By,ผู้เสนอราคา
Tax Invoice,ใบกำกับภาษี
Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
"Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53)",ค่าแห่งกู๊ดวิลล์ ค่าลิขสิทธิ์ หรือสิทธิอย่างอื่น - มาตรา 40(3) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
"Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53)",ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นนิติบุคคล ยกเว้นธนาคาร/บริษัทเงินทุน (ภ.ง.ด.53)
"Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2)",ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นบุคคลธรรมดา มาตรา 50(2) (ภ.ง.ด.2)
"Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53)",เงินปันผล เงินส่วนแบ่งของกำไร - มาตรา 40(4)(ข) (ภ.ง.ด.2 / ภ.ง.ด.53)
"Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53)",ค่าเช่าทรัพย์สิน ค่าเช่าซื้อสังหาริมทรัพย์ - มาตรา 40(5) (ภ.ง.ด.3 / ภ.ง.ด.53)
Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53),ค่าเช่าเรือตามกฎหมายว่าด้วยการส่งเสริมการพาณิชยนาวี - มาตรา 40(5) (ภ.ง.ด.53)
"Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53)",วิชาชีพอิสระ กฎหมาย การประกอบโรคศิลปะ วิศวกรรม สถาปัตยกรรม การบัญชี ประณีตศิลปกรรม - มาตรา 40(6) (ภ.ง.ด.3 / ภ.ง.ด.53)
"Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53)",ค่ารับเหมาที่ผู้รับเหมาต้องลงทุนจัดหาสัมภาระในส่วนสำคัญ - มาตรา 40(7) (ภ.ง.ด.3 / ภ.ง.ด.53)
"Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ค่าจ้างทำของ ค่าบริการ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
"Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53)",รางวัล ส่วนลด หรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53),ค่าโฆษณา - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
"Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ค่าขนส่งสินค้าหรือผู้โดยสาร ยกเว้นการขนส่งสาธารณะ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
Non-life insurance premiums - Sec. 40(8) (P.N.D.53),ค่าเบี้ยประกันวินาศภัย - มาตรา 40(8) (ภ.ง.ด.53)
"Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53)",รางวัลจากการประกวด การแข่งขัน การชิงโชค - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3),นักแสดงสาธารณะที่มีภูมิลำเนาในประเทศไทย - มาตรา 40(8) (ภ.ง.ด.3)
"Rubber, cassava, jute, rice, maize, sugarcane, coffee beans, oil palm - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ยางแผ่น มันสำปะหลัง ปอ ข้าว ข้าวโพด อ้อย เมล็ดกาแฟ ปาล์มน้ำมัน - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53),สัตว์น้ำที่ซื้อจากผู้ขายที่ไม่ได้จดทะเบียนภาษีมูลค่าเพิ่ม - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
"Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office)",การขายอสังหาริมทรัพย์ ผู้ขายเป็นนิติบุคคล - มาตรา 69 ตรี (นำส่ง ณ สำนักงานที่ดิน)
Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3),เงินได้ตามมาตรา 40(2)-(6) จ่ายให้บุคคลธรรมดาที่มิได้เป็นผู้อยู่ในประเทศไทย - มาตรา 50(2) (ภ.ง.ด.3)
"Sec. 40(2)-(6) income excl. dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินได้ตามมาตรา 40(2)-(6) ยกเว้นเงินปันผล จ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
"Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
Sales Invoice,ใบแจ้งหนี้
Sales Order,ใบสั่งขาย
Posting Date,วันที่
Payment Due Date,วันครบกำหนดชำระ
Contact Person,ผู้ติดต่อ
Mobile No,โทรศัพท์มือถือ
Contact Email,อีเมล
Description,รายละเอียด
Rate,ราคาต่อหน่วย
Amount,จำนวนเงิน
Total,รวม
Total (Without Tax),รวมก่อนภาษี
Net Total,ยอดรวมสุทธิ
Total After Discount,ยอดรวมหลังหักส่วนลด
Total Taxes and Charges,รวมภาษีและค่าธรรมเนียม
Grand Total,ยอดรวมทั้งสิ้น
Rounded Total,ยอดรวมปัดเศษ
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
Page {0} of {1},หน้า {0} จาก {1}
Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย
Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย
Output VAT,ภาษีขาย
Input VAT,ภาษีซื้อ
1 Net Payable After Withholding Tax ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
2 Withholding Tax ภาษีหัก ณ ที่จ่าย
3 Tax Withholding Category ประเภทภาษีหัก ณ ที่จ่าย
4 Withholding tax deducted by customer ภาษีหัก ณ ที่จ่ายที่ลูกค้าหักไว้
5 Tax Withholding Category {0} has no account for Company {1} ประเภทภาษีหัก ณ ที่จ่าย {0} ไม่มีบัญชีสำหรับบริษัท {1}
6 Account {0} not found for Company {1} ไม่พบบัญชี {0} สำหรับบริษัท {1}
7 Please set Tax Withholding Category on {0} {1} or on this Payment Entry กรุณาระบุประเภทภาษีหัก ณ ที่จ่ายใน {0} {1} หรือในรายการชำระเงินนี้
8 Tax ID เลขประจำตัวผู้เสียภาษี
9 Tax Id เลขประจำตัวผู้เสียภาษี
10 Signatures ลายเซ็น
11 On behalf of ในนาม
12 Receiver ผู้รับ
13 Issuer ผู้ออก
14 Accepted By ผู้อนุมัติสั่งซื้อ
15 Quoted By ผู้เสนอราคา
16 Tax Invoice ใบกำกับภาษี
17 Tax Invoice/Receipt ใบกำกับภาษี/ใบเสร็จรับเงิน
18 Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53) ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
19 Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53) ค่าแห่งกู๊ดวิลล์ ค่าลิขสิทธิ์ หรือสิทธิอย่างอื่น - มาตรา 40(3) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
20 Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53) ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นนิติบุคคล ยกเว้นธนาคาร/บริษัทเงินทุน (ภ.ง.ด.53)
21 Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2) ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นบุคคลธรรมดา มาตรา 50(2) (ภ.ง.ด.2)
22 Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53) เงินปันผล เงินส่วนแบ่งของกำไร - มาตรา 40(4)(ข) (ภ.ง.ด.2 / ภ.ง.ด.53)
23 Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53) ค่าเช่าทรัพย์สิน ค่าเช่าซื้อสังหาริมทรัพย์ - มาตรา 40(5) (ภ.ง.ด.3 / ภ.ง.ด.53)
24 Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53) ค่าเช่าเรือตามกฎหมายว่าด้วยการส่งเสริมการพาณิชยนาวี - มาตรา 40(5) (ภ.ง.ด.53)
25 Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53) วิชาชีพอิสระ กฎหมาย การประกอบโรคศิลปะ วิศวกรรม สถาปัตยกรรม การบัญชี ประณีตศิลปกรรม - มาตรา 40(6) (ภ.ง.ด.3 / ภ.ง.ด.53)
26 Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53) ค่ารับเหมาที่ผู้รับเหมาต้องลงทุนจัดหาสัมภาระในส่วนสำคัญ - มาตรา 40(7) (ภ.ง.ด.3 / ภ.ง.ด.53)
27 Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53) ค่าจ้างทำของ ค่าบริการ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
28 Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53) รางวัล ส่วนลด หรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
29 Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53) ค่าโฆษณา - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
30 Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53) ค่าขนส่งสินค้าหรือผู้โดยสาร ยกเว้นการขนส่งสาธารณะ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
31 Non-life insurance premiums - Sec. 40(8) (P.N.D.53) ค่าเบี้ยประกันวินาศภัย - มาตรา 40(8) (ภ.ง.ด.53)
32 Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53) รางวัลจากการประกวด การแข่งขัน การชิงโชค - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
33 Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3) นักแสดงสาธารณะที่มีภูมิลำเนาในประเทศไทย - มาตรา 40(8) (ภ.ง.ด.3)
34 Rubber, cassava, jute, rice, maize, sugarcane, coffee beans, oil palm - Sec. 40(8) (P.N.D.3 / P.N.D.53) ยางแผ่น มันสำปะหลัง ปอ ข้าว ข้าวโพด อ้อย เมล็ดกาแฟ ปาล์มน้ำมัน - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
35 Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53) สัตว์น้ำที่ซื้อจากผู้ขายที่ไม่ได้จดทะเบียนภาษีมูลค่าเพิ่ม - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
36 Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office) การขายอสังหาริมทรัพย์ ผู้ขายเป็นนิติบุคคล - มาตรา 69 ตรี (นำส่ง ณ สำนักงานที่ดิน)
37 Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3) เงินได้ตามมาตรา 40(2)-(6) จ่ายให้บุคคลธรรมดาที่มิได้เป็นผู้อยู่ในประเทศไทย - มาตรา 50(2) (ภ.ง.ด.3)
38 Sec. 40(2)-(6) income excl. dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54) เงินได้ตามมาตรา 40(2)-(6) ยกเว้นเงินปันผล จ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
39 Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54) เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
40 Sales Invoice ใบแจ้งหนี้
41 Sales Order ใบสั่งขาย
42 Posting Date วันที่
43 Payment Due Date วันครบกำหนดชำระ
44 Contact Person ผู้ติดต่อ
45 Mobile No โทรศัพท์มือถือ
46 Contact Email อีเมล
47 Description รายละเอียด
48 Rate ราคาต่อหน่วย
49 Amount จำนวนเงิน
50 Total รวม
51 Total (Without Tax) รวมก่อนภาษี
52 Net Total ยอดรวมสุทธิ
53 Total After Discount ยอดรวมหลังหักส่วนลด
54 Total Taxes and Charges รวมภาษีและค่าธรรมเนียม
55 Grand Total ยอดรวมทั้งสิ้น
56 Rounded Total ยอดรวมปัดเศษ
57 Terms and Conditions Details รายละเอียดข้อตกลงและเงื่อนไข
58 Page {0} of {1} หน้า {0} จาก {1}
59 Withholding Tax Payable ภาษีหัก ณ ที่จ่ายค้างจ่าย
60 Withholding Tax Receivable ภาษีถูกหัก ณ ที่จ่าย
61 Output VAT ภาษีขาย
62 Input VAT ภาษีซื้อ
-46
View File
@@ -1,30 +1,6 @@
import frappe
import frappe.utils
from erpnext import get_default_company
from frappe.contacts.doctype.address.address import get_default_address, render_address
from num2words import num2words
def money_in_words(amount, currency):
"""Amount in words for the active language.
frappe.utils.money_in_words renders "<currency> <words> only." in every
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
Currencies num2words cannot spell in Thai keep frappe's wording.
"""
if frappe.local.lang == "th":
try:
return num2words(amount, lang="th", to="currency", currency=currency)
except NotImplementedError:
pass
return frappe.utils.money_in_words(amount, currency)
def get_in_words(doc):
"""`in_words` for print formats: re-derived in the print language instead
of the language of whoever saved the document."""
amount = abs(doc.grand_total if doc.is_rounded_total_disabled() else doc.rounded_total)
return money_in_words(amount, doc.currency)
def get_letter_head_company(doc=None):
@@ -53,28 +29,6 @@ def get_letter_head_company(doc=None):
)
def get_company_bank_account(company=None):
"""Default company Bank Account for print formats.
Returns the enabled Bank Account flagged `Is Company Account` and
`Is Default Account` for `company`, or None when there is none.
"""
if not company:
return None
account = frappe.db.get_value(
"Bank Account",
{"company": company, "is_company_account": 1, "is_default": 1, "disabled": 0},
["account_name", "bank", "bank_account_no", "iban", "branch_code"],
as_dict=True,
)
if not account:
return None
account.swift_number = frappe.db.get_value("Bank", account.bank, "swift_number")
return account
def stash_letter_head_source(doc, method=None):
"""Fixture import re-inserts the Letter Head; remember the fixture's `source`."""
doc.flags.fixture_source = doc.source
-78
View File
@@ -1,78 +0,0 @@
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)