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) <noreply@anthropic.com>
This commit is contained in:
parent
eef0c82032
commit
91268cc466
|
|
@ -112,11 +112,25 @@ 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
|
# 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:
|
try:
|
||||||
sync_salary_components()
|
sync_salary_components()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Salary Component sync failed: {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
|
# 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()
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,66 @@ def create_default_leave_types(args):
|
||||||
frappe.db.commit()
|
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):
|
def create_default_company_accounts(args):
|
||||||
if not os.path.exists(COMPANY_DEFAULTS_JSON):
|
if not os.path.exists(COMPANY_DEFAULTS_JSON):
|
||||||
return
|
return
|
||||||
|
|
@ -209,6 +269,40 @@ def create_default_company_accounts(args):
|
||||||
frappe.db.commit()
|
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):
|
def setup_wizard_complete_handler(args):
|
||||||
"""
|
"""
|
||||||
Вызывается после завершения ERPNext setup wizard.
|
Вызывается после завершения ERPNext setup wizard.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue