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.
This commit is contained in:
kurogeek committed 2026-11-04 09:54:01 +00:00
1 parent c8fb983749
commit 035ab2fc7f
5 files changed
+71 -7

No files matched your search

+4
View File
@@ -236,6 +236,10 @@ doc_events = {
"default_thai_company.payroll.setup_company", "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": { "Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding", "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_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
@@ -0,0 +1,5 @@
from default_thai_company.payroll import setup_companies
def execute():
setup_companies()
+59 -7
View File
@@ -1,18 +1,67 @@
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 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",)
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")
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 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}"
@@ -39,24 +88,27 @@ def ensure_payroll_period(company, year):
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: this year's Payroll Period and the Salary Component
accounts for a Thai company."""
if doc.country == "Thailand" and hrms_installed(): if doc.country == "Thailand" and hrms_installed():
ensure_payroll_period(doc.name, getdate().year) ensure_payroll_period(doc.name, getdate().year)
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
this year's Payroll Period for every Thai company. The scheduler rolls the patch: this year's Payroll Period and the Salary Component accounts for
default over on 1 January.""" every Thai company. The scheduler rolls the Payroll Period over on 1 January."""
if not hrms_installed(): if not hrms_installed():
return return
year = getdate().year year = getdate().year
for company in thai_companies(): for company in thai_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()
+2
View File
@@ -74,6 +74,8 @@ 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,รายการหักจากเงินเดือน
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 Withholding Tax Receivable ภาษีถูกหัก ณ ที่จ่าย
80 Output VAT ภาษีขาย
81 Input VAT ภาษีซื้อ