invoice_az/invoice_az/amas_api.py

2613 lines
100 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 - AMAS returns many Set-Cookie headers
http.client._MAXHEADERS = 1000
# AMAS Base URL
AMAS_BASE_URL = "https://eroom.e-social.gov.az"
# AMAS API Endpoints
AMAS_SSO_CALLBACK = "/ssoAsan/v2"
AMAS_CHANGE_ACCOUNT = "/service/aas.changeAccount"
AMAS_EXECUTE_REPORT = "/service/magusbi.executeReport"
AMAS_GET_DOMAINS = "/service/aas.domains"
AMAS_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"
# AMAS OAuth Client ID (from HAR analysis)
AMAS_CLIENT_ID = "64942e33ec8d49059333a1e0ebad7fe2"
AMAS_REDIRECT_URI = f"{AMAS_BASE_URL}{AMAS_SSO_CALLBACK}"
# AMAS request headers
AMAS_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": AMAS_BASE_URL,
"Referer": f"{AMAS_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 AMAS 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_amas(asan_login_name):
"""
Connect to AMAS using MyGovID JWT token.
This is the main entry point for AMAS connection.
If MyGovID token exists - uses it to get AMAS 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_amas_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.amas_session = json.dumps(session_data)
doc.amas_csrf_token = session_data.get("xsrf_token")
doc.amas_auth_status = "Connected"
doc.amas_last_activity = frappe.utils.now_datetime()
# Clear any previous account selection - user must choose
doc.amas_account_oid = None
doc.amas_account_name = None
doc.amas_account_number = None
doc.save(ignore_permissions=True)
frappe.db.commit()
# Step 4: Fetch available accounts from /request/accounts
accounts_result = get_amas_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_amas_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": AMAS_CLIENT_ID,
"state": state,
"response_type": "code",
"redirect_uri": AMAS_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"{AMAS_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_amas_session_data(asan_login_name):
"""Get EMAS session data from document"""
doc = frappe.get_doc("Asan Login", asan_login_name)
if not doc.amas_session:
return None
try:
return json.loads(doc.amas_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_amas_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"{AMAS_BASE_URL}/core.dashboard",
cookies=cookies,
timeout=30
)
if response.status_code != 200:
return session_data, doc.amas_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.amas_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.amas_session = json.dumps(session_data)
doc.amas_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.amas_csrf_token
def make_amas_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 = AMAS_HEADERS.copy()
csrf_token = csrf_token or doc.amas_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"{AMAS_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.amas_csrf_token = decoded_token
session_data["xsrf_token"] = decoded_token
doc.amas_session = json.dumps(session_data)
doc.amas_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.amas_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.amas_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_amas_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 = AMAS_HEADERS.copy()
csrf_token = csrf_token or doc.amas_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"{AMAS_BASE_URL}{AMAS_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.amas_csrf_token = decoded_token
session_data["xsrf_token"] = decoded_token
doc.amas_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_amas_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_amas_request(
asan_login_name,
AMAS_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.amas_account_oid = account_oid
doc.amas_account_name = account_name
doc.amas_account_number = account_number
doc.amas_auth_status = "Connected"
doc.amas_last_activity = frappe.utils.now_datetime()
# Update session with token from response
token = response_data.get("token")
if token:
session_data = get_amas_session_data(asan_login_name)
if session_data:
session_data["token"] = token
doc.amas_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.amas_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_amas_request(asan_login_name, AMAS_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.amas_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_amas_request(asan_login_name, AMAS_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_amas_connection(asan_login_name):
"""
Check if EMAS connection is active.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
return {
"success": True,
"amas_auth_status": doc.amas_auth_status or "Not Connected",
"amas_account_name": doc.amas_account_name,
"amas_account_number": doc.amas_account_number,
"amas_last_activity": str(doc.amas_last_activity) if doc.amas_last_activity else None,
"has_session": bool(doc.amas_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_amas_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_amas_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"{AMAS_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.amas_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.amas_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.amas_session = json.dumps(session_data)
doc.amas_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_amas(asan_login_name):
"""
Disconnect from EMAS by clearing session data.
"""
try:
doc = frappe.get_doc("Asan Login", asan_login_name)
doc.amas_session = None
doc.amas_csrf_token = None
doc.amas_account_oid = None
doc.amas_account_name = None
doc.amas_account_number = None
doc.amas_auth_status = "Not Connected"
doc.amas_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.
Prioritizes is_default = 1, similar to e-taxes.
"""
try:
# First try to get default Asan Login with ƏMAS connection
default_login = frappe.get_all(
"Asan Login",
filters={
"is_default": 1,
"amas_auth_status": "Connected",
"amas_account_oid": ["is", "set"]
},
fields=["name", "amas_account_name", "amas_account_number", "amas_account_oid"],
limit=1
)
if default_login:
# Use default login
asan_logins = default_login
else:
# Fall back to any connected login
asan_logins = frappe.get_all(
"Asan Login",
filters={
"amas_auth_status": "Connected",
"amas_account_oid": ["is", "set"]
},
fields=["name", "amas_account_name", "amas_account_number", "amas_account_oid"],
limit=1
)
result = []
for al in asan_logins:
result.append({
"name": al.name,
"organization_name": al.amas_account_name or "Unknown",
"organization_voen": al.amas_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
AMAS_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.amas_account_name
organization_voen = doc.amas_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(
"Amas Employees",
{"identification_number": fin, "organization_voen": organization_voen}
)
if existing:
employee_doc = frappe.get_doc("Amas 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("Amas 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(
"Amas Employees",
{
"identification_number": fin,
"organization_voen": organization_voen
}
)
if existing:
employee_doc = frappe.get_doc("Amas 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("Amas 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 AMAS_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_amas_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 map_gender(amas_gender):
"""
Transform AMAS gender to ERPNext gender.
AMAS values: "Kişi" (Male), "Qadın" (Female), "Male", "Female"
ERPNext values: "Male", "Female", "Other"
"""
if not amas_gender:
return "Male" # Default fallback
gender_str = str(amas_gender).strip().lower()
# Map Azerbaijani values
if gender_str in ("kişi", "kisi", "male", "m", "erkək", "erkek"):
return "Male"
elif gender_str in ("qadın", "qadin", "female", "f", "qız", "qiz"):
return "Female"
return "Male" # Default fallback
def map_marital_status(amas_status):
"""
Transform AMAS marital status to ERPNext marital status.
AMAS values: "Evli", "Subay", "Married", "Single", etc.
ERPNext values: "Single", "Married", "Divorced", "Widowed"
"""
if not amas_status:
return None
status_str = str(amas_status).strip().lower()
# Map Azerbaijani values
if status_str in ("evli", "married", "nikahli", "nikаhlı"):
return "Married"
elif status_str in ("subay", "subаy", "single", "bekar", "bekаr"):
return "Single"
elif status_str in ("boşanmış", "bosanmis", "divorced"):
return "Divorced"
elif status_str in ("dul", "widowed", "widow"):
return "Widowed"
return None
def map_blood_group(amas_blood):
"""
Transform AMAS blood type to ERPNext blood group.
AMAS format: "A(II) Rh+", "O(I) Rh-", etc.
ERPNext values: "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"
"""
if not amas_blood:
return None
blood_str = str(amas_blood).strip().upper()
# Extract blood type and Rh factor
# Examples: "A(II) Rh+", "AB(IV) Rh-", "O(I) Rh+"
# Map blood group letter
if "AB" in blood_str or "IV" in blood_str:
blood_type = "AB"
elif "A" in blood_str or "II" in blood_str:
blood_type = "A"
elif "B" in blood_str or "III" in blood_str:
blood_type = "B"
elif "O" in blood_str or "I" in blood_str or "0" in blood_str:
blood_type = "O"
else:
return None
# Map Rh factor
if "+" in blood_str or "POSITIVE" in blood_str or "POZİTİV" in blood_str:
rh_factor = "+"
elif "-" in blood_str or "NEGATIVE" in blood_str or "NEQATİV" in blood_str:
rh_factor = "-"
else:
rh_factor = "+" # Default to positive
return f"{blood_type}{rh_factor}"
def build_full_address(address, city, district):
"""
Combine AMAS address fields into single address string.
"""
parts = []
if address:
parts.append(str(address).strip())
if district:
parts.append(str(district).strip())
if city:
parts.append(str(city).strip())
return ", ".join(parts) if parts else 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_single_employee_from_amas(asan_login_name, employee_data, company, create_designation=0):
"""
Create/update single Employee record from ƏMAS data.
Used for sequential processing with frontend progress updates.
"""
try:
if isinstance(employee_data, str):
employee_data = json.loads(employee_data)
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.amas_account_name
organization_voen = doc.amas_account_number
# Status mapping
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",
}
doc_oid = employee_data.get("doc_oid")
doc_no = employee_data.get("doc_no")
doc_type = employee_data.get("doc_type", "docType_47")
# Fetch detail data
detail_data = None
full_name = str(employee_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
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 '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 = employee_data.get("identification_number")
if not fin:
return {"success": False, "message": f"{full_name}: No identification_number"}
fin = str(fin).strip()
# Save/update Amas Employees record
amas_existing = None
if doc_oid:
amas_existing = frappe.db.exists("Amas Employees", {"doc_oid": str(doc_oid)})
if not amas_existing:
amas_existing = frappe.db.exists(
"Amas Employees",
{"identification_number": fin, "organization_voen": organization_voen}
)
if amas_existing:
amas_doc = frappe.get_doc("Amas Employees", amas_existing)
update_employee_from_data(amas_doc, employee_data)
if detail_data:
update_employee_from_detail_data(amas_doc, detail_data)
if not amas_doc.identification_number:
amas_doc.identification_number = fin
amas_doc.save(ignore_permissions=True)
else:
amas_doc = frappe.new_doc("Amas Employees")
amas_doc.asan_login = asan_login_name
amas_doc.organization_name = organization_name
amas_doc.organization_voen = organization_voen
amas_doc.identification_number = fin
update_employee_from_data(amas_doc, employee_data)
if detail_data:
update_employee_from_detail_data(amas_doc, detail_data)
if not amas_doc.identification_number:
amas_doc.identification_number = fin
amas_doc.insert(ignore_permissions=True)
# Create/update Employee record
existing_employee = frappe.db.exists("Employee", {"passport_number": fin})
# Split full_name
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
dob = None
doj = None
contract_end = None
if detail_data:
if 'iamasBean' in detail_data and detail_data.get('iamasBean'):
dob = parse_amas_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_amas_date(details.get('beginDate'))
contract_end = parse_amas_date(details.get('endDate'))
else:
dob = parse_amas_date(employee_data.get("birthday"))
doj = parse_amas_date(employee_data.get("begin_date"))
contract_end = parse_amas_date(employee_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(employee_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 = employee_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 = employee_data.get("work_position_text")
designation = resolve_designation(position_text, create_designation)
# Extract additional fields from amas_doc (already populated with detail data)
middle_name = amas_doc.father_name if hasattr(amas_doc, 'father_name') else None
gender = map_gender(amas_doc.gender if hasattr(amas_doc, 'gender') else None)
cell_number = amas_doc.contact_phone if hasattr(amas_doc, 'contact_phone') else None
personal_email = amas_doc.contact_email if hasattr(amas_doc, 'contact_email') else None
marital_status = map_marital_status(amas_doc.marital_status if hasattr(amas_doc, 'marital_status') else None)
blood_group = map_blood_group(amas_doc.blood_type if hasattr(amas_doc, 'blood_type') else None)
date_of_issue = parse_amas_date(amas_doc.document_issue_date if hasattr(amas_doc, 'document_issue_date') else None)
valid_upto = parse_amas_date(amas_doc.document_expiry_date if hasattr(amas_doc, 'document_expiry_date') else None)
place_of_issue = amas_doc.document_issued_by if hasattr(amas_doc, 'document_issued_by') else None
current_address = build_full_address(
amas_doc.personal_address if hasattr(amas_doc, 'personal_address') else None,
amas_doc.personal_city if hasattr(amas_doc, 'personal_city') else None,
amas_doc.personal_district if hasattr(amas_doc, 'personal_district') else None
)
if existing_employee:
emp = frappe.get_doc("Employee", existing_employee)
emp.first_name = first_name
if middle_name:
emp.middle_name = middle_name
emp.last_name = last_name
emp.employee_name = full_name or first_name
emp.gender = gender
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
if cell_number:
emp.cell_number = cell_number
if personal_email:
emp.personal_email = personal_email
if marital_status:
emp.marital_status = marital_status
if blood_group:
emp.blood_group = blood_group
if date_of_issue:
emp.date_of_issue = date_of_issue
if valid_upto:
emp.valid_upto = valid_upto
if place_of_issue:
emp.place_of_issue = place_of_issue
if current_address:
emp.current_address = current_address
# Link to AMAS Employees record
emp.custom_amas_employee = amas_doc.name
emp.save(ignore_permissions=True)
# Link Amas Employees → Employee
amas_doc.employee = emp.name
amas_doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"updated": True,
"created": False,
"employee_name": emp.name
}
else:
emp = frappe.new_doc("Employee")
emp.first_name = first_name
if middle_name:
emp.middle_name = middle_name
emp.last_name = last_name
emp.company = company
emp.passport_number = fin
emp.employee_number = fin
emp.date_of_birth = dob or "2000-01-01"
emp.date_of_joining = doj or frappe.utils.today()
emp.gender = gender
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
if cell_number:
emp.cell_number = cell_number
if personal_email:
emp.personal_email = personal_email
if marital_status:
emp.marital_status = marital_status
if blood_group:
emp.blood_group = blood_group
if date_of_issue:
emp.date_of_issue = date_of_issue
if valid_upto:
emp.valid_upto = valid_upto
if place_of_issue:
emp.place_of_issue = place_of_issue
if current_address:
emp.current_address = current_address
# Link to AMAS Employees record
emp.custom_amas_employee = amas_doc.name
emp.insert(ignore_permissions=True)
# Link Amas Employees → Employee
amas_doc.employee = emp.name
amas_doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"created": True,
"updated": False,
"employee_name": emp.name
}
except Exception as e:
frappe.log_error(
f"Single employee create error: {str(e)}\n{frappe.get_traceback()}",
"ƏMAS Employee Create Error"
)
return {
"success": False,
"message": str(e)
}
@frappe.whitelist()
def create_single_employee_from_amas(asan_login_name, emp_data, company, create_designation=0):
"""
Create/update a SINGLE Employee record from ƏMAS data.
This is called from frontend in a loop for progress bar updates.
Args:
asan_login_name: Asan Login name
emp_data: Single employee data dict from ƏMAS API
company: Company name
create_designation: 1/0 to create designation if not found
Returns:
{
"success": True/False,
"action": "created"/"updated"/"error",
"employee_name": "...",
"full_name": "...",
"message": "..."
}
"""
try:
if isinstance(emp_data, str):
emp_data = json.loads(emp_data)
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.amas_account_name
organization_voen = doc.amas_account_number
# Status mapping
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",
}
doc_oid = emp_data.get("doc_oid")
doc_no = emp_data.get("doc_no")
doc_type = emp_data.get("doc_type", "docType_47")
# Fetch detail data for complete employee information
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:
return {
"success": False,
"action": "error",
"full_name": full_name,
"message": f"{full_name}: No identification_number"
}
fin = str(fin).strip()
# 1. Save/update Amas Employees record
amas_existing = None
if doc_oid:
amas_existing = frappe.db.exists("Amas Employees", {"doc_oid": str(doc_oid)})
if not amas_existing:
amas_existing = frappe.db.exists(
"Amas Employees",
{"identification_number": fin, "organization_voen": organization_voen}
)
if amas_existing:
amas_doc = frappe.get_doc("Amas Employees", amas_existing)
update_employee_from_data(amas_doc, emp_data)
if detail_data:
update_employee_from_detail_data(amas_doc, detail_data)
if not amas_doc.identification_number:
amas_doc.identification_number = fin
amas_doc.save(ignore_permissions=True)
else:
amas_doc = frappe.new_doc("Amas Employees")
amas_doc.asan_login = asan_login_name
amas_doc.organization_name = organization_name
amas_doc.organization_voen = organization_voen
amas_doc.identification_number = fin
update_employee_from_data(amas_doc, emp_data)
if detail_data:
update_employee_from_detail_data(amas_doc, detail_data)
if not amas_doc.identification_number:
amas_doc.identification_number = fin
amas_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:
if 'iamasBean' in detail_data and detail_data.get('iamasBean'):
dob = parse_amas_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_amas_date(details.get('beginDate'))
contract_end = parse_amas_date(details.get('endDate'))
else:
dob = parse_amas_date(emp_data.get("birthday"))
doj = parse_amas_date(emp_data.get("begin_date"))
contract_end = parse_amas_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)
# Extract additional fields from amas_doc
middle_name = amas_doc.father_name if hasattr(amas_doc, 'father_name') else None
gender = map_gender(amas_doc.gender if hasattr(amas_doc, 'gender') else None)
cell_number = amas_doc.contact_phone if hasattr(amas_doc, 'contact_phone') else None
personal_email = amas_doc.contact_email if hasattr(amas_doc, 'contact_email') else None
marital_status_mapped = map_marital_status(amas_doc.marital_status if hasattr(amas_doc, 'marital_status') else None)
blood_group_mapped = map_blood_group(amas_doc.blood_type if hasattr(amas_doc, 'blood_type') else None)
date_of_issue = parse_amas_date(amas_doc.document_issue_date if hasattr(amas_doc, 'document_issue_date') else None)
valid_upto = parse_amas_date(amas_doc.document_expiry_date if hasattr(amas_doc, 'document_expiry_date') else None)
place_of_issue = amas_doc.document_issued_by if hasattr(amas_doc, 'document_issued_by') else None
current_address = build_full_address(
amas_doc.personal_address if hasattr(amas_doc, 'personal_address') else None,
amas_doc.personal_city if hasattr(amas_doc, 'personal_city') else None,
amas_doc.personal_district if hasattr(amas_doc, 'personal_district') else None
)
if existing_employee:
emp = frappe.get_doc("Employee", existing_employee)
emp.first_name = first_name
if middle_name:
emp.middle_name = middle_name
emp.last_name = last_name
emp.employee_name = full_name or first_name
emp.gender = gender
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
if cell_number:
emp.cell_number = cell_number
if personal_email:
emp.personal_email = personal_email
if marital_status_mapped:
emp.marital_status = marital_status_mapped
if blood_group_mapped:
emp.blood_group = blood_group_mapped
if date_of_issue:
emp.date_of_issue = date_of_issue
if valid_upto:
emp.valid_upto = valid_upto
if place_of_issue:
emp.place_of_issue = place_of_issue
if current_address:
emp.current_address = current_address
emp.save(ignore_permissions=True)
action = "updated"
else:
emp = frappe.new_doc("Employee")
emp.first_name = first_name
if middle_name:
emp.middle_name = middle_name
emp.last_name = last_name
emp.company = company
emp.passport_number = fin
emp.employee_number = fin
emp.date_of_birth = dob or "2000-01-01"
emp.date_of_joining = doj or frappe.utils.today()
emp.gender = gender
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
if cell_number:
emp.cell_number = cell_number
if personal_email:
emp.personal_email = personal_email
if marital_status_mapped:
emp.marital_status = marital_status_mapped
if blood_group_mapped:
emp.blood_group = blood_group_mapped
if date_of_issue:
emp.date_of_issue = date_of_issue
if valid_upto:
emp.valid_upto = valid_upto
if place_of_issue:
emp.place_of_issue = place_of_issue
if current_address:
emp.current_address = current_address
emp.insert(ignore_permissions=True)
action = "created"
# Link Amas Employees → Employee
amas_doc.employee = emp.name
amas_doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"action": action,
"employee_name": emp.name,
"full_name": full_name,
"message": f"{action.capitalize()}: {full_name}"
}
except Exception as e:
fin_val = emp_data.get("identification_number", "?") if isinstance(emp_data, dict) else "?"
name_val = emp_data.get("full_name", "?") if isinstance(emp_data, dict) else "?"
error_msg = f"{name_val} ({fin_val}): {str(e)}"
frappe.log_error(
f"{error_msg}\n{frappe.get_traceback()}",
"EMAS Single Employee Create Error"
)
return {
"success": False,
"action": "error",
"full_name": name_val,
"message": error_msg
}
@frappe.whitelist()
def create_employees_from_amas(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.amas_account_name
organization_voen = doc.amas_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",
}
total_employees = len(employees)
for idx, emp_data in enumerate(employees, start=1):
try:
# Publish progress
frappe.publish_progress(
percent=(idx / total_employees) * 100,
title=f"Loading employee {idx} of {total_employees}",
description=f"Processing {emp_data.get('full_name', 'employee')}..."
)
doc_oid = emp_data.get("doc_oid")
doc_no = emp_data.get("doc_no")
doc_type = emp_data.get("doc_type", "docType_47")
# Fetch detail data for complete employee information
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
amas_existing = None
if doc_oid:
amas_existing = frappe.db.exists("Amas Employees", {"doc_oid": str(doc_oid)})
if not amas_existing:
amas_existing = frappe.db.exists(
"Amas Employees",
{"identification_number": fin, "organization_voen": organization_voen}
)
if amas_existing:
amas_doc = frappe.get_doc("Amas Employees", amas_existing)
# Always populate basic fields from list data first
update_employee_from_data(amas_doc, emp_data)
# Then overlay with detail data if available
if detail_data:
update_employee_from_detail_data(amas_doc, detail_data)
# Ensure FIN is set (required field)
if not amas_doc.identification_number:
amas_doc.identification_number = fin
amas_doc.save(ignore_permissions=True)
else:
amas_doc = frappe.new_doc("Amas Employees")
amas_doc.asan_login = asan_login_name
amas_doc.organization_name = organization_name
amas_doc.organization_voen = organization_voen
# Set FIN first (required field)
amas_doc.identification_number = fin
# Always populate basic fields from list data first
update_employee_from_data(amas_doc, emp_data)
# Then overlay with detail data if available
if detail_data:
update_employee_from_detail_data(amas_doc, detail_data)
# Ensure FIN is still set after mapping
if not amas_doc.identification_number:
amas_doc.identification_number = fin
amas_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_amas_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_amas_date(details.get('beginDate'))
contract_end = parse_amas_date(details.get('endDate'))
else:
# Fallback to list data
dob = parse_amas_date(emp_data.get("birthday"))
doj = parse_amas_date(emp_data.get("begin_date"))
contract_end = parse_amas_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)
# Extract additional fields from amas_doc (already populated with detail data)
middle_name = amas_doc.father_name if hasattr(amas_doc, 'father_name') else None
gender = map_gender(amas_doc.gender if hasattr(amas_doc, 'gender') else None)
cell_number = amas_doc.contact_phone if hasattr(amas_doc, 'contact_phone') else None
personal_email = amas_doc.contact_email if hasattr(amas_doc, 'contact_email') else None
marital_status_mapped = map_marital_status(amas_doc.marital_status if hasattr(amas_doc, 'marital_status') else None)
blood_group_mapped = map_blood_group(amas_doc.blood_type if hasattr(amas_doc, 'blood_type') else None)
date_of_issue = parse_amas_date(amas_doc.document_issue_date if hasattr(amas_doc, 'document_issue_date') else None)
valid_upto = parse_amas_date(amas_doc.document_expiry_date if hasattr(amas_doc, 'document_expiry_date') else None)
place_of_issue = amas_doc.document_issued_by if hasattr(amas_doc, 'document_issued_by') else None
current_address = build_full_address(
amas_doc.personal_address if hasattr(amas_doc, 'personal_address') else None,
amas_doc.personal_city if hasattr(amas_doc, 'personal_city') else None,
amas_doc.personal_district if hasattr(amas_doc, 'personal_district') else None
)
if existing_employee:
emp = frappe.get_doc("Employee", existing_employee)
emp.first_name = first_name
if middle_name:
emp.middle_name = middle_name
emp.last_name = last_name
emp.employee_name = full_name or first_name
emp.gender = gender
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
if cell_number:
emp.cell_number = cell_number
if personal_email:
emp.personal_email = personal_email
if marital_status_mapped:
emp.marital_status = marital_status_mapped
if blood_group_mapped:
emp.blood_group = blood_group_mapped
if date_of_issue:
emp.date_of_issue = date_of_issue
if valid_upto:
emp.valid_upto = valid_upto
if place_of_issue:
emp.place_of_issue = place_of_issue
if current_address:
emp.current_address = current_address
emp.save(ignore_permissions=True)
updated_count += 1
else:
emp = frappe.new_doc("Employee")
emp.first_name = first_name
if middle_name:
emp.middle_name = middle_name
emp.last_name = last_name
emp.company = company
emp.passport_number = fin
emp.employee_number = fin
emp.date_of_birth = dob or "2000-01-01"
emp.date_of_joining = doj or frappe.utils.today()
emp.gender = gender
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
if cell_number:
emp.cell_number = cell_number
if personal_email:
emp.personal_email = personal_email
if marital_status_mapped:
emp.marital_status = marital_status_mapped
if blood_group_mapped:
emp.blood_group = blood_group_mapped
if date_of_issue:
emp.date_of_issue = date_of_issue
if valid_upto:
emp.valid_upto = valid_upto
if place_of_issue:
emp.place_of_issue = place_of_issue
if current_address:
emp.current_address = current_address
emp.insert(ignore_permissions=True)
created_count += 1
# Link Amas Employees → Employee
amas_doc.employee = emp.name
amas_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_amas_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_amas_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_amas_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_amas_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_amas_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_amas_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_amas_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_amas_date(details.get('beginDate'))
employee_doc.contract_end_date = parse_amas_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_amas_date(person.get('issueDate'))
employee_doc.document_expiry_date = parse_amas_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.
"""
amas_records = frappe.get_all(
"Amas Employees",
filters={"employee": doc.name},
fields=["name"]
)
for rec in amas_records:
frappe.delete_doc("Amas Employees", rec.name, ignore_permissions=True)