invoice_az/invoice_az/client/company.js

963 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
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();
}
});
}