added emas integration and emas employees
This commit is contained in:
parent
95f685a3a7
commit
002b317958
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
# Copyright (c) 2026, Your Company and contributors
|
||||
# For license information, please see license.txt
|
||||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)}"}
|
||||
Loading…
Reference in New Issue