Production-grade tool to synchronise SAP SuccessFactors Organisational Management (OM) foundation objects from a PRD tenant to a Dev (lower) tenant via OData v2 APIs.
Available as a CLI, Python library, and Flask web UI.
| Level | Canonical Name | OData Entity Set |
|---|---|---|
| 5 | Sub Department | cust_SubDepartment |
| 4 | Department | FODepartment |
| 3 | Division | FODivision |
| 2 | Business Unit | FOBusinessUnit |
| 1 | Legal Entity | FOCompany |
Input rows specify Sub Department or Department only. The tool automatically resolves and syncs every ancestor up to Legal Entity.
# 1. Clone and enter the directory
cd SAPSF_ObjectSync
# 2. Install core dependencies
pip install -r requirements.txt
# 3. Configure credentials
cp .env.example .env
# Edit .env and fill in SF_SOURCE_URL, SF_SOURCE_USER, SF_SOURCE_PASSWORD,
# SF_TARGET_URL, SF_TARGET_USER, SF_TARGET_PASSWORD
# 4. Download the sample input template
# (or run: python sample_data/generate_template.py)
# Open sample_data/foundation_objects_template.xlsx and fill in your codes.
# 5a. Run via CLI (dry run - safe preview, no writes)
python -m src.sync_engine --file sample_data/foundation_objects_template.xlsx --dry-run
# 5b. OR launch the web UI
pip install -r requirements-web.txt
python web_ui/app.py
# Then open http://127.0.0.1:5000
# 6. Check output/sync_*.log for the run log
# Check output/sync_report_*.xlsx for the full Excel reportSet AUTH_METHOD in .env (default: basic):
AUTH_METHOD=basic
SF_SOURCE_USER=api_user@company.com
SF_SOURCE_PASSWORD=your_password
SF_TARGET_USER=api_user@company.com
SF_TARGET_PASSWORD=your_passwordAUTH_METHOD=oauth
SF_SOURCE_CLIENT_ID=your_client_id
SF_SOURCE_CLIENT_SECRET=your_client_secret
SF_SOURCE_TOKEN_URL=https://<prd-host>/oauth/token
SF_TARGET_CLIENT_ID=your_client_id
SF_TARGET_CLIENT_SECRET=your_client_secret
SF_TARGET_TOKEN_URL=https://<dev-host>/oauth/tokenAUTH_METHOD=certificate
SF_SOURCE_CERT_PATH=credentials/source_cert.pem
SF_SOURCE_KEY_PATH=credentials/source_key.pem
SF_TARGET_CERT_PATH=credentials/target_cert.pem
SF_TARGET_KEY_PATH=credentials/target_key.pemcp .env.example .env| Variable | Required | Description |
|---|---|---|
SF_SOURCE_URL |
Yes | PRD OData v2 base URL (https://<host>/odata/v2) |
SF_SOURCE_USER |
Basic only | PRD API username |
SF_SOURCE_PASSWORD |
Basic only | PRD API password |
SF_TARGET_URL |
Yes | Dev OData v2 base URL |
SF_TARGET_USER |
Basic only | Dev API username |
SF_TARGET_PASSWORD |
Basic only | Dev API password |
AUTH_METHOD |
No | basic (default) | oauth | certificate |
DRY_RUN |
No | true (default) | false |
LOG_LEVEL |
No | INFO (default) | DEBUG |
OUTPUT_DIR |
No | ./output (default) |
Security: Never commit
.envto version control. It is listed in.gitignore.
pip install -r requirements.txtpip install -r requirements.txt -r requirements-web.txt# Dry run (env-based credentials):
python -m src.sync_engine --file input.xlsx --dry-run
# Live upload (set DRY_RUN=false in .env or omit --dry-run flag with DRY_RUN=false):
python -m src.sync_engine --file input.xlsx
# Verbose (DEBUG logging):
python -m src.sync_engine --file input.xlsx --dry-run --verbose
# Custom output dir:
python -m src.sync_engine --file input.xlsx --output-dir /tmp/sf_sync_outputpip install -r requirements.txt -r requirements-web.txt
python web_ui/app.pyOpen http://127.0.0.1:5000 in your browser.
The UI provides:
- File upload (
.xlsx/.csv) - Auth method dropdown (Basic / OAuth / Certificate)
- Source + Target environment URL fields (falls back to
.envif left blank) - Dry-run checkbox
- Real-time progress bar
- Results page with colour-coded log and Excel report download
# Generate the template:
python sample_data/generate_template.py
# Download via web UI:
# http://127.0.0.1:5000/download_templateSee sample_data/README.md for column reference and rules.
from src.sync_engine import sync_objects
result = sync_objects(
source_config={
"base_url": "https://prd.example.com/odata/v2",
"username": "api_user@company.com",
"password": "secret",
},
target_config={
"base_url": "https://dev.example.com/odata/v2",
"username": "api_user@company.com",
"password": "secret",
},
input_file_path="input.xlsx",
dry_run=True,
output_dir="./output",
progress_callback=lambda phase, msg, pct: print(f"[{pct}%] {phase}: {msg}"),
)
print(result["success"]) # entities created (or would create in dry run)
print(result["skipped"]) # already existed in target
print(result["failure"]) # failed uploads
print(result["report_path"]) # path to Excel reportsf_object_sync/
├── .env.example # Credential template - copy to .env
├── .gitignore # Excludes .env, config.yaml, mock_responses/
├── README.md
├── requirements.txt # Core dependencies
├── requirements-web.txt # Additional deps for web UI
├── sample_data/
│ ├── foundation_objects_template.xlsx
│ ├── generate_template.py
│ └── README.md
├── src/
│ ├── __init__.py
│ ├── sync_engine.py # Programmatic API + CLI entry point
│ ├── auth_handler.py # Basic / OAuth / Certificate auth
│ ├── entity_config.py # ENTITY_CONFIG dict + constants
│ ├── sf_client.py # OData v2 HTTP client (retry, pagination)
│ ├── hierarchy_resolver.py # PRD parent chain traversal
│ ├── gap_checker.py # Dev existence checks
│ ├── payload_builder.py # POST payload construction per entity
│ ├── uploader.py # Live upload orchestrator
│ ├── audit_logger.py # JSONL event writer
│ └── report_generator.py # 4-sheet Excel report
├── web_ui/
│ ├── app.py # Flask application
│ ├── templates/
│ │ ├── index.html # Upload + config form
│ │ ├── status.html # Progress polling page
│ │ └── results.html # Sync results + log viewer
│ └── static/
│ └── style.css
├── output/ # Runtime artefacts (git-ignored)
└── tests/
├── mock_responses/ # Sample OData JSON per entity (git-ignored)
├── test_sync_engine.py # sync_engine unit tests
├── test_validator.py
├── test_hierarchy_resolver.py
├── test_gap_checker.py
└── test_payload_builder.py
Note:
config.yamlandtests/mock_responses/are git-ignored for security. See.gitignorefor full exclusion list.
All files are written to OUTPUT_DIR (default: ./output/).
| File | Description |
|---|---|
sync_<ts>.log |
Full run log (plain text) |
audit_<ts>.jsonl |
One JSON line per event - machine-readable audit trail |
dry_run_summary_<ts>.txt |
Human-readable dry-run payloads and integrity report |
sync_report_<ts>.xlsx |
4-sheet Excel workbook (Summary, Object Detail, Payload Log, Validation Errors) |
python -m pytest tests/ -vAll tests mock SFClient - no live API calls are made.
Note: Mock response JSON files in
tests/mock_responses/are excluded from version control. See test files for inline mock data or generate fresh mocks locally.
| Status | Phase | Meaning |
|---|---|---|
VALIDATION_FAILED |
Phase 1 | Row rejected by input validation |
PRD_NOT_FOUND |
Phase 2/3 | Entity or ancestor not found in PRD |
HIERARCHY_BROKEN |
Phase 3 | Parent field is null/empty in PRD |
DEV_EXISTS |
Phase 4 | Entity already exists in Dev |
DEV_MISSING |
Phase 4 | Entity not found in Dev - will be created |
DRY_RUN_OK |
Phase 5 | Payload built successfully in dry run |
DRY_RUN_INTEGRITY_FAIL |
Phase 5 | Payload build error in dry run |
UPLOAD_SUCCESS |
Phase 6 | POST returned HTTP 201 |
UPLOAD_FAILED |
Phase 6 | POST returned 4xx/5xx |
VERIFICATION_OK |
Phase 6 | Re-query confirmed entity exists |
VERIFICATION_FAILED |
Phase 6 | Re-query could not confirm entity |
SKIPPED_DUE_TO_PARENT_FAILURE |
Phase 6 | Parent entity failed; child skipped |
| Entity | Date Field | Status Field | Parent → Child Link Field |
|---|---|---|---|
cust_SubDepartment |
effectiveStartDate |
mdfSystemStatus |
cust_parentDepartment → FODepartment |
FODepartment |
startDate |
status |
cust_DivisionProp → FODivision (not parent) |
FODivision |
startDate |
status |
cust_BusinessUnitProp → FOBusinessUnit (not parent) |
FOBusinessUnit |
startDate |
status |
cust_legalEntityProp → FOCompany (not cust_parentBusinessUnit) |
FOCompany |
startDate |
status |
(top of hierarchy) |
The
parentfield inFODepartmentandFODivisionis a self-referencing tree - it is not the cross-entity link. Always usecust_DivisionProp/cust_BusinessUnitProp/cust_legalEntityProp.
The HTTP client retries automatically on HTTP 429, 500, 502, 503, 504 with exponential back-off: 1s → 2s → 4s (3 attempts total). Adjust REQUEST_TIMEOUT_SEC in .env if your tenant is slow.
- Never commit credentials -
.envandconfig.yamlare git-ignored - Rotate credentials immediately if accidentally committed to version control
- Use certificate auth for production environments where possible
- Restrict API user permissions to the minimum required (read on PRD, write on Dev)
- Review
.gitignorebefore first commit to ensure sensitive files are excluded
Apache 2.0 - see LICENSE for details.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature) - Commit your changes (
git commit -m 'Add feature') - Push to the branch (
git push origin feature/your-feature) - Open a Pull Request
Ensure all tests pass (pytest tests/ -v) before submitting.
For issues, questions, or feature requests, please open an issue on GitHub.
This project is part of a wider SAP SuccessFactors supplementary tools suite.
Start with SF Compass for the full hub: https://sahirvhora.github.io/sf-compass/
| Tool | Purpose |
|---|---|
| SF Compass | Feasibility answers, implementation guidance, and links to the full tool suite |
| SF Release Update | Release impact tracking and testing focus |
| SF Pay Transparency | EU Pay Transparency readiness and evidence workflow framing |
| SF Value Navigator | Value realisation and sponsor-facing consulting framework |
| SF Position Integrity Checker | Position hierarchy, incumbency, and EC data-quality validation |
| SAPSF ObjectSync | Controlled foundation-object synchronisation between SF environments |