From 6df400f31dab892e009cb801bb7aded7a70e9f17 Mon Sep 17 00:00:00 2001 From: Marsway Date: Wed, 15 Jul 2026 23:11:38 +0800 Subject: [PATCH] fix(sync): match EHR users by AD account regardless of status Co-authored-by: Cursor --- extensions/sync_ehr_to_ad/job.py | 21 ++------ tests/extensions/sync_ehr_to_ad/test_job.py | 59 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 tests/extensions/sync_ehr_to_ad/test_job.py diff --git a/extensions/sync_ehr_to_ad/job.py b/extensions/sync_ehr_to_ad/job.py index f63e4d8..1ca93ab 100644 --- a/extensions/sync_ehr_to_ad/job.py +++ b/extensions/sync_ehr_to_ad/job.py @@ -418,16 +418,11 @@ def _root_org_name(org: dict[str, Any], org_by_oid: dict[str, dict[str, Any]]) - return last_name -def _is_current_employee(item: dict[str, Any], current_status_values: set[str]) -> bool: +def _is_syncable_employee_record(item: dict[str, Any]) -> bool: rec = item.get("recordInfo") or {} emp = item.get("employeeInfo") or {} if not isinstance(rec, dict) or not isinstance(emp, dict): return False - if str(rec.get("lastWorkDate") or "").strip(): - return False - status = _field_translate_or_value(item, "EmployeeStatus") - if current_status_values and status and status not in current_status_values: - return False for key in ("isDeleted", "IsDeleted", "deleted", "disabled", "Disabled"): raw = emp.get(key, rec.get(key)) if _to_bool_or_none(raw) is True: @@ -500,14 +495,6 @@ class SyncEhrToAdUserJob(BaseJob): default_company = str(params.get("default_company") or "").strip() target_sam_accounts = _parse_target_sam_accounts(params) - current_status_values_param = params.get("current_status_values") - if isinstance(current_status_values_param, list): - current_status_values = {str(x).strip() for x in current_status_values_param if str(x).strip()} - elif str(current_status_values_param or "").strip(): - current_status_values = {x.strip() for x in str(current_status_values_param).split(",") if x.strip()} - else: - current_status_values = set() - location_mappings = params.get("location_mappings") location_mappings = location_mappings if isinstance(location_mappings, dict) else None @@ -554,7 +541,7 @@ class SyncEhrToAdUserJob(BaseJob): users_by_sam: dict[str, dict[str, Any]] = {} user_id_to_sam: dict[int, str] = {} for item in emp_rows: - if not isinstance(item, dict) or not _is_current_employee(item, current_status_values): + if not isinstance(item, dict) or not _is_syncable_employee_record(item): continue sam = _field_value(item, domain_account_key) if not sam: @@ -571,7 +558,7 @@ class SyncEhrToAdUserJob(BaseJob): break logger.info( - "EHR 当前用户准备完成:employee_rows=%s current_with_ad_account=%s org_rows=%s target_sam_accounts=%s", + "EHR AD 用户准备完成:employee_rows=%s users_with_ad_account=%s org_rows=%s target_sam_accounts=%s", len(emp_rows), len(users_by_sam), len(org_rows), @@ -760,7 +747,7 @@ class SyncEhrToAdUserJob(BaseJob): "filtered_by_target_sam": bool(target_sam_accounts), "target_sam_accounts": len(target_sam_accounts), "ehr_employee_rows": len(emp_rows), - "ehr_current_users_with_ad_account": len(users_by_sam), + "ehr_users_with_ad_account": len(users_by_sam), "processed": processed, "updated": updated, "skipped_unchanged": skipped_unchanged, diff --git a/tests/extensions/sync_ehr_to_ad/test_job.py b/tests/extensions/sync_ehr_to_ad/test_job.py new file mode 100644 index 0000000..7c7da50 --- /dev/null +++ b/tests/extensions/sync_ehr_to_ad/test_job.py @@ -0,0 +1,59 @@ +import sys +import types +import importlib.util +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(ROOT)) + +base_module = types.ModuleType("app.jobs.base") + + +class BaseJob: + pass + + +base_module.BaseJob = BaseJob +sys.modules["app.jobs.base"] = base_module + +api_module = types.ModuleType("extensions.sync_ehr_to_ad.api") +api_module.ActiveDirectoryClient = object +api_module.SyncEhrToAdApi = object +sys.modules["extensions.sync_ehr_to_ad.api"] = api_module + +spec = importlib.util.spec_from_file_location("sync_ehr_to_ad_job", ROOT / "extensions/sync_ehr_to_ad/job.py") +job_module = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(job_module) + +_is_syncable_employee_record = job_module._is_syncable_employee_record + + +def _ehr_item(*, employee_status="离职", last_work_date="", emp_flags=None, rec_flags=None): + emp_flags = emp_flags or {} + rec_flags = rec_flags or {} + return { + "employeeInfo": { + "translateProperties": {"EmployeeStatus": employee_status}, + "customProperties": {"extADAccountName_606508_511687157": "zhangsan"}, + **emp_flags, + }, + "recordInfo": { + "lastWorkDate": last_work_date, + **rec_flags, + }, + } + + +def test_syncable_employee_record_includes_any_employee_status(): + assert _is_syncable_employee_record(_ehr_item(employee_status="离职")) is True + + +def test_syncable_employee_record_includes_records_with_last_work_date(): + assert _is_syncable_employee_record(_ehr_item(last_work_date="2026-07-01")) is True + + +def test_syncable_employee_record_excludes_deleted_or_disabled_records(): + assert _is_syncable_employee_record(_ehr_item(emp_flags={"isDeleted": True})) is False + assert _is_syncable_employee_record(_ehr_item(rec_flags={"disabled": True})) is False