From 91268cc466b51a531722b65741276a006ae5ba91 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 1 May 2026 12:55:27 +0000 Subject: [PATCH] feat(default-data): auto-sync leave types and company defaults on migrate Mirrors the salary-component pattern for two more pieces of master data: - sync_leave_types(): full upsert from default_data/leave_types.json. Creates missing, updates existing, deletes those no longer in JSON. Leave Type has no `disabled` field, so when a deletion fails due to linked Leave Allocation/Application records, the sync logs and skips (vs. marking disabled like Salary Component does). - sync_company_defaults(): for every Company, resolves each account_number in default_data/company_defaults.json to the company-scoped Account name and sets the corresponding Company field. Non-destructive: if the company has no account with that number, the field is left as-is. Both hooked into after_migrate_combined after sync_salary_components, since Leave Type's earning_component references a Salary Component. Co-Authored-By: Claude Opus 4.7 (1M context) --- jey_erp/hooks.py | 16 ++++- jey_erp/setup/setup_wizard_handler.py | 94 +++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/jey_erp/hooks.py b/jey_erp/hooks.py index cc52694..60ccdd1 100644 --- a/jey_erp/hooks.py +++ b/jey_erp/hooks.py @@ -112,11 +112,25 @@ def after_migrate_combined(): rebuild_tree("Tax Free Indicator") except Exception as e: print(f"Tax Free Indicator NSM rebuild failed: {e}") - from jey_erp.setup.setup_wizard_handler import sync_salary_components + # Order matters: Salary Component must sync first because Leave Type's + # earning_component field references it. + from jey_erp.setup.setup_wizard_handler import ( + sync_salary_components, + sync_leave_types, + sync_company_defaults, + ) try: sync_salary_components() except Exception as e: print(f"Salary Component sync failed: {e}") + try: + sync_leave_types() + except Exception as e: + print(f"Leave Type sync failed: {e}") + try: + sync_company_defaults() + except Exception as e: + print(f"Company defaults sync failed: {e}") # REMOVED: Asset categories creation moved to setup_wizard_complete hook # from jey_erp.custom.create_asset_categories import create_asset_categories # create_asset_categories() diff --git a/jey_erp/setup/setup_wizard_handler.py b/jey_erp/setup/setup_wizard_handler.py index 1f223fe..8ff4e21 100644 --- a/jey_erp/setup/setup_wizard_handler.py +++ b/jey_erp/setup/setup_wizard_handler.py @@ -182,6 +182,66 @@ def create_default_leave_types(args): frappe.db.commit() +LEAVE_TYPE_SYNC_FIELDS = ( + "max_leaves_allowed", "applicable_after", "max_continuous_days_allowed", + "is_carry_forward", "is_lwp", "is_ppl", + "fraction_of_daily_salary_per_leave", "is_optional_leave", + "allow_negative", "allow_over_allocation", "include_holiday", + "is_compensatory", "maximum_carry_forwarded_leaves", + "expire_carry_forwarded_leaves_after_days", + "allow_encashment", "max_encashable_leaves", "non_encashable_leaves", + "earning_component", "is_earned_leave", "earned_leave_frequency", + "allocate_on_day", "rounding", +) + + +def sync_leave_types(): + """Полная синхронизация Leave Type с default_data/leave_types.json. + JSON — single source of truth. Создаёт отсутствующие, обновляет + существующие, удаляет лишние. Если удалить нельзя из-за linked records + (Leave Allocation/Application) — пишет в Error Log и пропускает + (у Leave Type нет поля disabled). Идемпотентна. + Должна запускаться ПОСЛЕ sync_salary_components, т.к. earning_component + может ссылаться на Salary Component. + """ + if not os.path.exists(LEAVE_TYPES_JSON): + return + + with open(LEAVE_TYPES_JSON, encoding="utf-8") as f: + leave_types = json.load(f) + + json_names = set() + for data in leave_types: + name = data.get("leave_type_name") + if not name: + continue + json_names.add(name) + + if frappe.db.exists("Leave Type", name): + doc = frappe.get_doc("Leave Type", name) + for field in LEAVE_TYPE_SYNC_FIELDS: + if field in data: + doc.set(field, data[field]) + doc.save(ignore_permissions=True) + else: + new_data = dict(data) + new_data["doctype"] = "Leave Type" + frappe.get_doc(new_data).insert(ignore_permissions=True) + + for name in frappe.get_all("Leave Type", pluck="name"): + if name in json_names: + continue + try: + frappe.delete_doc("Leave Type", name, ignore_permissions=True) + except Exception as e: + frappe.log_error( + f"Cannot delete Leave Type '{name}' (likely linked records): {e}", + "Leave Type Sync", + ) + + frappe.db.commit() + + def create_default_company_accounts(args): if not os.path.exists(COMPANY_DEFAULTS_JSON): return @@ -209,6 +269,40 @@ def create_default_company_accounts(args): frappe.db.commit() +def sync_company_defaults(): + """Синхронизирует default-счета на ВСЕХ Company с default_data/company_defaults.json. + Для каждого поля из JSON ищет Account по account_number в каждой компании + и проставляет полное имя счёта на Company. Поля, которых в JSON нет, + не трогает. Если в компании нет аккаунта с таким номером — поле этой + компании не меняется (silent skip). Идемпотентна. + """ + if not os.path.exists(COMPANY_DEFAULTS_JSON): + return + + with open(COMPANY_DEFAULTS_JSON, encoding="utf-8") as f: + defaults = json.load(f) + + if not defaults: + return + + for company in frappe.get_all("Company", pluck="name"): + doc = frappe.get_doc("Company", company) + updated = False + for fieldname, account_number in defaults.items(): + full = frappe.db.get_value( + "Account", + {"account_number": account_number, "company": company}, + "name", + ) + if full and doc.get(fieldname) != full: + doc.set(fieldname, full) + updated = True + if updated: + doc.save(ignore_permissions=True) + + frappe.db.commit() + + def setup_wizard_complete_handler(args): """ Вызывается после завершения ERPNext setup wizard.