Compare commits

..
2 Commits
Author SHA1 Message Date
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
5 changed files with 115 additions and 8 deletions

No files matched your search

+4
View File
@@ -236,6 +236,10 @@ doc_events = {
"default_thai_company.payroll.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": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
},
+1
View File
@@ -8,3 +8,4 @@ default_thai_company.patches.create_vat_accounts
default_thai_company.patches.create_vat_templates
default_thai_company.patches.create_shipping_rules
default_thai_company.patches.create_payroll_periods
default_thai_company.patches.setup_salary_components
@@ -0,0 +1,5 @@
from default_thai_company.payroll import setup_companies
def execute():
setup_companies()
+100 -8
View File
@@ -1,18 +1,102 @@
import frappe
from frappe.utils import getdate
from default_thai_company.tax_withholding import thai_companies
from default_thai_company.tax_withholding import get_or_create_account, thai_companies
# Thai personal income tax (P.N.D.1/91) is assessed per calendar year whatever
# the company's fiscal year, so the Payroll Period is January to December.
# Payroll Period is named by prompt (site-wide unique) but bound to one company,
# so each Thai company gets "<year> - <abbr>", like the Shipping Rule.
#
# 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.
# The employer's equal share is not an employee deduction and is left to the
# accounts. Thai books carry the withheld amount as ประกันสังคมค้างจ่าย until
# it is remitted by the 15th, so it gets its own liability.
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"
def hrms_installed():
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):
abbr = frappe.get_cached_value("Company", company, "abbr")
return f"{year} - {abbr}"
@@ -39,24 +123,32 @@ def ensure_payroll_period(company, year):
def setup_company(doc, method=None):
"""Company.on_update: this year's Payroll Period for a Thai company."""
"""Company.on_update: this year's Payroll Period, the Social Security
component and the Salary Component accounts for a Thai company."""
if doc.country == "Thailand" and hrms_installed():
ensure_payroll_period(doc.name, getdate().year)
ensure_social_security_component()
ensure_component_accounts(doc.name)
def setup_companies():
"""after_install, the daily scheduler, and the create_payroll_periods patch:
this year's Payroll Period for every Thai company. The scheduler rolls the
default over on 1 January."""
"""after_install, the daily scheduler, and the setup_salary_components
patch: 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."""
if not hrms_installed():
return
year = getdate().year
for company in thai_companies():
companies = thai_companies()
if companies:
ensure_social_security_component()
for company in companies:
ensure_payroll_period(company, year)
ensure_component_accounts(company)
def after_app_install(app_name):
"""Payroll Period only exists once hrms is installed; catch companies that
were created before it."""
"""Payroll Period and Salary Component accounts only exist once hrms is
installed; catch companies that were created before it."""
if app_name == "hrms":
setup_companies()
+5
View File
@@ -74,6 +74,11 @@ Rounded Total,ยอดรวมปัดเศษ
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
Page {0} of {1},หน้า {0} จาก {1}
Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย
Withholding Tax Payable (P.N.D.1),ภาษีหัก ณ ที่จ่ายค้างจ่าย (ภ.ง.ด.1)
Salary Deductions,รายการหักจากเงินเดือน
Social Security,ประกันสังคม
Social Security Payable,ประกันสังคมค้างจ่าย
Social Security Fund contribution (Sec. 33 employee),เงินสมทบกองทุนประกันสังคม (ผู้ประกันตนมาตรา 33)
Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย
Output 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 Fund contribution (Sec. 33 employee) เงินสมทบกองทุนประกันสังคม (ผู้ประกันตนมาตรา 33)
82 Withholding Tax Receivable ภาษีถูกหัก ณ ที่จ่าย
83 Output VAT ภาษีขาย
84 Input VAT ภาษีซื้อ