Author SHA1 Message Date
kurogeek 68f0817125 fix: gross discount layout for VAT-inclusive invoices
With inclusive pricing and a discount on Grand Total the Total line is
the tax-inclusive item total and the discount is printed as entered,
followed by Total After Discount, which the Net Total and charge lines
then break down. Other cases keep the net layout: Total (net before
discount), the discount's net share, Net Total, charges. Thai label for
Total After Discount.
2026-09-17 10:30:43 +00:00
kurogeek 4a19c60ba2 fix: print the posted discount and tax amounts
ERPNext takes an Additional Discount off the items' net amounts and,
for a discount on Grand Total, splits it between net and taxes: each
charge is booked at tax_amount_after_discount_amount and Total Taxes
and Charges sums those. The print showed the discount as entered and
the pre-discount tax_amount, so on a Grand Total discount the VAT line
disagreed with the invoice.

The totals includes now print the discount's net share (sum of
item.distributed_discount_amount), Net Total, and each charge after
discount, so the lines add up to Grand Total whichever total the
discount applies on and with inclusive or exclusive tax. A discount
that is not distributed (cash / non-trade) stays after the charges.
2026-09-17 10:19:30 +00:00
kurogeek 71eda0bec9 fix: print Total, Additional Discount, Net Total in that order
Default Standard Sales Invoice and Tax Invoice/Receipt printed the
Additional Discount Amount among the charges, after the Total line but
with no total after the discount. App copies of ERPNext's totals
includes are swapped in via doc.print_templates:

- taxes.html: a discount on Net Total is followed by the Net Total line
  ahead of the charges; a discount on Grand Total stays after them.
- total.html: with inclusive tax, "Total (Without Tax)" is the total
  before the discount (net_total + discount_amount); upstream printed
  net_total, which already has the discount taken off, so the same
  figure appeared before and after the discount line.
2026-09-17 10:02:23 +00:00
kurogeek 8cba58ba02 feat: default Shipping Rule per Thai company
"Shipping Charges - <abbr>": Selling, Fixed, posting to a "Shipping
Charges" income account under Direct Income (created if the chart lacks
it) with the default cost center. Shipping Rule is named by its label,
so the company abbreviation keeps one rule per company, as ERPNext
names its tax templates. Created on Company save, after install, and by
a patch for existing companies.

The amount is entered on the transaction, not the rule: ERPNext
re-applies the rule on every recalculation and would reset the charge
row to the rule's fixed amount. ThaiShippingRule overrides the doctype
class so a Fixed rule with no amount only seeds the row and leaves the
entered amount alone.
2026-09-17 10:02:23 +00:00
kurogeek 4785878025 feat: default Location tree and Asset Categories
Location fixture: "All Locations" group with Head Office, Branch Office and
Warehouse. Fixture import re-inserts tree nodes with fresh lft/rgt, which
strands locations users add beneath them, so after_migrate rebuilds the
Location tree.

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

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

Category caps follow HRMS validation: a sub-category max must not exceed
its category max and the declaration caps the category total, so groups
with a real combined limit carry it, groups without one use the sum of
their sub caps, and uncapped per-person/percentage items live in 0-cap
categories. Social Security uses the 2026 ceiling (10,500/yr); SSF is
kept inactive. sync_fixtures skips these files on sites without hrms.
2026-09-16 11:22:28 +00:00
kurogeek d074657511 feat: add Default Standard Quotation print format 2026-09-16 07:46:59 +00:00
kurogeek 04735ca813 feat: Thai VAT schemes as Sales/Purchase Taxes and Charges Templates
Per Thai company: Thailand VAT 7%, Thailand VAT 7% (Included) for
VAT-inclusive pricing, Thailand VAT 0% for zero-rated supplies. Sales
schemes post to Output VAT, purchase schemes to Input VAT (category
Total, so recoverable VAT stays out of valuation). The 7% scheme is the
default when the company has none. Created on Company save, after
install, and by a patch for existing companies.
2026-09-15 09:35:15 +00:00
kurogeek 381aab2c83 feat: Output VAT and Input VAT accounts for every Thai company
Thai VAT is filed on P.P.30 as output tax (ภาษีขาย, collected on sales)
net of input tax (ภาษีซื้อ, paid on purchases); ERPNext's wizard makes a
single VAT account. Both join the per-company tax accounts created on
Company save and fixture import; a patch adds them to existing companies.
Account names are translated for the chart of accounts tree.
2026-09-15 09:19:43 +00:00
kurogeek caad6e77da fix: heading dates wrapped at the hyphen in PDF
The floated block shrinks to fit and WebKit breaks 15-09-2026 after a
hyphen; keep each date line on one line.
2026-09-15 07:11:51 +00:00
kurogeek 8aeecfca2f feat: no bank account details on Default Standard Tax Invoice/Receipt
A receipt acknowledges payment; the bank block belongs on the invoice.
2026-09-15 07:03:46 +00:00
kurogeek e85ec3a585 feat: Tax Invoice/Receipt print format headed Tax Invoice/Receipt
The heading fell back to the DocType name; a Print Heading on the
document still takes precedence.
2026-09-15 06:56:22 +00:00
kurogeek 015ce48639 feat: print formats hide Disable Rounded Total and Rounded Total when disabled
rounded_total keeps a value when rounding is disabled, so the standard
layout printed it alongside Grand Total.
2026-09-15 06:50:55 +00:00
kurogeek 9a8602f74e feat: Thai language for every customization
- translations/th.csv: custom field labels, messages, print format and
  letter head text, the 22 Tax Withholding Category names; overrides the
  upstream Thai for the standard labels the print formats show
- print formats embed Sarabun (public/fonts, OFL) so PDFs render Thai on
  servers without a Thai font
- letter head labels go through _()
- withholding row description is translated when stored
- in_words: Thai "...บาทถ้วน" wording via num2words when the language is
  Thai (ThaiSalesInvoice.set_total_in_words); print formats re-derive it
  in the print language (get_in_words)
- fixture `modified` bumped so migrate re-imports the changed documents
2026-09-15 06:50:55 +00:00
kurogeek 33310c51ae feat: add Default Standard Tax Invoice/Receipt print format
Exact copy of Default Standard Sales Invoice, shipped as a fixture
alongside it.
2026-09-14 10:13:33 +00:00
kurogeek f0961de744 feat: customer contact column on Default Standard Sales Invoice
- Split the Customer block into two columns: name / address / tax id on
  the left, contact person with phone, mobile and email on the right
  (only when the invoice has a contact person; phone read from Contact)
- Thailand Address Template: drop the blank line between the address and
  the phone / fax / email lines
2026-09-14 09:42:27 +00:00
kurogeek dc8509d6d7 feat: tidy Default Standard Sales Invoice layout
- Hide Total Quantity, Update Billed Amount in Delivery Note, Amount
  Eligible for Commission and Company Tax ID (already in the letterhead)
- Move Posting Date / Payment Due Date into the print heading, right-aligned
- Add a Customer block (name, address, contact, tax id) under the heading
- Render Total (Without Tax) in the same right-half grid as Taxes / Totals
- Put In Words in the empty left column of Totals, level with Grand Total
- Print Format CSS: one 1.8 line-height rhythm with no row margins, 5px
  letterhead margin, 5px margins around the item table
2026-09-14 08:57:08 +00:00
kurogeek 2d93fe45c2 fix: get_payment_entry override rejected request-only args
frappe.call filters kwargs to the target signature; the *args/**kwargs
wrapper accepted 'cmd' and forwarded it to ERPNext's function. Mirror
the original signature.
2026-09-13 07:59:49 +00:00
kurogeek 9b99be9abd feat: apply customer withholding automatically on new receipts
Tick Apply Tax Withholding Amount on unsaved customer receipts whose
references include a Sales Invoice carrying withholding: in the
get_payment_entry override (Create > Payment), in the
allocate_amount_to_references doc method (Get Outstanding Invoices /
Paid Amount changes, so the tax row updates live) and on first save.
Saved entries keep the user's choice; unticking now removes the row.
Client script recomputes the row when the checkbox or category changes.
2026-09-13 07:56:21 +00:00
kurogeek 6a082edc56 fix: post customer withholding as a Deduct tax row, not a deduction
With a deduction row ERPNext treats Paid Amount as cash received and
credits the party for paid + deduction, so a gross Paid Amount (what Get
Outstanding Invoices fills) over-credited Debtors and posted the gross
to the bank. Use the taxes table with add_deduct_tax=Deduct like
supplier TDS: Paid Amount is the gross settlement, bank receives paid
minus withholding, receivable is debited. Override Payment Entry
build_gl_map to flag the bank entry post_net_value so the merged bank
line shows the net amount.
2026-09-13 07:45:11 +00:00
kurogeek abeb2af0c8 refactor: ship Custom Fields and Property Setters as module customizations
Move the Sales Invoice withholding fields and both Property Setters from
fixtures to default_thai_company/custom/{sales_invoice,payment_entry}.json
with sync_on_migrate. sync_customizations updates Custom Fields in place
instead of the delete/re-insert that fixtures do on every migrate.
2026-09-12 13:13:49 +00:00
kurogeek ed4062590a feat: show expected withholding tax on Sales Invoice
Custom fields Tax Withholding Category (fetched from Customer),
Withholding Tax and Net Payable After Withholding Tax on Sales Invoice;
computed in the Thai set_tax_withholding override from the category
rate and single threshold, informational only (totals/GL unchanged),
printed under Rounded Total by the standard layout. Payment Entry now
prefers each invoice's category over the customer's.
2026-09-11 10:20:03 +00:00
kurogeek 4767d4bb46 test: withholding tax accounts, fixture re-import, and customer receipts
Integration tests on a throwaway Thai company: account creation and
category linking on Company insert, site-configured account surviving
fixture re-import, Sales Invoice left un-grossed, and Payment Entry
deductions (full, prorated, below threshold, unchecked). Run with
CI=1 bench --site <site> run-tests --app default_thai_company.
2026-09-11 10:09:01 +00:00
kurogeek 6819242521 feat: customer-side withholding tax on Payment Entry
Thai customers withhold tax at payment; ERPNext's customer-side handling
is Indian TCS which adds tax on top of the Sales Invoice. Override
SalesInvoice.set_tax_withholding to no-op for Thai companies, expose
"Apply Tax Withholding Amount" on Customer/Receive Payment Entries, and
on validate compute rate x pre-VAT amount of each allocated reference
(prorated, single threshold honoured) as a deduction to "Withholding Tax
Receivable". Client script fetches the Customer's category when the box
is ticked.
2026-09-11 10:00:24 +00:00
kurogeek 8b3cf9af1e feat: Thai Tax Withholding Category fixtures with per-company accounts
Add 22 Tax Withholding Category fixtures covering Thailand domestic
withholding (Taw.Paw. 4/2528, Sec. 50(2), Sec. 70) with rates and
thresholds. Fixtures ship without accounts; on import each Thai company
gets "Withholding Tax Payable" (Liability) and "Withholding Tax
Receivable" (Asset) created and the payable account linked. Company
on_update does the same for companies created later. Site-configured
accounts survive fixture re-import on migrate.
2026-09-11 09:13:45 +00:00
33 changed files with 3037 additions and 40 deletions
+8
View File
@@ -12,6 +12,14 @@ bench get-app $URL_OF_THIS_REPO --branch develop
bench install-app default_thai_company 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 ### Contributing
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository: This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
+91
View File
@@ -0,0 +1,91 @@
import frappe
from frappe.utils.nestedset import rebuild_tree
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Asset Category -> fixed asset account. Names follow ERPNext's Standard chart so
# setup-wizard companies reuse their accounts; the rest are created under the
# same "Fixed Assets" group. Depreciation accounts are left to the Company
# defaults (Asset falls back to them), so one change there covers every category.
FIXED_ASSET_ACCOUNTS = {
"Land": "Land",
"Buildings": "Buildings",
"Plant and Machinery": "Plants and Machineries",
"Vehicles": "Vehicles",
"Furniture and Fixtures": "Furnitures and Fixtures",
"Office Equipment": "Office Equipments",
"Computers and Electronics": "Electronic Equipments",
"Software": "Softwares",
"Intangible Assets": "Intangible Assets",
}
FIXED_ASSET_GROUPS = ("Fixed Assets",)
def fixed_asset_account(company, category):
return get_or_create_account(
company, FIXED_ASSET_ACCOUNTS[category], "Asset", "Fixed Asset", FIXED_ASSET_GROUPS
)
def link_company(category, company):
"""Append the company's fixed asset account row to the category if missing."""
if any(row.company_name == company for row in category.accounts):
return False
category.append(
"accounts",
{"company_name": company, "fixed_asset_account": fixed_asset_account(company, category.name)},
)
return True
def prepare_fixture_accounts(doc, method=None):
"""Asset Category fixtures ship without `accounts` (company-specific).
Same contract as the Tax Withholding Category fixtures: fixture import
re-inserts the doc on every migrate, so carry over the rows already on this
site, then link every Thai company. Mandatory is relaxed for sites without a
Thai company yet; `setup_company` links them later.
"""
if doc.name not in FIXED_ASSET_ACCOUNTS:
return
doc.flags.ignore_mandatory = True
if not doc.accounts and frappe.db.exists(doc.doctype, doc.name):
for row in frappe.get_all(
"Asset Category Account",
filters={"parent": doc.name, "parenttype": doc.doctype},
fields=[
"company_name",
"fixed_asset_account",
"accumulated_depreciation_account",
"depreciation_expense_account",
"capital_work_in_progress_account",
],
order_by="idx",
):
doc.append("accounts", row)
for company in thai_companies():
link_company(doc, company)
def setup_company(doc, method=None):
"""Company.on_update: link a Thai company on every default Asset Category."""
if not company_ready(doc):
return
for name in FIXED_ASSET_ACCOUNTS:
if not frappe.db.exists("Asset Category", name):
continue
category = frappe.get_doc("Asset Category", name)
if link_company(category, doc.name):
category.save(ignore_permissions=True)
def rebuild_locations():
"""after_migrate: fixture import re-inserts the Location tree nodes (fresh
lft/rgt), which strands locations users added beneath them; rebuild."""
if frappe.db.exists("Location", "All Locations"):
rebuild_tree("Location")
@@ -0,0 +1,32 @@
{
"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
}
@@ -0,0 +1,225 @@
{
"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, "is_default": 1,
"modified": "2026-09-09 17:23:33.559202", "modified": "2026-09-09 17:23:33.559202",
"name": "Thailand", "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 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" "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"
} }
] ]
@@ -0,0 +1,237 @@
[
{
"accounts": [],
"asset_category_name": "Land",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [],
"modified": "2026-09-16 09:00:00.000000",
"name": "Land",
"non_depreciable_category": 1
},
{
"accounts": [],
"asset_category_name": "Buildings",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Buildings",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 240
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Buildings",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Plant and Machinery",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Plant and Machinery",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Plant and Machinery",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Vehicles",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Vehicles",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Vehicles",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Furniture and Fixtures",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Furniture and Fixtures",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Furniture and Fixtures",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Office Equipment",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Office Equipment",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Office Equipment",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Computers and Electronics",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Computers and Electronics",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Computers and Electronics",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Software",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Software",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Software",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Intangible Assets",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Intangible Assets",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 120
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Intangible Assets",
"non_depreciable_category": 0
}
]
@@ -0,0 +1,106 @@
[
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 270000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Personal and Family Allowances"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child and Dependant Allowances"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Life and Health Insurance"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 25000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Family Insurance"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Retirement Savings and Investments"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESG Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 600000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 10500.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Housing"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Donations"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Political Party Donation"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Income Exemptions"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 250000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Government Stimulus Measures"
}
]
@@ -0,0 +1,299 @@
[
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse allowance (spouse without income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 90000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Adopted child allowance (30,000 per child, max 3)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Parent allowance (30,000 per parent, max 2)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse's parent allowance (30,000 per parent, max 2)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child allowance (30,000 per child)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child allowance - 2nd child onward born 2018 or later (60,000 per child)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Disabled or incapacitated dependant care (60,000 per person)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Prenatal care and childbirth expenses (60,000 per pregnancy)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Life and Health Insurance",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Life insurance premium (policy term 10 years or more)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Life and Health Insurance",
"is_active": 1,
"max_amount": 25000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Health insurance premium (self)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Family Insurance",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse life insurance premium (spouse without income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Family Insurance",
"is_active": 1,
"max_amount": 15000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Parents' health insurance premium"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Provident fund contribution (15% of wages)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Government Pension Fund contribution (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Private Teacher Aid Fund contribution (15% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Retirement Mutual Fund - RMF (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Pension life insurance premium (15% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 30000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "National Savings Fund contribution"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 0,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Super Savings Fund - SSF (30% of income, tax years 2020-2024)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESG Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESG fund (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESGX Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX fund - new units (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESGX Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX fund - LTF switch (500,000 spread over tax years 2025-2029)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 10500.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 33 employee"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 5184.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 39 voluntary"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 3600.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 40 informal worker"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Housing",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Home loan interest"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Donations",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "General donations (10% of income after allowances)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Donations",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Education, sports, hospital and social development donations (2x, 10% of income after allowances)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Political Party Donation",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Political party donation"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Income Exemptions",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Taxpayer aged 65 or over - income exemption"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Income Exemptions",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Disabled taxpayer under 65 - income exemption"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Government Stimulus Measures",
"is_active": 1,
"max_amount": 50000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Easy E-Receipt (e-Tax Invoice / e-Receipt purchases)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Government Stimulus Measures",
"is_active": 1,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Solar rooftop installation (tax years 2025-2027)"
}
]
@@ -0,0 +1,113 @@
[
{
"allow_tax_exemption": 1,
"currency": "THB",
"disabled": 0,
"docstatus": 1,
"doctype": "Income Tax Slab",
"effective_from": "2017-01-01",
"modified": "2026-09-16 09:00:00.000000",
"name": "Thailand Personal Income Tax",
"other_taxes_and_charges": [],
"slabs": [
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 0.0,
"idx": 1,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 0.0,
"to_amount": 150000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 150001.0,
"idx": 2,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 5.0,
"to_amount": 300000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 300001.0,
"idx": 3,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 10.0,
"to_amount": 500000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 500001.0,
"idx": 4,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 15.0,
"to_amount": 750000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 750001.0,
"idx": 5,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 20.0,
"to_amount": 1000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 1000001.0,
"idx": 6,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 25.0,
"to_amount": 2000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 2000001.0,
"idx": 7,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 30.0,
"to_amount": 5000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 5000001.0,
"idx": 8,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 35.0,
"to_amount": 0.0
}
],
"standard_tax_exemption_amount": 160000.0,
"tax_relief_limit": 0.0
}
]
@@ -1,7 +1,7 @@
[ [
{ {
"align": "Left", "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, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Letter Head", "doctype": "Letter Head",
@@ -18,7 +18,7 @@
"image_width": 0.0, "image_width": 0.0,
"is_default": 1, "is_default": 1,
"letter_head_name": "Default Letter Head", "letter_head_name": "Default Letter Head",
"modified": "2026-09-10 09:51:23.933752", "modified": "2026-09-15 06:44:37.653888",
"name": "Default Letter Head", "name": "Default Letter Head",
"source": "HTML" "source": "HTML"
} }
@@ -0,0 +1,42 @@
[
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 1,
"location_name": "All Locations",
"modified": "2026-09-16 09:00:00.000000",
"name": "All Locations",
"parent_location": null
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Head Office",
"modified": "2026-09-16 09:00:00.000000",
"name": "Head Office",
"parent_location": "All Locations"
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Branch Office",
"modified": "2026-09-16 09:00:00.000000",
"name": "Branch Office",
"parent_location": "All Locations"
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Warehouse",
"modified": "2026-09-16 09:00:00.000000",
"name": "Warehouse",
"parent_location": "All Locations"
}
]
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
[
{
"default_value": null,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Property Setter",
"doctype_or_field": "DocType",
"field_name": null,
"is_system_generated": 1,
"modified": "2026-09-10 13:55:13.996942",
"module": null,
"name": "Sales Invoice-main-default_print_format",
"property": "default_print_format",
"property_type": "Data",
"row_name": null,
"value": "Default Standard Sales Invoice"
}
]
@@ -0,0 +1,574 @@
[
{
"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
}
]
+81 -14
View File
@@ -43,7 +43,7 @@ required_apps = ["erpnext"]
# page_js = {"page" : "public/js/file.js"} # page_js = {"page" : "public/js/file.js"}
# include js in doctype views # include js in doctype views
# doctype_js = {"doctype" : "public/js/doctype.js"} doctype_js = {"Payment Entry": "public/js/payment_entry.js"}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
@@ -78,27 +78,75 @@ jinja = {
"methods": [ "methods": [
"default_thai_company.utils.get_letter_head_company", "default_thai_company.utils.get_letter_head_company",
"default_thai_company.utils.get_company_bank_account", "default_thai_company.utils.get_company_bank_account",
"default_thai_company.utils.get_in_words",
], ],
} }
# Fixtures # 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 = [ fixtures = [
{"doctype": "Letter Head", "filters": [["name", "in", ["Default Letter Head"]]]}, {"doctype": "Letter Head", "filters": [["name", "in", ["Default Letter Head"]]]},
{"doctype": "Address Template", "filters": [["name", "in", ["Thailand"]]]}, {"doctype": "Address Template", "filters": [["name", "in", ["Thailand"]]]},
{"doctype": "Print Format", "filters": [["name", "in", ["Default Standard Sales Invoice"]]]},
{ {
"doctype": "Property Setter", "doctype": "Print Format",
"filters": [["name", "in", ["Sales Invoice-main-default_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 # Installation
# ------------ # ------------
# before_install = "default_thai_company.install.before_install" # before_install = "default_thai_company.install.before_install"
# after_install = "default_thai_company.install.after_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"
# Uninstallation # Uninstallation
# ------------ # ------------
@@ -144,9 +192,11 @@ fixtures = [
# --------------- # ---------------
# Override standard doctype classes # Override standard doctype classes
# override_doctype_class = { override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo" "Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
# } "Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
"Shipping Rule": "default_thai_company.shipping.ThaiShippingRule",
}
# Document Events # Document Events
# --------------- # ---------------
@@ -156,7 +206,24 @@ doc_events = {
"Letter Head": { "Letter Head": {
"before_import": "default_thai_company.utils.stash_letter_head_source", "before_import": "default_thai_company.utils.stash_letter_head_source",
"before_insert": "default_thai_company.utils.restore_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 # Scheduled Tasks
@@ -187,11 +254,11 @@ doc_events = {
# Overriding Methods # Overriding Methods
# ------------------------------ # ------------------------------
#
# override_whitelisted_methods = { override_whitelisted_methods = {
# "frappe.desk.doctype.event.event.get_events": "default_thai_company.event.get_events" "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry": "default_thai_company.tax_withholding.get_payment_entry",
# } }
#
# each overriding function accepts a `data` argument; # each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard, # generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps # along with any modifications made in other Frappe apps
+3
View File
@@ -4,3 +4,6 @@
[post_model_sync] [post_model_sync]
# Patches added in this section will be executed after doctypes are migrated # Patches added in this section will be executed after doctypes are migrated
default_thai_company.patches.create_vat_accounts
default_thai_company.patches.create_vat_templates
default_thai_company.patches.create_shipping_rules
@@ -0,0 +1,5 @@
from default_thai_company.shipping import setup_companies
def execute():
setup_companies()
@@ -0,0 +1,8 @@
from default_thai_company.tax_withholding import ensure_company_accounts, thai_companies
def execute():
"""Output VAT / Input VAT were added to COMPANY_ACCOUNTS; Company.on_update
only runs on save, so give existing Thai companies the pair."""
for company in thai_companies():
ensure_company_accounts(company)
@@ -0,0 +1,5 @@
from default_thai_company.vat import setup_companies
def execute():
setup_companies()
+93
View File
@@ -0,0 +1,93 @@
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.
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,66 @@
import erpnext
import frappe
from erpnext.accounts.doctype.shipping_rule.shipping_rule import ShippingRule
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Shipping Rule is named by its label (site-wide unique) but bound to one company,
# so each Thai company gets "<LABEL> - <abbr>", the way ERPNext names accounts and
# tax templates. The label doubles as the charge description on the transaction.
LABEL = "Shipping Charges"
# Shipping billed to customers is revenue; the courier's bill stays an expense.
SHIPPING_ACCOUNT = "Shipping Charges"
SHIPPING_ACCOUNT_GROUPS = ("Direct Income", "Income")
class ThaiShippingRule(ShippingRule):
def add_shipping_rule_to_tax_table(self, doc, shipping_amount):
"""A Fixed rule without an amount only seeds the charge row; the amount is
entered on the transaction. ERPNext re-applies the rule on every
recalculation, which would otherwise reset the row to 0."""
manual = self.calculate_based_on == "Fixed" and not self.shipping_amount
entered = [(row, row.tax_amount) for row in doc.get("taxes")] if manual else ()
super().add_shipping_rule_to_tax_table(doc, shipping_amount)
for row, amount in entered:
row.tax_amount = amount
def shipping_rule_name(company):
abbr = frappe.get_cached_value("Company", company, "abbr")
return f"{LABEL} - {abbr}"
def ensure_shipping_rule(company):
"""Selling Shipping Rule for `company` with the amount entered per
transaction (see ThaiShippingRule). No-op when the rule exists."""
name = shipping_rule_name(company)
if frappe.db.exists("Shipping Rule", name):
return
frappe.get_doc(
{
"doctype": "Shipping Rule",
"label": name,
"company": company,
"shipping_rule_type": "Selling",
"calculate_based_on": "Fixed",
"shipping_amount": 0,
"account": get_or_create_account(
company, SHIPPING_ACCOUNT, "Income", "Income Account", SHIPPING_ACCOUNT_GROUPS
),
"cost_center": erpnext.get_default_cost_center(company),
}
).insert(ignore_permissions=True)
def setup_company(doc, method=None):
"""Company.on_update: default Shipping Rule for a Thai company."""
if company_ready(doc):
ensure_shipping_rule(doc.name)
def setup_companies():
"""after_install and the create_shipping_rules patch: companies that exist
before this code did never pass through `setup_company`."""
for company in thai_companies():
ensure_shipping_rule(company)
+376
View File
@@ -0,0 +1,376 @@
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
@@ -0,0 +1,50 @@
{#- ERPNext's templates/print_formats/includes/taxes.html, printing the amounts that are posted.
ERPNext takes an Additional Discount off the items' net amounts (item.distributed_discount_amount;
a discount on Grand Total is split between net and taxes) and books each charge at
tax_amount_after_discount_amount, while upstream prints the discount as entered and the
pre-discount tax_amount, so the lines do not add up to Grand Total.
Inclusive tax with a discount on Grand Total: the Total line is the tax-inclusive item total
and the discount is printed as entered; Total After Discount is then broken down into
Net Total and the charges. Otherwise the Total line is the net before the discount, so the
discount is its net share, followed by Net Total and the charges. A discount that is not
distributed (cash / non-trade) comes off the grand total only and stays after the charges,
as upstream. -#}
{%- macro amount_row(label, value) -%}
<div class="row">
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ label }}</label>
</div>
<div class="col-xs-7 text-right">
{{ value }}
</div>
</div>
{%- endmacro -%}
{%- macro currency(value) -%}
{{ frappe.format_value(value, {"fieldtype": "Currency", "options": "currency"}, doc) }}
{%- endmacro -%}
{%- set net_discount = doc.get("items")|map(attribute="distributed_discount_amount")|select|sum -%}
{%- set gross_discount = net_discount and doc.apply_discount_on == "Grand Total" and doc.flags.show_inclusive_tax_in_print -%}
<div class="row">
<div class="col-xs-6"></div>
<div class="col-xs-6">
{%- if gross_discount -%}
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ doc.get_formatted("discount_amount", doc)) }}
{{ amount_row(_("Total After Discount"), currency(doc.total - doc.discount_amount)) }}
{{ amount_row(_(doc.meta.get_label("net_total")), doc.get_formatted("net_total", doc)) }}
{%- elif net_discount -%}
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ currency(net_discount)) }}
{{ amount_row(_(doc.meta.get_label("net_total")), doc.get_formatted("net_total", doc)) }}
{%- endif -%}
{%- for charge in data -%}
{%- if (charge.tax_amount or print_settings.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
{{ amount_row(charge.get_formatted("description"), charge.get_formatted("tax_amount_after_discount_amount", doc)) }}
{%- endif -%}
{%- endfor -%}
{%- if doc.discount_amount and not net_discount -%}
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ doc.get_formatted("discount_amount", doc)) }}
{%- endif -%}
</div>
</div>
@@ -0,0 +1,22 @@
{#- ERPNext's templates/print_formats/includes/total.html, paired with the taxes include:
with inclusive tax and a discount on Grand Total the Total line is the tax-inclusive item
total (the discount is printed as entered below it); with inclusive tax otherwise it is
the net before the Additional Discount, since net_total already has the discount's net
share (item.distributed_discount_amount) taken off. -#}
{%- set net_discount = doc.get("items")|map(attribute="distributed_discount_amount")|select|sum -%}
{%- set gross_discount = net_discount and doc.apply_discount_on == "Grand Total" and doc.flags.show_inclusive_tax_in_print -%}
<div class="row {% if df.bold %}important{% endif %} data-field">
{% if doc.flags.show_inclusive_tax_in_print and not gross_discount %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _("Total (Without Tax)") }}</label></div>
<div class="col-xs-7 text-right value">
{{ frappe.format_value(doc.net_total + net_discount, {"fieldtype": "Currency", "options": "currency"}, doc) }}
</div>
{% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(df.label) }}</label></div>
<div class="col-xs-7 text-right value">
{{ doc.get_formatted("total", doc) }}
</div>
{% endif %}
</div>
@@ -0,0 +1,332 @@
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
@@ -0,0 +1,62 @@
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 ภาษีซื้อ
+24
View File
@@ -1,6 +1,30 @@
import frappe import frappe
import frappe.utils
from erpnext import get_default_company from erpnext import get_default_company
from frappe.contacts.doctype.address.address import get_default_address, render_address 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): def get_letter_head_company(doc=None):
+78
View File
@@ -0,0 +1,78 @@
import erpnext
import frappe
from default_thai_company.tax_withholding import (
INPUT_VAT_ACCOUNT,
OUTPUT_VAT_ACCOUNT,
company_ready,
get_or_create_account,
thai_companies,
)
RATE = 7.0
# title, rate, included_in_print_rate. Exempt supplies take no template.
SCHEMES = (
("Thailand VAT 7%", RATE, 0),
("Thailand VAT 7% (Included)", RATE, 1), # VAT-inclusive prices, the retail norm
("Thailand VAT 0%", 0.0, 0), # zero-rated: exports, international transport
)
DEFAULT_SCHEME = "Thailand VAT 7%"
# doctype, account, root type, extra row fields. Input VAT is recoverable, so on
# purchases it is "Total" (added to the bill, kept out of item valuation).
TEMPLATES = (
("Sales Taxes and Charges Template", OUTPUT_VAT_ACCOUNT, "Liability", {}),
(
"Purchase Taxes and Charges Template",
INPUT_VAT_ACCOUNT,
"Asset",
{"category": "Total", "add_deduct_tax": "Add"},
),
)
def ensure_vat_templates(company):
"""Create the VAT schemes for `company`, skipping titles that exist. The 7%
scheme becomes the default when the company has no default template."""
cost_center = erpnext.get_default_cost_center(company)
for doctype, account_name, root_type, extra in TEMPLATES:
account = get_or_create_account(company, account_name, root_type)
has_default = frappe.db.exists(doctype, {"company": company, "is_default": 1})
for title, rate, included in SCHEMES:
if frappe.db.exists(doctype, {"company": company, "title": title}):
continue
is_default = title == DEFAULT_SCHEME and not has_default
frappe.get_doc(
{
"doctype": doctype,
"title": title,
"company": company,
"is_default": int(is_default),
"taxes": [
{
"charge_type": "On Net Total",
"account_head": account,
"rate": rate,
"description": f"VAT {rate:g}%",
"included_in_print_rate": included,
"cost_center": cost_center,
**extra,
}
],
}
).insert(ignore_permissions=True)
has_default = has_default or is_default
def setup_company(doc, method=None):
"""Company.on_update: VAT schemes for a Thai company."""
if company_ready(doc):
ensure_vat_templates(doc.name)
def setup_companies():
"""after_install and the create_vat_templates patch: companies that exist
before this code did never pass through `setup_company`."""
for company in thai_companies():
ensure_vat_templates(company)