Compare commits

..
4 Commits
Author SHA1 Message Date
kurogeek cf687e24cc feat: Social Security Contribution (Employer) expense account
The employer's equal share of the Social Security Fund contribution is
not an employee deduction: it is booked against Social Security
Payable from its own expense account. Every Thai company gets
"Social Security Contribution (Employer)" under Indirect Expenses,
whether or not it runs payroll here (Company.on_update, after_install
and a patch for existing sites).
2026-11-05 10:03:02 +00:00
kurogeek b4c7274dce feat: Bank Fee expense account for every Thai company
Thai banks charge transfer and collection fees that customers net off
their payments and that the bank nets off ours; Payment Entry books
them as deductions, which need an expense account. The Standard chart
has none, so every Thai company gets "Bank Fee" under Indirect Expenses
(Company.on_update, after_install and a patch for existing sites).
2026-11-05 04:29:04 +00:00
kurogeek 6dd557ee97 feat: Social Security deduction component
Employee's Social Security Fund contribution (Sec. 33): 5% of wages,
wages taken as at least 1,650 and at most 17,500 a month (2026-2028
ceiling), fractions of a baht rounded half up. Computed from the Basic
earning rather than `base` so a partial month is 5% of the wages
actually paid and the ceiling applies after proration. The contribution
is deductible from taxable income in full, so the component is
exempted_from_income_tax. Withheld amounts are held in "Social Security
Payable" (ประกันสังคมค้างจ่าย) until remitted.

Created once a Thai company exists (Company.on_update, install hooks,
scheduler, the setup_salary_components patch); the Salary Component
validate hook links every Thai company.
2026-11-04 09:54:01 +00:00
kurogeek 035ab2fc7f feat: an account on every Salary Component for every Thai company
Payroll Entry refuses to post a component without an account for its
company. Link every Thai company on every Salary Component: on
Salary Component.validate (covers the components HRMS seeds with the
first company and the ones users add) and on Company.on_update, with
the install hooks, the daily scheduler and a patch for existing sites.

Earnings expense to "Salary" (Standard chart, Indirect Expenses). The
income tax component is tax withheld from employees, remitted on
P.N.D.1; Thai books carry it as ภาษีหัก ณ ที่จ่ายค้างจ่าย, split by
return form when split at all, so it gets "Withholding Tax Payable
(P.N.D.1)" beside the P.N.D.3/53 account the Tax Withholding Categories
post to. Other deductions are held in "Salary Deductions" (Current
Liabilities) until remitted. Statistical components never post and get
none.
2026-11-04 09:54:01 +00:00
8 changed files with 179 additions and 9 deletions

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Thai banks charge transfer and collection fees that customers net off their
# payments and that the bank nets off ours; Payment Entry books them as
# deductions, which need an expense account. The Standard chart has none, so
# every Thai company gets one under Indirect Expenses.
BANK_FEE_ACCOUNT = "Bank Fee"
BANK_FEE_ACCOUNT_GROUPS = ("Indirect Expenses", "Expenses")
def ensure_bank_fee_account(company):
return get_or_create_account(company, BANK_FEE_ACCOUNT, "Expense", "", BANK_FEE_ACCOUNT_GROUPS)
def setup_company(doc, method=None):
"""Company.on_update: Bank Fee account for a Thai company."""
if company_ready(doc):
ensure_bank_fee_account(doc.name)
def setup_companies():
"""after_install and the create_bank_fee_accounts patch: companies that
exist before this code did never pass through `setup_company`."""
for company in thai_companies():
ensure_bank_fee_account(company)
+6
View File
@@ -157,6 +157,7 @@ after_install = [
"default_thai_company.vat.setup_companies", "default_thai_company.vat.setup_companies",
"default_thai_company.shipping.setup_companies", "default_thai_company.shipping.setup_companies",
"default_thai_company.payroll.setup_companies", "default_thai_company.payroll.setup_companies",
"default_thai_company.banking.setup_companies",
] ]
# Fixture import re-inserts the Location tree; restore lft/rgt for user-added nodes. # Fixture import re-inserts the Location tree; restore lft/rgt for user-added nodes.
after_migrate = "default_thai_company.assets.rebuild_locations" after_migrate = "default_thai_company.assets.rebuild_locations"
@@ -234,8 +235,13 @@ doc_events = {
"default_thai_company.assets.setup_company", "default_thai_company.assets.setup_company",
"default_thai_company.shipping.setup_company", "default_thai_company.shipping.setup_company",
"default_thai_company.payroll.setup_company", "default_thai_company.payroll.setup_company",
"default_thai_company.banking.setup_company",
], ],
}, },
# HRMS doctype; the hook only fires on sites that have it.
"Salary Component": {
"validate": "default_thai_company.payroll.set_component_accounts",
},
"Payment Entry": { "Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding", "validate": "default_thai_company.tax_withholding.set_customer_withholding",
}, },
+3
View File
@@ -8,3 +8,6 @@ default_thai_company.patches.create_vat_accounts
default_thai_company.patches.create_vat_templates default_thai_company.patches.create_vat_templates
default_thai_company.patches.create_shipping_rules default_thai_company.patches.create_shipping_rules
default_thai_company.patches.create_payroll_periods default_thai_company.patches.create_payroll_periods
default_thai_company.patches.setup_salary_components
default_thai_company.patches.create_bank_fee_accounts
default_thai_company.patches.create_employer_social_security_accounts
@@ -0,0 +1,5 @@
from default_thai_company.banking import setup_companies
def execute():
setup_companies()
@@ -0,0 +1,5 @@
from default_thai_company.payroll import setup_companies
def execute():
setup_companies()
@@ -0,0 +1,5 @@
from default_thai_company.payroll import setup_companies
def execute():
setup_companies()
+123 -9
View File
@@ -1,18 +1,106 @@
import frappe import frappe
from frappe.utils import getdate from frappe.utils import getdate
from default_thai_company.tax_withholding import thai_companies from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Thai personal income tax (P.N.D.1/91) is assessed per calendar year whatever # Thai personal income tax (P.N.D.1/91) is assessed per calendar year whatever
# the company's fiscal year, so the Payroll Period is January to December. # the company's fiscal year, so the Payroll Period is January to December.
# Payroll Period is named by prompt (site-wide unique) but bound to one company, # Payroll Period is named by prompt (site-wide unique) but bound to one company,
# so each Thai company gets "<year> - <abbr>", like the Shipping Rule. # so each Thai company gets "<year> - <abbr>", like the Shipping Rule.
#
# Payroll Entry refuses to post a component without an account for its company,
# so every Salary Component gets one for every Thai company (Salary
# Component.validate for the components HRMS seeds with the first company and
# the ones users add; Company.on_update for companies added later).
# Earnings expense to "Salary" (Standard chart, Indirect Expenses). The income
# tax component is tax withheld from employees, remitted on P.N.D.1; Thai books
# carry it as ภาษีหัก ณ ที่จ่ายค้างจ่าย, split by return form when split at all,
# so it gets its own withholding account beside the P.N.D.3/53 one the Tax
# Withholding Categories post to. Other deductions (social security, loans)
# are held in "Salary Deductions" (Current Liabilities) until remitted.
EARNING_ACCOUNT = "Salary"
EARNING_ACCOUNT_GROUPS = ("Indirect Expenses", "Expenses")
INCOME_TAX_ACCOUNT = "Withholding Tax Payable (P.N.D.1)"
DEDUCTION_ACCOUNT = "Salary Deductions"
DEDUCTION_ACCOUNT_GROUPS = ("Current Liabilities",)
# Employee's Social Security Fund contribution (Sec. 33): 5% of wages, wages
# taken as at least 1,650 and at most 17,500 a month (2026-2028 ceiling; 20,000
# from 2029), fractions of a baht rounded half up. Computed from the Basic
# earning rather than `base` so a partial month is 5% of the wages actually paid
# and the ceiling applies after proration, as the Social Security Office
# assesses it. The contribution is deductible from taxable income in full, so
# the component is exempted_from_income_tax; the "Social Security Fund"
# exemption sub-category is for contributions not run through payroll here.
# Thai books carry the withheld amount as ประกันสังคมค้างจ่าย until it is
# remitted by the 15th, so it gets its own liability. The employer's equal share
# is not an employee deduction: it is booked against the same liability from
# its own expense account, which every Thai company gets whether or not it runs
# payroll here.
SOCIAL_SECURITY_COMPONENT = {
"doctype": "Salary Component",
"salary_component": "Social Security",
"salary_component_abbr": "SSO",
"type": "Deduction",
"description": "Social Security Fund contribution (Sec. 33 employee)",
"depends_on_payment_days": 0,
"exempted_from_income_tax": 1,
"condition": "B > 0",
"amount_based_on_formula": 1,
"formula": "int((1650 if B < 1650 else 17500 if B > 17500 else B) * 5 / 100 + 0.5)",
}
SOCIAL_SECURITY_ACCOUNT = "Social Security Payable"
EMPLOYER_SOCIAL_SECURITY_ACCOUNT = "Social Security Contribution (Employer)"
EMPLOYER_SOCIAL_SECURITY_ACCOUNT_GROUPS = ("Indirect Expenses", "Expenses")
def hrms_installed(): def hrms_installed():
return "hrms" in frappe.get_installed_apps() return "hrms" in frappe.get_installed_apps()
def salary_component_account(company, component):
if component.type == "Earning":
return get_or_create_account(company, EARNING_ACCOUNT, "Expense", "", EARNING_ACCOUNT_GROUPS)
if component.is_income_tax_component:
return get_or_create_account(company, INCOME_TAX_ACCOUNT, "Liability")
if component.salary_component == SOCIAL_SECURITY_COMPONENT["salary_component"]:
return get_or_create_account(
company, SOCIAL_SECURITY_ACCOUNT, "Liability", "", DEDUCTION_ACCOUNT_GROUPS
)
return get_or_create_account(company, DEDUCTION_ACCOUNT, "Liability", "", DEDUCTION_ACCOUNT_GROUPS)
def link_company(component, company):
"""Append the company's account row to the component if missing. Statistical
components never post, so they get none."""
if component.statistical_component or any(row.company == company for row in component.accounts):
return False
component.append(
"accounts", {"company": company, "account": salary_component_account(company, component)}
)
return True
def set_component_accounts(doc, method=None):
"""Salary Component.validate: an account for every Thai company."""
for company in thai_companies():
link_company(doc, company)
def ensure_component_accounts(company):
for name in frappe.get_all("Salary Component", pluck="name"):
component = frappe.get_doc("Salary Component", name)
if link_company(component, company):
component.save(ignore_permissions=True)
def ensure_social_security_component():
"""Social Security deduction; the validate hook links every Thai company."""
if frappe.db.exists("Salary Component", SOCIAL_SECURITY_COMPONENT["salary_component"]):
return
frappe.get_doc(SOCIAL_SECURITY_COMPONENT).insert(ignore_permissions=True)
def payroll_period_name(company, year): def payroll_period_name(company, year):
abbr = frappe.get_cached_value("Company", company, "abbr") abbr = frappe.get_cached_value("Company", company, "abbr")
return f"{year} - {abbr}" return f"{year} - {abbr}"
@@ -38,25 +126,51 @@ def ensure_payroll_period(company, year):
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
def ensure_employer_social_security_account(company):
return get_or_create_account(
company,
EMPLOYER_SOCIAL_SECURITY_ACCOUNT,
"Expense",
"",
EMPLOYER_SOCIAL_SECURITY_ACCOUNT_GROUPS,
)
def setup_company(doc, method=None): def setup_company(doc, method=None):
"""Company.on_update: this year's Payroll Period for a Thai company.""" """Company.on_update: the employer's Social Security expense account, this
if doc.country == "Thailand" and hrms_installed(): year's Payroll Period, the Social Security component and the Salary
Component accounts for a Thai company."""
if doc.country != "Thailand":
return
if company_ready(doc):
ensure_employer_social_security_account(doc.name)
if hrms_installed():
ensure_payroll_period(doc.name, getdate().year) ensure_payroll_period(doc.name, getdate().year)
ensure_social_security_component()
ensure_component_accounts(doc.name)
def setup_companies(): def setup_companies():
"""after_install, the daily scheduler, and the create_payroll_periods patch: """after_install, the daily scheduler, and the setup_salary_components and
this year's Payroll Period for every Thai company. The scheduler rolls the create_employer_social_security_accounts patches: the employer's Social
default over on 1 January.""" Security expense account, this year's Payroll Period, the Social Security
component and the Salary Component accounts for every Thai company. The
scheduler rolls the Payroll Period over on 1 January."""
companies = thai_companies()
for company in companies:
ensure_employer_social_security_account(company)
if not hrms_installed(): if not hrms_installed():
return return
year = getdate().year year = getdate().year
for company in thai_companies(): if companies:
ensure_social_security_component()
for company in companies:
ensure_payroll_period(company, year) ensure_payroll_period(company, year)
ensure_component_accounts(company)
def after_app_install(app_name): def after_app_install(app_name):
"""Payroll Period only exists once hrms is installed; catch companies that """Payroll Period and Salary Component accounts only exist once hrms is
were created before it.""" installed; catch companies that were created before it."""
if app_name == "hrms": if app_name == "hrms":
setup_companies() setup_companies()
+7
View File
@@ -74,6 +74,13 @@ Rounded Total,ยอดรวมปัดเศษ
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
Page {0} of {1},หน้า {0} จาก {1} Page {0} of {1},หน้า {0} จาก {1}
Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย
Withholding Tax Payable (P.N.D.1),ภาษีหัก ณ ที่จ่ายค้างจ่าย (ภ.ง.ด.1)
Salary Deductions,รายการหักจากเงินเดือน
Social Security,ประกันสังคม
Social Security Payable,ประกันสังคมค้างจ่าย
Social Security Contribution (Employer),เงินสมทบประกันสังคม (ส่วนนายจ้าง)
Social Security Fund contribution (Sec. 33 employee),เงินสมทบกองทุนประกันสังคม (ผู้ประกันตนมาตรา 33)
Bank Fee,ค่าธรรมเนียมธนาคาร
Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย
Output VAT,ภาษีขาย Output VAT,ภาษีขาย
Input VAT,ภาษีซื้อ Input VAT,ภาษีซื้อ
1 Net Payable After Withholding Tax ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
74 Terms and Conditions Details รายละเอียดข้อตกลงและเงื่อนไข
75 Page {0} of {1} หน้า {0} จาก {1}
76 Withholding Tax Payable ภาษีหัก ณ ที่จ่ายค้างจ่าย
77 Withholding Tax Payable (P.N.D.1) ภาษีหัก ณ ที่จ่ายค้างจ่าย (ภ.ง.ด.1)
78 Salary Deductions รายการหักจากเงินเดือน
79 Social Security ประกันสังคม
80 Social Security Payable ประกันสังคมค้างจ่าย
81 Social Security Contribution (Employer) เงินสมทบประกันสังคม (ส่วนนายจ้าง)
82 Social Security Fund contribution (Sec. 33 employee) เงินสมทบกองทุนประกันสังคม (ผู้ประกันตนมาตรา 33)
83 Bank Fee ค่าธรรมเนียมธนาคาร
84 Withholding Tax Receivable ภาษีถูกหัก ณ ที่จ่าย
85 Output VAT ภาษีขาย
86 Input VAT ภาษีซื้อ