added emas integration and emas employees

This commit is contained in:
Ali 2026-02-10 17:53:47 +04:00
parent 95f685a3a7
commit 002b317958
21 changed files with 63944 additions and 8 deletions

View File

@ -0,0 +1,89 @@
frappe.ui.form.on('Emas Employees', {
refresh: function(frm) {
// Add "View Contract" button if contract HTML exists
if (frm.doc.contract_html && frm.doc.contract_html.trim().length > 0) {
frm.add_custom_button(__('View Contract'), function() {
show_contract_dialog(frm.doc.contract_html, frm.doc.full_name);
});
}
}
});
function show_contract_dialog(html_content, employee_name) {
// Create dialog with proper size
const d = new frappe.ui.Dialog({
title: __('Employment Contract') + (employee_name ? ': ' + employee_name : ''),
size: 'large',
fields: [
{
fieldname: 'contract_preview',
fieldtype: 'HTML',
options: '<div id="contract-iframe-wrapper"></div>'
}
],
primary_action_label: __('Print'),
primary_action: function() {
// Get iframe and trigger print
const iframe = document.getElementById('contract-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.print();
}
},
secondary_action_label: __('Close')
});
d.show();
// Set dialog to full width
d.$wrapper.find('.modal-dialog').css({
'max-width': '90%',
'width': '90%',
'margin': '30px auto'
});
// Wait for dialog to fully render, then create iframe
setTimeout(() => {
const wrapper = d.$wrapper.find('#contract-iframe-wrapper');
// Create iframe with proper styling
wrapper.html(`
<iframe
id="contract-iframe"
style="width: 100%; height: 70vh; border: 1px solid #d1d8dd; border-radius: 4px;">
</iframe>
`);
// Wait a bit more for iframe to be added to DOM
setTimeout(() => {
const iframe = d.$wrapper.find('#contract-iframe')[0];
if (iframe) {
// Wrap content in proper HTML structure
const full_html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* Ensure print styles work correctly */
@media print {
body {
margin: 0;
padding: 20px;
}
}
</style>
</head>
<body>
${html_content}
</body>
</html>`;
// Use contentWindow.document.write for better compatibility
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(full_html);
doc.close();
}
}, 200);
}, 100);
}

View File

@ -0,0 +1,344 @@
// Load employees from ƏMAS (e-social.gov.az) directly into ERPNext Employee
// Pattern: same as e-taxes import — button on list → API call → selection → create
frappe.ui.form.on('Employee', {});
// List view — add "Load from ƏMAS" button
if (frappe.listview_settings['Employee']) {
const orig_onload = frappe.listview_settings['Employee'].onload;
frappe.listview_settings['Employee'].onload = function(listview) {
if (orig_onload) orig_onload(listview);
add_emas_load_button(listview);
};
} else {
frappe.listview_settings['Employee'] = {
onload: function(listview) {
add_emas_load_button(listview);
}
};
}
function add_emas_load_button(listview) {
listview.page.add_inner_button(__('Load from ƏMAS'), function() {
start_emas_employee_import(listview);
});
}
// Step 1: Get default connected Asan Login → fetch employees from ƏMAS API
function start_emas_employee_import(listview) {
frappe.call({
method: 'invoice_az.emas_api.get_connected_asan_logins',
freeze: true,
freeze_message: __('Checking ƏMAS connection...'),
callback: function(r) {
if (!r.message || !r.message.success) {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to check ƏMAS connections')
});
return;
}
const asan_logins = r.message.asan_logins || [];
if (asan_logins.length === 0) {
frappe.msgprint({
title: __('No ƏMAS Connection'),
indicator: 'orange',
message: __('No Asan Login with active ƏMAS connection found. Please connect to ƏMAS first in Asan Login.')
});
return;
}
// Use the first (default) connected login
fetch_employees_from_emas(listview, asan_logins[0].name, asan_logins[0].organization_name);
}
});
}
// Step 2: Fetch employees from ƏMAS API
function fetch_employees_from_emas(listview, asan_login_name, organization_name) {
frappe.show_alert({
message: __('Fetching employees from ƏMAS...'),
indicator: 'blue'
}, 3);
frappe.call({
method: 'invoice_az.emas_api.get_employees_report',
args: {
asan_login_name: asan_login_name,
offset: 0,
limit: 1000
},
freeze: true,
freeze_message: __('Loading employees from') + ' ' + organization_name + '...',
callback: function(r) {
if (!r.message || !r.message.success) {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to load employees from ƏMAS')
});
return;
}
const employees = r.message.employees || [];
if (employees.length === 0) {
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
message: __('No employees found in ƏMAS for this organization.')
});
return;
}
frappe.show_alert({
message: __('Processing ') + employees.length + __(' employees...'),
indicator: 'blue'
}, 3);
check_existing_and_show(listview, asan_login_name, organization_name, employees);
}
});
}
// Step 3: Check existing employees and show selection dialog
function check_existing_and_show(listview, asan_login_name, organization_name, employees) {
const fins = employees
.map(e => e.identification_number)
.filter(f => f);
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Employee',
filters: { passport_number: ['in', fins] },
fields: ['name', 'passport_number', 'employee_name'],
limit_page_length: 0
},
callback: function(r) {
const existing_map = {};
if (r.message) {
r.message.forEach(function(emp) {
if (emp.passport_number) {
existing_map[emp.passport_number] = emp.name;
}
});
}
show_employee_selection(listview, asan_login_name, organization_name, employees, existing_map);
}
});
}
// Step 4: Show selection dialog (e-taxes style)
function show_employee_selection(listview, asan_login_name, organization_name, employees, existing_map) {
const new_count = employees.filter(e => !existing_map[e.identification_number]).length;
const existing_count = employees.length - new_count;
// Build table (e-taxes style)
let invoiceTable = '<div style="max-height: 500px; overflow-y: auto;">' +
'<table class="table table-bordered emas-employees-table" style="width: 100%; table-layout: fixed;">' +
'<thead><tr>' +
'<th style="width: 4%;"><input type="checkbox" class="select-all-employees"></th>' +
'<th style="width: 20%;">' + __('Full Name') + '</th>' +
'<th style="width: 10%;">' + __('FIN') + '</th>' +
'<th style="width: 20%;">' + __('Position') + '</th>' +
'<th style="width: 8%;">' + __('Salary') + '</th>' +
'<th style="width: 12%;">' + __('Status') + '</th>' +
'<th style="width: 10%;">' + __('Begin Date') + '</th>' +
'<th style="width: 16%;">' + __('In System') + '</th>' +
'</tr></thead><tbody>';
employees.forEach(function(emp, idx) {
const fin = emp.identification_number || '';
const existing_id = existing_map[fin];
const status_cell = existing_id
? '<span class="indicator-pill green" style="font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; display: inline-block;">' + frappe.utils.escape_html(existing_id) + '</span>'
: '';
invoiceTable += '<tr>' +
'<td><input type="checkbox" class="select-employee" data-index="' + idx + '"></td>' +
'<td style="word-break: break-word;">' + frappe.utils.escape_html(emp.full_name || '') + '</td>' +
'<td>' + frappe.utils.escape_html(fin) + '</td>' +
'<td style="word-break: break-word;">' + frappe.utils.escape_html(emp.work_position_text || '') + '</td>' +
'<td style="text-align: right;">' + frappe.utils.escape_html(emp.monthly_salary || '') + '</td>' +
'<td>' + frappe.utils.escape_html(emp.contract_status || '') + '</td>' +
'<td>' + frappe.utils.escape_html(emp.begin_date || '') + '</td>' +
'<td>' + status_cell + '</td>' +
'</tr>';
});
invoiceTable += '</tbody></table></div>';
const infoMessage = '<div class="alert alert-info">' +
__('Employees found: ') + employees.length +
' (' + __('New') + ': ' + new_count + ', ' + __('Already in system') + ': ' + existing_count + ')' +
'</div>';
const default_company = frappe.defaults.get_user_default('Company') || frappe.defaults.get_global_default('company');
const d = new frappe.ui.Dialog({
title: __('Select employees from ƏMAS'),
size: 'large',
fields: [
{
fieldname: 'settings_section',
fieldtype: 'Section Break',
label: __('Settings')
},
{
fieldname: 'company',
fieldtype: 'Link',
label: __('Company'),
options: 'Company',
reqd: 1,
default: default_company
},
{
fieldname: 'create_designation',
fieldtype: 'Check',
label: __('Create Designation if not found'),
default: 1
},
{
fieldname: 'employees_section',
fieldtype: 'Section Break',
label: __('Employees')
},
{
fieldname: 'info_html',
fieldtype: 'HTML',
options: infoMessage
},
{
fieldname: 'employees_html',
fieldtype: 'HTML',
options: invoiceTable
}
],
primary_action_label: __('Load selected'),
primary_action: function(values) {
const selected_employees = [];
d.$wrapper.find('.select-employee:checked').each(function() {
const idx = $(this).data('index');
selected_employees.push(employees[idx]);
});
if (selected_employees.length === 0) {
frappe.msgprint({
title: __('Warning'),
indicator: 'orange',
message: __('No employees selected')
});
return;
}
d.hide();
create_employees(listview, asan_login_name, selected_employees, values.company, values.create_designation);
},
secondary_action_label: __('Cancel'),
secondary_action: function() { d.hide(); }
});
// Match e-taxes dialog sizing
d.$wrapper.find('.modal-dialog').css({
'max-width': '80%',
'width': '80%',
'margin': '30px auto'
});
d.$wrapper.find('.modal-body').css({
'padding': '15px'
});
d.show();
// Select all checkbox
d.$wrapper.find('.select-all-employees').on('change', function() {
const isChecked = $(this).prop('checked');
d.$wrapper.find('.select-employee').prop('checked', isChecked);
});
}
// Step 5: Create employees via backend
function create_employees(listview, asan_login_name, selected_employees, company, create_designation) {
// Show progress indicator
frappe.show_progress(
__('Loading from ƏMAS'),
0,
selected_employees.length,
__('Fetching employee details...')
);
frappe.call({
method: 'invoice_az.emas_api.create_employees_from_emas',
args: {
asan_login_name: asan_login_name,
employees: selected_employees,
company: company,
create_designation: create_designation ? 1 : 0
},
freeze: true,
freeze_message: __('Creating employees from ƏMAS...'),
callback: function(r) {
frappe.hide_progress();
if (!r.message || !r.message.success) {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to create employees')
});
return;
}
const res = r.message;
const total = res.created_count + res.updated_count + res.error_count;
if (res.error_count === 0) {
let msg = __('All ') + total + __(' employees were processed successfully.');
if (res.created_count > 0) {
msg += '<br>' + __('Created: ') + '<strong>' + res.created_count + '</strong>';
}
if (res.updated_count > 0) {
msg += '<br>' + __('Updated: ') + '<strong>' + res.updated_count + '</strong>';
}
frappe.msgprint({
title: __('Import Completed Successfully'),
indicator: 'green',
message: msg
});
} else {
let msg = '<div style="margin-bottom: 15px;">';
if (res.created_count > 0) {
msg += __('Successfully created: ') + '<strong>' + res.created_count + '</strong><br>';
}
if (res.updated_count > 0) {
msg += __('Updated: ') + '<strong>' + res.updated_count + '</strong><br>';
}
msg += __('Failed: ') + '<strong>' + res.error_count + '</strong>';
msg += '</div>';
if (res.errors && res.errors.length > 0) {
msg += '<div style="max-height: 200px; overflow-y: auto;">';
msg += '<ul style="color: red; font-size: 12px; margin: 0; padding-left: 20px;">';
res.errors.forEach(function(err) {
msg += '<li>' + frappe.utils.escape_html(err) + '</li>';
});
msg += '</ul></div>';
}
frappe.msgprint({
title: res.created_count > 0 ? __('Import Completed with Errors') : __('Import Failed'),
indicator: res.created_count > 0 ? 'orange' : 'red',
message: msg
});
}
listview.refresh();
}
});
}

1864
invoice_az/emas_api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,8 @@ doctype_js = {
"Sales Order": "client/sales_order.js",
"Sales Invoice": "client/sales_invoice.js",
"Journal Entry": "client/journal_entry.js",
"Supplier": "client/supplier.js"
"Supplier": "client/supplier.js",
"Emas Employees": "client/emas_employees.js"
}
doctype_list_js = {
@ -24,7 +25,8 @@ doctype_list_js = {
"Purchase Invoice": "client/purchase_invoice.js",
"Sales Order": "client/sales_order.js",
"Sales Invoice": "client/sales_invoice.js",
"Journal Entry": "client/journal_entry.js"
"Journal Entry": "client/journal_entry.js",
"Employee": "client/employee.js"
}
# Хуки для добавления обработчиков событий Purchase Order
@ -52,6 +54,9 @@ doc_events = {
},
"E-Taxes Settings": {
"on_update": "invoice_az.api.update_mapped_statuses"
},
"Employee": {
"on_trash": "invoice_az.emas_api.on_delete_employee"
}
}

View File

@ -5,7 +5,28 @@ frappe.ui.form.on('Asan Login', {
clearInterval(window.authStatusInterval);
window.authStatusInterval = null;
}
// Add EMAS buttons
add_emas_buttons(frm);
// Кнопка для MyGovID Login
frm.add_custom_button(__('MyGovID Login'), function() {
// Проверяем, что поля phone и user_id заполнены
if (!frm.doc.phone || !frm.doc.user_id) {
frappe.msgprint(__('Please fill in Phone and User ID fields'));
return;
}
// Показываем сообщение о том, что запрос отправлен
frappe.show_alert({
message: __('MyGovID authentication request sent. Please confirm on your phone.'),
indicator: 'blue'
}, 5);
// Start MyGovID login with status dialog
startMyGovIDLogin(frm);
}, __('Login'));
// Кнопка для получения токена
frm.add_custom_button(__('Login with Asan Imza'), function() {
// Проверяем, что поля phone и user_id заполнены
@ -807,3 +828,842 @@ function pollAuthenticationStatus(frm, successCallback) {
// Начинаем опрос
pollStatus();
}
// Функция для MyGovID Login с диалогом статуса
function startMyGovIDLogin(frm) {
// Флаг для остановки
window.mygovidStopLogin = false;
// Создаем всплывающее окно с информацией
const status_dialog = new frappe.ui.Dialog({
title: __('MyGovID Authentication'),
fields: [
{
fieldname: 'status_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="mygovid-spinner" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: mygovid-spin 1.2s linear infinite;"></div>
</div>
<style>
@keyframes mygovid-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="mygovid_status_message">
<h4>Waiting for confirmation on your phone</h4>
<p>Please check your phone and confirm the authentication request in ASAN Sign app.</p>
</div>
<!-- Блок для отображения кода верификации -->
<div id="mygovid_verification_code_container" style="display:none; margin-top: 15px;">
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
<span id="mygovid_verification_code">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
</div>
`
}
],
primary_action_label: __('Cancel'),
primary_action: function() {
window.mygovidStopLogin = true;
status_dialog.hide();
frm.reload_doc();
}
});
status_dialog.show();
// Start long-polling POST request for login
frappe.call({
method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.start_mygovid_login',
args: {
'asan_login_name': frm.doc.name
},
timeout: 130000, // 130 seconds timeout
callback: function(r) {
if (window.mygovidStopLogin) return;
if (r.message && r.message.success) {
// Успех
$('#mygovid_status_message').html(`
<h4 style="color: green;">Authentication Successful!</h4>
<p>MyGovID token has been saved. The page will reload shortly.</p>
`);
$('.mygovid-spinner').css('display', 'none');
$('.mygovid-spinner').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
status_dialog.set_primary_action(__('Continue'), function() {
status_dialog.hide();
frm.reload_doc();
});
// Автоматически закрываем через 2 секунды
setTimeout(function() {
status_dialog.hide();
frm.reload_doc();
}, 2000);
} else {
// Ошибка
$('#mygovid_status_message').html(`
<h4 style="color: red;">Authentication Failed</h4>
<p>${r.message ? r.message.message : 'An unknown error occurred.'}</p>
`);
$('.mygovid-spinner').css('display', 'none');
$('.mygovid-spinner').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
</div>
`);
$('#mygovid_verification_code_container').hide();
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
frm.reload_doc();
});
}
},
error: function() {
if (window.mygovidStopLogin) return;
$('#mygovid_status_message').html(`
<h4 style="color: red;">Authentication Timeout</h4>
<p>The request timed out. Please try again.</p>
`);
$('.mygovid-spinner').css('display', 'none');
$('#mygovid_verification_code_container').hide();
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
frm.reload_doc();
});
}
});
// Get verification code immediately after starting login
setTimeout(function() {
frappe.call({
method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code',
args: { 'asan_login_name': frm.doc.name },
callback: function(r) {
if (r.message && r.message.success) {
$('#mygovid_verification_code').text(r.message.code);
$('#mygovid_verification_code_container').show();
$('#mygovid_verification_code_container').attr('style', 'display: block !important; margin-top: 15px;');
}
}
});
}, 500);
}
// ============================================
// ƏMAS (e-social.gov.az) Integration Functions
// ============================================
function add_emas_buttons(frm) {
// Only show ƏMAS buttons if user is authenticated with e-taxes or MyGovID
const is_authenticated = frm.doc.auth_status === 'Authenticated' ||
frm.doc.auth_status === 'Fully Authenticated' ||
frm.doc.mygovid_token;
if (!is_authenticated) {
return;
}
// Check current ƏMAS status
const emas_status = frm.doc.emas_auth_status || 'Not Connected';
if (emas_status === 'Connected' && frm.doc.emas_account_oid) {
// ƏMAS is connected - show employee buttons
frm.add_custom_button(__('Get Employees'), function() {
show_employees_dialog(frm);
}, __('ƏMAS'));
frm.add_custom_button(__('Contract Stats'), function() {
show_contract_stats(frm);
}, __('ƏMAS'));
frm.add_custom_button(__('Refresh Session'), function() {
refresh_emas_session(frm);
}, __('ƏMAS'));
frm.add_custom_button(__('Disconnect'), function() {
disconnect_emas(frm);
}, __('ƏMAS'));
} else if (emas_status === 'Connected' && !frm.doc.emas_account_oid) {
// Connected but no account selected
frm.add_custom_button(__('Select Organization'), function() {
show_accounts_dialog(frm);
}, __('ƏMAS'));
frm.add_custom_button(__('Refresh Session'), function() {
refresh_emas_session(frm);
}, __('ƏMAS'));
frm.add_custom_button(__('Disconnect'), function() {
disconnect_emas(frm);
}, __('ƏMAS'));
} else {
// Not connected - show connect button
frm.add_custom_button(__('Connect ƏMAS'), function() {
start_emas_connection(frm);
}, __('ƏMAS'));
}
}
function start_emas_connection(frm) {
// Show loading indicator
frappe.show_alert({
message: __('Connecting to ƏMAS...'),
indicator: 'blue'
}, 3);
frappe.call({
method: 'invoice_az.emas_api.connect_emas',
args: {
'asan_login_name': frm.doc.name
},
freeze: true,
freeze_message: __('Connecting to ƏMAS...'),
callback: function(r) {
if (r.message && r.message.success) {
// If accounts returned, show selection dialog
const accounts = r.message.accounts || [];
if (accounts.length > 0) {
frappe.show_alert({
message: __('ƏMAS connected! Please select an organization.'),
indicator: 'blue'
}, 5);
frm.reload_doc().then(function() {
show_accounts_dialog_with_data(frm, accounts);
});
} else {
frappe.show_alert({
message: __('ƏMAS connected but no organizations found.'),
indicator: 'orange'
}, 5);
frm.reload_doc();
}
} else if (r.message && r.message.need_mygovid_login) {
// Need to login with MyGovID first
frappe.confirm(
__('MyGovID authentication required. Do you want to login with MyGovID now?'),
function() {
// Start MyGovID login, then connect to ƏMAS
start_mygovid_then_emas(frm);
}
);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to connect to ƏMAS')
});
}
}
});
}
function start_mygovid_then_emas(frm) {
// Show MyGovID login dialog with ƏMAS callback
frappe.show_alert({
message: __('Starting MyGovID authentication...'),
indicator: 'blue'
}, 3);
// Create status dialog
const status_dialog = new frappe.ui.Dialog({
title: __('MyGovID Authentication for ƏMAS'),
fields: [
{
fieldname: 'status_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="mygovid-emas-spinner" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: mygovid-emas-spin 1.2s linear infinite;"></div>
</div>
<style>
@keyframes mygovid-emas-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="mygovid_emas_status_message">
<h4>Waiting for confirmation on your phone</h4>
<p>Please check your phone and confirm the authentication request in ASAN Sign app.</p>
</div>
<div id="mygovid_emas_code_container" style="display:none; margin-top: 15px;">
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
<span id="mygovid_emas_code">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
</div>
`
}
],
primary_action_label: __('Cancel'),
primary_action: function() {
window.mygovidEmasStop = true;
status_dialog.hide();
frm.reload_doc();
}
});
status_dialog.show();
window.mygovidEmasStop = false;
// Start MyGovID login
frappe.call({
method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.start_mygovid_login',
args: {
'asan_login_name': frm.doc.name
},
timeout: 130000,
callback: function(r) {
if (window.mygovidEmasStop) return;
if (r.message && r.message.success) {
// MyGovID login successful, now connect to ƏMAS
$('#mygovid_emas_status_message').html(`
<h4 style="color: blue;">Connecting to ƏMAS...</h4>
<p>MyGovID authentication successful. Now connecting to ƏMAS...</p>
`);
$('#mygovid_emas_code_container').hide();
// Now connect to ƏMAS
frappe.call({
method: 'invoice_az.emas_api.connect_emas',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r2) {
if (r2.message && r2.message.success) {
$('#mygovid_emas_status_message').html(`
<h4 style="color: green;">ƏMAS Connected!</h4>
<p>Successfully connected to ƏMAS.</p>
`);
$('.mygovid-emas-spinner').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
status_dialog.set_primary_action(__('Continue'), function() {
status_dialog.hide();
const accounts = r2.message.accounts || [];
frm.reload_doc().then(function() {
if (accounts.length > 0) {
show_accounts_dialog_with_data(frm, accounts);
}
});
});
// Auto close after 2 seconds
setTimeout(function() {
status_dialog.hide();
const accounts = r2.message.accounts || [];
frm.reload_doc().then(function() {
if (accounts.length > 0) {
show_accounts_dialog_with_data(frm, accounts);
}
});
}, 2000);
} else {
$('#mygovid_emas_status_message').html(`
<h4 style="color: red;">ƏMAS Connection Failed</h4>
<p>${r2.message ? r2.message.message : 'Unknown error'}</p>
`);
$('.mygovid-emas-spinner').css('display', 'none');
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
frm.reload_doc();
});
}
}
});
} else {
// MyGovID login failed
$('#mygovid_emas_status_message').html(`
<h4 style="color: red;">Authentication Failed</h4>
<p>${r.message ? r.message.message : 'Unknown error'}</p>
`);
$('.mygovid-emas-spinner').css('display', 'none');
$('#mygovid_emas_code_container').hide();
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
frm.reload_doc();
});
}
},
error: function() {
if (window.mygovidEmasStop) return;
$('#mygovid_emas_status_message').html(`
<h4 style="color: red;">Authentication Timeout</h4>
<p>The request timed out. Please try again.</p>
`);
$('.mygovid-emas-spinner').css('display', 'none');
$('#mygovid_emas_code_container').hide();
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
frm.reload_doc();
});
}
});
// Get verification code
setTimeout(function() {
frappe.call({
method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code',
args: { 'asan_login_name': frm.doc.name },
callback: function(r) {
if (r.message && r.message.success) {
$('#mygovid_emas_code').text(r.message.code);
$('#mygovid_emas_code_container').show();
}
}
});
}, 500);
}
function show_accounts_dialog_with_data(frm, accounts) {
if (!accounts || accounts.length === 0) {
frappe.msgprint(__('No organizations found in ƏMAS'));
return;
}
// Always show dialog - user must explicitly select account
// (don't auto-select even if only one account, as it might be employee not company)
// Build accounts table
let table_html = '<div style="max-height: 400px; overflow-y: auto;">';
table_html += '<table class="table table-bordered table-hover">';
table_html += '<thead><tr><th>Organization Name</th><th>VOEN</th><th>Role</th><th>Action</th></tr></thead>';
table_html += '<tbody>';
accounts.forEach(function(account, index) {
const name = account.accountName || account.name || 'Unknown';
const voen = account.accountNumber || account.voen || account.number || '';
const role = account.roleName || account.role || '';
const oid = account.accountOid || account.oid || '';
table_html += `<tr>
<td>${frappe.utils.escape_html(name)}</td>
<td>${frappe.utils.escape_html(voen)}</td>
<td>${frappe.utils.escape_html(role)}</td>
<td>
<button class="btn btn-xs btn-primary select-emas-account"
data-oid="${frappe.utils.escape_html(oid)}"
data-name="${frappe.utils.escape_html(name)}"
data-number="${frappe.utils.escape_html(voen)}"
data-role="${frappe.utils.escape_html(role)}">
Select
</button>
</td>
</tr>`;
});
table_html += '</tbody></table></div>';
const d = new frappe.ui.Dialog({
title: __('Select ƏMAS Organization'),
fields: [{
fieldname: 'accounts_html',
fieldtype: 'HTML',
options: table_html
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Add click handler for account selection
d.$wrapper.find('.select-emas-account').on('click', function() {
const oid = $(this).data('oid');
const name = $(this).data('name');
const number = $(this).data('number');
const role = $(this).data('role');
select_emas_account(frm, d, oid, name, number, role);
});
}
function show_accounts_dialog(frm) {
frappe.call({
method: 'invoice_az.emas_api.get_emas_accounts',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
const accounts = r.message.accounts || [];
if (accounts.length === 0) {
frappe.msgprint(__('No organizations found in ƏMAS'));
return;
}
// Build accounts table
let table_html = '<div style="max-height: 400px; overflow-y: auto;">';
table_html += '<table class="table table-bordered table-hover">';
table_html += '<thead><tr><th>Organization Name</th><th>VOEN</th><th>Role</th><th>Action</th></tr></thead>';
table_html += '<tbody>';
accounts.forEach(function(account, index) {
const name = account.accountName || account.name || 'Unknown';
const voen = account.accountNumber || account.voen || account.number || '';
const role = account.roleName || account.role || '';
const oid = account.accountOid || account.oid || '';
table_html += `<tr>
<td>${frappe.utils.escape_html(name)}</td>
<td>${frappe.utils.escape_html(voen)}</td>
<td>${frappe.utils.escape_html(role)}</td>
<td>
<button class="btn btn-xs btn-primary select-emas-account"
data-oid="${frappe.utils.escape_html(oid)}"
data-name="${frappe.utils.escape_html(name)}"
data-number="${frappe.utils.escape_html(voen)}"
data-role="${frappe.utils.escape_html(role)}">
Select
</button>
</td>
</tr>`;
});
table_html += '</tbody></table></div>';
const d = new frappe.ui.Dialog({
title: __('Select ƏMAS Organization'),
fields: [{
fieldname: 'accounts_html',
fieldtype: 'HTML',
options: table_html
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Add click handler for account selection
d.$wrapper.find('.select-emas-account').on('click', function() {
const oid = $(this).data('oid');
const name = $(this).data('name');
const number = $(this).data('number');
const role = $(this).data('role');
select_emas_account(frm, d, oid, name, number, role);
});
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to get ƏMAS accounts')
});
}
}
});
}
function select_emas_account(frm, dialog, oid, name, number, role) {
frappe.call({
method: 'invoice_az.emas_api.change_emas_account',
args: {
'asan_login_name': frm.doc.name,
'account_oid': oid,
'account_name': name,
'account_number': number,
'role_name': role || 'chairman'
},
freeze: true,
freeze_message: __('Selecting organization...'),
callback: function(r) {
if (r.message && r.message.success) {
if (dialog) {
dialog.hide();
}
frappe.show_alert({
message: __('Organization selected: ') + name,
indicator: 'green'
}, 5);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to select organization')
});
}
}
});
}
function show_employees_dialog(frm) {
const d = new frappe.ui.Dialog({
title: __('ƏMAS Employees'),
size: 'extra-large',
fields: [
{
fieldname: 'loading_html',
fieldtype: 'HTML',
options: `
<div class="text-center" id="emas_loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p class="mt-3">Loading employees...</p>
</div>
<div id="emas_employees_container" style="display: none;">
<div class="mb-3">
<strong>Organization:</strong> ${frm.doc.emas_account_name || ''}
(VOEN: ${frm.doc.emas_account_number || ''})
</div>
<div id="emas_employees_table"></div>
<div id="emas_pagination" class="mt-3"></div>
</div>
`
}
],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Load employees
load_employees(frm, d, 0, 100);
}
function load_employees(frm, dialog, offset, limit) {
frappe.call({
method: 'invoice_az.emas_api.get_employees_report',
args: {
'asan_login_name': frm.doc.name,
'offset': offset,
'limit': limit
},
callback: function(r) {
dialog.$wrapper.find('#emas_loading').hide();
dialog.$wrapper.find('#emas_employees_container').show();
if (r.message && r.message.success) {
const employees = r.message.employees || [];
render_employees_table(dialog, employees, offset, limit, r.message.has_more);
} else {
dialog.$wrapper.find('#emas_employees_table').html(`
<div class="alert alert-danger">
${r.message ? r.message.message : __('Failed to load employees')}
</div>
`);
}
}
});
}
function render_employees_table(dialog, employees, offset, limit, has_more) {
if (employees.length === 0) {
dialog.$wrapper.find('#emas_employees_table').html(`
<div class="alert alert-info">
${__('No employees found')}
</div>
`);
return;
}
// Build table
let table_html = '<div style="max-height: 500px; overflow-y: auto;">';
table_html += '<table class="table table-bordered table-striped table-hover" style="font-size: 12px;">';
table_html += '<thead><tr>';
// Get column names from first employee
const columns = Object.keys(employees[0]);
// Map column names to readable labels
const column_labels = {
'adi': 'Name',
'soyadi': 'Surname',
'ataAdi': 'Father Name',
'fin': 'FIN',
'vezife': 'Position',
'muqavileNo': 'Contract No',
'muqavileTarixi': 'Contract Date',
'status': 'Status',
'emekHaqqi': 'Salary',
'isBaslamaTarixi': 'Start Date',
'isBitmeTarixi': 'End Date'
};
columns.forEach(function(col) {
const label = column_labels[col] || col;
table_html += `<th>${frappe.utils.escape_html(label)}</th>`;
});
table_html += '</tr></thead><tbody>';
employees.forEach(function(emp) {
table_html += '<tr>';
columns.forEach(function(col) {
const value = emp[col] || '';
table_html += `<td>${frappe.utils.escape_html(String(value))}</td>`;
});
table_html += '</tr>';
});
table_html += '</tbody></table></div>';
dialog.$wrapper.find('#emas_employees_table').html(table_html);
// Pagination info
const showing_from = offset + 1;
const showing_to = offset + employees.length;
let pagination_html = `<p>Showing ${showing_from} - ${showing_to} employees`;
if (has_more) {
pagination_html += ` <button class="btn btn-sm btn-default load-more-employees">Load More</button>`;
}
pagination_html += '</p>';
dialog.$wrapper.find('#emas_pagination').html(pagination_html);
}
function show_contract_stats(frm) {
frappe.call({
method: 'invoice_az.emas_api.get_contract_stats',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
const stats = r.message.stats || {};
const d = new frappe.ui.Dialog({
title: __('ƏMAS Contract Statistics'),
fields: [{
fieldname: 'stats_html',
fieldtype: 'HTML',
options: `
<div style="padding: 20px;">
<h4>${frm.doc.emas_account_name || 'Organization'}</h4>
<p class="text-muted">VOEN: ${frm.doc.emas_account_number || ''}</p>
<hr>
<div class="row">
<div class="col-md-4 text-center">
<h2 class="text-success">${stats.aktiv || 0}</h2>
<p>Active Contracts</p>
</div>
<div class="col-md-4 text-center">
<h2 class="text-warning">${stats.icrada || 0}</h2>
<p>In Process</p>
</div>
<div class="col-md-4 text-center">
<h2 class="text-info">${stats.emrler || 0}</h2>
<p>Orders</p>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6 text-center">
<h2 class="text-danger">${stats.etibarsiz || 0}</h2>
<p>Invalid</p>
</div>
<div class="col-md-6 text-center">
<h2 class="text-muted">${stats.quvvedendusmus || 0}</h2>
<p>Terminated</p>
</div>
</div>
</div>
`
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to get contract statistics')
});
}
}
});
}
function refresh_emas_session(frm) {
frappe.call({
method: 'invoice_az.emas_api.refresh_emas_session',
args: {
'asan_login_name': frm.doc.name
},
freeze: true,
freeze_message: __('Refreshing ƏMAS session...'),
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('ƏMAS session refreshed successfully'),
indicator: 'green'
}, 3);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to refresh ƏMAS session')
});
}
}
});
}
function disconnect_emas(frm) {
frappe.confirm(
__('Are you sure you want to disconnect from ƏMAS?'),
function() {
frappe.call({
method: 'invoice_az.emas_api.disconnect_emas',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Disconnected from ƏMAS'),
indicator: 'blue'
}, 3);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to disconnect from ƏMAS')
});
}
}
});
}
);
}

View File

@ -22,7 +22,18 @@
"selected_certificate_json",
"taxpayer_section",
"main_token",
"choose_taxpayer_response"
"choose_taxpayer_response",
"mygovid_section",
"mygovid_token",
"emas_section",
"emas_auth_status",
"emas_account_name",
"emas_account_number",
"column_break_emas",
"emas_account_oid",
"emas_last_activity",
"emas_session",
"emas_csrf_token"
],
"fields": [
{
@ -47,6 +58,7 @@
{
"fieldname": "bearer_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Bearer Token"
},
{
@ -109,6 +121,7 @@
"description": "Primary token for all API operations",
"fieldname": "main_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Main Token"
},
{
@ -122,20 +135,87 @@
{
"fieldname": "verification_code",
"fieldtype": "Data",
"hidden": 1,
"label": "Verification Code"
},
{
"fieldname": "mygovid_section",
"fieldtype": "Section Break",
"label": "MyGovID"
},
{
"description": "Token from mygovid.gov.az for other government services",
"fieldname": "mygovid_token",
"fieldtype": "Small Text",
"label": "MyGovID Token"
},
{
"fieldname": "last_activity_time",
"fieldtype": "Datetime",
"label": "Last Activity Time"
},
{
"fieldname": "emas_section",
"fieldtype": "Section Break",
"label": "ƏMAS (e-social.gov.az)"
},
{
"default": "Not Connected",
"fieldname": "emas_auth_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "ƏMAS Status",
"options": "Not Connected\nWaiting for SSO\nConnected\nError",
"read_only": 1
},
{
"fieldname": "emas_account_name",
"fieldtype": "Data",
"label": "ƏMAS Account Name",
"read_only": 1
},
{
"fieldname": "emas_account_number",
"fieldtype": "Data",
"label": "ƏMAS Account VOEN",
"read_only": 1
},
{
"fieldname": "column_break_emas",
"fieldtype": "Column Break"
},
{
"fieldname": "emas_account_oid",
"fieldtype": "Small Text",
"hidden": 1,
"label": "EMAS Account OID"
},
{
"fieldname": "emas_last_activity",
"fieldtype": "Datetime",
"label": "ƏMAS Last Activity",
"read_only": 1
},
{
"fieldname": "emas_session",
"fieldtype": "Small Text",
"hidden": 1,
"label": "ƏMAS Session"
},
{
"fieldname": "emas_csrf_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "ƏMAS CSRF Token"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-21 14:13:47.059215",
"modified": "2026-02-03 19:43:35.729888",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "Asan Login",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
@ -151,7 +231,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@ -1,7 +1,19 @@
from __future__ import unicode_literals
import frappe
import requests
from frappe.model.document import Document
MYGOVID_BASE_URL = "https://api.mygovid.gov.az"
MYGOVID_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "az",
"Content-Type": "application/json",
"Origin": "https://mygovid.gov.az",
"Referer": "https://mygovid.gov.az/",
}
class AsanLogin(Document):
def before_save(self):
# Если установлен флаг is_default, сбрасываем его у других документов
@ -24,6 +36,111 @@ class AsanLogin(Document):
# Проверяем обязательные поля
if not self.phone:
frappe.throw("Phone is required")
if not self.user_id:
frappe.throw("User ID is required")
frappe.throw("User ID is required")
@frappe.whitelist()
def start_mygovid_login(asan_login_name):
"""
Start MyGovID ASAN Sign login.
This is a long-polling request that waits for user confirmation in ASAN Sign app.
Returns JWT token after user confirms.
"""
doc = frappe.get_doc("Asan Login", asan_login_name)
if not doc.user_id or not doc.phone:
return {"success": False, "message": "User ID and phone number are required"}
url = f"{MYGOVID_BASE_URL}/ssoauth/api/v1/logIn/asanSignLogin"
payload = {
"mobileKey": None,
"phoneNumber": doc.phone,
"userId": doc.user_id,
"recaptcha3Response": "",
}
try:
# Long timeout - waits for user to confirm in ASAN Sign app
response = requests.post(
url, json=payload, headers=MYGOVID_HEADERS, timeout=120
)
if response.status_code in [200, 201]:
data = response.json()
token = data.get("data", {}).get("token")
# Save token to document
if token:
doc.mygovid_token = token
doc.save(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"token": token,
"transaction": data.get("transaction"),
}
else:
return {
"success": False,
"message": f"API error: status {response.status_code}",
"response_data": response.text,
}
except requests.exceptions.Timeout:
return {
"success": False,
"message": "Request timed out. User did not confirm in ASAN Sign app.",
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"MyGovID API error: {str(e)}", "MyGovID API Error")
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
frappe.log_error(
f"MyGovID unexpected error: {str(e)}\n{frappe.get_traceback()}",
"MyGovID API Error",
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def get_mygovid_verification_code(asan_login_name):
"""
Get verification code for MyGovID ASAN Sign.
Call this immediately after starting login to get the 4-digit code
that user needs to confirm in ASAN Sign app.
"""
doc = frappe.get_doc("Asan Login", asan_login_name)
if not doc.user_id:
return {"success": False, "message": "User ID is required"}
url = f"{MYGOVID_BASE_URL}/ssoauth/api/v1/logIn/asanSignLogin/verificationCode/{doc.user_id}"
try:
response = requests.get(url, headers=MYGOVID_HEADERS, timeout=30)
if response.status_code in [200, 201]:
data = response.json()
return {
"success": True,
"code": data.get("data", {}).get("code"),
"transaction": data.get("transaction"),
}
else:
return {
"success": False,
"message": f"API error: status {response.status_code}",
"response_data": response.text,
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"MyGovID API error: {str(e)}", "MyGovID API Error")
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
frappe.log_error(
f"MyGovID unexpected error: {str(e)}\n{frappe.get_traceback()}",
"MyGovID API Error",
)
return {"success": False, "message": f"Error: {str(e)}"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
# Copyright (c) 2026, Your Company and contributors
# For license information, please see license.txt

View File

@ -0,0 +1,20 @@
// Copyright (c) 2026, Your Company and contributors
// For license information, please see license.txt
frappe.ui.form.on('Emas Employees', {
refresh: function(frm) {
// Form view - show employee details
if (!frm.is_new()) {
// Add button to view raw data if needed
if (frm.doc.raw_data) {
frm.add_custom_button(__('View Raw Data'), function() {
frappe.msgprint({
title: __('Raw Data from ƏMAS'),
message: `<pre style="max-height: 400px; overflow: auto;">${frm.doc.raw_data}</pre>`,
wide: true
});
});
}
}
}
});

View File

@ -0,0 +1,958 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:EMAS-EMP-{####}",
"creation": "2026-02-06 10:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"employee_section",
"full_name",
"identification_number",
"ssn",
"column_break_emp1",
"birthday",
"work_position_text",
"monthly_salary",
"personal_info_section",
"father_name",
"gender",
"nationality",
"blood_type",
"column_break_personal1",
"marital_status",
"military_status",
"personal_address",
"personal_city",
"personal_district",
"document_section",
"document_number",
"document_issue_date",
"document_expiry_date",
"column_break_document1",
"document_issued_by",
"contract_section",
"doc_no",
"doc_date",
"doc_type",
"contract_type",
"column_break_contract1",
"contract_status",
"begin_date",
"end_date",
"main_contract_begin_date",
"contract_details_section",
"contract_number",
"prev_contract",
"is_signed",
"contract_validity",
"column_break_contract_details1",
"staff_oid",
"structure_unit_oid",
"entity_oid",
"entity_details_oid",
"contract_details_section2",
"contract_reason",
"from_migration",
"contract_begin_date",
"contract_end_date",
"position_activity_section",
"work_position",
"position_id",
"position_text",
"activity_code",
"column_break_position1",
"activity_name",
"staff_unit",
"structure_path",
"workplace_sector",
"salary_compensation_section",
"base_salary",
"salary_add",
"salary_add_hw",
"award_amount",
"award_type",
"column_break_salary1",
"overtime_amount",
"bad_condition_amount",
"bad_condition_id",
"currency_type",
"payment_type",
"payment_first_day",
"bank_oid",
"work_schedule_section",
"work_mode_type",
"is_full_time",
"work_time",
"working_time_cumulative",
"column_break_schedule1",
"first_shift_start",
"first_shift_end",
"second_shift_start",
"second_shift_end",
"lunch_start",
"lunch_end",
"vacation_section",
"vacation_days_count",
"vacation_general_duration",
"vacation_main_duration",
"vacation_add_dur_one",
"column_break_vacation1",
"vacation_add_dur_two",
"vacation_add_dur_four",
"vacation_amount",
"profession_employee_for_vacation",
"education_section",
"education",
"scientific_degree",
"speciality_degree",
"column_break_education1",
"state_speciality_degree",
"qualification_degree_salary_add",
"contact_info_section",
"contact_phone",
"contact_email",
"column_break_contact1",
"work_address_oid",
"work_address_street",
"work_address_district",
"employer_info_section",
"employer_pin",
"employer_tin",
"chairman_pin",
"additional_metadata_section",
"doc_state",
"created_date",
"labour_function",
"column_break_metadata1",
"protection_tools",
"payment_other_cond",
"activity_name_detail",
"rule_text",
"dates_section",
"emp_job_start_date",
"sign_type",
"is_valid",
"column_break_dates1",
"dur_state",
"operation",
"after_exec_label",
"details_section",
"workplace_type",
"form_size",
"flow",
"column_break_details1",
"flow_alias",
"tree_path_name",
"view_type",
"doc_oid",
"organization_section",
"asan_login",
"organization_name",
"organization_voen",
"column_break_org1",
"employee",
"import_date",
"contract_document_section",
"contract_html",
"raw_data"
],
"fields": [
{
"fieldname": "employee_section",
"fieldtype": "Section Break",
"label": "Employee"
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Full Name"
},
{
"fieldname": "identification_number",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "FIN",
"reqd": 1
},
{
"fieldname": "ssn",
"fieldtype": "Data",
"label": "SSN"
},
{
"fieldname": "column_break_emp1",
"fieldtype": "Column Break"
},
{
"fieldname": "birthday",
"fieldtype": "Data",
"label": "Birthday"
},
{
"fieldname": "work_position_text",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Position"
},
{
"fieldname": "monthly_salary",
"fieldtype": "Data",
"label": "Monthly Salary"
},
{
"fieldname": "personal_info_section",
"fieldtype": "Section Break",
"label": "Personal Information",
"collapsible": 1
},
{
"fieldname": "father_name",
"fieldtype": "Data",
"label": "Father Name"
},
{
"fieldname": "gender",
"fieldtype": "Data",
"label": "Gender"
},
{
"fieldname": "nationality",
"fieldtype": "Data",
"label": "Nationality"
},
{
"fieldname": "blood_type",
"fieldtype": "Data",
"label": "Blood Type"
},
{
"fieldname": "column_break_personal1",
"fieldtype": "Column Break"
},
{
"fieldname": "marital_status",
"fieldtype": "Data",
"label": "Marital Status"
},
{
"fieldname": "military_status",
"fieldtype": "Data",
"label": "Military Status"
},
{
"fieldname": "personal_address",
"fieldtype": "Small Text",
"label": "Personal Address"
},
{
"fieldname": "personal_city",
"fieldtype": "Data",
"label": "Personal City"
},
{
"fieldname": "personal_district",
"fieldtype": "Data",
"label": "Personal District"
},
{
"fieldname": "document_section",
"fieldtype": "Section Break",
"label": "Document Information",
"collapsible": 1
},
{
"fieldname": "document_number",
"fieldtype": "Data",
"label": "Document Number"
},
{
"fieldname": "document_issue_date",
"fieldtype": "Date",
"label": "Document Issue Date"
},
{
"fieldname": "document_expiry_date",
"fieldtype": "Date",
"label": "Document Expiry Date"
},
{
"fieldname": "column_break_document1",
"fieldtype": "Column Break"
},
{
"fieldname": "document_issued_by",
"fieldtype": "Data",
"label": "Document Issued By"
},
{
"fieldname": "contract_section",
"fieldtype": "Section Break",
"label": "Contract"
},
{
"fieldname": "doc_no",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Document No"
},
{
"fieldname": "doc_date",
"fieldtype": "Data",
"label": "Document Date"
},
{
"fieldname": "doc_type",
"fieldtype": "Data",
"label": "Document Type"
},
{
"fieldname": "contract_type",
"fieldtype": "Data",
"label": "Contract Type"
},
{
"fieldname": "column_break_contract1",
"fieldtype": "Column Break"
},
{
"fieldname": "contract_status",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Contract Status"
},
{
"fieldname": "begin_date",
"fieldtype": "Data",
"label": "Begin Date"
},
{
"fieldname": "end_date",
"fieldtype": "Data",
"label": "End Date"
},
{
"fieldname": "main_contract_begin_date",
"fieldtype": "Data",
"label": "Main Contract Begin Date"
},
{
"fieldname": "contract_details_section",
"fieldtype": "Section Break",
"label": "Contract Details",
"collapsible": 1
},
{
"fieldname": "contract_number",
"fieldtype": "Data",
"label": "Contract Number"
},
{
"fieldname": "prev_contract",
"fieldtype": "Data",
"label": "Previous Contract"
},
{
"fieldname": "is_signed",
"fieldtype": "Check",
"label": "Is Signed"
},
{
"fieldname": "contract_validity",
"fieldtype": "Data",
"label": "Contract Validity"
},
{
"fieldname": "column_break_contract_details1",
"fieldtype": "Column Break"
},
{
"fieldname": "staff_oid",
"fieldtype": "Data",
"label": "Staff OID"
},
{
"fieldname": "structure_unit_oid",
"fieldtype": "Data",
"label": "Structure Unit OID"
},
{
"fieldname": "entity_oid",
"fieldtype": "Data",
"label": "Entity OID"
},
{
"fieldname": "entity_details_oid",
"fieldtype": "Data",
"label": "Entity Details OID"
},
{
"fieldname": "contract_details_section2",
"fieldtype": "Section Break",
"collapsible": 1
},
{
"fieldname": "contract_reason",
"fieldtype": "Data",
"label": "Contract Reason"
},
{
"fieldname": "from_migration",
"fieldtype": "Check",
"label": "From Migration"
},
{
"fieldname": "contract_begin_date",
"fieldtype": "Date",
"label": "Contract Begin Date"
},
{
"fieldname": "contract_end_date",
"fieldtype": "Date",
"label": "Contract End Date"
},
{
"fieldname": "position_activity_section",
"fieldtype": "Section Break",
"label": "Position & Activity",
"collapsible": 1
},
{
"fieldname": "work_position",
"fieldtype": "Data",
"label": "Work Position"
},
{
"fieldname": "position_id",
"fieldtype": "Data",
"label": "Position ID"
},
{
"fieldname": "position_text",
"fieldtype": "Data",
"label": "Position Text"
},
{
"fieldname": "activity_code",
"fieldtype": "Data",
"label": "Activity Code"
},
{
"fieldname": "column_break_position1",
"fieldtype": "Column Break"
},
{
"fieldname": "activity_name",
"fieldtype": "Data",
"label": "Activity Name"
},
{
"fieldname": "staff_unit",
"fieldtype": "Data",
"label": "Staff Unit"
},
{
"fieldname": "structure_path",
"fieldtype": "Small Text",
"label": "Structure Path"
},
{
"fieldname": "workplace_sector",
"fieldtype": "Data",
"label": "Workplace Sector"
},
{
"fieldname": "salary_compensation_section",
"fieldtype": "Section Break",
"label": "Salary & Compensation",
"collapsible": 1
},
{
"fieldname": "base_salary",
"fieldtype": "Float",
"label": "Base Salary"
},
{
"fieldname": "salary_add",
"fieldtype": "Float",
"label": "Salary Add"
},
{
"fieldname": "salary_add_hw",
"fieldtype": "Float",
"label": "Salary Add HW"
},
{
"fieldname": "award_amount",
"fieldtype": "Float",
"label": "Award Amount"
},
{
"fieldname": "award_type",
"fieldtype": "Data",
"label": "Award Type"
},
{
"fieldname": "column_break_salary1",
"fieldtype": "Column Break"
},
{
"fieldname": "overtime_amount",
"fieldtype": "Float",
"label": "Overtime Amount"
},
{
"fieldname": "bad_condition_amount",
"fieldtype": "Float",
"label": "Bad Condition Amount"
},
{
"fieldname": "bad_condition_id",
"fieldtype": "Data",
"label": "Bad Condition ID"
},
{
"fieldname": "currency_type",
"fieldtype": "Data",
"label": "Currency Type"
},
{
"fieldname": "payment_type",
"fieldtype": "Data",
"label": "Payment Type"
},
{
"fieldname": "payment_first_day",
"fieldtype": "Int",
"label": "Payment First Day"
},
{
"fieldname": "bank_oid",
"fieldtype": "Data",
"label": "Bank OID"
},
{
"fieldname": "work_schedule_section",
"fieldtype": "Section Break",
"label": "Work Schedule",
"collapsible": 1
},
{
"fieldname": "work_mode_type",
"fieldtype": "Data",
"label": "Work Mode Type"
},
{
"fieldname": "is_full_time",
"fieldtype": "Data",
"label": "Is Full Time"
},
{
"fieldname": "work_time",
"fieldtype": "Float",
"label": "Work Time"
},
{
"fieldname": "working_time_cumulative",
"fieldtype": "Check",
"label": "Working Time Cumulative"
},
{
"fieldname": "column_break_schedule1",
"fieldtype": "Column Break"
},
{
"fieldname": "first_shift_start",
"fieldtype": "Time",
"label": "First Shift Start"
},
{
"fieldname": "first_shift_end",
"fieldtype": "Time",
"label": "First Shift End"
},
{
"fieldname": "second_shift_start",
"fieldtype": "Time",
"label": "Second Shift Start"
},
{
"fieldname": "second_shift_end",
"fieldtype": "Time",
"label": "Second Shift End"
},
{
"fieldname": "lunch_start",
"fieldtype": "Time",
"label": "Lunch Start"
},
{
"fieldname": "lunch_end",
"fieldtype": "Time",
"label": "Lunch End"
},
{
"fieldname": "vacation_section",
"fieldtype": "Section Break",
"label": "Vacation",
"collapsible": 1
},
{
"fieldname": "vacation_days_count",
"fieldtype": "Int",
"label": "Vacation Days Count"
},
{
"fieldname": "vacation_general_duration",
"fieldtype": "Int",
"label": "Vacation General Duration"
},
{
"fieldname": "vacation_main_duration",
"fieldtype": "Int",
"label": "Vacation Main Duration"
},
{
"fieldname": "vacation_add_dur_one",
"fieldtype": "Int",
"label": "Vacation Add Duration 1"
},
{
"fieldname": "column_break_vacation1",
"fieldtype": "Column Break"
},
{
"fieldname": "vacation_add_dur_two",
"fieldtype": "Int",
"label": "Vacation Add Duration 2"
},
{
"fieldname": "vacation_add_dur_four",
"fieldtype": "Int",
"label": "Vacation Add Duration 4"
},
{
"fieldname": "vacation_amount",
"fieldtype": "Float",
"label": "Vacation Amount"
},
{
"fieldname": "profession_employee_for_vacation",
"fieldtype": "Data",
"label": "Profession Employee For Vacation"
},
{
"fieldname": "education_section",
"fieldtype": "Section Break",
"label": "Education",
"collapsible": 1
},
{
"fieldname": "education",
"fieldtype": "Data",
"label": "Education"
},
{
"fieldname": "scientific_degree",
"fieldtype": "Data",
"label": "Scientific Degree"
},
{
"fieldname": "speciality_degree",
"fieldtype": "Data",
"label": "Speciality Degree"
},
{
"fieldname": "column_break_education1",
"fieldtype": "Column Break"
},
{
"fieldname": "state_speciality_degree",
"fieldtype": "Data",
"label": "State Speciality Degree"
},
{
"fieldname": "qualification_degree_salary_add",
"fieldtype": "Float",
"label": "Qualification Degree Salary Add"
},
{
"fieldname": "contact_info_section",
"fieldtype": "Section Break",
"label": "Contact Information",
"collapsible": 1
},
{
"fieldname": "contact_phone",
"fieldtype": "Data",
"label": "Contact Phone"
},
{
"fieldname": "contact_email",
"fieldtype": "Data",
"label": "Contact Email"
},
{
"fieldname": "column_break_contact1",
"fieldtype": "Column Break"
},
{
"fieldname": "work_address_oid",
"fieldtype": "Data",
"label": "Work Address OID"
},
{
"fieldname": "work_address_street",
"fieldtype": "Data",
"label": "Work Address Street"
},
{
"fieldname": "work_address_district",
"fieldtype": "Data",
"label": "Work Address District"
},
{
"fieldname": "employer_info_section",
"fieldtype": "Section Break",
"label": "Employer Info",
"collapsible": 1
},
{
"fieldname": "employer_pin",
"fieldtype": "Data",
"label": "Employer PIN"
},
{
"fieldname": "employer_tin",
"fieldtype": "Data",
"label": "Employer TIN"
},
{
"fieldname": "chairman_pin",
"fieldtype": "Data",
"label": "Chairman PIN"
},
{
"fieldname": "additional_metadata_section",
"fieldtype": "Section Break",
"label": "Additional Metadata",
"collapsible": 1
},
{
"fieldname": "doc_state",
"fieldtype": "Data",
"label": "Document State"
},
{
"fieldname": "created_date",
"fieldtype": "Datetime",
"label": "Created Date"
},
{
"fieldname": "labour_function",
"fieldtype": "Small Text",
"label": "Labour Function"
},
{
"fieldname": "column_break_metadata1",
"fieldtype": "Column Break"
},
{
"fieldname": "protection_tools",
"fieldtype": "Small Text",
"label": "Protection Tools"
},
{
"fieldname": "payment_other_cond",
"fieldtype": "Small Text",
"label": "Payment Other Conditions"
},
{
"fieldname": "activity_name_detail",
"fieldtype": "Small Text",
"label": "Activity Name Detail"
},
{
"fieldname": "rule_text",
"fieldtype": "Small Text",
"label": "Rule Text"
},
{
"fieldname": "dates_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fieldname": "emp_job_start_date",
"fieldtype": "Data",
"label": "Job Start Date"
},
{
"fieldname": "sign_type",
"fieldtype": "Data",
"label": "Sign Type"
},
{
"fieldname": "is_valid",
"fieldtype": "Data",
"label": "Is Valid"
},
{
"fieldname": "column_break_dates1",
"fieldtype": "Column Break"
},
{
"fieldname": "dur_state",
"fieldtype": "Data",
"label": "Duration State"
},
{
"fieldname": "operation",
"fieldtype": "Data",
"label": "Operation"
},
{
"fieldname": "after_exec_label",
"fieldtype": "Data",
"label": "After Execution Label"
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Additional",
"collapsible": 1
},
{
"fieldname": "workplace_type",
"fieldtype": "Data",
"label": "Workplace Type"
},
{
"fieldname": "form_size",
"fieldtype": "Data",
"label": "Form Size"
},
{
"fieldname": "flow",
"fieldtype": "Data",
"label": "Flow"
},
{
"fieldname": "column_break_details1",
"fieldtype": "Column Break"
},
{
"fieldname": "flow_alias",
"fieldtype": "Data",
"label": "Flow Alias"
},
{
"fieldname": "tree_path_name",
"fieldtype": "Small Text",
"label": "Tree Path Name"
},
{
"fieldname": "view_type",
"fieldtype": "Data",
"label": "View Type"
},
{
"fieldname": "doc_oid",
"fieldtype": "Data",
"label": "Document OID",
"unique": 1
},
{
"fieldname": "organization_section",
"fieldtype": "Section Break",
"label": "Organization"
},
{
"fieldname": "asan_login",
"fieldtype": "Link",
"label": "Asan Login",
"options": "Asan Login"
},
{
"fieldname": "organization_name",
"fieldtype": "Data",
"label": "Organization Name",
"read_only": 1
},
{
"fieldname": "organization_voen",
"fieldtype": "Data",
"label": "Organization VOEN",
"read_only": 1
},
{
"fieldname": "column_break_org1",
"fieldtype": "Column Break"
},
{
"fieldname": "employee",
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"read_only": 1,
"in_list_view": 1
},
{
"fieldname": "import_date",
"fieldtype": "Datetime",
"label": "Import Date",
"read_only": 1
},
{
"fieldname": "contract_document_section",
"fieldtype": "Section Break",
"label": "Contract Document",
"collapsible": 1
},
{
"fieldname": "contract_html",
"fieldtype": "Code",
"label": "Employment Contract (HTML)",
"options": "HTML",
"hidden": 1
},
{
"fieldname": "raw_data",
"fieldtype": "Code",
"hidden": 1,
"label": "Raw Data (JSON)",
"options": "JSON"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-10 10:00:00",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "Emas Employees",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "full_name",
"track_changes": 1
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2026, Your Company and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class EmasEmployees(Document):
pass

View File

@ -0,0 +1,285 @@
// Copyright (c) 2026, Your Company and contributors
// For license information, please see license.txt
frappe.listview_settings['Emas Employees'] = {
onload: function(listview) {
listview.page.add_inner_button(__('Import from ƏMAS'), function() {
show_import_employees_dialog();
});
}
};
function show_import_employees_dialog() {
frappe.call({
method: 'invoice_az.emas_api.get_connected_asan_logins',
callback: function(r) {
if (r.message && r.message.success) {
const asan_logins = r.message.asan_logins || [];
if (asan_logins.length === 0) {
frappe.msgprint({
title: __('No ƏMAS Connection'),
indicator: 'orange',
message: __('No Asan Login with active ƏMAS connection found. Please connect to ƏMAS first in Asan Login.')
});
return;
}
if (asan_logins.length === 1) {
load_employees_for_import(asan_logins[0].name, asan_logins[0].organization_name);
return;
}
// Multiple logins - show selection
let options = asan_logins.map(al => `${al.organization_name} (${al.organization_voen})`).join('\n');
const d = new frappe.ui.Dialog({
title: __('Select Organization'),
fields: [{
fieldname: 'asan_login',
fieldtype: 'Select',
label: __('Organization'),
reqd: 1,
options: options
}],
primary_action_label: __('Load Employees'),
primary_action: function(values) {
const selected_index = options.split('\n').indexOf(values.asan_login);
if (selected_index >= 0) {
const selected = asan_logins[selected_index];
d.hide();
load_employees_for_import(selected.name, selected.organization_name);
}
}
});
d.show();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to get Asan Logins')
});
}
}
});
}
function load_employees_for_import(asan_login_name, organization_name) {
const loading_dialog = new frappe.ui.Dialog({
title: __('Loading Employees from ƏMAS'),
fields: [{
fieldname: 'loading_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p class="mt-3">${__('Loading employees from')} ${frappe.utils.escape_html(organization_name)}...</p>
</div>
`
}]
});
loading_dialog.show();
frappe.call({
method: 'invoice_az.emas_api.get_employees_report',
args: {
'asan_login_name': asan_login_name,
'offset': 0,
'limit': 1000
},
callback: function(r) {
loading_dialog.hide();
if (r.message && r.message.success) {
const employees = r.message.employees || [];
if (employees.length === 0) {
frappe.msgprint({
title: __('No Employees'),
indicator: 'blue',
message: __('No employees found in ƏMAS for this organization.')
});
return;
}
show_employees_selection_dialog(asan_login_name, organization_name, employees);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to load employees from ƏMAS')
});
}
}
});
}
function show_employees_selection_dialog(asan_login_name, organization_name, employees) {
// Dynamically detect columns from the first employee
const columns = Object.keys(employees[0]);
// Known column label mappings
const column_labels = {
'adi': 'Ad',
'soyadi': 'Soyad',
'ataAdi': 'Ata adı',
'fin': 'FIN',
'vezife': 'Vəzifə',
'muqavileNo': 'Müqavilə No',
'muqavileTarixi': 'Müqavilə tarixi',
'status': 'Status',
'emekHaqqi': 'Əmək haqqı',
'isBaslamaTarixi': 'İş başlama',
'isBitmeTarixi': 'İş bitmə',
'dogumTarixi': 'Doğum tarixi',
'cinsi': 'Cinsi',
'isSaati': 'İş saatı',
'isGunu': 'İş günü'
};
let table_html = `
<div class="mb-3">
<strong>${__('Organization')}:</strong> ${frappe.utils.escape_html(organization_name)}
<span class="ml-3"><strong>${__('Total')}:</strong> ${employees.length}</span>
</div>
<div class="mb-2">
<button class="btn btn-xs btn-default select-all-employees">${__('Select All')}</button>
<button class="btn btn-xs btn-default deselect-all-employees ml-2">${__('Deselect All')}</button>
</div>
<div style="max-height: 400px; overflow-y: auto;">
<table class="table table-bordered table-striped table-hover" style="font-size: 12px;">
<thead>
<tr>
<th style="width: 30px;"><input type="checkbox" class="select-all-checkbox"></th>
`;
columns.forEach(function(col) {
const label = column_labels[col] || col;
table_html += `<th>${frappe.utils.escape_html(label)}</th>`;
});
table_html += '</tr></thead><tbody>';
employees.forEach(function(emp, index) {
table_html += `<tr><td><input type="checkbox" class="employee-checkbox" data-index="${index}"></td>`;
columns.forEach(function(col) {
const value = emp[col] !== null && emp[col] !== undefined ? String(emp[col]) : '';
table_html += `<td>${frappe.utils.escape_html(value)}</td>`;
});
table_html += '</tr>';
});
table_html += '</tbody></table></div>';
const d = new frappe.ui.Dialog({
title: __('Select Employees to Import'),
size: 'extra-large',
fields: [{
fieldname: 'employees_html',
fieldtype: 'HTML',
options: table_html
}],
primary_action_label: __('Import Selected'),
primary_action: function() {
const selected_indices = [];
d.$wrapper.find('.employee-checkbox:checked').each(function() {
selected_indices.push($(this).data('index'));
});
if (selected_indices.length === 0) {
frappe.msgprint(__('Please select at least one employee to import.'));
return;
}
d.hide();
import_selected_employees(asan_login_name, employees, selected_indices);
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
d.hide();
}
});
d.show();
d.$wrapper.find('.select-all-checkbox').on('change', function() {
d.$wrapper.find('.employee-checkbox').prop('checked', $(this).prop('checked'));
});
d.$wrapper.find('.select-all-employees').on('click', function() {
d.$wrapper.find('.employee-checkbox').prop('checked', true);
d.$wrapper.find('.select-all-checkbox').prop('checked', true);
});
d.$wrapper.find('.deselect-all-employees').on('click', function() {
d.$wrapper.find('.employee-checkbox').prop('checked', false);
d.$wrapper.find('.select-all-checkbox').prop('checked', false);
});
}
function import_selected_employees(asan_login_name, employees, selected_indices) {
const selected_employees = selected_indices.map(i => employees[i]);
const progress_dialog = new frappe.ui.Dialog({
title: __('Importing Employees'),
fields: [{
fieldname: 'progress_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p class="mt-3">${__('Importing')} ${selected_employees.length} ${__('employees')}...</p>
</div>
`
}]
});
progress_dialog.show();
frappe.call({
method: 'invoice_az.emas_api.import_employees',
args: {
'asan_login_name': asan_login_name,
'employees': selected_employees
},
callback: function(r) {
progress_dialog.hide();
if (r.message && r.message.success) {
let summary_msg = `<p><strong>${__('Import Summary')}:</strong></p>`;
summary_msg += `<p>${__('Imported')}: ${r.message.imported_count}</p>`;
if (r.message.updated_count > 0) {
summary_msg += `<p>${__('Updated')}: ${r.message.updated_count}</p>`;
}
if (r.message.error_count > 0) {
summary_msg += `<p style="color: red;">${__('Errors')}: ${r.message.error_count}</p>`;
if (r.message.errors && r.message.errors.length > 0) {
summary_msg += '<ul style="color: red; font-size: 12px;">';
r.message.errors.forEach(function(err) {
summary_msg += `<li>${frappe.utils.escape_html(err)}</li>`;
});
summary_msg += '</ul>';
}
}
frappe.msgprint({
title: __('Import Complete'),
indicator: r.message.error_count > 0 ? 'orange' : 'green',
message: summary_msg
});
cur_list.refresh();
} else {
frappe.msgprint({
title: __('Import Failed'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to import employees')
});
}
}
});
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
# Copyright (c) 2026, Jey ERP and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTesttestapi(IntegrationTestCase):
"""
Integration tests for testapi.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -0,0 +1,144 @@
// Copyright (c) 2026, Your Company and contributors
// For license information, please see license.txt
frappe.ui.form.on("testapi", {
refresh: function (frm) {
// Button to start full ASAN Sign login flow
frm.add_custom_button(
__("Start ASAN Login"),
function () {
if (!frm.doc.user_id) {
frappe.msgprint(__("Please fill ASAN ID field"));
return;
}
if (!frm.doc.phone) {
frappe.msgprint(__("Please fill Phone field"));
return;
}
frm.set_value("status", "Waiting for confirmation...");
frm.set_value("verification_code", "");
frm.set_value("response_data", "");
// Step 1: Start login (this is a long-polling request)
// It will wait until user confirms in ASAN Sign app
frappe.call({
method: "invoice_az.invoice_az.doctype.testapi.testapi.start_asan_sign_login",
args: {
user_id: frm.doc.user_id,
phone_number: frm.doc.phone,
},
timeout: 130000, // 130 seconds timeout
callback: function (r) {
if (r.message && r.message.success) {
frm.set_value("status", "Success - Token received");
frm.set_value(
"response_data",
JSON.stringify(r.message.response_data, null, 2)
);
frm.save();
frappe.show_alert({
message: __("Login successful! Token received."),
indicator: "green",
});
} else {
frm.set_value("status", "Failed");
frm.set_value(
"response_data",
r.message.response_data || r.message.message
);
frappe.msgprint({
title: __("Login Failed"),
indicator: "red",
message: r.message
? r.message.message
: __("Failed to complete ASAN Sign login"),
});
}
},
error: function () {
frm.set_value("status", "Error/Timeout");
},
});
// Step 2: Get verification code immediately after starting login
// Small delay to ensure POST request is sent first
setTimeout(function () {
frappe.call({
method: "invoice_az.invoice_az.doctype.testapi.testapi.get_verification_code",
args: { user_id: frm.doc.user_id },
callback: function (r) {
if (r.message && r.message.success) {
frm.set_value("verification_code", r.message.code);
frappe.show_alert({
message: __(
"Enter this code in ASAN Sign: {0}",
[r.message.code]
),
indicator: "blue",
});
// Show dialog with verification code
frappe.msgprint({
title: __("Verification Code"),
indicator: "blue",
message: __(
"<h1 style='text-align:center; font-size: 48px;'>{0}</h1><p>Enter this code in your ASAN Sign mobile app to confirm login.</p>",
[r.message.code]
),
});
} else {
frappe.show_alert({
message: __("Could not get verification code"),
indicator: "red",
});
}
},
});
}, 500);
},
__("MyGovID")
);
// Separate button to just get verification code (for testing)
frm.add_custom_button(
__("Get Code Only"),
function () {
if (!frm.doc.user_id) {
frappe.msgprint(__("Please fill ASAN ID field"));
return;
}
frappe.call({
method: "invoice_az.invoice_az.doctype.testapi.testapi.get_verification_code",
args: { user_id: frm.doc.user_id },
freeze: true,
freeze_message: __("Getting verification code..."),
callback: function (r) {
if (r.message && r.message.success) {
frm.set_value("verification_code", r.message.code);
frm.set_value("status", "Code received");
frm.set_value(
"response_data",
JSON.stringify(r.message.response_data, null, 2)
);
frappe.show_alert({
message: __("Verification code: {0}", [r.message.code]),
indicator: "green",
});
} else {
frm.set_value("status", "Failed");
frappe.msgprint({
title: __("Error"),
indicator: "red",
message: r.message
? r.message.message
: __("Failed to get verification code"),
});
}
},
});
},
__("MyGovID")
);
},
});

View File

@ -0,0 +1,89 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2026-02-03 15:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user_id",
"phone",
"section_break_1",
"verification_code",
"pin",
"section_break_2",
"status",
"response_data"
],
"fields": [
{
"fieldname": "user_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "ASAN ID",
"reqd": 1
},
{
"fieldname": "phone",
"fieldtype": "Data",
"label": "Телефон",
"reqd": 1
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
},
{
"fieldname": "verification_code",
"fieldtype": "Data",
"label": "Verification Code",
"read_only": 1
},
{
"fieldname": "pin",
"fieldtype": "Data",
"label": "PIN",
"read_only": 1
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "status",
"fieldtype": "Data",
"label": "Status",
"read_only": 1
},
{
"fieldname": "response_data",
"fieldtype": "Code",
"label": "Response Data",
"options": "JSON",
"read_only": 1
}
],
"modified": "2026-02-03 15:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "testapi",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,112 @@
import frappe
import requests
from frappe.model.document import Document
MYGOVID_BASE_URL = "https://api.mygovid.gov.az"
MYGOVID_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "az",
"Content-Type": "application/json",
"Origin": "https://mygovid.gov.az",
"Referer": "https://mygovid.gov.az/",
}
class testapi(Document):
pass
@frappe.whitelist()
def start_asan_sign_login(user_id, phone_number, recaptcha_response=None):
"""
Step 1: Initiate ASAN Sign login.
This is a long-polling request that waits for user confirmation in ASAN Sign app.
Returns JWT token after user confirms.
"""
if not user_id or not phone_number:
return {"success": False, "message": "User ID and phone number are required"}
url = f"{MYGOVID_BASE_URL}/ssoauth/api/v1/logIn/asanSignLogin"
payload = {
"mobileKey": None,
"phoneNumber": phone_number,
"userId": user_id,
"recaptcha3Response": recaptcha_response or "",
}
try:
# Long timeout - waits for user to confirm in ASAN Sign app
response = requests.post(
url, json=payload, headers=MYGOVID_HEADERS, timeout=120
)
if response.status_code in [200, 201]:
data = response.json()
return {
"success": True,
"token": data.get("data", {}).get("token"),
"transaction": data.get("transaction"),
"response_data": data,
}
else:
return {
"success": False,
"message": f"API error: status {response.status_code}",
"response_data": response.text,
}
except requests.exceptions.Timeout:
return {
"success": False,
"message": "Request timed out. User did not confirm in ASAN Sign app.",
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"MyGovID API error: {str(e)}", "MyGovID API Error")
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
frappe.log_error(
f"MyGovID unexpected error: {str(e)}\n{frappe.get_traceback()}",
"MyGovID API Error",
)
return {"success": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def get_verification_code(user_id):
"""
Step 2: Get verification code for ASAN Sign.
Call this immediately after starting login to get the 4-digit code
that user needs to confirm in ASAN Sign app.
"""
if not user_id:
return {"success": False, "message": "User ID is required"}
url = f"{MYGOVID_BASE_URL}/ssoauth/api/v1/logIn/asanSignLogin/verificationCode/{user_id}"
try:
response = requests.get(url, headers=MYGOVID_HEADERS, timeout=30)
if response.status_code in [200, 201]:
data = response.json()
return {
"success": True,
"code": data.get("data", {}).get("code"),
"transaction": data.get("transaction"),
"response_data": data,
}
else:
return {
"success": False,
"message": f"API error: status {response.status_code}",
"response_data": response.text,
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"MyGovID API error: {str(e)}", "MyGovID API Error")
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
frappe.log_error(
f"MyGovID unexpected error: {str(e)}\n{frappe.get_traceback()}",
"MyGovID API Error",
)
return {"success": False, "message": f"Error: {str(e)}"}