1656 lines
72 KiB
JavaScript
1656 lines
72 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() {
|
||
// Флаг ставим ПЕРЕД закрытием: hide() его больше не сбрасывает,
|
||
// поэтому in-flight callback'и пагинации увидят отмену и выйдут.
|
||
ETaxes.cancelLoading = true;
|
||
ETaxes.dialogs.hide();
|
||
}
|
||
});
|
||
|
||
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.$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>
|
||
`);
|
||
|
||
$('#loading_message').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#loading_submessage').text(submessage);
|
||
$('#loading_submessage').toggle(!!submessage);
|
||
}
|
||
|
||
$('#verification_code_container').hide();
|
||
|
||
if (showCloseButton) {
|
||
ETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
|
||
}
|
||
},
|
||
|
||
// Скрыть диалог
|
||
hide: function() {
|
||
// Мягкий loadingDialog.hide() в jey_theme/Bootstrap НЕ всегда убирает модал
|
||
// (он зависает и следующий диалог накладывается сверху). Принудительно:
|
||
// modal('hide') + remove() + чистка осиротевшего backdrop.
|
||
// cancelLoading здесь НЕ сбрасываем — иначе отмена потеряется для in-flight
|
||
// callback'ов; флаг сбрасывается в начале операции / при создании диалога.
|
||
if (ETaxes.loadingDialog) {
|
||
try {
|
||
ETaxes.loadingDialog.$wrapper.modal('hide');
|
||
ETaxes.loadingDialog.$wrapper.remove();
|
||
} catch (e) {
|
||
console.error("Error hiding loading dialog:", e);
|
||
}
|
||
ETaxes.loadingDialog = null;
|
||
}
|
||
if ($('.modal:visible').length === 0) {
|
||
$('.modal-backdrop').remove();
|
||
$('body').removeClass('modal-open');
|
||
}
|
||
},
|
||
|
||
// Получить 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);
|
||
});
|
||
}
|
||
};
|
||
|
||
// ======= УПРАВЛЕНИЕ ОШИБКАМИ =======
|
||
ETaxes.errors = {
|
||
// Добавить ошибку в лог
|
||
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
|
||
const errorEntry = {
|
||
invoice_id: invoiceData.id || 'Unknown',
|
||
serial_number: invoiceData.serialNumber || 'Unknown',
|
||
sender_name: invoiceData.sender ? invoiceData.sender.name : 'Unknown',
|
||
creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown',
|
||
error_type: errorType,
|
||
error_message: errorMessage,
|
||
timestamp: new Date().toISOString(),
|
||
...additionalData
|
||
};
|
||
|
||
ETaxes.loadingErrors.push(errorEntry);
|
||
console.error('E-Taxes Import Error:', errorEntry);
|
||
},
|
||
|
||
// Очистить лог ошибок
|
||
clear: function() {
|
||
frappe.confirm(
|
||
__('Are you sure you want to clear the error log?'),
|
||
function() {
|
||
ETaxes.loadingErrors = [];
|
||
frappe.show_alert({
|
||
message: __('Error log cleared'),
|
||
indicator: 'green'
|
||
}, 2);
|
||
}
|
||
);
|
||
},
|
||
|
||
// Показать детальный лог ошибок
|
||
showDetails: function() {
|
||
if (ETaxes.loadingErrors.length === 0) {
|
||
frappe.msgprint(__('No errors to display'));
|
||
return;
|
||
}
|
||
|
||
let errorsHtml = '<div class="error-log-container">';
|
||
|
||
ETaxes.loadingErrors.forEach(function(error, index) {
|
||
let documentDate = 'No date';
|
||
if (error.creation_date && error.creation_date !== 'Unknown') {
|
||
try {
|
||
documentDate = moment(error.creation_date).format('DD.MM.YYYY');
|
||
} catch (e) {
|
||
documentDate = 'Invalid date';
|
||
}
|
||
}
|
||
|
||
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
|
||
|
||
// Заголовок ошибки
|
||
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||
'<strong>' + (error.serial_number || error.invoice_id) + '</strong></h5>';
|
||
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
||
errorsHtml += '</div>';
|
||
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
|
||
errorsHtml += '</div>';
|
||
|
||
// Сообщение об ошибке
|
||
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
|
||
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
|
||
errorsHtml += '</div>';
|
||
|
||
// Детали ошибки
|
||
if (error.unmatched_items && error.unmatched_items.length > 0) {
|
||
errorsHtml += '<div class="error-details">';
|
||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped items:') + '</div>';
|
||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_items.forEach(function(item, idx) {
|
||
errorsHtml += '<li><strong>Row ' + (idx + 1) + ':</strong> ' + item.name;
|
||
if (item.code) {
|
||
errorsHtml += ' <em>(Code: ' + item.code + ')</em>';
|
||
}
|
||
errorsHtml += '</li>';
|
||
});
|
||
errorsHtml += '</ul></div>';
|
||
} else if (error.unmatched_parties && error.unmatched_parties.length > 0) {
|
||
errorsHtml += '<div class="error-details">';
|
||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped parties:') + '</div>';
|
||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_parties.forEach(function(party, idx) {
|
||
errorsHtml += '<li><strong>Party ' + (idx + 1) + ':</strong> ' + party.name;
|
||
if (party.tin) {
|
||
errorsHtml += ' <em>(TIN: ' + party.tin + ')</em>';
|
||
}
|
||
if (party.type) {
|
||
errorsHtml += ' <span class="label label-info" style="font-size: 10px;">' + party.type + '</span>';
|
||
}
|
||
errorsHtml += '</li>';
|
||
});
|
||
errorsHtml += '</ul></div>';
|
||
} else if (error.unmatched_units && error.unmatched_units.length > 0) {
|
||
errorsHtml += '<div class="error-details">';
|
||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped units:') + '</div>';
|
||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_units.forEach(function(unit, idx) {
|
||
errorsHtml += '<li><strong>Unit ' + (idx + 1) + ':</strong> ' + unit.name + '</li>';
|
||
});
|
||
errorsHtml += '</ul></div>';
|
||
}
|
||
|
||
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
||
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
||
errorsHtml += '</div>';
|
||
|
||
errorsHtml += '</div>';
|
||
});
|
||
|
||
errorsHtml += '</div>';
|
||
|
||
// Статистика
|
||
const errorTypes = {};
|
||
ETaxes.loadingErrors.forEach(function(error) {
|
||
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
|
||
});
|
||
|
||
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
|
||
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||
statsHtml += '<div>';
|
||
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + ETaxes.loadingErrors.length + '<br>';
|
||
statsHtml += '<span style="color: var(--text-muted);">';
|
||
Object.keys(errorTypes).forEach(function(type, index) {
|
||
if (index > 0) statsHtml += ' • ';
|
||
statsHtml += type + ': ' + errorTypes[type];
|
||
});
|
||
statsHtml += '</span>';
|
||
statsHtml += '</div>';
|
||
statsHtml += '<div>';
|
||
statsHtml += '<button class="btn btn-default btn-sm" id="export-csv-dialog-btn" style="margin-right: 5px;">' +
|
||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
|
||
statsHtml += '<button class="btn btn-warning btn-sm" id="clear-log-dialog-btn">' +
|
||
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
|
||
statsHtml += '</div></div>';
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Error Details') + ' (' + ETaxes.loadingErrors.length + ')',
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'stats_html',
|
||
fieldtype: 'HTML',
|
||
options: statsHtml
|
||
},
|
||
{
|
||
fieldname: 'errors_html',
|
||
fieldtype: 'HTML',
|
||
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
|
||
}
|
||
],
|
||
primary_action_label: __('Close'),
|
||
primary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
|
||
// Добавляем обработчики событий
|
||
setTimeout(function() {
|
||
$('#export-csv-dialog-btn').off('click').on('click', function() {
|
||
ETaxes.errors.exportCSV();
|
||
});
|
||
|
||
$('#clear-log-dialog-btn').off('click').on('click', function() {
|
||
ETaxes.errors.clear();
|
||
d.hide();
|
||
});
|
||
}, 200);
|
||
},
|
||
|
||
// Экспорт в CSV
|
||
exportCSV: function() {
|
||
if (ETaxes.loadingErrors.length === 0) {
|
||
frappe.msgprint(__('No errors to export'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let csvContent = "data:text/csv;charset=utf-8,";
|
||
csvContent += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
|
||
|
||
ETaxes.loadingErrors.forEach(function(error) {
|
||
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||
const cleanSender = error.sender_name.replace(/"/g, '""');
|
||
|
||
const row = [
|
||
error.invoice_id,
|
||
error.serial_number,
|
||
error.creation_date,
|
||
cleanSender,
|
||
error.error_type,
|
||
cleanMessage,
|
||
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
|
||
].map(function(field) {
|
||
return '"' + (field || '') + '"';
|
||
}).join(',');
|
||
|
||
csvContent += row + "\n";
|
||
});
|
||
|
||
const encodedUri = encodeURI(csvContent);
|
||
const link = document.createElement("a");
|
||
link.setAttribute("href", encodedUri);
|
||
link.setAttribute("download", "etaxes_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
frappe.show_alert({
|
||
message: __('Error log exported successfully'),
|
||
indicator: 'green'
|
||
}, 3);
|
||
} catch (e) {
|
||
console.error('Export error:', e);
|
||
frappe.show_alert({
|
||
message: __('Failed to export error log'),
|
||
indicator: 'red'
|
||
}, 3);
|
||
}
|
||
},
|
||
|
||
// Показать сводку результатов
|
||
showSummary: function(totalInvoices, processedCount, errorsCount) {
|
||
let title = '';
|
||
let indicator = '';
|
||
let message = '';
|
||
|
||
if (errorsCount === 0) {
|
||
title = __('Import Completed Successfully');
|
||
indicator = 'green';
|
||
message = __('All ') + totalInvoices + __(' invoices were imported successfully.');
|
||
|
||
frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
} else {
|
||
title = errorsCount === totalInvoices ? __('Import Failed') : __('Import Completed with Errors');
|
||
indicator = processedCount > 0 ? 'orange' : 'red';
|
||
|
||
message = '<div style="margin-bottom: 15px;">';
|
||
if (processedCount > 0) {
|
||
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' invoices') + '<br>';
|
||
}
|
||
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' invoices');
|
||
message += '</div>';
|
||
|
||
message += '<div style="text-align: center;">' +
|
||
'<button class="btn btn-primary btn-sm" id="view-error-details-btn" style="margin-right: 10px;">' +
|
||
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
|
||
'<button class="btn btn-default btn-sm" id="export-error-log-btn">' +
|
||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
||
'</div>';
|
||
|
||
const summaryDialog = frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
|
||
// Добавляем обработчики событий
|
||
setTimeout(function() {
|
||
$('#view-error-details-btn').off('click').on('click', function() {
|
||
summaryDialog.hide();
|
||
ETaxes.errors.showDetails();
|
||
});
|
||
|
||
$('#export-error-log-btn').off('click').on('click', function() {
|
||
ETaxes.errors.exportCSV();
|
||
});
|
||
}, 200);
|
||
}
|
||
}
|
||
};
|
||
|
||
// ======= МОДУЛЬ ИМПОРТА ИНВОЙСОВ =======
|
||
ETaxes.import = {
|
||
// Показать диалог импорта из E-Taxes
|
||
showDialog: function() {
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
const minDate = moment("2020-01-01", "YYYY-MM-DD");
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Import invoice from E-Taxes'),
|
||
fields: [
|
||
{
|
||
fieldname: 'date_range_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Period')
|
||
},
|
||
{
|
||
fieldname: 'creationDateFrom',
|
||
fieldtype: 'Date',
|
||
label: __('Date from'),
|
||
default: moment().subtract(1, 'month').format('YYYY-MM-DD')
|
||
},
|
||
{
|
||
fieldname: 'creationDateTo',
|
||
fieldtype: 'Date',
|
||
label: __('Date to'),
|
||
default: moment().format('YYYY-MM-DD')
|
||
},
|
||
{
|
||
fieldname: 'settings_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Settings')
|
||
},
|
||
{
|
||
fieldname: 'warehouse',
|
||
fieldtype: 'Link',
|
||
label: __('Default warehouse'),
|
||
options: 'Warehouse',
|
||
reqd: true,
|
||
get_query: function() {
|
||
return {
|
||
filters: {
|
||
'is_group': 0,
|
||
'disabled': 0
|
||
}
|
||
};
|
||
}
|
||
}
|
||
],
|
||
primary_action_label: __('Search'),
|
||
primary_action: function() {
|
||
const values = d.get_values();
|
||
|
||
// Validate date range
|
||
const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
|
||
const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
|
||
const todayMoment = moment();
|
||
|
||
if (fromDateMoment.isBefore(minDate)) {
|
||
frappe.msgprint(__('From Date cannot be earlier than January 1, 2020'));
|
||
return;
|
||
}
|
||
|
||
if (toDateMoment.isAfter(todayMoment)) {
|
||
frappe.msgprint(__('To Date cannot be later than today'));
|
||
return;
|
||
}
|
||
|
||
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
|
||
frappe.msgprint(__('The date range cannot exceed one year.'));
|
||
return;
|
||
}
|
||
|
||
if (!values.warehouse) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('Please select a default warehouse')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const fromDate = moment(values.creationDateFrom).format('DD-MM-YYYY 00:00');
|
||
const toDate = moment(values.creationDateTo).format('DD-MM-YYYY 23:59');
|
||
|
||
d.hide();
|
||
ETaxes.import.loadInvoices(fromDate, toDate, values.warehouse);
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
});
|
||
},
|
||
|
||
// Загрузить инвойсы с фильтрацией дубликатов
|
||
loadInvoices: function(fromDate, toDate, warehouse, accumulatedInvoices = [], offset = 0) {
|
||
ETaxes.cancelLoading = false;
|
||
$(document).off('progress-cancel.etaxes_import');
|
||
|
||
if (offset === 0) {
|
||
frappe.show_alert({
|
||
message: __('Fetching E-Taxes invoices...'),
|
||
indicator: 'blue'
|
||
}, 3);
|
||
}
|
||
|
||
ETaxes.utils.getDefaultLogin(function(loginResponse) {
|
||
if (ETaxes.cancelLoading) return;
|
||
|
||
if (loginResponse && loginResponse.found) {
|
||
const mainToken = loginResponse.main_token;
|
||
|
||
if (!mainToken) {
|
||
frappe.msgprint({
|
||
title: __('Authentication Error'),
|
||
indicator: 'red',
|
||
message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const maxCount = 200;
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_invoices',
|
||
args: {
|
||
'token': mainToken,
|
||
'filters': JSON.stringify({
|
||
"creationDateFrom": fromDate,
|
||
"creationDateTo": toDate,
|
||
"maxCount": maxCount,
|
||
"offset": offset
|
||
})
|
||
},
|
||
callback: function(r) {
|
||
if (ETaxes.cancelLoading) return;
|
||
|
||
if (r.message && !r.message.error) {
|
||
const currentInvoices = r.message.data || r.message.invoices || [];
|
||
const hasMore = r.message.hasMore || false;
|
||
const allInvoices = accumulatedInvoices.concat(currentInvoices);
|
||
|
||
if (hasMore && currentInvoices.length > 0) {
|
||
const newOffset = offset + currentInvoices.length;
|
||
ETaxes.import.loadInvoices(fromDate, toDate, warehouse, allInvoices, newOffset);
|
||
} else {
|
||
if (allInvoices.length > 0) {
|
||
frappe.show_alert({
|
||
message: __('Processing ') + allInvoices.length + __(' invoices...'),
|
||
indicator: 'blue'
|
||
}, 3);
|
||
|
||
ETaxes.import.filterDuplicates(allInvoices, mainToken, warehouse);
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Information'),
|
||
indicator: 'blue',
|
||
message: __('No invoices found for the specified period')
|
||
});
|
||
}
|
||
}
|
||
} else if (r.message && r.message.error === 'unauthorized') {
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||
function() {
|
||
ETaxes.auth.startProcess(function() {
|
||
ETaxes.import.loadInvoices(fromDate, toDate, warehouse);
|
||
});
|
||
},
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: r.message ? r.message.message : __('Error loading invoices')
|
||
});
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error("Error fetching invoices:", error);
|
||
frappe.msgprint({
|
||
title: __('Network Error'),
|
||
indicator: 'red',
|
||
message: __('Error fetching data from server: ') + (error || 'Unknown error')
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('E-Taxes authentication settings not found')
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// Фильтрация дубликатов
|
||
filterDuplicates: function(allInvoices, token, warehouse) {
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_etaxes_purchases',
|
||
callback: function(etaxesR) {
|
||
try {
|
||
if (ETaxes.cancelLoading) return;
|
||
|
||
const importedInvoices = {};
|
||
|
||
if (etaxesR.message && etaxesR.message.success && etaxesR.message.purchases) {
|
||
etaxesR.message.purchases.forEach(function(purchase) {
|
||
if (purchase.etaxes_id) {
|
||
const normId = String(purchase.etaxes_id).trim();
|
||
importedInvoices[normId] = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
const filteredInvoices = [];
|
||
|
||
for (let i = 0; i < allInvoices.length; i++) {
|
||
const invoice = allInvoices[i];
|
||
const invoiceId = String(invoice.id).trim();
|
||
|
||
if (!importedInvoices.hasOwnProperty(invoiceId)) {
|
||
filteredInvoices.push(invoice);
|
||
}
|
||
}
|
||
|
||
if (filteredInvoices.length > 0) {
|
||
ETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Information'),
|
||
indicator: 'blue',
|
||
message: __('No new invoices found for the specified period (all ' +
|
||
allInvoices.length + ' are already imported)')
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error("Error processing purchase data:", e);
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('An error occurred while processing data: ') + e.message
|
||
});
|
||
}
|
||
},
|
||
error: function(err) {
|
||
console.error("Error fetching purchases:", err);
|
||
ETaxes.import.showInvoiceSelection(allInvoices, token, warehouse);
|
||
frappe.show_alert({
|
||
message: __('Failed to check for duplicates. Showing all invoices.'),
|
||
indicator: 'orange'
|
||
}, 5);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Показать диалог выбора инвойсов
|
||
showInvoiceSelection: function(invoices, token, warehouse) {
|
||
let invoiceTable = '<div class="etaxes-purchase-scroll" style="max-height: calc(100vh - 360px); min-height: 220px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 6px;"><table class="table table-bordered etaxes-invoices-table" style="width: 100%; table-layout: fixed; margin-bottom: 0;">';
|
||
invoiceTable += '<thead style="position: sticky; top: 0; z-index: 1;"><tr>' +
|
||
'<th style="width: 6%; background: var(--control-bg, var(--bg-color));"><input type="checkbox" class="select-all-invoices"></th>' +
|
||
'<th style="width: 24%; background: var(--control-bg, var(--bg-color));">' + __('Number') + '</th>' +
|
||
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Date') + '</th>' +
|
||
'<th style="width: 38%; background: var(--control-bg, var(--bg-color));">' + __('Supplier') + '</th>' +
|
||
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Amount') + '</th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
invoices.forEach(function(invoice) {
|
||
const serialNumber = invoice.serialNumber || '';
|
||
|
||
let creationDate = '';
|
||
if (invoice.createdAt) {
|
||
creationDate = moment(invoice.createdAt).format('DD.MM.YYYY');
|
||
} else if (invoice.creationDate) {
|
||
creationDate = moment(invoice.creationDate).format('DD.MM.YYYY');
|
||
}
|
||
|
||
const senderName = invoice.sender ? invoice.sender.name : (invoice.senderName || '');
|
||
const amount = invoice.totalAmount || invoice.amount || 0;
|
||
|
||
invoiceTable += '<tr>' +
|
||
'<td><input type="checkbox" class="select-invoice" data-id="' + invoice.id + '"></td>' +
|
||
'<td style="word-break: break-word;">' + serialNumber + '</td>' +
|
||
'<td>' + creationDate + '</td>' +
|
||
'<td style="word-break: break-word;">' + senderName + '</td>' +
|
||
'<td style="text-align: right;">' + ETaxes.utils.formatCurrency(amount) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
invoiceTable += '</tbody></table></div>';
|
||
|
||
const infoMessage =
|
||
'<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:8px;">' +
|
||
'<span class="indicator-pill blue" style="font-size:13px;">' +
|
||
__('New invoices found: ') + '<strong>' + invoices.length + '</strong>' +
|
||
'</span>' +
|
||
'<span style="color: var(--text-muted);">' +
|
||
__('Selected warehouse: ') + '<strong>' + frappe.utils.escape_html(warehouse) + '</strong>' +
|
||
'</span>' +
|
||
'</div>';
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Select invoice'),
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'info_html',
|
||
fieldtype: 'HTML',
|
||
options: infoMessage
|
||
},
|
||
{
|
||
fieldname: 'invoices_html',
|
||
fieldtype: 'HTML',
|
||
options: invoiceTable
|
||
}
|
||
],
|
||
primary_action_label: __('Load selected'),
|
||
primary_action: function() {
|
||
const selectedInvoiceIds = [];
|
||
d.$wrapper.find('.select-invoice:checked').each(function() {
|
||
selectedInvoiceIds.push($(this).data('id'));
|
||
});
|
||
|
||
if (selectedInvoiceIds.length === 0) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('No invoices selected')
|
||
});
|
||
return;
|
||
}
|
||
|
||
d.hide();
|
||
ETaxes.cancelLoading = false;
|
||
ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
|
||
},
|
||
secondary_action_label: __('Cancel'),
|
||
secondary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
d.$wrapper.find('.modal-dialog').css({
|
||
'max-width': '80%',
|
||
'width': '80%',
|
||
'margin': '30px auto'
|
||
});
|
||
|
||
d.$wrapper.find('.modal-body').css({
|
||
'padding': '15px'
|
||
});
|
||
|
||
d.show();
|
||
|
||
// Обработчик для чекбокса "выбрать все"
|
||
d.$wrapper.find('.select-all-invoices').on('change', function() {
|
||
const isChecked = $(this).prop('checked');
|
||
d.$wrapper.find('.select-invoice').prop('checked', isChecked);
|
||
});
|
||
},
|
||
|
||
// Надёжное закрытие прогресс-бара. frappe.hide_progress() в jey_theme/Bootstrap
|
||
// не всегда убирает модал (он зависает, и summary/диалог накладывается сверху).
|
||
// Принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
|
||
_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');
|
||
}
|
||
},
|
||
|
||
// Загрузка выбранных инвойсов
|
||
loadSelectedInvoices: function(invoiceIds, token, warehouse, processedCount = 0, currentIndex = 0) {
|
||
if (processedCount === 0 && currentIndex === 0) {
|
||
ETaxes.loadingErrors = [];
|
||
window.totalInvoicesToProcess = invoiceIds.length;
|
||
}
|
||
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
if (invoiceIds.length === 0) {
|
||
ETaxes.import._closeProgress();
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
const totalInvoices = window.totalInvoicesToProcess || processedCount;
|
||
|
||
ETaxes.errors.showSummary(totalInvoices, processedCount, errorsCount);
|
||
window.totalInvoicesToProcess = null;
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (ETaxes.cancelLoading) {
|
||
invoiceIds = [];
|
||
ETaxes.import._closeProgress();
|
||
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
|
||
return;
|
||
}
|
||
|
||
const invoiceId = invoiceIds.shift();
|
||
const isLastInvoice = invoiceIds.length === 0;
|
||
const totalToProcess = window.totalInvoicesToProcess || (processedCount + invoiceIds.length + 1);
|
||
|
||
currentIndex++;
|
||
|
||
frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
|
||
__('Loading invoice ') + currentIndex + __(' of ') + totalToProcess, null, true);
|
||
|
||
$(document).on('progress-cancel.loading_invoices', function() {
|
||
ETaxes.cancelLoading = true;
|
||
frappe.show_alert({
|
||
message: __('Cancelling import... Finishing current invoice.'),
|
||
indicator: 'orange'
|
||
}, 3);
|
||
ETaxes.import._closeProgress();
|
||
});
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_invoice_details',
|
||
args: {
|
||
'token': token,
|
||
'invoice_id': invoiceId
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && !r.message.error) {
|
||
const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
|
||
const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
|
||
const serialNumber = r.message.serialNumber || '';
|
||
const senderName = r.message.sender ? r.message.sender.name : '';
|
||
const total = r.message.totalAmount || r.message.amount || 0;
|
||
|
||
frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
|
||
__('Importing invoice: ') + serialNumber);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.import_invoice_with_mapping',
|
||
args: {
|
||
'invoice_data': r.message,
|
||
'purchase_order_name': null,
|
||
'schedule_date': scheduleDate,
|
||
'warehouse': warehouse
|
||
},
|
||
callback: function(importR) {
|
||
ETaxes.import._handleImportResult(importR, r.message, invoiceId, scheduleDate, senderName, total,
|
||
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
},
|
||
error: function(xhr, status, error) {
|
||
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
|
||
ETaxes.errors.add(r.message, 'Network Error', errorMessage, {
|
||
invoice_items: r.message.items || [],
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
});
|
||
} else if (r.message && r.message.error === 'unauthorized') {
|
||
ETaxes.import._closeProgress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||
function() {
|
||
ETaxes.auth.startProcess(function() {
|
||
ETaxes.utils.getDefaultLogin(function(loginR) {
|
||
if (loginR && loginR.found && loginR.main_token) {
|
||
invoiceIds.unshift(invoiceId);
|
||
ETaxes.import.loadSelectedInvoices(invoiceIds, loginR.main_token, warehouse, processedCount, currentIndex - 1);
|
||
} else {
|
||
ETaxes.errors.add({id: invoiceId}, 'Authentication Error',
|
||
'Failed to get token after authentication');
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||
}
|
||
}, false);
|
||
});
|
||
},
|
||
function() {
|
||
ETaxes.errors.add({id: invoiceId}, 'Authentication Cancelled',
|
||
'User cancelled authentication process');
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||
}
|
||
);
|
||
} else {
|
||
const errorMessage = r.message ? r.message.message : 'Failed to load invoice details';
|
||
ETaxes.errors.add({id: invoiceId}, 'Invoice Data Error', errorMessage, {
|
||
server_response: r.message
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
const errorMessage = 'Network error loading invoice details: ' + (error || 'Unknown network error');
|
||
ETaxes.errors.add({id: invoiceId}, 'Network Error', errorMessage, {
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Обработка результата импорта
|
||
_handleImportResult: function(importR, invoiceMessage, invoiceId, scheduleDate, senderName, total,
|
||
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
|
||
if (importR.message && importR.message.success) {
|
||
frappe.show_alert({
|
||
message: __('Invoice successfully imported: ') + (invoiceMessage.serialNumber || ''),
|
||
indicator: 'green'
|
||
}, 3);
|
||
|
||
const purchaseOrderName = importR.message.purchase_order;
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.create_etaxes_purchase',
|
||
args: {
|
||
'etaxes_id': invoiceId,
|
||
'date': scheduleDate,
|
||
'party': senderName,
|
||
'total': total
|
||
},
|
||
callback: function(etaxesR) {
|
||
if (etaxesR.message && etaxesR.message.success) {
|
||
frappe.call({
|
||
method: 'invoice_az.api.link_purchase_order_to_etaxes',
|
||
args: {
|
||
'purchase_order': purchaseOrderName,
|
||
'etaxes_purchase': etaxesR.message.name
|
||
},
|
||
callback: function(linkR) {
|
||
processedCount++;
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.errors.add(invoiceMessage, 'E-Taxes Purchase Creation Error',
|
||
etaxesR.message ? etaxesR.message.message : 'Failed to create E-Taxes Purchase record', {
|
||
server_response: etaxesR.message
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
}
|
||
});
|
||
} else if (importR.message && (importR.message.unmatched_items || importR.message.unmatched_parties || importR.message.unmatched_units)) {
|
||
let errorType = '';
|
||
const additionalData = { invoice_items: invoiceMessage.items || [] };
|
||
|
||
if (importR.message.unmatched_items) {
|
||
errorType = 'Unmapped Items';
|
||
additionalData.unmatched_items = importR.message.unmatched_items;
|
||
} else if (importR.message.unmatched_parties) {
|
||
errorType = 'Unmapped Parties';
|
||
additionalData.unmatched_parties = importR.message.unmatched_parties;
|
||
} else if (importR.message.unmatched_units) {
|
||
errorType = 'Unmapped Units';
|
||
additionalData.unmatched_units = importR.message.unmatched_units;
|
||
}
|
||
|
||
additionalData.server_response = importR.message;
|
||
ETaxes.errors.add(invoiceMessage, errorType, importR.message.message, additionalData);
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
} else {
|
||
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
|
||
const additionalData = {
|
||
invoice_items: invoiceMessage.items || [],
|
||
server_response: importR.message
|
||
};
|
||
|
||
if (importR.exc) {
|
||
additionalData.stack_trace = importR.exc;
|
||
}
|
||
|
||
ETaxes.errors.add(invoiceMessage, 'Import Error', errorMessage, additionalData);
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
},
|
||
|
||
// Продолжить или завершить обработку
|
||
_continueOrFinish: function(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
|
||
if (isLastInvoice) {
|
||
ETaxes.import._closeProgress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
const totalToProcess = window.totalInvoicesToProcess || processedCount;
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoiceIds.length === 0) {
|
||
ETaxes.import._closeProgress();
|
||
}
|
||
|
||
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
|
||
}, ETaxes.PROGRESS_UPDATE_DELAY);
|
||
},
|
||
|
||
// Socket.IO bulk import
|
||
loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
|
||
ETaxes.loadingErrors = [];
|
||
const total = invoiceIds.length;
|
||
// Защита от гонки 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');
|
||
}
|
||
};
|
||
|
||
// Progress + cancel are shown by the global Background Tasks widget
|
||
// (bottom-right). No blocking modal here; we keep only the complete
|
||
// subscription to show the final import summary.
|
||
frappe.realtime.off('purchase_import_complete');
|
||
frappe.realtime.on('purchase_import_complete', function(data) {
|
||
importFinished = true;
|
||
frappe.realtime.off('purchase_import_progress');
|
||
frappe.realtime.off('purchase_import_complete');
|
||
closeProgress();
|
||
|
||
(data.errors || []).forEach(function(err) {
|
||
ETaxes.loadingErrors.push({
|
||
invoice_id: err.invoice_id || 'Unknown',
|
||
error_type: err.unmatched_items ? 'Unmapped Items'
|
||
: err.unmatched_parties ? 'Unmapped Parties'
|
||
: err.unmatched_units ? 'Unmapped Units'
|
||
: 'Import Error',
|
||
error_message: err.error || 'Unknown error',
|
||
unmatched_items: err.unmatched_items,
|
||
unmatched_parties: err.unmatched_parties,
|
||
unmatched_units: err.unmatched_units
|
||
});
|
||
});
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(data.total, data.imported, errorsCount);
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
});
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.import_bulk_purchase_invoices',
|
||
args: {
|
||
invoice_ids: JSON.stringify(invoiceIds),
|
||
token: token,
|
||
warehouse: warehouse
|
||
},
|
||
callback: function(r) {
|
||
if (!r.message || !r.message.enqueued) {
|
||
frappe.realtime.off('purchase_import_progress');
|
||
frappe.realtime.off('purchase_import_complete');
|
||
closeProgress();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('Failed to start import job')
|
||
});
|
||
}
|
||
},
|
||
error: function() {
|
||
frappe.realtime.off('purchase_import_progress');
|
||
frappe.realtime.off('purchase_import_complete');
|
||
closeProgress();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('Network error starting import')
|
||
});
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
// ======= ОСНОВНЫЕ ОБРАБОТЧИКИ ФОРМ И СПИСКОВ =======
|
||
|
||
// Purchase Order Form
|
||
frappe.ui.form.on('Purchase Order', {
|
||
refresh: function(frm) {
|
||
if (frm.doc.is_taxes_doc) {
|
||
frm.set_read_only(true);
|
||
frm.page.set_indicator(__('Tax Document'), 'blue');
|
||
}
|
||
},
|
||
|
||
onload: function(frm) {
|
||
frm.set_df_property('is_taxes_doc', 'read_only', 1);
|
||
}
|
||
});
|
||
|
||
// Purchase Order List
|
||
frappe.listview_settings['Purchase Order'] = {
|
||
add_fields: ['supplier', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
|
||
|
||
get_indicator: function(doc) {
|
||
if (doc.status === 'Closed') {
|
||
return [__('Closed'), 'green', 'status,=,Closed'];
|
||
} else if (doc.status === 'On Hold') {
|
||
return [__('On Hold'), 'orange', 'status,=,On Hold'];
|
||
} else if (doc.status === 'Delivered') {
|
||
return [__('Delivered'), 'green', 'status,=,Delivered'];
|
||
} else if (doc.docstatus == 1) {
|
||
return [__('To Receive and Bill'), 'orange', 'docstatus,=,1'];
|
||
} else if (doc.docstatus == 0) {
|
||
return [__('Draft'), 'red', 'docstatus,=,0'];
|
||
} else if (doc.docstatus == 2) {
|
||
return [__('Cancelled'), 'grey', 'docstatus,=,2'];
|
||
}
|
||
},
|
||
|
||
onload: function(listview) {
|
||
listview.page.add_inner_button(__('Import from E-Taxes'), function() {
|
||
ETaxes.import.showDialog();
|
||
}, __('E-Taxes'));
|
||
|
||
listview.page.add_inner_button(__('Open Settings'), function() {
|
||
frappe.set_route('Form', 'E-Taxes Settings');
|
||
}, __('E-Taxes'));
|
||
|
||
listview.columns.push({
|
||
type: 'Check',
|
||
df: {
|
||
label: __('Tax Doc'),
|
||
fieldname: 'is_taxes_doc'
|
||
},
|
||
width: 120
|
||
});
|
||
|
||
listview.refresh();
|
||
},
|
||
|
||
formatters: {
|
||
is_taxes_doc: function(value) {
|
||
return value ?
|
||
`<span class="indicator-pill green"></span>` :
|
||
`<span class="indicator-pill gray"></span>`;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Purchase Invoice Form
|
||
frappe.ui.form.on('Purchase Invoice', {
|
||
refresh: function(frm) {
|
||
if (frm.doc.is_taxes_doc) {
|
||
frm.set_read_only(true);
|
||
frm.page.set_indicator(__('Tax Document'), 'blue');
|
||
}
|
||
},
|
||
|
||
onload: function(frm) {
|
||
frm.set_df_property('is_taxes_doc', 'read_only', 1);
|
||
}
|
||
});
|
||
|
||
// Purchase Invoice List
|
||
frappe.listview_settings['Purchase Invoice'] = {
|
||
onload: function(listview) {
|
||
listview.columns.push({
|
||
type: 'Check',
|
||
df: {
|
||
label: __('Tax Doc'),
|
||
fieldname: 'is_taxes_doc'
|
||
},
|
||
width: 120
|
||
});
|
||
|
||
listview.refresh();
|
||
},
|
||
|
||
formatters: {
|
||
is_taxes_doc: function(value) {
|
||
return value ?
|
||
`<span class="indicator-pill green"></span>` :
|
||
`<span class="indicator-pill gray"></span>`;
|
||
}
|
||
}
|
||
}; |