invoice_az/invoice_az/emas_api.py

1865 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
ƏMAS (e-social.gov.az) Integration API
This module provides functions to interact with ƏMAS (Employment Management System)
of Azerbaijan's Ministry of Labour and Social Protection.
ƏMAS uses MyGovID/ASAN authentication. The flow is:
1. Get MyGovID JWT token via ASAN Sign (same as existing MyGovID Login)
2. Use JWT token to get authorization code for ƏMAS via MyGovID OAuth API
3. Exchange authorization code for ƏMAS session via SSO callback
4. Use session cookies for ƏMAS API calls
"""
import http.client
import json
import random
import string
from urllib.parse import unquote
import frappe
import requests
# Increase header limit - EMAS returns many Set-Cookie headers
http.client._MAXHEADERS = 1000
# EMAS Base URL
EMAS_BASE_URL = "https://eroom.e-social.gov.az"
# EMAS API Endpoints
EMAS_SSO_CALLBACK = "/ssoAsan/v2"
EMAS_CHANGE_ACCOUNT = "/service/aas.changeAccount"
EMAS_EXECUTE_REPORT = "/service/magusbi.executeReport"
EMAS_GET_DOMAINS = "/service/aas.domains"
EMAS_GET_ACCOUNTS = "/request/accounts"
# MyGovID OAuth API
MYGOVID_API_URL = "https://api.mygovid.gov.az"
MYGOVID_AUTH_CODES_URL = f"{MYGOVID_API_URL}/ssoauth/oauth2/auth/codes"
# EMAS OAuth Client ID (from HAR analysis)
EMAS_CLIENT_ID = "64942e33ec8d49059333a1e0ebad7fe2"
EMAS_REDIRECT_URI = f"{EMAS_BASE_URL}{EMAS_SSO_CALLBACK}"
# EMAS request headers
EMAS_HEADERS = {
"Accept": "text/plain, */*; q=0.01",
"Accept-Language": "en-US,en;q=0.9",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": EMAS_BASE_URL,
"Referer": f"{EMAS_BASE_URL}/",
"X-MAGUS-RECAPTCHA": "null",
}
MYGOVID_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "az",
"Content-Type": "application/json",
"Origin": "https://mygovid.gov.az",
"Referer": "https://mygovid.gov.az/",
}
def generate_request_number():
"""Generate a unique request number for EMAS API calls"""
# Format: 1XXXXXXXXXXXXXXX (alphanumeric)
chars = string.ascii_uppercase + string.digits
return "1" + "".join(random.choices(chars, k=13))
def generate_state():
"""Generate a unique state for SSO flow"""
# Format: XXXXXXXXXXXXX:eroom:emas
chars = string.hexdigits.lower()[:16]
return "".join(random.choices(chars, k=13)) + ":eroom:emas"
def parse_cookies_from_response(response):
"""Extract cookies from response and return as dict"""
cookies = {}
for cookie in response.cookies:
cookies[cookie.name] = cookie.value
return cookies
@frappe.whitelist()
def connect_emas(asan_login_name):
"""
Connect to EMAS using MyGovID JWT token.
This is the main entry point for EMAS connection.
If MyGovID token exists - uses it to get EMAS session automatically.
If not - returns error asking user to login with MyGovID first.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
# Check if we have MyGovID token
if not doc.mygovid_token:
return {
"success": False,
"need_mygovid_login": True,
"message": "MyGovID token not found. Please login with MyGovID first."
}
# Step 1: Get authorization code from MyGovID
auth_code_result = get_emas_auth_code(doc.mygovid_token)
if not auth_code_result.get("success"):
# Token might be expired
if "401" in str(auth_code_result.get("message", "")):
return {
"success": False,
"need_mygovid_login": True,
"message": "MyGovID token expired. Please login with MyGovID again."
}
return auth_code_result
auth_code = auth_code_result.get("code")
state = auth_code_result.get("state")
# Step 2: Exchange authorization code for EMAS session
session_result = exchange_code_for_session(auth_code, state)
if not session_result.get("success"):
return session_result
# Step 3: Save session data (but don't save account yet - user must select)
session_data = session_result.get("session_data")
doc.emas_session = json.dumps(session_data)
doc.emas_csrf_token = session_data.get("xsrf_token")
doc.emas_auth_status = "Connected"
doc.emas_last_activity = frappe.utils.now_datetime()
# Clear any previous account selection - user must choose
doc.emas_account_oid = None
doc.emas_account_name = None
doc.emas_account_number = None
doc.save(ignore_permissions=True)
frappe.db.commit()
# Step 4: Fetch available accounts from /request/accounts
accounts_result = get_emas_accounts(asan_login_name)
accounts = accounts_result.get("accounts", []) if accounts_result.get("success") else []
return {
"success": True,
"message": "ƏMAS connected! Please select an organization.",
"accounts": accounts,
"has_accounts": len(accounts) > 0,
"need_account_selection": True
}
except Exception as e:
frappe.log_error(
f"EMAS connect error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
def get_emas_auth_code(mygovid_token):
"""
Get EMAS authorization code using MyGovID JWT token.
This calls the MyGovID OAuth API to get an authorization code
that can be exchanged for EMAS session.
"""
try:
state = generate_state()
payload = {
"client_id": EMAS_CLIENT_ID,
"state": state,
"response_type": "code",
"redirect_uri": EMAS_REDIRECT_URI
}
headers = MYGOVID_HEADERS.copy()
headers["Authorization"] = mygovid_token
response = requests.post(
MYGOVID_AUTH_CODES_URL,
json=payload,
headers=headers,
timeout=30
)
if response.status_code == 201:
data = response.json()
return {
"success": True,
"code": data.get("code"),
"state": data.get("state", state)
}
elif response.status_code == 401:
return {
"success": False,
"message": "MyGovID token expired (401)"
}
else:
return {
"success": False,
"message": f"Failed to get auth code: status {response.status_code}"
}
except requests.exceptions.RequestException as e:
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
return {"success": False, "message": f"Error: {str(e)}"}
def parse_config_from_html(html_text):
"""
Parse the 'var config = {...}' JavaScript object from EMAS HTML page.
Returns a dict with account info.
"""
import re
config = {}
# Extract the var config = {...} block
# Pattern matches: var config = { ... };
config_match = re.search(r'var\s+config\s*=\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}', html_text)
if not config_match:
return config
config_text = config_match.group(1)
# Parse individual fields using pattern: 'key' : 'value' or 'key' : value
# Fields we want to extract
fields = [
'token', 'accountOid', 'accountName', 'accountNumber',
'firstName', 'lastName', 'middleName', 'userId',
'userLevel', 'userBranchId', 'userBranchName', 'userPosId',
'customerOid', 'domain', 'loginType', 'entityType'
]
for field in fields:
# Try 'field' : 'value' pattern (string value)
pattern = rf"'{field}'\s*:\s*'([^']*)'"
match = re.search(pattern, config_text)
if match:
config[field] = match.group(1)
continue
# Try 'field' : value pattern (non-string value)
pattern = rf"'{field}'\s*:\s*([^,\n]+)"
match = re.search(pattern, config_text)
if match:
value = match.group(1).strip().strip("'\"")
config[field] = value
return config
def exchange_code_for_session(code, state):
"""
Exchange authorization code for EMAS session.
Makes request to EMAS SSO callback endpoint and captures session cookies.
Also parses the account info from the HTML response config.
"""
try:
# Build SSO callback URL
callback_url = f"{EMAS_REDIRECT_URI}?code={code}&state={state}"
# Create session to capture cookies
session = requests.Session()
response = session.get(
callback_url,
allow_redirects=True,
timeout=30
)
# Extract cookies
cookies = {}
for cookie in session.cookies:
cookies[cookie.name] = cookie.value
# Look for required cookies
session_cookie = cookies.get("emek_ve_mesgulluq_alt_sistemi_session")
xsrf_token_raw = cookies.get("XSRF-TOKEN")
# Also check for _s cookie (Laravel session with data)
s_cookie = cookies.get("emek_ve_mesgulluq_alt_sistemi_s")
if not session_cookie:
return {
"success": False,
"message": "Failed to get ƏMAS session cookie"
}
# IMPORTANT: Laravel's XSRF-TOKEN cookie is URL-encoded, must decode it!
# The cookie value contains %3D instead of = etc.
xsrf_token = unquote(xsrf_token_raw) if xsrf_token_raw else None
# Try to extract CSRF token from HTML meta tag (preferred - already decoded)
import re
meta_csrf_token = None
match = re.search(r'name="csrf-token" content="([^"]+)"', response.text)
if match:
meta_csrf_token = match.group(1)
# Prefer meta tag token over cookie (meta tag is already decoded)
csrf_token = meta_csrf_token or xsrf_token
# Parse token and account info from HTML config
html_config = parse_config_from_html(response.text)
token = html_config.get("token")
# Extract account info from the config
account_info = None
if html_config.get("accountOid"):
account_info = {
"accountOid": html_config.get("accountOid"),
"accountName": html_config.get("accountName", ""),
"accountNumber": html_config.get("accountNumber", ""),
"userId": html_config.get("userId", ""),
"userLevel": html_config.get("userLevel", "owner"),
"branchId": html_config.get("userBranchId", "0000"),
"positionId": html_config.get("userPosId", "0000"),
}
session_data = {
"session_cookie": session_cookie,
"xsrf_token": csrf_token,
"s_cookie": s_cookie,
"token": token,
"all_cookies": cookies,
"account_info": account_info,
"html_config": html_config
}
return {
"success": True,
"session_data": session_data
}
except requests.exceptions.RequestException as e:
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
return {"success": False, "message": f"Error: {str(e)}"}
def get_emas_session_data(asan_login_name):
"""Get EMAS session data from document"""
doc = frappe.get_doc("Asan Login", asan_login_name)
if not doc.emas_session:
return None
try:
return json.loads(doc.emas_session)
except (json.JSONDecodeError, TypeError):
return None
def refresh_csrf_token_internal(asan_login_name):
"""
Refresh CSRF token by making a GET request to EMAS dashboard.
EMAS requires fresh CSRF token for EVERY operation.
Returns updated session_data and csrf_token.
"""
import re
session_data = get_emas_session_data(asan_login_name)
if not session_data:
return None, None
doc = frappe.get_doc("Asan Login", asan_login_name)
cookies = session_data.get("all_cookies", {})
try:
# Make GET request to dashboard to get fresh CSRF token
response = requests.get(
f"{EMAS_BASE_URL}/core.dashboard",
cookies=cookies,
timeout=30
)
if response.status_code != 200:
return session_data, doc.emas_csrf_token
# Update cookies from response
for cookie in response.cookies:
cookies[cookie.name] = cookie.value
session_data["all_cookies"] = cookies
# Extract CSRF token from HTML meta tag (preferred)
csrf_token = None
match = re.search(r'name="csrf-token" content="([^"]+)"', response.text)
if match:
csrf_token = match.group(1)
# Also try from cookie
if not csrf_token and "XSRF-TOKEN" in cookies:
csrf_token = unquote(cookies["XSRF-TOKEN"])
if csrf_token:
doc.emas_csrf_token = csrf_token
session_data["xsrf_token"] = csrf_token
# Update token from page config if available
html_config = parse_config_from_html(response.text)
if html_config.get("token"):
session_data["token"] = html_config.get("token")
# Save updated session
doc.emas_session = json.dumps(session_data)
doc.emas_last_activity = frappe.utils.now_datetime()
doc.save(ignore_permissions=True)
frappe.db.commit()
return session_data, csrf_token
except Exception as e:
frappe.log_error(f"CSRF refresh error: {str(e)}", "ƏMAS API Error")
return session_data, doc.emas_csrf_token
def make_emas_request(asan_login_name, endpoint, data):
"""
Make an authenticated request to EMAS API.
Handles session cookies, CSRF token, and request headers.
IMPORTANT: Refreshes CSRF token before EVERY request (EMAS requirement).
"""
# IMPORTANT: Refresh CSRF token before EVERY operation
# EMAS changes CSRF token after each request
session_data, csrf_token = refresh_csrf_token_internal(asan_login_name)
if not session_data:
return {
"success": False,
"message": "ƏMAS session not found. Please connect to ƏMAS first."
}
doc = frappe.get_doc("Asan Login", asan_login_name)
# Build request headers with fresh CSRF token
headers = EMAS_HEADERS.copy()
csrf_token = csrf_token or doc.emas_csrf_token or session_data.get("xsrf_token", "")
headers["X-CSRF-TOKEN"] = csrf_token
headers["MAGUS-REQUEST-NUMBER"] = generate_request_number()
# Build cookies (updated by refresh)
cookies = session_data.get("all_cookies", {})
# Add required data fields with fresh token
data["requestNumber"] = headers["MAGUS-REQUEST-NUMBER"]
data["_token"] = csrf_token
# Add currentTabToken if available
# NOTE: The token from config is already in format "{uuid}.{accountOid}"
# Do NOT append accountOid again!
token = session_data.get("token")
if token:
data["currentTabToken"] = token
try:
url = f"{EMAS_BASE_URL}{endpoint}"
response = requests.post(
url,
data=data,
headers=headers,
cookies=cookies,
timeout=60
)
# Update cookies from response
new_cookies = parse_cookies_from_response(response)
if new_cookies:
cookies.update(new_cookies)
session_data["all_cookies"] = cookies
# Update CSRF token if changed - IMPORTANT: decode URL-encoded token!
if "XSRF-TOKEN" in new_cookies:
decoded_token = unquote(new_cookies["XSRF-TOKEN"])
doc.emas_csrf_token = decoded_token
session_data["xsrf_token"] = decoded_token
doc.emas_session = json.dumps(session_data)
doc.emas_last_activity = frappe.utils.now_datetime()
doc.save(ignore_permissions=True)
frappe.db.commit()
if response.status_code == 200:
try:
result = response.json()
# Check for token/CSRF errors in response
response_info = result.get("response", {})
error_code = response_info.get("code", "")
error_msg = str(response_info.get("message", ""))
# Handle various token errors
if error_code in ("CSRF", "TOKENS_ARE_NOT_SAME", "CSRF_TOKEN_MISMATCH"):
frappe.log_error(
f"Token error: {error_code}. CSRF: {csrf_token[:20]}..., TabToken: {data.get('currentTabToken', 'N/A')[:30]}...",
"ƏMAS Token Error"
)
return {
"success": False,
"csrf_error": True,
"message": f"Token error ({error_code}). Please reconnect to ƏMAS."
}
if "CSRF" in error_msg or "TOKEN" in error_msg.upper():
frappe.log_error(
f"Token error in message: {error_msg}",
"ƏMAS Token Error"
)
return {
"success": False,
"csrf_error": True,
"message": f"Token error. Please reconnect to ƏMAS."
}
# Update token if present in response
if result.get("token"):
session_data["token"] = result.get("token")
doc.emas_session = json.dumps(session_data)
doc.save(ignore_permissions=True)
frappe.db.commit()
return {"success": True, "data": result}
except json.JSONDecodeError:
return {
"success": False,
"message": "Invalid response from ƏMAS"
}
elif response.status_code == 419:
# Laravel CSRF token mismatch status code
return {
"success": False,
"csrf_error": True,
"message": "CSRF token expired. Please reconnect to ƏMAS."
}
elif response.status_code == 401:
doc.emas_auth_status = "Error"
doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": False,
"session_expired": True,
"message": "ƏMAS session expired. Please reconnect."
}
else:
# Log unexpected errors for debugging
frappe.log_error(
f"ƏMAS API error: status {response.status_code}\nResponse: {response.text[:500]}",
"ƏMAS API Error"
)
return {
"success": False,
"message": f"ƏMAS API error: status {response.status_code}"
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"ƏMAS API network error: {str(e)}", "ƏMAS API Error")
return {"success": False, "message": f"Network error: {str(e)}"}
@frappe.whitelist()
def get_emas_accounts(asan_login_name):
"""
Get list of organizations/accounts user has access to in EMAS.
Fetches from /request/accounts endpoint.
"""
try:
# IMPORTANT: Refresh CSRF token before EVERY operation
session_data, csrf_token = refresh_csrf_token_internal(asan_login_name)
if not session_data:
return {"success": False, "accounts": [], "message": "No ƏMAS session"}
doc = frappe.get_doc("Asan Login", asan_login_name)
# Build request headers with fresh CSRF token
headers = EMAS_HEADERS.copy()
csrf_token = csrf_token or doc.emas_csrf_token or session_data.get("xsrf_token", "")
headers["X-CSRF-TOKEN"] = csrf_token
headers["MAGUS-REQUEST-NUMBER"] = generate_request_number()
# Build cookies (updated by refresh)
cookies = session_data.get("all_cookies", {})
# Build request data with fresh token
data = {
"requestNumber": headers["MAGUS-REQUEST-NUMBER"],
"_token": csrf_token,
}
# Add currentTabToken
token = session_data.get("token")
if token:
data["currentTabToken"] = token
# Make request to /request/accounts
response = requests.post(
f"{EMAS_BASE_URL}{EMAS_GET_ACCOUNTS}",
data=data,
headers=headers,
cookies=cookies,
timeout=30
)
# Update cookies from response
new_cookies = parse_cookies_from_response(response)
if new_cookies:
cookies.update(new_cookies)
session_data["all_cookies"] = cookies
# Update CSRF token if changed
if "XSRF-TOKEN" in new_cookies:
decoded_token = unquote(new_cookies["XSRF-TOKEN"])
doc.emas_csrf_token = decoded_token
session_data["xsrf_token"] = decoded_token
doc.emas_session = json.dumps(session_data)
doc.save(ignore_permissions=True)
frappe.db.commit()
if response.status_code == 200:
try:
accounts = response.json()
# The response is a direct array of accounts
if isinstance(accounts, list):
return {"success": True, "accounts": accounts}
else:
return {"success": False, "accounts": [], "message": "Unexpected response format"}
except json.JSONDecodeError:
return {"success": False, "accounts": [], "message": "Invalid JSON response"}
else:
return {"success": False, "accounts": [], "message": f"HTTP {response.status_code}"}
except Exception as e:
frappe.log_error(
f"EMAS get accounts error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}", "accounts": []}
@frappe.whitelist()
def change_emas_account(asan_login_name, account_oid, account_name, account_number, role_name="chairman"):
"""
Select/switch to a specific organization account in EMAS.
"""
try:
data = {
"accountOid": account_oid,
"accountName": account_name,
"accountNumber": account_number,
"roleName": role_name,
"branchId": "0000",
"positionId": "0000",
"domain": "emas"
}
result = make_emas_request(
asan_login_name,
EMAS_CHANGE_ACCOUNT,
data
)
if not result.get("success"):
return result
response_data = result.get("data", {})
# Check response code
response_info = response_data.get("response", {})
if response_info.get("code") != "0":
return {
"success": False,
"message": response_info.get("message", "Failed to change account")
}
# Store selected account info
doc = frappe.get_doc("Asan Login", asan_login_name)
doc.emas_account_oid = account_oid
doc.emas_account_name = account_name
doc.emas_account_number = account_number
doc.emas_auth_status = "Connected"
doc.emas_last_activity = frappe.utils.now_datetime()
# Update session with token from response
token = response_data.get("token")
if token:
session_data = get_emas_session_data(asan_login_name)
if session_data:
session_data["token"] = token
doc.emas_session = json.dumps(session_data)
doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"message": f"Switched to account: {account_name}"
}
except Exception as e:
frappe.log_error(
f"EMAS change account error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def get_employees_report(asan_login_name, offset=0, limit=100):
"""
Get list of employees from EMAS.
Uses the emasValidEmploymentContractOnline report.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
if not doc.emas_account_oid:
return {
"success": False,
"message": "No ƏMAS account selected. Please select an organization first."
}
data = {
"reportId": "emasValidEmploymentContractOnline",
"pagination[offset]": str(offset),
"pagination[limit]": str(limit),
"attributes[newExcel]": "true"
}
result = make_emas_request(asan_login_name, EMAS_EXECUTE_REPORT, data)
if not result.get("success"):
return result
response_data = result.get("data", {})
# Check response code
response_info = response_data.get("response", {})
if response_info.get("code") != "0":
return {
"success": False,
"message": response_info.get("message", "Failed to get employees report")
}
# Extract employees from result
employees = response_data.get("result", [])
has_more = response_data.get("hasMore", False)
return {
"success": True,
"employees": employees,
"has_more": has_more,
"offset": offset,
"limit": limit
}
except Exception as e:
frappe.log_error(
f"EMAS employees report error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def get_contract_stats(asan_login_name):
"""
Get employment contract statistics from EMAS.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
if not doc.emas_account_oid:
return {
"success": False,
"message": "No ƏMAS account selected. Please select an organization first."
}
data = {
"reportId": "graphContractStatsEmployer",
"pagination[offset]": "0",
"pagination[limit]": "100"
}
result = make_emas_request(asan_login_name, EMAS_EXECUTE_REPORT, data)
if not result.get("success"):
return result
response_data = result.get("data", {})
response_info = response_data.get("response", {})
if response_info.get("code") != "0":
return {
"success": False,
"message": response_info.get("message", "Failed to get contract stats")
}
stats = response_data.get("result", [])
return {
"success": True,
"stats": stats[0] if stats else {}
}
except Exception as e:
frappe.log_error(
f"EMAS contract stats error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def check_emas_connection(asan_login_name):
"""
Check if EMAS connection is active.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
return {
"success": True,
"emas_auth_status": doc.emas_auth_status or "Not Connected",
"emas_account_name": doc.emas_account_name,
"emas_account_number": doc.emas_account_number,
"emas_last_activity": str(doc.emas_last_activity) if doc.emas_last_activity else None,
"has_session": bool(doc.emas_session),
"has_mygovid_token": bool(doc.mygovid_token)
}
except Exception as e:
frappe.log_error(
f"EMAS check connection error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def refresh_emas_session(asan_login_name):
"""
Refresh EMAS session by making a GET request to get fresh CSRF token.
Use this when you get CSRF_TOKEN_MISMATCH errors.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
session_data = get_emas_session_data(asan_login_name)
if not session_data:
return {
"success": False,
"message": "No ƏMAS session found. Please connect first."
}
cookies = session_data.get("all_cookies", {})
# Make GET request to EMAS dashboard to get fresh CSRF token
response = requests.get(
f"{EMAS_BASE_URL}/core.dashboard",
cookies=cookies,
timeout=30
)
if response.status_code != 200:
return {
"success": False,
"message": f"Failed to refresh session: status {response.status_code}"
}
# Update cookies
for cookie in response.cookies:
cookies[cookie.name] = cookie.value
session_data["all_cookies"] = cookies
# Extract new CSRF token from HTML
import re
match = re.search(r'name="csrf-token" content="([^"]+)"', response.text)
if match:
new_csrf_token = match.group(1)
doc.emas_csrf_token = new_csrf_token
session_data["xsrf_token"] = new_csrf_token
# Also update from cookie if available
if "XSRF-TOKEN" in cookies:
decoded_token = unquote(cookies["XSRF-TOKEN"])
# Prefer meta tag token, but update session data
if not match:
doc.emas_csrf_token = decoded_token
session_data["xsrf_token"] = decoded_token
# Parse config for updated token
html_config = parse_config_from_html(response.text)
if html_config.get("token"):
session_data["token"] = html_config.get("token")
doc.emas_session = json.dumps(session_data)
doc.emas_last_activity = frappe.utils.now_datetime()
doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"message": "ƏMAS session refreshed successfully"
}
except Exception as e:
frappe.log_error(
f"EMAS refresh session error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def disconnect_emas(asan_login_name):
"""
Disconnect from EMAS by clearing session data.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
doc.emas_session = None
doc.emas_csrf_token = None
doc.emas_account_oid = None
doc.emas_account_name = None
doc.emas_account_number = None
doc.emas_auth_status = "Not Connected"
doc.emas_last_activity = None
doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"message": "Disconnected from ƏMAS"
}
except Exception as e:
frappe.log_error(
f"EMAS disconnect error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def get_connected_asan_logins():
"""
Get list of Asan Logins with active ƏMAS connection.
Used for employee import dialog.
"""
try:
asan_logins = frappe.get_all(
"Asan Login",
filters={
"emas_auth_status": "Connected",
"emas_account_oid": ["is", "set"]
},
fields=["name", "emas_account_name", "emas_account_number", "emas_account_oid"]
)
result = []
for al in asan_logins:
result.append({
"name": al.name,
"organization_name": al.emas_account_name or "Unknown",
"organization_voen": al.emas_account_number or ""
})
return {
"success": True,
"asan_logins": result
}
except Exception as e:
frappe.log_error(
f"Get connected Asan Logins error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}", "asan_logins": []}
# Exact list of API fields that map 1:1 to Əmas Employees DocType fields
EMAS_EMPLOYEE_FIELDS = [
"full_name",
"identification_number",
"ssn",
"birthday",
"work_position_text",
"monthly_salary",
"doc_no",
"doc_date",
"doc_type",
"contract_type",
"contract_status",
"begin_date",
"end_date",
"main_contract_begin_date",
"emp_job_start_date",
"sign_type",
"is_valid",
"dur_state",
"operation",
"after_exec_label",
"workplace_type",
"form_size",
"flow",
"flow_alias",
"tree_path_name",
"view_type",
"doc_oid",
]
@frappe.whitelist()
def import_employees(asan_login_name, employees):
"""
Import employees from ƏMAS to Əmas Employees doctype.
NOW FETCHES COMPLETE DETAIL DATA for each employee.
Args:
asan_login_name: Name of the Asan Login document
employees: List of employee data from ƏMAS API (list report)
"""
try:
if isinstance(employees, str):
employees = json.loads(employees)
doc = frappe.get_doc("Asan Login", asan_login_name)
organization_name = doc.emas_account_name
organization_voen = doc.emas_account_number
imported_count = 0
updated_count = 0
error_count = 0
errors = []
for emp_data in employees:
try:
doc_oid = emp_data.get('doc_oid')
doc_no = emp_data.get('doc_no')
doc_type = emp_data.get('doc_type', 'docType_47')
if not doc_oid:
# Can't fetch details without doc_oid, skip
name_val = emp_data.get('full_name', '?')
errors.append(f"Employee {name_val}: No doc_oid")
error_count += 1
continue
# FETCH COMPLETE DETAIL DATA
detail_result = get_employee_detail(asan_login_name, doc_oid, doc_no, doc_type)
if not detail_result.get('success'):
# Detail fetch failed, use list data as fallback
name_val = emp_data.get('full_name', '?')
frappe.log_error(
f"Failed to fetch detail for {doc_oid}: {detail_result.get('message')}",
"EMAS Employee Detail Fetch Error"
)
errors.append(f"{name_val}: Detail fetch failed, using list data")
# Fallback to list data
fin = emp_data.get("identification_number")
if not fin:
error_count += 1
continue
fin = str(fin).strip()
existing = frappe.db.exists(
"Emas Employees",
{"identification_number": fin, "organization_voen": organization_voen}
)
if existing:
employee_doc = frappe.get_doc("Emas Employees", existing)
update_employee_from_data(employee_doc, emp_data)
employee_doc.save(ignore_permissions=True)
updated_count += 1
else:
employee_doc = frappe.new_doc("Emas Employees")
employee_doc.asan_login = asan_login_name
employee_doc.organization_name = organization_name
employee_doc.organization_voen = organization_voen
update_employee_from_data(employee_doc, emp_data)
employee_doc.insert(ignore_permissions=True)
imported_count += 1
continue
detail_data = detail_result.get('data')
# Get FIN from detail data (more reliable than list)
fin = None
if detail_data and 'EmasContractsEnt' in detail_data and isinstance(detail_data.get('EmasContractsEnt'), list) and len(detail_data['EmasContractsEnt']) > 0:
fin = detail_data['EmasContractsEnt'][0].get('pin')
if not fin:
name_val = emp_data.get('full_name', '?')
errors.append(f"{name_val}: No FIN in detail data")
error_count += 1
continue
fin = str(fin).strip()
# Check if exists
existing = frappe.db.exists(
"Emas Employees",
{
"identification_number": fin,
"organization_voen": organization_voen
}
)
if existing:
employee_doc = frappe.get_doc("Emas Employees", existing)
# First populate basic fields from list data
update_employee_from_data(employee_doc, emp_data)
# Then overlay with detailed data
update_employee_from_detail_data(employee_doc, detail_data)
# Ensure FIN is set (fallback to verified FIN)
if not employee_doc.identification_number:
employee_doc.identification_number = fin
employee_doc.save(ignore_permissions=True)
updated_count += 1
else:
employee_doc = frappe.new_doc("Emas Employees")
employee_doc.asan_login = asan_login_name
employee_doc.organization_name = organization_name
employee_doc.organization_voen = organization_voen
# Set FIN first (required field)
employee_doc.identification_number = fin
# First populate basic fields from list data
update_employee_from_data(employee_doc, emp_data)
# Then overlay with detailed data
update_employee_from_detail_data(employee_doc, detail_data)
# Ensure FIN is still set after mapping
if not employee_doc.identification_number:
employee_doc.identification_number = fin
employee_doc.insert(ignore_permissions=True)
imported_count += 1
except Exception as e:
name_val = emp_data.get('full_name', '?')
error_msg = f"{name_val}: {str(e)}"
frappe.log_error(
f"{error_msg}\n{frappe.get_traceback()}",
"EMAS Employee Import Error"
)
errors.append(error_msg)
error_count += 1
frappe.db.commit()
return {
"success": True,
"imported_count": imported_count,
"updated_count": updated_count,
"error_count": error_count,
"errors": errors[:10],
"total": len(employees)
}
except Exception as e:
frappe.log_error(
f"Import employees error: {str(e)}\n{frappe.get_traceback()}",
"EMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
def update_employee_from_data(employee_doc, emp_data):
"""
Update employee document fields from ƏMAS API data.
Hard 1:1 mapping - API field names match DocType field names exactly.
"""
# Map all API fields directly to DocType fields
for field in EMAS_EMPLOYEE_FIELDS:
value = emp_data.get(field)
if value is not None:
setattr(employee_doc, field, str(value))
else:
setattr(employee_doc, field, None)
# Store raw data for reference
employee_doc.raw_data = json.dumps(emp_data, ensure_ascii=False, indent=2)
# Import timestamp
employee_doc.import_date = frappe.utils.now_datetime()
def parse_emas_date(date_str):
"""
Parse date string from ƏMAS into YYYY-MM-DD format for Frappe Date fields.
ƏMAS dates can be in various formats.
"""
if not date_str or not str(date_str).strip():
return None
date_str = str(date_str).strip()
from datetime import datetime
for fmt in ("%d.%m.%Y", "%Y-%m-%d", "%d/%m/%Y", "%Y-%m-%dT%H:%M:%S"):
try:
return datetime.strptime(date_str, fmt).strftime("%Y-%m-%d")
except (ValueError, TypeError):
continue
return None
def resolve_designation(position_text, create_if_missing):
"""
Find or create a Designation from ƏMAS position text.
Returns designation name or None.
"""
if not position_text or not str(position_text).strip():
return None
position_text = str(position_text).strip()
# Search for existing designation by name
existing = frappe.db.exists("Designation", position_text)
if existing:
return existing
# Try case-insensitive search by designation_name field
result = frappe.db.get_value(
"Designation",
filters={"designation_name": ["like", position_text]},
fieldname="name"
)
if result:
return result
# Not found — create if allowed
if create_if_missing:
designation = frappe.new_doc("Designation")
designation.designation_name = position_text
designation.insert(ignore_permissions=True)
return designation.name
return None
@frappe.whitelist()
def create_employees_from_emas(asan_login_name, employees, company, create_designation=0):
"""
Create Employee records directly from ƏMAS API data.
Receives raw employee data from the ƏMAS API (same format as get_employees_report returns).
Also saves to Emas Employees doctype for record keeping.
Field mapping (ƏMAS → Employee):
full_name → first_name / last_name (split by space)
identification_number → passport_number
birthday → date_of_birth
begin_date → date_of_joining
end_date → contract_end_date
monthly_salary → ctc
contract_status → status
work_position_text → designation (Link, searched/created)
"""
try:
if isinstance(employees, str):
employees = json.loads(employees)
create_designation = int(create_designation)
if not company:
return {"success": False, "message": "Company is required"}
doc = frappe.get_doc("Asan Login", asan_login_name)
organization_name = doc.emas_account_name
organization_voen = doc.emas_account_number
created_count = 0
updated_count = 0
error_count = 0
errors = []
# Status mapping: ƏMAS contract_status → Employee status
status_map = {
"qüvvədədir": "Active",
"aktiv": "Active",
"active": "Active",
"ləğv edilib": "Left",
"xitam": "Left",
"terminated": "Left",
"dayandırılıb": "Suspended",
"suspended": "Suspended",
}
for emp_data in employees:
try:
doc_oid = emp_data.get("doc_oid")
doc_no = emp_data.get("doc_no")
doc_type = emp_data.get("doc_type", "docType_47")
# Try to fetch detail data
detail_data = None
full_name = str(emp_data.get("full_name") or "").strip()
if doc_oid:
detail_result = get_employee_detail(asan_login_name, doc_oid, doc_no, doc_type)
if detail_result.get('success'):
detail_data = detail_result.get('data')
# Get FIN from detail or list data
fin = None
if detail_data and 'EmasContractsEnt' in detail_data and isinstance(detail_data.get('EmasContractsEnt'), list) and len(detail_data['EmasContractsEnt']) > 0:
fin = detail_data['EmasContractsEnt'][0].get('pin')
# Also update full_name from detail data if available
if 'iamasBean' in detail_data and detail_data.get('iamasBean'):
person = detail_data['iamasBean']
name = person.get('name', '')
surname = person.get('surname', '')
full_name = f"{name} {surname}".strip()
if not fin:
fin = emp_data.get("identification_number")
if not fin:
errors.append(f"{full_name}: No identification_number")
error_count += 1
continue
fin = str(fin).strip()
# 1. Save/update Emas Employees record
emas_existing = None
if doc_oid:
emas_existing = frappe.db.exists("Emas Employees", {"doc_oid": str(doc_oid)})
if not emas_existing:
emas_existing = frappe.db.exists(
"Emas Employees",
{"identification_number": fin, "organization_voen": organization_voen}
)
if emas_existing:
emas_doc = frappe.get_doc("Emas Employees", emas_existing)
# Always populate basic fields from list data first
update_employee_from_data(emas_doc, emp_data)
# Then overlay with detail data if available
if detail_data:
update_employee_from_detail_data(emas_doc, detail_data)
# Ensure FIN is set (required field)
if not emas_doc.identification_number:
emas_doc.identification_number = fin
emas_doc.save(ignore_permissions=True)
else:
emas_doc = frappe.new_doc("Emas Employees")
emas_doc.asan_login = asan_login_name
emas_doc.organization_name = organization_name
emas_doc.organization_voen = organization_voen
# Set FIN first (required field)
emas_doc.identification_number = fin
# Always populate basic fields from list data first
update_employee_from_data(emas_doc, emp_data)
# Then overlay with detail data if available
if detail_data:
update_employee_from_detail_data(emas_doc, detail_data)
# Ensure FIN is still set after mapping
if not emas_doc.identification_number:
emas_doc.identification_number = fin
emas_doc.insert(ignore_permissions=True)
# 2. Create/update Employee record
existing_employee = frappe.db.exists("Employee", {"passport_number": fin})
# Split full_name into first/last
name_parts = full_name.split() if full_name else []
first_name = name_parts[0] if name_parts else "?"
last_name = " ".join(name_parts[1:]) if len(name_parts) > 1 else ""
# Parse dates from detail data if available, otherwise from list data
dob = None
doj = None
contract_end = None
if detail_data:
# Get from detail data
if 'iamasBean' in detail_data and detail_data.get('iamasBean'):
dob = parse_emas_date(detail_data['iamasBean'].get('birthDate'))
if 'EmasContractDetailsEnt' in detail_data and isinstance(detail_data.get('EmasContractDetailsEnt'), list) and len(detail_data['EmasContractDetailsEnt']) > 0:
details = detail_data['EmasContractDetailsEnt'][0]
doj = parse_emas_date(details.get('beginDate'))
contract_end = parse_emas_date(details.get('endDate'))
else:
# Fallback to list data
dob = parse_emas_date(emp_data.get("birthday"))
doj = parse_emas_date(emp_data.get("begin_date"))
contract_end = parse_emas_date(emp_data.get("end_date"))
# Map status
raw_status = ""
if detail_data and 'EmasContractsEnt' in detail_data and isinstance(detail_data.get('EmasContractsEnt'), list) and len(detail_data['EmasContractsEnt']) > 0:
raw_status = str(detail_data['EmasContractsEnt'][0].get('contractStatus') or "").strip().lower()
else:
raw_status = str(emp_data.get("contract_status") or "").strip().lower()
status = status_map.get(raw_status, "Active")
# Parse salary
salary = None
if detail_data and 'EmasContractDetailsEnt' in detail_data and isinstance(detail_data.get('EmasContractDetailsEnt'), list) and len(detail_data['EmasContractDetailsEnt']) > 0:
raw_salary = detail_data['EmasContractDetailsEnt'][0].get('monthlySalary')
else:
raw_salary = emp_data.get("monthly_salary")
if raw_salary:
try:
salary = float(str(raw_salary).replace(",", ".").replace(" ", ""))
except (ValueError, TypeError):
pass
# Resolve designation
position_text = None
if detail_data and 'staffData' in detail_data and detail_data.get('staffData'):
position_text = detail_data['staffData'].get('positionText')
else:
position_text = emp_data.get("work_position_text")
designation = resolve_designation(position_text, create_designation)
if existing_employee:
emp = frappe.get_doc("Employee", existing_employee)
emp.first_name = first_name
emp.last_name = last_name
emp.employee_name = full_name or first_name
if dob:
emp.date_of_birth = dob
if doj:
emp.date_of_joining = doj
if contract_end:
emp.contract_end_date = contract_end
if salary is not None:
emp.ctc = salary
if designation:
emp.designation = designation
emp.save(ignore_permissions=True)
updated_count += 1
else:
emp = frappe.new_doc("Employee")
emp.first_name = first_name
emp.last_name = last_name
emp.company = company
emp.passport_number = fin
emp.date_of_birth = dob or "2000-01-01"
emp.date_of_joining = doj or frappe.utils.today()
emp.gender = "Male" # default, user can change later
emp.status = status
if contract_end:
emp.contract_end_date = contract_end
if salary is not None:
emp.ctc = salary
if designation:
emp.designation = designation
emp.insert(ignore_permissions=True)
created_count += 1
# Link Emas Employees → Employee
emas_doc.employee = emp.name
emas_doc.save(ignore_permissions=True)
except Exception as e:
fin_val = emp_data.get("identification_number", "?")
name_val = emp_data.get("full_name", "?")
error_msg = f"{name_val} ({fin_val}): {str(e)}"
frappe.log_error(error_msg, "EMAS Employee Create Error")
errors.append(error_msg)
error_count += 1
frappe.db.commit()
return {
"success": True,
"created_count": created_count,
"updated_count": updated_count,
"error_count": error_count,
"errors": errors[:10]
}
except Exception as e:
frappe.log_error(
f"Create employees from EMAS error: {str(e)}\n{frappe.get_traceback()}",
"EMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
# ==================== EMPLOYEE DETAIL API FUNCTIONS ====================
# These functions fetch complete employee details from ƏMAS
# (called when viewing employee details in ƏMAS, not from list report)
def get_edit_form_data(asan_login_name, doc_oid, doc_no, doc_type):
"""
Call eroom.getEditForm to get contract form data.
Returns: EmasContractsEnt, EmasContractDetailsEnt, EmasContractTextDetailsEnt
"""
data = {
"serviceName": "AppEmploymentContractOnline",
"docOid": doc_oid,
"docNo": doc_no,
"docType": doc_type
}
return make_emas_request(asan_login_name, "/service/eroom.getEditForm", data)
def get_staff_data(asan_login_name, staff_oid):
"""Call embas.getStaffData for position info"""
data = {"staffOid": staff_oid}
return make_emas_request(asan_login_name, "/service/embas.getStaffData", data)
def get_person_data(asan_login_name, entity_oid):
"""Call refdata.getPersonDataByEntOid for personal data"""
data = {"entityOid": entity_oid} # Fixed: was "entOid", should be "entityOid"
return make_emas_request(asan_login_name, "/service/refdata.getPersonDataByEntOid", data)
def get_address_data(asan_login_name, address_oid):
"""Call refdata.getAddressData for work address"""
data = {"addressOid": address_oid}
return make_emas_request(asan_login_name, "/service/refdata.getAddressData", data)
def get_doc_main_data(asan_login_name, doc_oid, doc_no, doc_type):
"""Call eroom.getDocMainData for document metadata"""
data = {
"docOid": doc_oid,
"docNo": doc_no,
"docType": doc_type
}
return make_emas_request(asan_login_name, "/service/eroom.getDocMainData", data)
def get_contract_attachments(asan_login_name, doc_oid, attach_type="attachType_5"):
"""
Call eroom.getAttachListByType to get list of contract attachments.
Returns list of attachments with fileId for downloading.
"""
data = {
"docOid[]": doc_oid,
"attachType": attach_type
}
return make_emas_request(asan_login_name, "/service/eroom.getAttachListByType", data)
def download_contract_file(asan_login_name, file_id):
"""
Call request/readFromStorage to download contract HTML file.
Returns HTML content of the employment contract.
"""
data = {"fileId": file_id}
return make_emas_request(asan_login_name, "/request/readFromStorage", data)
@frappe.whitelist()
def get_employee_detail(asan_login_name, doc_oid, doc_no, doc_type="docType_47"):
"""
Fetch complete employee detail from ƏMAS.
Makes 7 API calls to collect all data.
Returns combined dict with:
- EmasContractsEnt, EmasContractDetailsEnt, EmasContractTextDetailsEnt
- staffData, iamasBean, addressBean, workAddress
- docMainData, contractHtml
"""
combined_data = {}
try:
# 1. Get contract form data
form_result = get_edit_form_data(asan_login_name, doc_oid, doc_no, doc_type)
if form_result and form_result.get('success'):
data = form_result.get('data')
if data:
result_data = data.get('resultData', {})
if result_data:
combined_data.update(result_data)
# 2. Get staff data (need staff_oid from contract)
if 'EmasContractsEnt' in combined_data and isinstance(combined_data['EmasContractsEnt'], list) and len(combined_data['EmasContractsEnt']) > 0:
staff_oid = combined_data['EmasContractsEnt'][0].get('staff')
if staff_oid:
staff_result = get_staff_data(asan_login_name, staff_oid)
if staff_result and staff_result.get('success'):
data = staff_result.get('data')
if data:
# staffData is on top level, not in resultData
combined_data['staffData'] = data.get('staffData')
# 3. Get person data (need entity_oid from contract)
if 'EmasContractsEnt' in combined_data and isinstance(combined_data['EmasContractsEnt'], list) and len(combined_data['EmasContractsEnt']) > 0:
entity_oid = combined_data['EmasContractsEnt'][0].get('entity')
if entity_oid:
person_result = get_person_data(asan_login_name, entity_oid)
if person_result and person_result.get('success'):
data = person_result.get('data')
if data:
# iamasBean and addressBean are on top level, not in resultData
combined_data['iamasBean'] = data.get('iamasBean')
combined_data['addressBean'] = data.get('addressBean')
# 4. Get work address (need address_oid from contract details)
if 'EmasContractDetailsEnt' in combined_data and isinstance(combined_data['EmasContractDetailsEnt'], list) and len(combined_data['EmasContractDetailsEnt']) > 0:
address_oid = combined_data['EmasContractDetailsEnt'][0].get('address')
if address_oid:
addr_result = get_address_data(asan_login_name, address_oid)
if addr_result and addr_result.get('success'):
data = addr_result.get('data')
if data:
# addressData is on top level, not in resultData
combined_data['workAddress'] = data.get('addressData')
# 5. Get document main data
doc_result = get_doc_main_data(asan_login_name, doc_oid, doc_no, doc_type)
if doc_result and doc_result.get('success'):
data = doc_result.get('data')
if data:
# Document fields are on top level, not in resultData
# Build docMainData from top-level fields
combined_data['docMainData'] = {
'docOid': data.get('docOid'),
'docNo': data.get('docNo'),
'docDate': data.get('docDate'),
'docType': data.get('docType'),
'docState': data.get('docState'),
'listDocInfo': data.get('listDocInfo', [])
}
# 6. Get contract document (HTML)
attachments_result = get_contract_attachments(asan_login_name, doc_oid)
if attachments_result and attachments_result.get('success'):
data = attachments_result.get('data')
if data and 'attachList' in data:
attach_list = data.get('attachList', [])
if attach_list and len(attach_list) > 0:
# Get the first attachment (main contract document)
file_id = attach_list[0].get('fileId')
if file_id:
file_result = download_contract_file(asan_login_name, file_id)
if file_result and file_result.get('success'):
file_data = file_result.get('data')
if file_data:
combined_data['contractHtml'] = file_data.get('fileContent')
return {
"success": True,
"data": combined_data
}
except Exception as e:
frappe.log_error(
f"Get employee detail error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS API Error"
)
return {"success": False, "message": f"Error: {str(e)}"}
def update_employee_from_detail_data(employee_doc, detail_data):
"""
Map all 115 fields from detail API responses to Emas Employees document.
Handles nested objects from 6 different API calls.
"""
if not detail_data:
return
# Map EmasContractsEnt fields (contract info)
if 'EmasContractsEnt' in detail_data and isinstance(detail_data.get('EmasContractsEnt'), list) and len(detail_data['EmasContractsEnt']) > 0:
contract = detail_data['EmasContractsEnt'][0]
employee_doc.contract_number = contract.get('contractNumber')
employee_doc.ssn = contract.get('ssn')
employee_doc.contract_type = contract.get('contractType')
employee_doc.contract_status = contract.get('contractStatus')
# Only set identification_number if it exists in detail data
pin = contract.get('pin')
if pin:
employee_doc.identification_number = pin
employee_doc.employer_tin = contract.get('employerTin')
employee_doc.employer_pin = contract.get('employerPin')
employee_doc.prev_contract = contract.get('prevContract')
employee_doc.is_signed = 1 if contract.get('isSigned') else 0
employee_doc.contract_validity = contract.get('validity')
employee_doc.workplace_type = contract.get('workplaceType')
employee_doc.staff_oid = contract.get('staff')
employee_doc.structure_unit_oid = contract.get('structureUnitOid')
employee_doc.entity_oid = contract.get('entity')
employee_doc.entity_details_oid = contract.get('entityDetails')
employee_doc.from_migration = 1 if contract.get('fromMigration') else 0
employee_doc.doc_oid = contract.get('fkDocOid')
# Map EmasContractDetailsEnt fields (salary, schedule, vacation)
if 'EmasContractDetailsEnt' in detail_data and isinstance(detail_data.get('EmasContractDetailsEnt'), list) and len(detail_data['EmasContractDetailsEnt']) > 0:
details = detail_data['EmasContractDetailsEnt'][0]
employee_doc.base_salary = details.get('salary')
employee_doc.monthly_salary = str(details.get('monthlySalary') or '')
employee_doc.salary_add = details.get('salaryAdd')
employee_doc.salary_add_hw = details.get('salaryAddHw')
employee_doc.award_amount = details.get('awardAmount')
employee_doc.award_type = details.get('awardType')
employee_doc.overtime_amount = details.get('overTimeAmount')
employee_doc.bad_condition_amount = details.get('badCondAmount')
employee_doc.bad_condition_id = details.get('badCondId')
employee_doc.currency_type = details.get('currencyType')
employee_doc.payment_type = details.get('paymentType')
employee_doc.payment_first_day = details.get('paymentFirstDay')
employee_doc.bank_oid = details.get('bank')
# Work schedule
employee_doc.work_mode_type = details.get('workModeType')
employee_doc.is_full_time = details.get('isFullTime')
employee_doc.first_shift_start = details.get('firstShiftStart')
employee_doc.first_shift_end = details.get('firstShiftEnd')
employee_doc.second_shift_start = details.get('secondShiftStart')
employee_doc.second_shift_end = details.get('secondShiftEnd')
employee_doc.lunch_start = details.get('lunchStart')
employee_doc.lunch_end = details.get('lunchEnd')
employee_doc.work_time = details.get('workTime')
employee_doc.working_time_cumulative = 1 if details.get('workingTimeCumulative') else 0
# Vacation
employee_doc.vacation_days_count = details.get('vacationDaysCount')
employee_doc.vacation_general_duration = details.get('vacGenDur')
employee_doc.vacation_main_duration = details.get('vacMainDur')
employee_doc.vacation_add_dur_one = details.get('vacAddDurOne')
employee_doc.vacation_add_dur_two = details.get('vacAddDurTwo')
employee_doc.vacation_add_dur_four = details.get('vacAddDurFour')
employee_doc.vacation_amount = details.get('vacAmount')
employee_doc.profession_employee_for_vacation = details.get('professionEmployeeForVacation')
# Education
employee_doc.education = details.get('education')
employee_doc.scientific_degree = details.get('scientificDegree')
employee_doc.speciality_degree = details.get('specialityDegree')
employee_doc.state_speciality_degree = details.get('stateSpecialityDegree')
employee_doc.qualification_degree_salary_add = details.get('qualificationDegreeSalaryAdd')
# Dates
employee_doc.main_contract_begin_date = str(details.get('mainContractBeginDate') or '')
employee_doc.contract_begin_date = parse_emas_date(details.get('beginDate'))
employee_doc.contract_end_date = parse_emas_date(details.get('endDate'))
employee_doc.emp_job_start_date = str(details.get('empJobStartDate') or '')
employee_doc.begin_date = str(details.get('beginDate') or '')
employee_doc.end_date = str(details.get('endDate') or '')
# Other fields
employee_doc.position_id = details.get('positionId')
employee_doc.activity_code = details.get('activityCode')
employee_doc.work_address_oid = details.get('address')
employee_doc.chairman_pin = details.get('chairmanPin')
employee_doc.contract_reason = details.get('contractReason')
employee_doc.workplace_sector = details.get('workplaceSector')
# Map EmasContractTextDetailsEnt fields
if 'EmasContractTextDetailsEnt' in detail_data and isinstance(detail_data.get('EmasContractTextDetailsEnt'), list) and len(detail_data['EmasContractTextDetailsEnt']) > 0:
text_details = detail_data['EmasContractTextDetailsEnt'][0]
if text_details:
employee_doc.work_position = text_details.get('workPosition')
employee_doc.activity_name_detail = text_details.get('activityName')
employee_doc.labour_function = text_details.get('labourFunction')
employee_doc.protection_tools = text_details.get('protectionTools')
employee_doc.payment_other_cond = text_details.get('paymentOtherCond')
employee_doc.rule_text = text_details.get('ruleText')
# Map staffData fields
if 'staffData' in detail_data and detail_data.get('staffData'):
staff = detail_data['staffData']
employee_doc.position_text = staff.get('positionText')
employee_doc.tree_path_name = staff.get('treePathName')
employee_doc.staff_unit = staff.get('staffUnit')
employee_doc.structure_path = staff.get('treePathName')
employee_doc.work_position_text = staff.get('positionText')
# Map iamasBean (personal data)
if 'iamasBean' in detail_data and detail_data.get('iamasBean'):
person = detail_data['iamasBean']
# Construct full name
name = person.get('name', '')
surname = person.get('surname', '')
employee_doc.full_name = f"{name} {surname}".strip()
employee_doc.father_name = person.get('fatherName')
employee_doc.gender = person.get('gender')
employee_doc.birthday = str(person.get('birthDate') or '')
employee_doc.nationality = person.get('nationality')
employee_doc.blood_type = person.get('bloodType')
employee_doc.marital_status = person.get('maritalStatus')
employee_doc.military_status = person.get('militaryStatus')
employee_doc.document_number = person.get('documentNumber')
employee_doc.document_issue_date = parse_emas_date(person.get('issueDate'))
employee_doc.document_expiry_date = parse_emas_date(person.get('expireDate'))
employee_doc.document_issued_by = person.get('issuedBy')
employee_doc.personal_address = person.get('address')
employee_doc.personal_city = person.get('city')
employee_doc.personal_district = person.get('district')
# Map addressBean (contact info)
if 'addressBean' in detail_data and detail_data.get('addressBean'):
address = detail_data['addressBean']
employee_doc.contact_phone = address.get('mobilePhoneNumber')
employee_doc.contact_email = address.get('email')
# Map workAddress
if 'workAddress' in detail_data and detail_data.get('workAddress'):
work_addr = detail_data['workAddress']
employee_doc.work_address_street = work_addr.get('street')
employee_doc.work_address_district = work_addr.get('district')
# Map docMainData
if 'docMainData' in detail_data and detail_data.get('docMainData'):
doc_main = detail_data['docMainData']
employee_doc.doc_no = doc_main.get('docNo')
employee_doc.doc_date = str(doc_main.get('docDate') or '')
employee_doc.doc_type = doc_main.get('docType')
employee_doc.doc_state = doc_main.get('docState')
employee_doc.flow = doc_main.get('flow')
employee_doc.flow_alias = doc_main.get('flowAlias')
if 'listDocInfo' in doc_main and isinstance(doc_main['listDocInfo'], list) and len(doc_main['listDocInfo']) > 0:
employee_doc.after_exec_label = doc_main['listDocInfo'][0].get('afterExecLabel')
created_date_str = doc_main['listDocInfo'][0].get('createDate')
if created_date_str:
employee_doc.created_date = created_date_str
# Map contract HTML document
if 'contractHtml' in detail_data and detail_data.get('contractHtml'):
employee_doc.contract_html = detail_data.get('contractHtml')
# Store complete raw data as JSON (only if detail_data has meaningful content)
# Check if detail_data has any of the expected keys
has_detail_content = any(key in detail_data for key in [
'EmasContractsEnt', 'EmasContractDetailsEnt', 'EmasContractTextDetailsEnt',
'staffData', 'iamasBean', 'addressBean', 'workAddress', 'docMainData', 'contractHtml'
])
if has_detail_content:
employee_doc.raw_data = json.dumps(detail_data, ensure_ascii=False, indent=2)
employee_doc.import_date = frappe.utils.now_datetime()
def on_delete_employee(doc, method):
"""
When an Employee is deleted, also delete linked Emas Employees records.
"""
emas_records = frappe.get_all(
"Emas Employees",
filters={"employee": doc.name},
fields=["name"]
)
for rec in emas_records:
frappe.delete_doc("Emas Employees", rec.name, ignore_permissions=True)