963 lines
37 KiB
JavaScript
963 lines
37 KiB
JavaScript
// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
|
||
const ETaxes = {
|
||
// Константы
|
||
PROGRESS_UPDATE_DELAY: 50,
|
||
AUTH_POLL_INTERVAL: 6000,
|
||
AUTH_MAX_ATTEMPTS: 20,
|
||
|
||
// Глобальные переменные
|
||
loadingDialog: null,
|
||
cancelLoading: false,
|
||
loadingErrors: [],
|
||
|
||
// Кэш
|
||
cache: {
|
||
defaultLogin: null,
|
||
cacheTime: null,
|
||
cacheDuration: 300000 // 5 минут
|
||
}
|
||
};
|
||
|
||
// ======= БАЗОВЫЕ УТИЛИТЫ =======
|
||
ETaxes.utils = {
|
||
// Кэшированное получение настроек входа
|
||
getDefaultLogin: function(callback, useCache = true) {
|
||
const now = Date.now();
|
||
|
||
if (useCache && ETaxes.cache.defaultLogin && ETaxes.cache.cacheTime &&
|
||
(now - ETaxes.cache.cacheTime) < ETaxes.cache.cacheDuration) {
|
||
callback(ETaxes.cache.defaultLogin);
|
||
return;
|
||
}
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.get_default_asan_login',
|
||
callback: function(r) {
|
||
if (r.message) {
|
||
ETaxes.cache.defaultLogin = r.message;
|
||
ETaxes.cache.cacheTime = now;
|
||
}
|
||
callback(r.message);
|
||
},
|
||
error: function() {
|
||
callback({found: false, error: 'Network error'});
|
||
}
|
||
});
|
||
},
|
||
|
||
// Проверка валидности токена
|
||
checkTokenValidity: function(callback) {
|
||
frappe.call({
|
||
method: 'invoice_az.auth.check_token_validity',
|
||
callback: function(r) {
|
||
callback(r.message && r.message.valid);
|
||
},
|
||
error: function() {
|
||
callback(false);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Форматирование валюты
|
||
formatCurrency: function(amount) {
|
||
return new Intl.NumberFormat('az-AZ', {
|
||
style: 'currency',
|
||
currency: 'AZN',
|
||
minimumFractionDigits: 2
|
||
}).format(amount || 0);
|
||
},
|
||
|
||
// Очистка кэша
|
||
clearCache: function() {
|
||
ETaxes.cache.defaultLogin = null;
|
||
ETaxes.cache.cacheTime = null;
|
||
}
|
||
};
|
||
|
||
// ======= ДИАЛОГИ ЗАГРУЗКИ =======
|
||
ETaxes.dialogs = {
|
||
// Показать диалог загрузки
|
||
showLoading: function(title, message, submessage) {
|
||
if (ETaxes.loadingDialog) {
|
||
this.updateLoading(title, message, submessage);
|
||
return;
|
||
}
|
||
|
||
ETaxes.loadingDialog = new frappe.ui.Dialog({
|
||
title: title,
|
||
fields: [{
|
||
fieldname: 'loading_html',
|
||
fieldtype: 'HTML',
|
||
options: this._getLoadingHTML(title, message, submessage)
|
||
}],
|
||
primary_action_label: __('Cancel'),
|
||
primary_action: function() {
|
||
ETaxes.cancelLoading = true;
|
||
ETaxes.loadingDialog.$wrapper.find('.primary-action').prop('disabled', true);
|
||
ETaxes.loadingDialog.$wrapper.find('.primary-action').html(__('Cancelling...'));
|
||
$('#status_message p').text(__('Cancelling the operation.'));
|
||
}
|
||
});
|
||
|
||
ETaxes.cancelLoading = false;
|
||
ETaxes.loadingDialog.show();
|
||
ETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
|
||
},
|
||
|
||
// Обновить сообщение в диалоге
|
||
updateLoading: function(title, message, submessage) {
|
||
if (!ETaxes.loadingDialog) return;
|
||
|
||
if (title) $('#loading_title').text(title);
|
||
if (message) $('#loading_message').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#loading_submessage').text(submessage);
|
||
$('#loading_submessage').toggle(!!submessage);
|
||
}
|
||
},
|
||
|
||
// Показать код верификации
|
||
showVerificationCode: function(code) {
|
||
if (ETaxes.loadingDialog && code) {
|
||
$('#verification_code').text(code);
|
||
$('#verification_code_container').show();
|
||
}
|
||
},
|
||
|
||
// Установить статус успеха
|
||
setSuccess: function(message, submessage, callback, delay = 2000) {
|
||
if (!ETaxes.loadingDialog) {
|
||
if (callback) callback();
|
||
return;
|
||
}
|
||
|
||
ETaxes.loadingDialog.$wrapper.find('.loading-animation').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>
|
||
`);
|
||
|
||
$('#loading_message').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#loading_submessage').text(submessage);
|
||
$('#loading_submessage').toggle(!!submessage);
|
||
}
|
||
|
||
$('#verification_code_container').hide();
|
||
ETaxes.loadingDialog.set_primary_action(null);
|
||
ETaxes.loadingDialog.set_secondary_action(null);
|
||
|
||
setTimeout(() => {
|
||
this.hide();
|
||
if (callback) callback();
|
||
}, delay);
|
||
},
|
||
|
||
// Установить статус ошибки
|
||
setError: function(message, submessage, showCloseButton = true) {
|
||
if (!ETaxes.loadingDialog) return;
|
||
|
||
ETaxes.loadingDialog.set_title(__('Error'));
|
||
|
||
ETaxes.loadingDialog.$wrapper.find('.loading-animation').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>
|
||
`);
|
||
|
||
ETaxes.loadingDialog.$wrapper.find('#loading_title').text('');
|
||
ETaxes.loadingDialog.$wrapper.find('#loading_submessage').remove();
|
||
|
||
const $msgElement = ETaxes.loadingDialog.$wrapper.find('#loading_message');
|
||
if ($msgElement.length) {
|
||
$msgElement.css({
|
||
'color': '#d9534f',
|
||
'font-weight': '500',
|
||
'font-size': '14px',
|
||
'margin-top': '15px'
|
||
});
|
||
|
||
if (submessage) {
|
||
$msgElement.html(submessage.replace(/\n/g, '<br>'));
|
||
} else {
|
||
$msgElement.text(message);
|
||
}
|
||
}
|
||
|
||
$('#verification_code_container').hide();
|
||
|
||
if (showCloseButton) {
|
||
ETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
|
||
}
|
||
},
|
||
|
||
// Скрыть диалог
|
||
hide: function() {
|
||
if (ETaxes.loadingDialog) {
|
||
try {
|
||
ETaxes.loadingDialog.hide();
|
||
} catch (e) {
|
||
console.error("Error hiding loading dialog:", e);
|
||
}
|
||
ETaxes.loadingDialog = null;
|
||
}
|
||
ETaxes.cancelLoading = false;
|
||
},
|
||
|
||
// Получить HTML для диалога загрузки
|
||
_getLoadingHTML: function(title, message, submessage) {
|
||
return `
|
||
<div class="text-center">
|
||
<div class="loading-animation" 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: lds-dual-ring 1.2s linear infinite;"></div>
|
||
</div>
|
||
<style>
|
||
@keyframes lds-dual-ring {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
<div class="mt-3" id="status_message">
|
||
<h4 id="loading_title">${title || __('Please wait...')}</h4>
|
||
<p id="loading_message">${message || __('The operation may take some time')}</p>
|
||
<p id="loading_submessage" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
|
||
</div>
|
||
<div id="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="verification_code">----</span>
|
||
</div>
|
||
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
};
|
||
|
||
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
|
||
ETaxes.auth = {
|
||
checkAndProcess: function(callback) {
|
||
ETaxes.utils.checkTokenValidity(function(isValid) {
|
||
if (isValid) {
|
||
callback();
|
||
} else {
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
|
||
function() {
|
||
ETaxes.auth.startProcess(callback);
|
||
},
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
}
|
||
});
|
||
},
|
||
|
||
startProcess: function(callback) {
|
||
ETaxes.dialogs.showLoading(
|
||
__('Starting Authentication'),
|
||
__('Getting Asan Login settings...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
ETaxes.utils.getDefaultLogin(function(loginData) {
|
||
if (loginData && loginData.found) {
|
||
const asanLoginName = loginData.name;
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Sending authentication request...'),
|
||
__('This will send a request to your Asan Imza mobile app')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.handle_authentication',
|
||
args: { 'asan_login_name': asanLoginName },
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
const bearerToken = r.message.bearer_token;
|
||
|
||
if (r.message.verification_code) {
|
||
ETaxes.dialogs.showVerificationCode(r.message.verification_code);
|
||
}
|
||
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request')
|
||
);
|
||
|
||
ETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
|
||
if (success) {
|
||
ETaxes.auth.complete(asanLoginName, callback);
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Failed'),
|
||
__('Could not authenticate with Asan Imza')
|
||
);
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('Authentication request failed')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('No Asan Login Settings'),
|
||
__('Please configure Asan Login settings first')
|
||
);
|
||
}
|
||
}, false);
|
||
},
|
||
|
||
pollStatus: function(asanLoginName, bearerToken, callback) {
|
||
let attempts = 0;
|
||
let stopPolling = false;
|
||
|
||
function pollStatusStep() {
|
||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
|
||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS) {
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Timeout'),
|
||
__('The authentication request has timed out. Please try again.')
|
||
);
|
||
callback(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request') +
|
||
' (' + attempts + '/' + ETaxes.AUTH_MAX_ATTEMPTS + ')'
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.poll_auth_status',
|
||
args: {
|
||
'asan_login_name': asanLoginName,
|
||
'bearer_token': bearerToken
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
if (r.message.authenticated) {
|
||
stopPolling = true;
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Authentication Successful'),
|
||
__('You are now authenticated with Asan Imza'),
|
||
function() {
|
||
callback(true);
|
||
}
|
||
);
|
||
} else {
|
||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||
}
|
||
} else {
|
||
stopPolling = true;
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('An unknown error occurred')
|
||
);
|
||
callback(false);
|
||
}
|
||
},
|
||
error: function(err) {
|
||
console.error('Error during status check:', err);
|
||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||
}
|
||
});
|
||
}
|
||
|
||
pollStatusStep();
|
||
},
|
||
|
||
complete: function(asanLoginName, callback) {
|
||
ETaxes.dialogs.showLoading(
|
||
__('Completing Authentication'),
|
||
__('Getting certificates...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.get_auth_certificates',
|
||
args: { 'asan_login_name': asanLoginName },
|
||
callback: function(certR) {
|
||
if (certR.message && certR.message.success) {
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Certificates retrieved'),
|
||
__('Selecting certificate and taxpayer')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'frappe.client.get',
|
||
args: {
|
||
doctype: 'Asan Login',
|
||
name: asanLoginName
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.selected_certificate_json) {
|
||
try {
|
||
const certData = JSON.parse(r.message.selected_certificate_json);
|
||
const certName = r.message.selected_certificate;
|
||
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Selecting certificate'),
|
||
certName
|
||
);
|
||
|
||
ETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
|
||
} catch (e) {
|
||
console.error('Error parsing certificate:', e);
|
||
ETaxes.dialogs.hide();
|
||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||
}
|
||
} else {
|
||
ETaxes.dialogs.hide();
|
||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Error'),
|
||
certR.message ? certR.message.message : __('Failed to get certificates')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
_selectCertificate: function(asanLoginName, certData, certName, callback) {
|
||
frappe.call({
|
||
method: 'invoice_az.auth.select_certificate',
|
||
args: {
|
||
'asan_login_name': asanLoginName,
|
||
'certificate_data': certData,
|
||
'certificate_name': certName
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Selecting taxpayer'),
|
||
__('Using certificate: ') + certName
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.select_taxpayer',
|
||
args: { 'asan_login_name': asanLoginName },
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
ETaxes.utils.clearCache();
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Authentication Complete'),
|
||
__('You can now access E-Taxes services'),
|
||
callback
|
||
);
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select taxpayer')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select certificate')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
_showCertificateSelector: function(certificates, asanLoginName, callback) {
|
||
if (!certificates || !certificates.length) {
|
||
frappe.msgprint(__('No certificates available'));
|
||
return;
|
||
}
|
||
|
||
let certHtml = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
|
||
certHtml += '<thead><tr><th>Type</th><th>Name</th><th>ID</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>';
|
||
|
||
certificates.forEach(function(cert, index) {
|
||
let name = '';
|
||
let id = '';
|
||
|
||
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
|
||
name = cert.individualInfo.name || '';
|
||
id = cert.individualInfo.fin || '';
|
||
} else if (cert.legalInfo) {
|
||
name = cert.legalInfo.name || '';
|
||
id = cert.legalInfo.tin || cert.legalInfo.voen || '';
|
||
}
|
||
|
||
certHtml += '<tr>' +
|
||
'<td>' + (cert.taxpayerType || '') + '</td>' +
|
||
'<td>' + name + '</td>' +
|
||
'<td>' + id + '</td>' +
|
||
'<td>' + (cert.position || '') + '</td>' +
|
||
'<td>' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + '</td>' +
|
||
'<td><button class="btn btn-xs btn-primary select-cert" data-index="' + index + '">Select</button></td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
certHtml += '</tbody></table></div>';
|
||
|
||
const dialog = new frappe.ui.Dialog({
|
||
title: __('Select Certificate'),
|
||
fields: [{
|
||
fieldtype: 'HTML',
|
||
fieldname: 'certificates',
|
||
options: certHtml
|
||
}]
|
||
});
|
||
|
||
dialog.show();
|
||
|
||
dialog.$wrapper.find('.select-cert').on('click', function() {
|
||
const index = $(this).data('index');
|
||
const cert = certificates[index];
|
||
|
||
let certName = '';
|
||
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
|
||
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
|
||
} else if (cert.legalInfo) {
|
||
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
|
||
} else {
|
||
certName = `Certificate ${index + 1}`;
|
||
}
|
||
|
||
dialog.hide();
|
||
|
||
ETaxes.dialogs.showLoading(
|
||
__('Selecting Certificate'),
|
||
__('Processing your selection...'),
|
||
certName
|
||
);
|
||
|
||
ETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
|
||
});
|
||
}
|
||
};
|
||
// ======= END ETaxes MODULE =======
|
||
|
||
|
||
// ======= COMPANY FORM: DATA LOADING =======
|
||
frappe.ui.form.on('Company', {
|
||
refresh: function(frm) {
|
||
if (frm.is_new()) return;
|
||
frm.add_custom_button(__('Data Loading'), function() {
|
||
show_data_loading_dialog(frm);
|
||
}, __('E-Taxes'));
|
||
render_all_reference_sections(frm);
|
||
}
|
||
});
|
||
|
||
// ======= REFERENCE LIST SECTIONS (Tax Policy tab) =======
|
||
|
||
const REFERENCE_SECTIONS = [
|
||
{
|
||
doctype: 'E-Taxes Object',
|
||
html_fieldname: 'objects_list_html',
|
||
list_route: '/app/e-taxes-object',
|
||
columns: [
|
||
{ label: 'Object Code', field: 'object_code', link_to_item: true },
|
||
{ label: 'Name', field: 'object_name' },
|
||
{ label: 'Status', field: 'object_status',
|
||
indicator: { active: 'green', liquidated: 'red' } }
|
||
]
|
||
},
|
||
{
|
||
doctype: 'E-Taxes Cash Register',
|
||
html_fieldname: 'cash_registers_list_html',
|
||
list_route: '/app/e-taxes-cash-register',
|
||
columns: [
|
||
{ label: 'Serial Number', field: 'number', link_to_item: true },
|
||
{ label: 'Model', field: 'model' },
|
||
{ label: 'E-Cash Status', field: 'ecash_status',
|
||
indicator: { ACTIVE: 'green', ELIMINATED: 'red' } },
|
||
{ label: 'Object', field: 'object',
|
||
link_to: (v) => '/app/e-taxes-object/' + encodeURIComponent(v) },
|
||
{ label: 'Installed At', field: 'installed_at', format: 'date' }
|
||
]
|
||
},
|
||
{
|
||
doctype: 'E-Taxes POS Terminal',
|
||
html_fieldname: 'pos_terminals_list_html',
|
||
list_route: '/app/e-taxes-pos-terminal',
|
||
columns: [
|
||
{ label: 'Registration Number', field: 'registration_number', link_to_item: true },
|
||
{ label: 'Serial Number', field: 'serial_number' },
|
||
{ label: 'Status', field: 'status',
|
||
indicator: { A: 'green', C: 'red' } },
|
||
{ label: 'Registration Date', field: 'registration_date', format: 'date' }
|
||
]
|
||
},
|
||
{
|
||
doctype: 'E-Taxes Bank Account',
|
||
html_fieldname: 'bank_accounts_list_html',
|
||
list_route: '/app/e-taxes-bank-account',
|
||
columns: [
|
||
{ label: 'Account Number', field: 'number', link_to_item: true },
|
||
{ label: 'Currency', field: 'currency' },
|
||
{ label: 'Status', field: 'status',
|
||
indicator: { A: 'green', C: 'red' } },
|
||
{ label: 'Bank Name', field: 'bank_name' }
|
||
]
|
||
},
|
||
{
|
||
doctype: 'E-Taxes Presented Certificate',
|
||
html_fieldname: 'presented_certificates_list_html',
|
||
list_route: '/app/e-taxes-presented-certificate',
|
||
columns: [
|
||
{ label: 'Cert No', field: 'cert_no', link_to_item: true },
|
||
{ label: 'Taxpayer', field: 'taxpayer_full_name' },
|
||
{ label: 'Category', field: 'cert_category',
|
||
indicator: { ACCEPTED: 'green', PRESENTED: 'blue', REJECTED: 'red', PENDING: 'orange' } }
|
||
]
|
||
}
|
||
];
|
||
|
||
function render_all_reference_sections(frm) {
|
||
// Defer to next tick so HTML fields are mounted in DOM.
|
||
setTimeout(function() {
|
||
REFERENCE_SECTIONS.forEach(function(section) {
|
||
load_reference_section(frm, section);
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
function section_container(frm, section) {
|
||
const field = frm.get_field(section.html_fieldname);
|
||
return (field && field.$wrapper) ? field.$wrapper : null;
|
||
}
|
||
|
||
function load_reference_section(frm, section) {
|
||
const $c = section_container(frm, section);
|
||
if (!$c) return;
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.company_api.get_company_reference_list',
|
||
args: { doctype: section.doctype, company: frm.doc.name, limit: 50 },
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
render_reference_list(frm, section, r.message.data, r.message.total_count);
|
||
} else {
|
||
const error_msg = r.message ? r.message.message : 'Unknown error';
|
||
$c.html(`<div class="form-section card-section">
|
||
<div class="section-body">
|
||
<div class="alert alert-warning">
|
||
<p>Error loading ${frappe.utils.escape_html(section.doctype)}: ${frappe.utils.escape_html(error_msg)}</p>
|
||
<button class="btn btn-sm btn-default retry-section-btn">Try Again</button>
|
||
</div>
|
||
</div>
|
||
</div>`);
|
||
$c.off('click', '.retry-section-btn').on('click', '.retry-section-btn', function() {
|
||
load_reference_section(frm, section);
|
||
});
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
$c.html(`<div class="form-section card-section">
|
||
<div class="section-body">
|
||
<div class="alert alert-danger">
|
||
<p>Network error loading ${frappe.utils.escape_html(section.doctype)}: ${frappe.utils.escape_html(error || '')}</p>
|
||
<button class="btn btn-sm btn-default retry-section-btn">Try Again</button>
|
||
</div>
|
||
</div>
|
||
</div>`);
|
||
$c.off('click', '.retry-section-btn').on('click', '.retry-section-btn', function() {
|
||
load_reference_section(frm, section);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function render_reference_list(frm, section, rows, total_count) {
|
||
const $c = section_container(frm, section);
|
||
if (!$c) return;
|
||
|
||
const esc = frappe.utils.escape_html;
|
||
|
||
let html = `<div class="frappe-control" style="margin-bottom: 15px;">
|
||
<div class="clearfix">
|
||
<div class="pull-left">
|
||
<h6 class="text-muted">${esc(section.doctype)} (${total_count} total)</h6>
|
||
</div>
|
||
<div class="pull-right">
|
||
<button class="btn btn-xs btn-default refresh-section-btn">
|
||
<i class="octicon octicon-sync"></i> Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
if (rows && rows.length > 0) {
|
||
html += '<div class="form-section card-section">';
|
||
html += '<div class="section-body">';
|
||
html += '<table class="table table-bordered">';
|
||
html += '<thead><tr>';
|
||
section.columns.forEach(function(col) { html += '<th>' + esc(__(col.label)) + '</th>'; });
|
||
html += '</tr></thead>';
|
||
html += '<tbody>';
|
||
|
||
rows.forEach(function(row) {
|
||
html += '<tr>';
|
||
section.columns.forEach(function(col) {
|
||
const val = row[col.field];
|
||
html += '<td>' + render_cell(row, col, val, section) + '</td>';
|
||
});
|
||
html += '</tr>';
|
||
});
|
||
|
||
html += '</tbody></table>';
|
||
html += '</div>'; // section-body
|
||
html += '</div>'; // form-section
|
||
|
||
if (total_count > rows.length) {
|
||
html += `<div class="text-center" style="margin-top: 10px;">
|
||
<p class="text-muted small">Showing ${rows.length} of ${total_count}</p>
|
||
<a href="${section.list_route}" class="btn btn-default btn-xs">View All ${esc(section.doctype)}</a>
|
||
</div>`;
|
||
}
|
||
} else {
|
||
html += '<div class="form-section card-section">';
|
||
html += '<div class="section-body">';
|
||
html += '<div class="empty-state">';
|
||
html += '<p class="text-muted">No records found. Use "Data Loading" to import from E-Taxes.</p>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
$c.html(html);
|
||
|
||
$c.off('click', '.refresh-section-btn').on('click', '.refresh-section-btn', function() {
|
||
$c.html('<div class="text-center" style="padding: 20px;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
|
||
load_reference_section(frm, section);
|
||
});
|
||
}
|
||
|
||
function render_cell(row, col, val, section) {
|
||
const esc = frappe.utils.escape_html;
|
||
if (val == null || val === '') return '-';
|
||
|
||
if (col.indicator) {
|
||
const color = col.indicator[val] || 'gray';
|
||
return '<span class="indicator-pill ' + color + '">' + esc(String(val)) + '</span>';
|
||
}
|
||
if (col.format === 'date') {
|
||
return esc(frappe.datetime.str_to_user(val));
|
||
}
|
||
if (col.link_to_item) {
|
||
return '<a href="' + section.list_route + '/' + encodeURIComponent(row.name) + '">' +
|
||
esc(String(val)) + '</a>';
|
||
}
|
||
if (typeof col.link_to === 'function') {
|
||
return '<a href="' + col.link_to(val) + '">' + esc(String(val)) + '</a>';
|
||
}
|
||
return esc(String(val));
|
||
}
|
||
|
||
// Ordered list of data types loadable from the Company → Data Loading dialog.
|
||
// Adding a new loader = append an entry here.
|
||
const DATA_LOADERS = [
|
||
{
|
||
key: 'load_objects',
|
||
label: __('Load Objects'),
|
||
description: __('Load company objects (taxpayer registered locations) from E-Taxes'),
|
||
default: 1,
|
||
method: 'invoice_az.company_api.load_company_objects',
|
||
freezeMessage: __('Loading objects from E-Taxes...')
|
||
},
|
||
{
|
||
key: 'load_cash_registers',
|
||
label: __('Load Cash Registers'),
|
||
description: __('Load cash registers (kassa aparatları) from E-Taxes. Uses pagination.'),
|
||
default: 1,
|
||
method: 'invoice_az.company_api.load_company_cash_registers',
|
||
freezeMessage: __('Loading cash registers from E-Taxes...')
|
||
},
|
||
{
|
||
key: 'load_pos_terminals',
|
||
label: __('Load POS Terminals'),
|
||
description: __('Load POS terminals from E-Taxes. Uses pagination.'),
|
||
default: 1,
|
||
method: 'invoice_az.company_api.load_company_pos_terminals',
|
||
freezeMessage: __('Loading POS terminals from E-Taxes...')
|
||
},
|
||
{
|
||
key: 'load_bank_accounts',
|
||
label: __('Load Bank Accounts'),
|
||
description: __('Load bank accounts (bank hesabları) from E-Taxes'),
|
||
default: 1,
|
||
method: 'invoice_az.company_api.load_company_bank_accounts',
|
||
freezeMessage: __('Loading bank accounts from E-Taxes...')
|
||
},
|
||
{
|
||
key: 'load_presented_certificates',
|
||
label: __('Load Presented Certificates'),
|
||
description: __('Load presented/accepted e-certificates from E-Taxes.'),
|
||
default: 1,
|
||
method: 'invoice_az.company_api.load_company_presented_certificates',
|
||
freezeMessage: __('Loading presented certificates from E-Taxes...')
|
||
}
|
||
];
|
||
|
||
function show_data_loading_dialog(frm) {
|
||
const fields = [{
|
||
fieldname: 'data_types_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Data Types to Load')
|
||
}];
|
||
|
||
DATA_LOADERS.forEach(function(loader) {
|
||
fields.push({
|
||
fieldname: loader.key,
|
||
fieldtype: 'Check',
|
||
label: loader.label,
|
||
default: loader.default,
|
||
description: loader.description
|
||
});
|
||
});
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Load Company Reference Data from E-Taxes'),
|
||
fields: fields,
|
||
size: 'small',
|
||
primary_action_label: __('Load'),
|
||
primary_action: function() {
|
||
const values = d.get_values();
|
||
const selected = DATA_LOADERS.filter(function(l) { return values[l.key]; });
|
||
if (!selected.length) {
|
||
frappe.msgprint(__('Please select at least one data type to load.'));
|
||
return;
|
||
}
|
||
d.hide();
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
run_data_loading(frm, selected);
|
||
});
|
||
}
|
||
});
|
||
d.show();
|
||
}
|
||
|
||
const PROGRESS_EVENT = 'company_data_loading_progress';
|
||
const COMPLETE_EVENT = 'company_data_loading_complete';
|
||
|
||
function run_data_loading(frm, loaders) {
|
||
const keys = loaders.map(function(l) { return l.key; });
|
||
|
||
// Handlers are closed over per-run so we can detach cleanly at end
|
||
let progress_handler = null;
|
||
let complete_handler = null;
|
||
// Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
|
||
// frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
|
||
// уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
|
||
let importFinished = false;
|
||
// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
|
||
// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
|
||
const closeProgress = function() {
|
||
if (frappe.cur_progress) {
|
||
try {
|
||
frappe.cur_progress.$wrapper.modal('hide');
|
||
frappe.cur_progress.$wrapper.remove();
|
||
} catch (e) {}
|
||
frappe.cur_progress = null;
|
||
}
|
||
if ($('.modal:visible').length === 0) {
|
||
$('.modal-backdrop').remove();
|
||
$('body').removeClass('modal-open');
|
||
}
|
||
};
|
||
|
||
const detach = function() {
|
||
if (progress_handler) frappe.realtime.off(PROGRESS_EVENT, progress_handler);
|
||
if (complete_handler) frappe.realtime.off(COMPLETE_EVENT, complete_handler);
|
||
progress_handler = null;
|
||
complete_handler = null;
|
||
};
|
||
|
||
progress_handler = function(data) {
|
||
if (importFinished) return;
|
||
if (!data || !data.total) return;
|
||
const pct = Math.round((data.current / data.total) * 100);
|
||
const desc = data.message || '';
|
||
frappe.show_progress(__('Loading E-Taxes Data'), pct, 100, desc);
|
||
};
|
||
|
||
complete_handler = function(data) {
|
||
importFinished = true;
|
||
detach();
|
||
closeProgress();
|
||
|
||
if (data && data.error === 'unauthorized') {
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||
function() {
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
run_data_loading(frm, loaders);
|
||
});
|
||
}
|
||
);
|
||
return;
|
||
}
|
||
|
||
const lines = [];
|
||
(data.summary || []).forEach(function(item) {
|
||
const res = item.result || {};
|
||
if (res.success) {
|
||
lines.push('<b>' + item.label + ':</b> ' + (res.message || __('OK')));
|
||
} else {
|
||
lines.push('<b style="color:#d9534f">' + item.label + ':</b> ' +
|
||
(res.message || __('Unknown error')));
|
||
}
|
||
});
|
||
|
||
frappe.msgprint({
|
||
title: __('Data Loading Complete'),
|
||
message: lines.join('<br>') || __('Nothing was loaded.'),
|
||
indicator: 'green'
|
||
});
|
||
};
|
||
|
||
frappe.realtime.on(PROGRESS_EVENT, progress_handler);
|
||
frappe.realtime.on(COMPLETE_EVENT, complete_handler);
|
||
|
||
frappe.show_progress(__('Loading E-Taxes Data'), 0, 100, __('Starting...'));
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.company_api.load_company_data_bulk',
|
||
args: { company: frm.doc.name, selections: JSON.stringify(keys) },
|
||
callback: function(r) {
|
||
const msg = r.message || {};
|
||
if (!msg.enqueued) {
|
||
detach();
|
||
closeProgress();
|
||
frappe.msgprint({
|
||
title: __('Failed to Start'),
|
||
message: msg.message || __('Could not enqueue the loading job.'),
|
||
indicator: 'red'
|
||
});
|
||
}
|
||
},
|
||
error: function() {
|
||
detach();
|
||
closeProgress();
|
||
}
|
||
});
|
||
}
|