feat(salary-components): auto-sync from default_data on migrate

Adds sync_salary_components() — full upsert from default_data JSON:
creates missing components, updates existing ones (formula, condition,
flags, accounts), and deletes those no longer in the JSON. If a
component cannot be deleted due to linked records, it is marked
disabled instead. Idempotent and safe to re-run.

Hooked into after_migrate_combined so every "bench migrate" picks up
the latest formulas — single source of truth is the JSON file.

Replaced default_data/salary_components.json with the current
production export (14 components, formulas use the f(base) + g(LE)
pattern for separate ƏH/MK accounting).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-01 12:45:11 +00:00
parent 3aa4e61a1d
commit 408bacfc84
3 changed files with 341 additions and 192 deletions

View File

@ -1,11 +1,11 @@
[ [
{ {
"salary_component": "İşsizlikdən sığorta haqqı (xərc şirkət 0,5%)", "salary_component": "Keyfiim bonusu",
"salary_component_abbr": "İŞ-XŞ", "salary_component_abbr": "KB",
"type": "Earning", "type": "Earning",
"description": "İşsizlik, şirkətdən tutulan vergi", "description": null,
"depends_on_payment_days": 0, "depends_on_payment_days": 1,
"is_tax_applicable": 0, "is_tax_applicable": 1,
"deduct_full_tax_on_selected_payroll_date": 0, "deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0, "variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0, "is_income_tax_component": 0,
@ -13,50 +13,15 @@
"round_to_the_nearest_integer": 0, "round_to_the_nearest_integer": 0,
"statistical_component": 0, "statistical_component": 0,
"accrual_component": 0, "accrual_component": 0,
"do_not_include_in_total": 1, "do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": "",
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 0.5 / 100",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "İşçinin işsizlik vergisinin xərcləri",
"doctype": "Salary Component Account"
}
]
},
{
"salary_component": "İcbari tibbi sığorta (xərc şirkət 2%)",
"salary_component_abbr": "İTS-XŞ",
"type": "Earning",
"description": "İcbari tibbi sığorta tutulması, şirkətdən (2%)",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 1,
"do_not_include_in_accounts": 0, "do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1, "remove_if_zero_valued": 1,
"arrear_component": 0, "arrear_component": 0,
"disabled": 0, "disabled": 0,
"condition": null, "condition": null,
"amount": 0, "amount": 0,
"amount_based_on_formula": 1, "amount_based_on_formula": 0,
"formula": "base * 0.02 if base <= 8000 else 160 + (base - 8000) * 0.005", "formula": null,
"is_flexible_benefit": 0, "is_flexible_benefit": 0,
"payout_method": "", "payout_method": "",
"final_cycle_accrual_payout": 0, "final_cycle_accrual_payout": 0,
@ -64,11 +29,41 @@
"doctype": "Salary Component", "doctype": "Salary Component",
"accounts": [ "accounts": [
{ {
"account": cbari tibbi sığorta xərcləri", "account": stehsal sahəsində çalışan işçilərin əmək haqqı xərcləri",
"doctype": "Salary Component Account" "doctype": "Salary Component Account"
} }
] ]
}, },
{
"salary_component": "Gəlir vergisi (yalnız işçi)",
"salary_component_abbr": "GV_1",
"type": "Deduction",
"description": "Gəlir vergisi",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 1,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 1,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "(\n (base - 200) * 0.03 if base <= 2500\n else (75 + (base - 2500) * 0.10 if base <= 8000\n else 625 + (base - 8000) * 0.14)\n)\nif getdate(start_date).year >= 2026\nelse 0",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": []
},
{ {
"salary_component": "DSMF (xərc şirkət)", "salary_component": "DSMF (xərc şirkət)",
"salary_component_abbr": "DSMF-XŞ", "salary_component_abbr": "DSMF-XŞ",
@ -87,7 +82,7 @@
"do_not_include_in_accounts": 0, "do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1, "remove_if_zero_valued": 1,
"arrear_component": 0, "arrear_component": 0,
"disabled": 0, "disabled": 1,
"condition": null, "condition": null,
"amount": 0, "amount": 0,
"amount_based_on_formula": 1, "amount_based_on_formula": 1,
@ -97,9 +92,99 @@
"final_cycle_accrual_payout": 0, "final_cycle_accrual_payout": 0,
"max_benefit_amount": 0, "max_benefit_amount": 0,
"doctype": "Salary Component", "doctype": "Salary Component",
"accounts": []
},
{
"salary_component": "İcbari tibbi sığorta (xərc şirkət 2%)",
"salary_component_abbr": "İTS-XŞ",
"type": "Earning",
"description": "İcbari tibbi sığorta tutulması, şirkətdən (2%)",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 1,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 1,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 0.02 if base <= 8000 else 160 + (base - 8000) * 0.005",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": []
},
{
"salary_component": "İşsizlikdən sığorta haqqı (xərc şirkət 0,5%)",
"salary_component_abbr": "İŞ-XŞ",
"type": "Earning",
"description": "İşsizlik, şirkətdən tutulan vergi",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 1,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 1,
"condition": "",
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 0.5 / 100",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": []
},
{
"salary_component": "Məzuniyyət Kompensasiyası",
"salary_component_abbr": "LE",
"type": "Earning",
"description": "Məzuniyyət Kompensasiyası",
"depends_on_payment_days": 0,
"is_tax_applicable": 1,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 12 / 365",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [ "accounts": [
{ {
"account": "Sosial sığorta xərcləri (DSMF Şirkət)", "account": "İnzibati işçi heyəti xərcləri",
"doctype": "Salary Component Account" "doctype": "Salary Component Account"
} }
] ]
@ -126,7 +211,7 @@
"condition": "", "condition": "",
"amount": 0, "amount": 0,
"amount_based_on_formula": 1, "amount_based_on_formula": 1,
"formula": "base * 0.5 / 100", "formula": "base * 0.005 + LE * 0.005",
"is_flexible_benefit": 0, "is_flexible_benefit": 0,
"payout_method": "", "payout_method": "",
"final_cycle_accrual_payout": 0, "final_cycle_accrual_payout": 0,
@ -161,7 +246,7 @@
"condition": "", "condition": "",
"amount": 0, "amount": 0,
"amount_based_on_formula": 1, "amount_based_on_formula": 1,
"formula": "base * 0.5 / 100", "formula": "base * 0.005 + LE * 0.005",
"is_flexible_benefit": 0, "is_flexible_benefit": 0,
"payout_method": "", "payout_method": "",
"final_cycle_accrual_payout": 0, "final_cycle_accrual_payout": 0,
@ -174,6 +259,111 @@
} }
] ]
}, },
{
"salary_component": "İcbari tibbi sığorta (şirkət 2%)",
"salary_component_abbr": "İTS-Ş",
"type": "Deduction",
"description": "İcbari tibbi sığorta tutulması, şirkətdən (2%)",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 1,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "(base * 0.02 if base <= 2500 else 50 + (base - 2500) * 0.005) + (LE * 0.02 if LE <= 2500 else 50 + (LE - 2500) * 0.005)",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "İcbari tibbi sığorta (şirkət)",
"doctype": "Salary Component Account"
}
]
},
{
"salary_component": "İcbari tibbi sığorta (işçi 2%)",
"salary_component_abbr": "İTS-İ",
"type": "Deduction",
"description": "İcbari tibbi sığorta tutulması, işçidən (2%)",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": "",
"amount": 0,
"amount_based_on_formula": 1,
"formula": "(base * 0.02 if base <= 2500 else 50 + (base - 2500) * 0.005) + (LE * 0.02 if LE <= 2500 else 50 + (LE - 2500) * 0.005)",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "İcbari tibbi sığorta (işçi)",
"doctype": "Salary Component Account"
}
]
},
{
"salary_component": "Gəlir vergisi",
"salary_component_abbr": "GV",
"type": "Deduction",
"description": "Gəlir vergisi",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 1,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "((base - 200) * 0.03 if base <= 2500 else 75 + (base - 2500) * 0.10 if base <= 8000 else 625 + (base - 8000) * 0.14) + (LE * 0.03 if LE <= 2500 else 75 + (LE - 2500) * 0.10 if LE <= 8000 else 625 + (LE - 8000) * 0.14)",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "Gəlir vergisi (işçi)",
"doctype": "Salary Component Account"
}
]
},
{ {
"salary_component": "Əsas əmək haqqı", "salary_component": "Əsas əmək haqqı",
"salary_component_abbr": "ƏH", "salary_component_abbr": "ƏH",
@ -204,42 +394,7 @@
"doctype": "Salary Component", "doctype": "Salary Component",
"accounts": [ "accounts": [
{ {
"account": "İşçilərin əmək haqqı xərcləri", "account": "İnzibati işçi heyəti xərcləri",
"doctype": "Salary Component Account"
}
]
},
{
"salary_component": "İcbari tibbi sığorta (şirkət 2%)",
"salary_component_abbr": "İTS-Ş",
"type": "Deduction",
"description": "İcbari tibbi sığorta tutulması, şirkətdən (2%)",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 1,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 0.02 if base <= 8000 else 160 + (base - 8000) * 0.005",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "İcbari tibbi sığorta (şirkət)",
"doctype": "Salary Component Account" "doctype": "Salary Component Account"
} }
] ]
@ -266,7 +421,7 @@
"condition": null, "condition": null,
"amount": 0, "amount": 0,
"amount_based_on_formula": 1, "amount_based_on_formula": 1,
"formula": "200 * 0.22 if base <= 200 else 44 + (base - 200) * 0.15", "formula": "(base * 0.22 if base <= 200 else 44 + (base - 200) * 0.15 if base <= 8000 else 1214 + (base - 8000) * 0.11) + (LE * 0.22 if LE <= 200 else 44 + (LE - 200) * 0.15 if LE <= 8000 else 1214 + (LE - 8000) * 0.11)",
"is_flexible_benefit": 0, "is_flexible_benefit": 0,
"payout_method": "", "payout_method": "",
"final_cycle_accrual_payout": 0, "final_cycle_accrual_payout": 0,
@ -274,77 +429,7 @@
"doctype": "Salary Component", "doctype": "Salary Component",
"accounts": [ "accounts": [
{ {
"account": "Sosial sığorta öhdəlikləri (DSMF Şirkət)", "account": "Sosial sığorta öhdəlikləri (DSMF şirkət)",
"doctype": "Salary Component Account"
}
]
},
{
"salary_component": "Gəlir vergisi (yalnız işçi)",
"salary_component_abbr": "GV",
"type": "Deduction",
"description": "Gəlir vergisi",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 1,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "(\n (base - 200) * 0.03 if base <= 2500\n else (75 + (base - 2500) * 0.10 if base <= 8000\n else 625 + (base - 8000) * 0.14)\n)\nif getdate(start_date).year >= 2026\nelse 0",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "Gəlir vergisi (işçi)",
"doctype": "Salary Component Account"
}
]
},
{
"salary_component": "İcbari tibbi sığorta (işçi 2%)",
"salary_component_abbr": "İTS-İ",
"type": "Deduction",
"description": "İcbari tibbi sığorta tutulması, işçidən (2%)",
"depends_on_payment_days": 0,
"is_tax_applicable": 0,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 0,
"condition": "",
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 0.02 if base <= 8000 else 160 + (base - 8000) * 0.005",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "İcbari tibbi sığorta (işçi)",
"doctype": "Salary Component Account" "doctype": "Salary Component Account"
} }
] ]
@ -371,7 +456,7 @@
"condition": "", "condition": "",
"amount": 0, "amount": 0,
"amount_based_on_formula": 1, "amount_based_on_formula": 1,
"formula": "200 * 0.03 if base <= 200 else 6 + (base - 200) * 0.10", "formula": "(base * 0.03 if base <= 200 else 6 + (base - 200) * 0.10 if base <= 8000 else 786 + (base - 8000) * 0.10) + (LE * 0.03 if LE <= 200 else 6 + (LE - 200) * 0.10 if LE <= 8000 else 786 + (LE - 8000) * 0.10)",
"is_flexible_benefit": 0, "is_flexible_benefit": 0,
"payout_method": "", "payout_method": "",
"final_cycle_accrual_payout": 0, "final_cycle_accrual_payout": 0,
@ -383,40 +468,5 @@
"doctype": "Salary Component Account" "doctype": "Salary Component Account"
} }
] ]
},
{
"salary_component": "Məzuniyyət Kompensasiyası",
"salary_component_abbr": "LE",
"type": "Earning",
"description": "Məzuniyyət Kompensasiyası",
"depends_on_payment_days": 0,
"is_tax_applicable": 1,
"deduct_full_tax_on_selected_payroll_date": 0,
"variable_based_on_taxable_salary": 0,
"is_income_tax_component": 0,
"exempted_from_income_tax": 0,
"round_to_the_nearest_integer": 0,
"statistical_component": 0,
"accrual_component": 0,
"do_not_include_in_total": 0,
"do_not_include_in_accounts": 0,
"remove_if_zero_valued": 1,
"arrear_component": 0,
"disabled": 1,
"condition": null,
"amount": 0,
"amount_based_on_formula": 1,
"formula": "base * 12 / 365",
"is_flexible_benefit": 0,
"payout_method": "",
"final_cycle_accrual_payout": 0,
"max_benefit_amount": 0,
"doctype": "Salary Component",
"accounts": [
{
"account": "Əməyin ödənişi üzrə işçi heyətinə olan borclar",
"doctype": "Salary Component Account"
}
]
} }
] ]

View File

@ -112,6 +112,11 @@ def after_migrate_combined():
rebuild_tree("Tax Free Indicator") rebuild_tree("Tax Free Indicator")
except Exception as e: except Exception as e:
print(f"Tax Free Indicator NSM rebuild failed: {e}") print(f"Tax Free Indicator NSM rebuild failed: {e}")
from jey_erp.setup.setup_wizard_handler import sync_salary_components
try:
sync_salary_components()
except Exception as e:
print(f"Salary Component sync failed: {e}")
# REMOVED: Asset categories creation moved to setup_wizard_complete hook # REMOVED: Asset categories creation moved to setup_wizard_complete hook
# from jey_erp.custom.create_asset_categories import create_asset_categories # from jey_erp.custom.create_asset_categories import create_asset_categories
# create_asset_categories() # create_asset_categories()

View File

@ -66,6 +66,100 @@ def create_default_salary_components(args):
frappe.db.commit() frappe.db.commit()
SC_SYNC_FIELDS = (
"salary_component_abbr", "type", "description",
"depends_on_payment_days", "is_tax_applicable",
"deduct_full_tax_on_selected_payroll_date",
"variable_based_on_taxable_salary", "is_income_tax_component",
"exempted_from_income_tax", "round_to_the_nearest_integer",
"statistical_component", "accrual_component",
"do_not_include_in_total", "do_not_include_in_accounts",
"remove_if_zero_valued", "arrear_component", "disabled",
"condition", "amount", "amount_based_on_formula", "formula",
"is_flexible_benefit", "payout_method",
"final_cycle_accrual_payout", "max_benefit_amount",
)
def sync_salary_components():
"""Полная синхронизация Salary Component с default_data/salary_components.json.
JSON single source of truth: создаёт отсутствующие, обновляет существующие,
удаляет лишние (не в JSON). Если удалить нельзя из-за linked records
компонент помечается disabled. Идемпотентна, безопасна для повторного запуска.
Запускается автоматически из after_migrate.
"""
if not os.path.exists(SALARY_COMPONENTS_JSON):
return
with open(SALARY_COMPONENTS_JSON, encoding="utf-8") as f:
components = json.load(f)
companies = frappe.get_all("Company", pluck="name")
def resolve_accounts(json_accounts):
out = []
for row in json_accounts or []:
account_name = row.get("account")
if not account_name:
continue
for company in companies:
full = frappe.db.get_value(
"Account",
{"account_name": account_name, "company": company, "is_group": 0},
"name",
)
if full:
out.append({"company": company, "account": full})
return out
json_names = set()
for data in components:
name = data.get("salary_component")
if not name:
continue
json_names.add(name)
accounts = resolve_accounts(data.get("accounts"))
if frappe.db.exists("Salary Component", name):
doc = frappe.get_doc("Salary Component", name)
for field in SC_SYNC_FIELDS:
if field in data:
doc.set(field, data[field])
doc.set("accounts", [])
for acc in accounts:
doc.append("accounts", acc)
doc.save(ignore_permissions=True)
else:
new_data = {k: v for k, v in data.items() if k != "accounts"}
new_data["doctype"] = "Salary Component"
new_data["accounts"] = accounts
frappe.get_doc(new_data).insert(ignore_permissions=True)
# Удаляем то, чего больше нет в JSON. Если удалить нельзя — disable.
for name in frappe.get_all("Salary Component", pluck="name"):
if name in json_names:
continue
try:
frappe.delete_doc("Salary Component", name, ignore_permissions=True)
except Exception:
try:
doc = frappe.get_doc("Salary Component", name)
doc.disabled = 1
doc.save(ignore_permissions=True)
frappe.log_error(
f"Cannot delete Salary Component '{name}' (linked records exist). "
f"Marked as disabled.",
"Salary Component Sync",
)
except Exception as inner:
frappe.log_error(
f"Failed to disable Salary Component '{name}': {inner}",
"Salary Component Sync",
)
frappe.db.commit()
def create_default_leave_types(args): def create_default_leave_types(args):
if not os.path.exists(LEAVE_TYPES_JSON): if not os.path.exists(LEAVE_TYPES_JSON):
return return