feat(employee-payroll-register): add per-contribution columns net of exemption

For each social contribution (DSMF, İcbari tibbi sığorta, İşsizlikdən
sığorta — işçi and şirkət, everything except Gəlir vergisi) add a column
per month that recomputes the amount on gross_pay minus that
contribution's "cəlb olunmayan gəlir" amount, using the Salary Component
formula. Sits next to the original column. 18 new columns total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-11 15:52:28 +00:00
parent 4e8bb96669
commit 671b2195fe
1 changed files with 35 additions and 9 deletions

View File

@ -35,6 +35,18 @@ DEDUCTION_TYPES = [
EH_COMPONENT = "Əsas əmək haqqı"
MK_COMPONENT = "Məzuniyyət Kompensasiyası"
# Social-contribution deductions (everything except Gəlir vergisi) get an extra
# column per month: the contribution recomputed on gross_pay net of its own
# "cəlb olunmayan gəlir" amount. Maps deduction name → Salary Slip exempt field.
CONTRIBUTION_EXEMPT_FIELD = {
"DSMF (işçi)": "custom_mdss_amount",
"DSMF (şirkət)": "custom_mdss_amount",
"İcbari tibbi sığorta (işçi 2%)": "custom_medical_insurance_amount",
"İcbari tibbi sığorta (şirkət 2%)": "custom_medical_insurance_amount",
"İşsizlikdən sığorta haqqı (işçi 0,5%)": "custom_unemployment_insurance_amount",
"İşsizlikdən sığorta haqqı (şirkət 0,5%)": "custom_unemployment_insurance_amount",
}
# Контекст для безопасного eval формул Salary Component. Зеркалит whitelisted_globals из HRMS.
FORMULA_GLOBALS = {
"int": int,
@ -170,8 +182,10 @@ def execute(filters=None):
# формул baseline = 0 (no-op), для Gəlir vergisi baseline = -6 из-за
# порога 200 AZN в base-части. Без вычитания MK-колонка для GV давала
# бы -3 вместо 3 (см. формулу `(base - 200) * 0.03 + LE * 0.03`).
rate = flt(ss.exchange_rate) if currency == company_currency else 1.0
eh_amount = flt(ss_earning_map.get(ss.name, {}).get(EH_COMPONENT, 0))
mk_amount = flt(ss_earning_map.get(ss.name, {}).get(MK_COMPONENT, 0))
gross_rated = flt(ss.gross_pay) * rate
for d in DEDUCTION_TYPES:
slug = frappe.scrub(d)
spec = ded_specs.get(d)
@ -179,15 +193,15 @@ def execute(filters=None):
row[prefix + "ded_eh_" + slug] += eval_deduction(spec, eh_amount, 0, ss)
row[prefix + "ded_mk_" + slug] += eval_deduction(spec, 0, mk_amount, ss) - baseline
if currency == company_currency:
rate = flt(ss.exchange_rate)
row[prefix + "gross_pay"] += flt(ss.gross_pay) * rate
row[prefix + "total_deduction"] += flt(ss.total_deduction) * rate
row[prefix + "net_pay"] += flt(ss.net_pay) * rate
else:
row[prefix + "gross_pay"] += flt(ss.gross_pay)
row[prefix + "total_deduction"] += flt(ss.total_deduction)
row[prefix + "net_pay"] += flt(ss.net_pay)
# Contribution recomputed on gross net of its own "cəlb olunmayan" amount.
exempt_field = CONTRIBUTION_EXEMPT_FIELD.get(d)
if exempt_field:
base_net = max(gross_rated - flt(ss.get(exempt_field) or 0) * rate, 0.0)
row[prefix + "ded_cg_" + slug] += eval_deduction(spec, base_net, 0, ss) - baseline
row[prefix + "gross_pay"] += gross_rated
row[prefix + "total_deduction"] += flt(ss.total_deduction) * rate
row[prefix + "net_pay"] += flt(ss.net_pay) * rate
for indicator_field, amount_field, _label in TAX_FREE_FIELDS:
row[prefix + indicator_field] = ss.get(indicator_field)
@ -228,6 +242,8 @@ def build_empty_row(ss, doj_map, currency, company_currency):
row[prefix + slug] = 0.0
row[prefix + "ded_eh_" + slug] = 0.0
row[prefix + "ded_mk_" + slug] = 0.0
if d in CONTRIBUTION_EXEMPT_FIELD:
row[prefix + "ded_cg_" + slug] = 0.0
for indicator_field, amount_field, _lbl in TAX_FREE_FIELDS:
row[prefix + indicator_field] = None
row[prefix + amount_field] = 0.0
@ -365,6 +381,16 @@ def get_columns():
"width": 140,
}
)
if deduction in CONTRIBUTION_EXEMPT_FIELD:
columns.append(
{
"label": deduction + " (cəlb edilən gəlirdən)" + suffix,
"fieldname": prefix + "ded_cg_" + frappe.scrub(deduction),
"fieldtype": "Currency",
"options": "currency",
"width": 200,
}
)
for deduction in DEDUCTION_TYPES:
columns.append(
{