1874 lines
91 KiB
JavaScript
1874 lines
91 KiB
JavaScript
// Глобальный массив для хранения ошибок загрузки
|
||
var loading_errors = [];
|
||
|
||
frappe.ui.form.on('Purchase Order', {
|
||
refresh: function(frm) {
|
||
// Проверяем, установлена ли галочка is_taxes_doc
|
||
if (frm.doc.is_taxes_doc) {
|
||
// Делаем все поля read-only
|
||
frm.set_read_only(true);
|
||
|
||
// Альтернативно, можно добавить визуальное предупреждение
|
||
frm.page.set_indicator(__('Tax Document'), 'blue');
|
||
}
|
||
},
|
||
|
||
// Делаем поле is_taxes_doc тоже read-only при открытии документа
|
||
onload: function(frm) {
|
||
frm.set_df_property('is_taxes_doc', 'read_only', 1);
|
||
}
|
||
});
|
||
|
||
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) {
|
||
// Добавляем кнопку для импорта из E-Taxes
|
||
listview.page.add_menu_item(__('Import from E-Taxes'), function() {
|
||
show_etaxes_import_dialog_for_list();
|
||
});
|
||
|
||
// Добавляем поле как колонку (правильный способ)
|
||
listview.columns.push({
|
||
type: 'Check',
|
||
df: {
|
||
label: __('Tax Doc'),
|
||
fieldname: 'is_taxes_doc'
|
||
},
|
||
width: 120
|
||
});
|
||
|
||
// Принудительно обновляем список с новой колонкой
|
||
listview.refresh();
|
||
},
|
||
|
||
// Форматируем отображение поля is_taxes_doc
|
||
formatters: {
|
||
is_taxes_doc: function(value) {
|
||
return value ?
|
||
`<span class="indicator-pill green"></span>` :
|
||
`<span class="indicator-pill gray"></span>`;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Глобальная переменная для хранения диалога загрузки для Asan Login
|
||
var asan_login_dialog = null;
|
||
|
||
// Глобальная переменная для отмены процесса загрузки
|
||
var cancel_loading = false;
|
||
|
||
// Функция для отображения диалога загрузки Asan Login с синим дизайном
|
||
function show_asan_loading_dialog(message) {
|
||
if (!asan_login_dialog) {
|
||
asan_login_dialog = new frappe.ui.Dialog({
|
||
title: __('Loading'),
|
||
fields: [
|
||
{
|
||
fieldname: 'status_html',
|
||
fieldtype: 'HTML',
|
||
options: `
|
||
<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>${message || __('Please wait...')}</h4>
|
||
<p>${__('The operation may take some time')}</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>
|
||
`
|
||
}
|
||
],
|
||
primary_action_label: __('Cancel'),
|
||
primary_action: function() {
|
||
cancel_loading = true;
|
||
asan_login_dialog.$wrapper.find('.primary-action').prop('disabled', true);
|
||
asan_login_dialog.$wrapper.find('.primary-action').html(__('Cancelling...'));
|
||
$('#status_message p').text(__('Cancelling the operation.'));
|
||
}
|
||
});
|
||
} else {
|
||
// Обновляем сообщение
|
||
$('#status_message h4').text(message || __('Please wait...'));
|
||
$('#status_message p').text(__('The operation may take some time'));
|
||
}
|
||
|
||
// Сбрасываем флаг отмены
|
||
cancel_loading = false;
|
||
|
||
asan_login_dialog.show();
|
||
}
|
||
|
||
// Функция для скрытия диалога загрузки Asan Login
|
||
function hide_asan_loading_dialog() {
|
||
if (asan_login_dialog) {
|
||
try {
|
||
asan_login_dialog.hide();
|
||
} catch (e) {
|
||
console.error("Error hiding Asan loading dialog:", e);
|
||
}
|
||
asan_login_dialog = null;
|
||
}
|
||
// Сбрасываем флаг отмены
|
||
cancel_loading = false;
|
||
}
|
||
|
||
// Функция для обновления сообщения в диалоге загрузки Asan Login
|
||
function update_asan_loading_message(message, submessage) {
|
||
if (asan_login_dialog) {
|
||
$('#status_message h4').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#status_message p').text(submessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Функция для отображения кода верификации в диалоге Asan Login
|
||
function show_asan_verification_code(code) {
|
||
if (asan_login_dialog && code) {
|
||
$('#verification_code').text(code);
|
||
$('#verification_code_container').show();
|
||
$('#verification_code_container').attr('style', 'display: block !important; margin-top: 15px;');
|
||
}
|
||
}
|
||
|
||
// Функция для установки статуса успеха в диалоге Asan Login
|
||
function set_asan_loading_success(message, submessage, callback, delay = 2000) {
|
||
if (asan_login_dialog) {
|
||
// Меняем анимацию на галочку
|
||
asan_login_dialog.$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>
|
||
`);
|
||
|
||
// Обновляем сообщения
|
||
$('#status_message h4').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#status_message p').text(submessage);
|
||
}
|
||
|
||
// Скрываем код верификации
|
||
$('#verification_code_container').hide();
|
||
|
||
// Удаляем кнопки
|
||
asan_login_dialog.set_primary_action(null);
|
||
asan_login_dialog.set_secondary_action(null);
|
||
|
||
// Автоматически закрываем через указанную задержку
|
||
setTimeout(function() {
|
||
if (asan_login_dialog) {
|
||
asan_login_dialog.hide();
|
||
asan_login_dialog = null;
|
||
}
|
||
if (callback) callback();
|
||
}, delay);
|
||
} else if (callback) {
|
||
callback();
|
||
}
|
||
}
|
||
|
||
// Функция для установки статуса ошибки в диалоге Asan Login
|
||
function set_asan_loading_error(message, submessage, show_close_button = true) {
|
||
if (asan_login_dialog) {
|
||
// Меняем анимацию на крестик
|
||
asan_login_dialog.$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>
|
||
`);
|
||
|
||
// Обновляем сообщения
|
||
$('#status_message h4').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#status_message p').text(submessage);
|
||
}
|
||
|
||
// Скрываем код верификации
|
||
$('#verification_code_container').hide();
|
||
|
||
// При необходимости добавляем кнопку закрытия
|
||
if (show_close_button) {
|
||
asan_login_dialog.set_primary_action(__('Close'), function() {
|
||
hide_asan_loading_dialog();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Функция для проверки токена и обработки аутентификации при необходимости
|
||
function check_token_before_api_call(callback) {
|
||
// Проверка токена без отображения загрузки
|
||
frappe.call({
|
||
method: 'invoice_az.api.check_token_validity',
|
||
callback: function(r) {
|
||
if (r.message && r.message.valid) {
|
||
// Токен действителен, продолжаем с callback-функцией сразу
|
||
callback();
|
||
} else {
|
||
// Токен недействителен, спрашиваем пользователя о ре-аутентификации
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
|
||
function() {
|
||
// Если пользователь согласен, запускаем процесс аутентификации
|
||
start_authentication_process(callback);
|
||
},
|
||
function() {
|
||
// Если пользователь отказался
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
}
|
||
},
|
||
error: function() {
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('Failed to check authentication status')
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для запуска полного процесса аутентификации
|
||
function start_authentication_process(callback) {
|
||
show_asan_loading_dialog(__('Starting Authentication'));
|
||
update_asan_loading_message(__('Getting Asan Login settings...'), __('Please wait'));
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_default_asan_login',
|
||
callback: function(r) {
|
||
if (r.message && r.message.found) {
|
||
var asan_login_name = r.message.name;
|
||
update_asan_loading_message(
|
||
__('Sending authentication request...'),
|
||
__('This will send a request to your Asan Imza mobile app')
|
||
);
|
||
|
||
// Запускаем процесс аутентификации
|
||
frappe.call({
|
||
method: 'invoice_az.api.handle_authentication',
|
||
args: {
|
||
'asan_login_name': asan_login_name
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
const bearer_token = r.message.bearer_token;
|
||
|
||
// Отображаем код верификации, если он есть в ответе
|
||
if (r.message.verification_code) {
|
||
show_asan_verification_code(r.message.verification_code);
|
||
}
|
||
|
||
// Изменяем сообщение для ожидания подтверждения
|
||
update_asan_loading_message(
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request')
|
||
);
|
||
|
||
// Опрашиваем статус аутентификации
|
||
poll_authentication_status(asan_login_name, bearer_token, function(success) {
|
||
if (success) {
|
||
// Если аутентификация успешна, продолжаем процесс
|
||
complete_authentication(asan_login_name, callback);
|
||
} else {
|
||
// Если аутентификация не удалась, показываем сообщение об ошибке
|
||
set_asan_loading_error(
|
||
__('Authentication Failed'),
|
||
__('Could not authenticate with Asan Imza')
|
||
);
|
||
}
|
||
});
|
||
} else {
|
||
// Если запрос не удался, показываем сообщение об ошибке
|
||
set_asan_loading_error(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('Authentication request failed')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
// Если настройки не найдены, показываем сообщение об ошибке
|
||
set_asan_loading_error(
|
||
__('No Asan Login Settings'),
|
||
__('Please configure Asan Login settings first')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для опроса статуса аутентификации
|
||
function poll_authentication_status(asan_login_name, bearer_token, callback) {
|
||
// Счетчик попыток и флаг для остановки опроса
|
||
let attempts = 0;
|
||
const maxAttempts = 20; // 20 попыток с интервалом 6 секунд
|
||
let stopPolling = false;
|
||
|
||
// Функция опроса
|
||
function pollStatus() {
|
||
// Если достигнуто максимальное количество попыток или установлен флаг остановки
|
||
if (attempts >= maxAttempts || stopPolling) {
|
||
if (attempts >= maxAttempts) {
|
||
// Показываем сообщение о таймауте
|
||
set_asan_loading_error(
|
||
__('Authentication Timeout'),
|
||
__('The authentication request has timed out. Please try again.')
|
||
);
|
||
callback(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
|
||
// Обновляем счетчик попыток в сообщении
|
||
update_asan_loading_message(
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request') +
|
||
' (' + attempts + '/' + maxAttempts + ')'
|
||
);
|
||
|
||
// Проверяем статус авторизации
|
||
frappe.call({
|
||
method: 'invoice_az.api.poll_auth_status',
|
||
args: {
|
||
'asan_login_name': asan_login_name,
|
||
'bearer_token': bearer_token
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
if (r.message.authenticated) {
|
||
// Авторизация успешна
|
||
stopPolling = true;
|
||
set_asan_loading_success(
|
||
__('Authentication Successful'),
|
||
__('You are now authenticated with Asan Imza'),
|
||
function() {
|
||
callback(true);
|
||
}
|
||
);
|
||
} else {
|
||
// Продолжаем опрос
|
||
setTimeout(pollStatus, 6000);
|
||
}
|
||
} else {
|
||
// Если ошибка, останавливаем опрос и показываем сообщение
|
||
stopPolling = true;
|
||
set_asan_loading_error(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('An unknown error occurred')
|
||
);
|
||
callback(false);
|
||
}
|
||
},
|
||
error: function(err) {
|
||
// В случае ошибки продолжаем опрос
|
||
setTimeout(pollStatus, 6000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Начинаем опрос
|
||
pollStatus();
|
||
}
|
||
|
||
// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
|
||
function complete_authentication(asan_login_name, callback) {
|
||
show_asan_loading_dialog(__('Completing Authentication'));
|
||
update_asan_loading_message(__('Getting certificates...'), __('Please wait'));
|
||
|
||
// Получаем сертификаты
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_auth_certificates',
|
||
args: {
|
||
'asan_login_name': asan_login_name
|
||
},
|
||
callback: function(cert_r) {
|
||
if (cert_r.message && cert_r.message.success) {
|
||
update_asan_loading_message(
|
||
__('Certificates retrieved'),
|
||
__('Selecting certificate and taxpayer')
|
||
);
|
||
|
||
// Получаем текущий документ для проверки выбранного сертификата
|
||
frappe.call({
|
||
method: 'frappe.client.get',
|
||
args: {
|
||
doctype: 'Asan Login',
|
||
name: asan_login_name
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.selected_certificate_json) {
|
||
try {
|
||
const cert_data = JSON.parse(r.message.selected_certificate_json);
|
||
const cert_name = r.message.selected_certificate;
|
||
|
||
update_asan_loading_message(
|
||
__('Selecting certificate'),
|
||
cert_name
|
||
);
|
||
|
||
// Выбираем сертификат
|
||
frappe.call({
|
||
method: 'invoice_az.api.select_certificate',
|
||
args: {
|
||
'asan_login_name': asan_login_name,
|
||
'certificate_data': cert_data,
|
||
'certificate_name': cert_name
|
||
},
|
||
callback: function(select_r) {
|
||
if (select_r.message && select_r.message.success) {
|
||
update_asan_loading_message(
|
||
__('Selecting taxpayer'),
|
||
__('Using certificate: ') + cert_name
|
||
);
|
||
|
||
// Выбираем налогоплательщика
|
||
frappe.call({
|
||
method: 'invoice_az.api.select_taxpayer',
|
||
args: {
|
||
'asan_login_name': asan_login_name
|
||
},
|
||
callback: function(tax_r) {
|
||
if (tax_r.message && tax_r.message.success) {
|
||
set_asan_loading_success(
|
||
__('Authentication Complete'),
|
||
__('You can now access E-Taxes services'),
|
||
callback
|
||
);
|
||
} else {
|
||
hide_asan_loading_dialog();
|
||
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
hide_asan_loading_dialog();
|
||
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
|
||
}
|
||
}
|
||
});
|
||
} catch (e) {
|
||
hide_asan_loading_dialog();
|
||
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
|
||
}
|
||
} else {
|
||
hide_asan_loading_dialog();
|
||
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
set_asan_loading_error(
|
||
__('Error'),
|
||
cert_r.message ? cert_r.message.message : __('Failed to get certificates')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для отображения селектора сертификатов
|
||
function show_certificate_selector(certificates, asan_login_name, callback) {
|
||
if (!certificates || !certificates.length) {
|
||
frappe.msgprint(__('No certificates available'));
|
||
return;
|
||
}
|
||
|
||
// Создаем таблицу сертификатов
|
||
var cert_html = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
|
||
cert_html += '<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) {
|
||
var name = '';
|
||
var 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 || '';
|
||
}
|
||
|
||
cert_html += '<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>';
|
||
});
|
||
|
||
cert_html += '</tbody></table></div>';
|
||
|
||
var dialog = new frappe.ui.Dialog({
|
||
title: __('Select Certificate'),
|
||
fields: [{
|
||
fieldtype: 'HTML',
|
||
fieldname: 'certificates',
|
||
options: cert_html
|
||
}]
|
||
});
|
||
|
||
dialog.show();
|
||
|
||
// Обработчик нажатия на кнопку выбора сертификата
|
||
dialog.$wrapper.find('.select-cert').on('click', function() {
|
||
var index = $(this).data('index');
|
||
var 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();
|
||
|
||
// Показываем прогресс
|
||
show_asan_loading_dialog(__('Selecting Certificate: ') + certName);
|
||
|
||
// Отправляем запрос на выбор сертификата
|
||
frappe.call({
|
||
method: 'invoice_az.api.select_certificate',
|
||
args: {
|
||
'asan_login_name': asan_login_name,
|
||
'certificate_data': cert,
|
||
'certificate_name': certName
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
update_asan_loading_message(
|
||
__('Certificate selected'),
|
||
__('Now selecting taxpayer information')
|
||
);
|
||
|
||
// Выбираем налогоплательщика
|
||
frappe.call({
|
||
method: 'invoice_az.api.select_taxpayer',
|
||
args: {
|
||
'asan_login_name': asan_login_name
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
set_asan_loading_success(
|
||
__('Authentication Complete'),
|
||
__('You can now access E-Taxes services'),
|
||
callback
|
||
);
|
||
} else {
|
||
set_asan_loading_error(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select taxpayer')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
set_asan_loading_error(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select certificate')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function show_etaxes_import_dialog_for_list() {
|
||
// Сначала проверяем токен
|
||
check_token_before_api_call(function() {
|
||
// Определяем минимальную дату - 1 января 2020
|
||
const minDate = moment("2020-01-01", "YYYY-MM-DD");
|
||
|
||
// Создаем диалог для выбора периода и склада
|
||
var 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() {
|
||
var 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();
|
||
|
||
// Check if the dates are within the allowed range
|
||
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;
|
||
}
|
||
|
||
// Форматируем даты
|
||
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : '';
|
||
let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : '';
|
||
let warehouse = values.warehouse;
|
||
|
||
if (!warehouse) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('Please select a default warehouse')
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Закрываем диалог
|
||
d.hide();
|
||
|
||
// Загружаем счета-фактуры (с пустым массивом и offset=0 для начала)
|
||
load_etaxes_invoices(null, fromDate, toDate, warehouse, [], 0);
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
});
|
||
}
|
||
|
||
// Функция для загрузки инвойсов с фильтрацией дубликатов
|
||
function load_etaxes_invoices(frm, fromDate, toDate, warehouse, accumulated_invoices = [], offset = 0) {
|
||
// Устанавливаем флаг отмены в false при начале новой загрузки
|
||
cancel_loading = false;
|
||
|
||
// Удаляем существующие обработчики отмены, если они есть
|
||
$(document).off('progress-cancel.etaxes_import');
|
||
|
||
// Показываем индикатор только в начале загрузки (при первом запросе)
|
||
if (offset === 0) {
|
||
frappe.show_alert({
|
||
message: __('Fetching E-Taxes invoices...'),
|
||
indicator: 'blue'
|
||
}, 3);
|
||
}
|
||
|
||
// Сначала получаем настройки авторизации
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_default_asan_login',
|
||
callback: function(login_response) {
|
||
if (cancel_loading) {
|
||
return;
|
||
}
|
||
|
||
if (login_response.message && login_response.message.found) {
|
||
// Получаем токен из настроек
|
||
let main_token = login_response.message.main_token;
|
||
|
||
if (!main_token) {
|
||
frappe.msgprint({
|
||
title: __('Authentication Error'),
|
||
indicator: 'red',
|
||
message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Используем фиксированный размер партии (maxCount) - 200 для продакшена
|
||
const maxCount = 200;
|
||
|
||
// Загружаем счета-фактуры с использованием полученного токена
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_invoices',
|
||
args: {
|
||
'token': main_token,
|
||
'filters': JSON.stringify({
|
||
"creationDateFrom": fromDate,
|
||
"creationDateTo": toDate,
|
||
"maxCount": maxCount,
|
||
"offset": offset
|
||
})
|
||
},
|
||
callback: function(r) {
|
||
if (cancel_loading) {
|
||
return;
|
||
}
|
||
|
||
if (r.message && !r.message.error) {
|
||
// Получаем данные о счетах-фактурах из текущей партии
|
||
let currentInvoices = r.message.data || r.message.invoices || [];
|
||
|
||
// Проверяем, есть ли в ответе hasMore
|
||
let hasMore = r.message.hasMore || false;
|
||
|
||
// Добавляем полученные инвойсы к уже накопленным
|
||
let allInvoices = accumulated_invoices.concat(currentInvoices);
|
||
|
||
// Не показываем промежуточные сообщения о каждой новой партии
|
||
|
||
if (hasMore && currentInvoices.length > 0) {
|
||
// Если есть еще инвойсы (hasMore = true), загружаем следующую партию
|
||
let newOffset = offset + currentInvoices.length;
|
||
|
||
// Рекурсивно вызываем функцию с увеличенным смещением
|
||
load_etaxes_invoices(frm, fromDate, toDate, warehouse, allInvoices, newOffset);
|
||
} else {
|
||
// Если больше нет инвойсов или получили пустой массив, переходим к обработке
|
||
if (allInvoices.length > 0) {
|
||
// Показываем итоговое сообщение только когда все инвойсы загружены
|
||
frappe.show_alert({
|
||
message: __('Processing ') + allInvoices.length + __(' invoices...'),
|
||
indicator: 'blue'
|
||
}, 3);
|
||
|
||
// Получаем список уже импортированных инвойсов из E-Taxes Purchase
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_etaxes_purchases',
|
||
callback: function(etaxes_r) {
|
||
try {
|
||
if (cancel_loading) {
|
||
return;
|
||
}
|
||
|
||
// Преобразуем данные E-Taxes Purchase в хеш-таблицу для быстрого поиска
|
||
let importedInvoices = {};
|
||
let importedCount = 0;
|
||
|
||
if (etaxes_r.message && etaxes_r.message.success && etaxes_r.message.purchases) {
|
||
importedCount = etaxes_r.message.purchases.length;
|
||
|
||
etaxes_r.message.purchases.forEach(function(purchase) {
|
||
// Проверяем и нормализуем ID
|
||
if (purchase.etaxes_id) {
|
||
// Нормализуем ID для более надежного сравнения
|
||
let normId = String(purchase.etaxes_id).trim();
|
||
importedInvoices[normId] = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Фильтруем список инвойсов, исключая дубликаты
|
||
let filteredInvoices = [];
|
||
let skippedCount = 0;
|
||
|
||
for (let i = 0; i < allInvoices.length; i++) {
|
||
const invoice = allInvoices[i];
|
||
// Нормализуем ID инвойса из API
|
||
let invoiceId = String(invoice.id).trim();
|
||
|
||
// Проверяем, есть ли запись с таким ID в E-Taxes Purchase
|
||
if (!importedInvoices.hasOwnProperty(invoiceId)) {
|
||
filteredInvoices.push(invoice);
|
||
} else {
|
||
skippedCount++;
|
||
}
|
||
}
|
||
|
||
if (filteredInvoices.length > 0) {
|
||
// Если есть не импортированные инвойсы, показываем их для выбора
|
||
show_invoice_selection_dialog(frm, filteredInvoices, main_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);
|
||
|
||
// В случае ошибки показываем все инвойсы без фильтрации
|
||
show_invoice_selection_dialog(frm, allInvoices, main_token, warehouse);
|
||
|
||
frappe.show_alert({
|
||
message: __('Failed to check for duplicates. Showing all invoices.'),
|
||
indicator: 'orange'
|
||
}, 5);
|
||
}
|
||
});
|
||
} 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() {
|
||
// Запускаем процесс аутентификации и после успеха повторяем загрузку
|
||
start_authentication_process(function() {
|
||
// После успешной аутентификации повторяем загрузку
|
||
load_etaxes_invoices(frm, 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')
|
||
});
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error("Error fetching Asan login settings:", error);
|
||
|
||
frappe.msgprint({
|
||
title: __('Network Error'),
|
||
indicator: 'red',
|
||
message: __('Error fetching authentication settings: ') + (error || 'Unknown error')
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для отображения диалога с выбором счета-фактуры (широкая версия)
|
||
function show_invoice_selection_dialog(frm, invoices, token, warehouse) {
|
||
// Создаем таблицу с инвойсами
|
||
var invoice_table = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered etaxes-invoices-table" style="width: 100%; table-layout: fixed;">';
|
||
invoice_table += '<thead><tr>' +
|
||
'<th style="width: 6%;"><input type="checkbox" class="select-all-invoices"></th>' +
|
||
'<th style="width: 24%;">' + __('Number') + '</th>' +
|
||
'<th style="width: 16%;">' + __('Date') + '</th>' +
|
||
'<th style="width: 38%;">' + __('Supplier') + '</th>' +
|
||
'<th style="width: 16%;">' + __('Amount') + '</th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
invoices.forEach(function(invoice) {
|
||
let serialNumber = invoice.serialNumber || '';
|
||
|
||
// Используем createdAt для отображения даты, с запасным вариантом creationDate
|
||
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');
|
||
}
|
||
|
||
let senderName = invoice.sender ? invoice.sender.name : (invoice.senderName || '');
|
||
let amount = invoice.totalAmount || invoice.amount || 0;
|
||
|
||
invoice_table += '<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;">' + format_currency(amount) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
invoice_table += '</tbody></table></div>';
|
||
|
||
// Информация о количестве найденных инвойсов
|
||
let info_message = '<div class="alert alert-info">' +
|
||
__('New invoices found: ') + invoices.length +
|
||
'</div>';
|
||
|
||
// Создаем диалог с оптимальной шириной
|
||
var d = new frappe.ui.Dialog({
|
||
title: __('Select invoice'),
|
||
size: 'large', // Используем large вместо extra-large
|
||
fields: [
|
||
{
|
||
fieldname: 'info_html',
|
||
fieldtype: 'HTML',
|
||
options: info_message
|
||
},
|
||
{
|
||
fieldname: 'invoices_html',
|
||
fieldtype: 'HTML',
|
||
options: invoice_table
|
||
},
|
||
{
|
||
fieldname: 'warehouse_display',
|
||
fieldtype: 'HTML',
|
||
options: '<div class="row"><div class="col-xs-12"><div class="alert alert-info">' +
|
||
__('Selected warehouse: ') + '<strong>' + warehouse + '</strong></div></div></div>'
|
||
}
|
||
],
|
||
primary_action_label: __('Load selected'),
|
||
primary_action: function() {
|
||
// Получаем выбранные инвойсы
|
||
var selected_invoice_ids = [];
|
||
d.$wrapper.find('.select-invoice:checked').each(function() {
|
||
selected_invoice_ids.push($(this).data('id'));
|
||
});
|
||
|
||
if (selected_invoice_ids.length === 0) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('No invoices selected')
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Закрываем диалог
|
||
d.hide();
|
||
|
||
// Сбрасываем флаг отмены
|
||
cancel_loading = false;
|
||
|
||
// Загружаем выбранные инвойсы последовательно
|
||
load_selected_invoices(frm, selected_invoice_ids, token, warehouse, 0);
|
||
},
|
||
secondary_action_label: __('Cancel'),
|
||
secondary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
// Добавляем оптимизированные CSS стили
|
||
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() {
|
||
var is_checked = $(this).prop('checked');
|
||
d.$wrapper.find('.select-invoice').prop('checked', is_checked);
|
||
});
|
||
}
|
||
|
||
// Модифицированная функция загрузки выбранных инвойсов (исправлен счетчик прогресса)
|
||
function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count = 0, current_index = 0) {
|
||
// Сброс массива ошибок в начале загрузки
|
||
if (processed_count === 0 && current_index === 0) {
|
||
loading_errors = [];
|
||
window.total_invoices_to_process = invoice_ids.length;
|
||
}
|
||
|
||
// Удаляем существующие обработчики отмены, если они есть
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
// Если все инвойсы обработаны или был запрос на отмену
|
||
if (invoice_ids.length === 0) {
|
||
// Скрываем прогресс-бар
|
||
frappe.hide_progress();
|
||
|
||
// Показываем сводку результатов
|
||
let errors_count = loading_errors.length;
|
||
let total_invoices = window.total_invoices_to_process || processed_count;
|
||
|
||
show_loading_summary(total_invoices, processed_count, errors_count);
|
||
|
||
// Сбрасываем счетчик
|
||
window.total_invoices_to_process = null;
|
||
|
||
// Обновляем форму или список
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// Проверяем, была ли отменена загрузка
|
||
if (cancel_loading) {
|
||
// Если была отмена, пропускаем все оставшиеся инвойсы
|
||
invoice_ids = [];
|
||
frappe.hide_progress();
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
return;
|
||
}
|
||
|
||
// Берем первый ID из списка
|
||
var invoice_id = invoice_ids.shift();
|
||
var is_last_invoice = invoice_ids.length === 0;
|
||
var total_to_process = window.total_invoices_to_process || (processed_count + invoice_ids.length + 1);
|
||
|
||
// ИСПРАВЛЕНО: используем current_index для корректного отображения прогресса
|
||
current_index++;
|
||
|
||
// Обновляем прогресс-бар
|
||
frappe.show_progress(__('Importing Invoices'), current_index - 1, total_to_process,
|
||
__('Loading invoice ') + current_index + __(' of ') + total_to_process, null, true);
|
||
|
||
// Обработчик отмены загрузки
|
||
$(document).on('progress-cancel.loading_invoices', function() {
|
||
cancel_loading = true;
|
||
frappe.show_alert({
|
||
message: __('Cancelling import... Finishing current invoice.'),
|
||
indicator: 'orange'
|
||
}, 3);
|
||
frappe.hide_progress();
|
||
});
|
||
|
||
// Получаем детали счета-фактуры
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_invoice_details',
|
||
args: {
|
||
'token': token,
|
||
'invoice_id': invoice_id
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && !r.message.error) {
|
||
// Получаем дату создания инвойса
|
||
let creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
|
||
let scheduleDate = moment(creationDate).format('YYYY-MM-DD');
|
||
let serialNumber = r.message.serialNumber || '';
|
||
let senderName = r.message.sender ? r.message.sender.name : '';
|
||
let total = r.message.totalAmount || r.message.amount || 0;
|
||
|
||
frappe.show_progress(__('Importing Invoices'), current_index - 1, total_to_process,
|
||
__('Importing invoice: ') + serialNumber);
|
||
|
||
// Импортируем данные инвойса с указанием даты и склада
|
||
frappe.call({
|
||
method: 'invoice_az.api.import_invoice_with_mapping',
|
||
args: {
|
||
'invoice_data': r.message,
|
||
'purchase_order_name': frm ? frm.doc.name : null,
|
||
'schedule_date': scheduleDate,
|
||
'warehouse': warehouse
|
||
},
|
||
callback: function(import_r) {
|
||
if (import_r.message && import_r.message.success) {
|
||
// Успешный импорт
|
||
frappe.show_alert({
|
||
message: __('Invoice successfully imported: ') + serialNumber,
|
||
indicator: 'green'
|
||
}, 3);
|
||
|
||
let purchase_order_name = import_r.message.purchase_order;
|
||
|
||
// Сохраняем информацию в E-Taxes Purchase
|
||
frappe.call({
|
||
method: 'invoice_az.api.create_etaxes_purchase',
|
||
args: {
|
||
'etaxes_id': invoice_id,
|
||
'date': scheduleDate,
|
||
'party': senderName,
|
||
'total': total
|
||
},
|
||
callback: function(etaxes_r) {
|
||
if (etaxes_r.message && etaxes_r.message.success) {
|
||
frappe.call({
|
||
method: 'invoice_az.api.link_purchase_order_to_etaxes',
|
||
args: {
|
||
'purchase_order': purchase_order_name,
|
||
'etaxes_purchase': etaxes_r.message.name
|
||
},
|
||
callback: function(link_r) {
|
||
processed_count++; // УСПЕШНО обработан
|
||
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) {
|
||
frm = null;
|
||
}
|
||
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
}
|
||
});
|
||
} else {
|
||
// Ошибка создания E-Taxes Purchase
|
||
add_loading_error(r.message, 'E-Taxes Purchase Creation Error',
|
||
etaxes_r.message ? etaxes_r.message.message : 'Failed to create E-Taxes Purchase record', {
|
||
server_response: etaxes_r.message
|
||
});
|
||
|
||
// НЕ увеличиваем processed_count для ошибок
|
||
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) {
|
||
frm = null;
|
||
}
|
||
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
}
|
||
}
|
||
});
|
||
} else if (import_r.message && (import_r.message.unmatched_items || import_r.message.unmatched_parties || import_r.message.unmatched_units)) {
|
||
// Несопоставленные элементы
|
||
let error_type = '';
|
||
let additional_data = {
|
||
invoice_items: r.message.items || []
|
||
};
|
||
|
||
if (import_r.message.unmatched_items) {
|
||
error_type = 'Unmapped Items';
|
||
additional_data.unmatched_items = import_r.message.unmatched_items;
|
||
} else if (import_r.message.unmatched_parties) {
|
||
error_type = 'Unmapped Parties';
|
||
additional_data.unmatched_parties = import_r.message.unmatched_parties;
|
||
} else if (import_r.message.unmatched_units) {
|
||
error_type = 'Unmapped Units';
|
||
additional_data.unmatched_units = import_r.message.unmatched_units;
|
||
}
|
||
|
||
additional_data.server_response = import_r.message;
|
||
|
||
add_loading_error(r.message, error_type, import_r.message.message, additional_data);
|
||
|
||
// НЕ увеличиваем processed_count для ошибок
|
||
|
||
// Продолжаем со следующим инвойсом
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
} else {
|
||
// Общая ошибка импорта
|
||
let error_message = import_r.message ? import_r.message.message : 'Unknown import error';
|
||
let additional_data = {
|
||
invoice_items: r.message.items || [],
|
||
server_response: import_r.message
|
||
};
|
||
|
||
// Пытаемся получить более детальную информацию об ошибке
|
||
if (import_r.exc) {
|
||
additional_data.stack_trace = import_r.exc;
|
||
}
|
||
|
||
add_loading_error(r.message, 'Import Error', error_message, additional_data);
|
||
|
||
// НЕ увеличиваем processed_count для ошибок
|
||
|
||
// Переходим к следующему инвойсу
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
// Ошибка вызова import_invoice_with_mapping
|
||
let error_message = 'Network error during import: ' + (error || 'Unknown network error');
|
||
let additional_data = {
|
||
invoice_items: r.message.items || [],
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
};
|
||
|
||
add_loading_error(r.message, 'Network Error', error_message, additional_data);
|
||
|
||
// НЕ увеличиваем processed_count для ошибок
|
||
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
}
|
||
});
|
||
} else if (r.message && r.message.error === 'unauthorized') {
|
||
// Ошибка авторизации
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||
function() {
|
||
start_authentication_process(function() {
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_default_asan_login',
|
||
callback: function(login_r) {
|
||
if (login_r.message && login_r.message.found && login_r.message.main_token) {
|
||
invoice_ids.unshift(invoice_id);
|
||
load_selected_invoices(frm, invoice_ids, login_r.message.main_token, warehouse, processed_count, current_index - 1);
|
||
} else {
|
||
add_loading_error({id: invoice_id}, 'Authentication Error',
|
||
'Failed to get token after authentication');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
},
|
||
function() {
|
||
add_loading_error({id: invoice_id}, 'Authentication Cancelled',
|
||
'User cancelled authentication process');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
}
|
||
);
|
||
} else {
|
||
// Ошибка получения данных инвойса
|
||
let error_message = r.message ? r.message.message : 'Failed to load invoice details';
|
||
add_loading_error({id: invoice_id}, 'Invoice Data Error', error_message, {
|
||
server_response: r.message
|
||
});
|
||
|
||
// НЕ увеличиваем processed_count для ошибок
|
||
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
// Ошибка вызова get_invoice_details
|
||
let error_message = 'Network error loading invoice details: ' + (error || 'Unknown network error');
|
||
add_loading_error({id: invoice_id}, 'Network Error', error_message, {
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
});
|
||
|
||
// НЕ увеличиваем processed_count для ошибок
|
||
|
||
if (is_last_invoice) {
|
||
frappe.hide_progress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
let errors_count = loading_errors.length;
|
||
show_loading_summary(total_to_process, processed_count, errors_count);
|
||
|
||
if (frm) {
|
||
frm.reload_doc();
|
||
} else if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoice_ids.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count, current_index);
|
||
}, 300);
|
||
}
|
||
});
|
||
}
|
||
|
||
function add_loading_error(invoice_data, error_type, error_message, additional_data = {}) {
|
||
let error_entry = {
|
||
invoice_id: invoice_data.id || 'Unknown',
|
||
serial_number: invoice_data.serialNumber || 'Unknown',
|
||
sender_name: invoice_data.sender ? invoice_data.sender.name : 'Unknown',
|
||
creation_date: invoice_data.creationDate || invoice_data.createdAt || invoice_data.date || 'Unknown', // ИСПРАВЛЕНО: добавлены альтернативы
|
||
error_type: error_type,
|
||
error_message: error_message,
|
||
timestamp: new Date().toISOString(),
|
||
...additional_data
|
||
};
|
||
|
||
loading_errors.push(error_entry);
|
||
console.error('E-Taxes Import Error:', error_entry);
|
||
}
|
||
|
||
// Функция для отображения сводки ошибок
|
||
function show_loading_summary(total_invoices, processed_count, errors_count) {
|
||
let title = '';
|
||
let indicator = '';
|
||
let message = '';
|
||
|
||
if (errors_count === 0) {
|
||
title = __('Import Completed Successfully');
|
||
indicator = 'green';
|
||
message = __('All ') + total_invoices + __(' invoices were imported successfully.');
|
||
|
||
frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
} else {
|
||
title = errors_count === total_invoices ? __('Import Failed') : __('Import Completed with Errors');
|
||
indicator = processed_count > 0 ? 'orange' : 'red';
|
||
|
||
message = '<div style="margin-bottom: 15px;">';
|
||
if (processed_count > 0) {
|
||
message += __('Successfully imported: ') + '<strong>' + processed_count + '</strong>' + __(' invoices') + '<br>';
|
||
}
|
||
message += __('Failed: ') + '<strong>' + errors_count + '</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>';
|
||
|
||
let summary_dialog = frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
|
||
// Добавляем обработчики событий после создания диалога
|
||
setTimeout(function() {
|
||
$('#view-error-details-btn').off('click').on('click', function() {
|
||
summary_dialog.hide(); // ИЗМЕНЕНИЕ: Закрываем сводку перед открытием деталей
|
||
show_detailed_error_log();
|
||
});
|
||
|
||
$('#export-error-log-btn').off('click').on('click', function() {
|
||
export_error_log();
|
||
});
|
||
}, 200);
|
||
}
|
||
}
|
||
|
||
// Функция для отображения детального лога ошибок (полная версия)
|
||
function show_detailed_error_log() {
|
||
if (loading_errors.length === 0) {
|
||
frappe.msgprint(__('No errors to display'));
|
||
return;
|
||
}
|
||
|
||
let errors_html = '<div class="error-log-container">';
|
||
|
||
loading_errors.forEach(function(error, index) {
|
||
// ИСПРАВЛЕНО: обработка даты документа из E-Taxes
|
||
let document_date = 'No date';
|
||
if (error.creation_date && error.creation_date !== 'Unknown') {
|
||
try {
|
||
document_date = moment(error.creation_date).format('DD.MM.YYYY');
|
||
} catch (e) {
|
||
document_date = 'Invalid date';
|
||
}
|
||
}
|
||
|
||
errors_html += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
|
||
|
||
// Заголовок ошибки
|
||
errors_html += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||
errors_html += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||
errors_html += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||
'<strong>' + (error.serial_number || error.invoice_id) + '</strong></h5>';
|
||
errors_html += '<span class="label label-danger">' + error.error_type + '</span>';
|
||
errors_html += '</div>';
|
||
// ИСПРАВЛЕНО: убрана дата, оставлен только supplier
|
||
errors_html += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
|
||
errors_html += '</div>';
|
||
|
||
// Сообщение об ошибке
|
||
errors_html += '<div class="error-message" style="margin-bottom: 10px;">';
|
||
errors_html += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
|
||
errors_html += '</div>';
|
||
|
||
// Детали ошибки (только одна категория для избежания дублирования)
|
||
if (error.unmatched_items && error.unmatched_items.length > 0) {
|
||
errors_html += '<div class="error-details">';
|
||
errors_html += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped items:') + '</div>';
|
||
errors_html += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_items.forEach(function(item, idx) {
|
||
errors_html += '<li><strong>Row ' + (idx + 1) + ':</strong> ' + item.name;
|
||
if (item.code) {
|
||
errors_html += ' <em>(Code: ' + item.code + ')</em>';
|
||
}
|
||
errors_html += '</li>';
|
||
});
|
||
errors_html += '</ul></div>';
|
||
} else if (error.unmatched_parties && error.unmatched_parties.length > 0) {
|
||
errors_html += '<div class="error-details">';
|
||
errors_html += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped parties:') + '</div>';
|
||
errors_html += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_parties.forEach(function(party, idx) {
|
||
errors_html += '<li><strong>Party ' + (idx + 1) + ':</strong> ' + party.name;
|
||
if (party.tin) {
|
||
errors_html += ' <em>(TIN: ' + party.tin + ')</em>';
|
||
}
|
||
if (party.type) {
|
||
errors_html += ' <span class="label label-info" style="font-size: 10px;">' + party.type + '</span>';
|
||
}
|
||
errors_html += '</li>';
|
||
});
|
||
errors_html += '</ul></div>';
|
||
} else if (error.unmatched_units && error.unmatched_units.length > 0) {
|
||
errors_html += '<div class="error-details">';
|
||
errors_html += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped units:') + '</div>';
|
||
errors_html += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_units.forEach(function(unit, idx) {
|
||
errors_html += '<li><strong>Unit ' + (idx + 1) + ':</strong> ' + unit.name + '</li>';
|
||
});
|
||
errors_html += '</ul></div>';
|
||
} else if (error.invoice_items && error.invoice_items.length > 0) {
|
||
// Показываем items только если нет других unmapped категорий
|
||
errors_html += '<div class="error-details">';
|
||
errors_html += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Invoice items:') + '</div>';
|
||
errors_html += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.invoice_items.forEach(function(item, idx) {
|
||
errors_html += '<li><strong>Row ' + (idx + 1) + ':</strong> ' + item.productName +
|
||
' <em>(Qty: ' + item.quantity + ', Price: ' + item.pricePerUnit + ')</em></li>';
|
||
});
|
||
errors_html += '</ul></div>';
|
||
}
|
||
|
||
// ИСПРАВЛЕНО: показываем дату документа из E-Taxes, а не timestamp
|
||
errors_html += '<div style="text-align: right; margin-top: 10px;">';
|
||
errors_html += '<small style="color: var(--text-muted);"><em>' + document_date + '</em></small>';
|
||
errors_html += '</div>';
|
||
|
||
errors_html += '</div>';
|
||
});
|
||
|
||
errors_html += '</div>';
|
||
|
||
// Группируем ошибки по типам для статистики
|
||
let error_types = {};
|
||
loading_errors.forEach(function(error) {
|
||
error_types[error.error_type] = (error_types[error.error_type] || 0) + 1;
|
||
});
|
||
|
||
// Простая статистика вверху
|
||
let stats_html = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
|
||
stats_html += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||
stats_html += '<div>';
|
||
stats_html += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + loading_errors.length + '<br>';
|
||
stats_html += '<span style="color: var(--text-muted);">';
|
||
Object.keys(error_types).forEach(function(type, index) {
|
||
if (index > 0) stats_html += ' • ';
|
||
stats_html += type + ': ' + error_types[type];
|
||
});
|
||
stats_html += '</span>';
|
||
stats_html += '</div>';
|
||
stats_html += '<div>';
|
||
stats_html += '<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>';
|
||
stats_html += '<button class="btn btn-info btn-sm" id="copy-clipboard-dialog-btn" style="margin-right: 5px;">' +
|
||
'<i class="fa fa-copy"></i> ' + __('Copy') + '</button>';
|
||
stats_html += '<button class="btn btn-warning btn-sm" id="clear-log-dialog-btn">' +
|
||
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
|
||
stats_html += '</div></div>';
|
||
|
||
var d = new frappe.ui.Dialog({
|
||
title: __('Error Details') + ' (' + loading_errors.length + ')',
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'stats_html',
|
||
fieldtype: 'HTML',
|
||
options: stats_html
|
||
},
|
||
{
|
||
fieldname: 'errors_html',
|
||
fieldtype: 'HTML',
|
||
options: '<div style="max-height: 500px; overflow-y: auto;">' + errors_html + '</div>'
|
||
}
|
||
],
|
||
primary_action_label: __('Close'),
|
||
primary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
|
||
// Добавляем обработчики событий после показа диалога
|
||
setTimeout(function() {
|
||
$('#export-csv-dialog-btn').off('click').on('click', function() {
|
||
export_error_log();
|
||
});
|
||
|
||
$('#copy-clipboard-dialog-btn').off('click').on('click', function() {
|
||
copy_error_log();
|
||
});
|
||
|
||
$('#clear-log-dialog-btn').off('click').on('click', function() {
|
||
clear_error_log();
|
||
d.hide(); // Закрываем диалог после очистки
|
||
});
|
||
}, 200);
|
||
}
|
||
|
||
// Функция для экспорта лога ошибок в CSV
|
||
function export_error_log() {
|
||
if (loading_errors.length === 0) {
|
||
frappe.msgprint(__('No errors to export'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Преобразуем ошибки в CSV формат
|
||
let csv_content = "data:text/csv;charset=utf-8,";
|
||
csv_content += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
|
||
|
||
loading_errors.forEach(function(error) {
|
||
let clean_message = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||
let clean_sender = error.sender_name.replace(/"/g, '""');
|
||
|
||
let row = [
|
||
error.invoice_id,
|
||
error.serial_number,
|
||
error.creation_date,
|
||
clean_sender,
|
||
error.error_type,
|
||
clean_message,
|
||
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
|
||
].map(function(field) {
|
||
return '"' + (field || '') + '"';
|
||
}).join(',');
|
||
|
||
csv_content += row + "\n";
|
||
});
|
||
|
||
// Создаем ссылку для скачивания
|
||
const encoded_uri = encodeURI(csv_content);
|
||
const link = document.createElement("a");
|
||
link.setAttribute("href", encoded_uri);
|
||
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);
|
||
}
|
||
}
|
||
|
||
// Функция для копирования лога ошибок в буфер обмена
|
||
function copy_error_log() {
|
||
if (loading_errors.length === 0) {
|
||
frappe.msgprint(__('No errors to copy'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let text_content = "E-TAXES IMPORT ERROR LOG\n";
|
||
text_content += "Generated: " + moment().format('DD.MM.YYYY HH:mm:ss') + "\n";
|
||
text_content += "Total Errors: " + loading_errors.length + "\n\n";
|
||
|
||
loading_errors.forEach(function(error, index) {
|
||
text_content += "=== ERROR " + (index + 1) + " ===\n";
|
||
text_content += "Invoice: " + (error.serial_number || error.invoice_id) + "\n";
|
||
text_content += "Date: " + error.creation_date + "\n";
|
||
text_content += "Supplier: " + error.sender_name + "\n";
|
||
text_content += "Error Type: " + error.error_type + "\n";
|
||
text_content += "Message: " + error.error_message + "\n";
|
||
|
||
if (error.unmatched_items && error.unmatched_items.length > 0) {
|
||
text_content += "Unmapped Items:\n";
|
||
error.unmatched_items.forEach(function(item, idx) {
|
||
text_content += " - Row " + (idx + 1) + ": " + item.name +
|
||
(item.code ? " (Code: " + item.code + ")" : "") + "\n";
|
||
});
|
||
}
|
||
|
||
if (error.unmatched_parties && error.unmatched_parties.length > 0) {
|
||
text_content += "Unmapped Parties:\n";
|
||
error.unmatched_parties.forEach(function(party, idx) {
|
||
text_content += " - Party " + (idx + 1) + ": " + party.name +
|
||
(party.tin ? " (TIN: " + party.tin + ")" : "") + "\n";
|
||
});
|
||
}
|
||
|
||
if (error.unmatched_units && error.unmatched_units.length > 0) {
|
||
text_content += "Unmapped Units:\n";
|
||
error.unmatched_units.forEach(function(unit, idx) {
|
||
text_content += " - Unit " + (idx + 1) + ": " + unit.name + "\n";
|
||
});
|
||
}
|
||
|
||
text_content += "Timestamp: " + moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss') + "\n\n";
|
||
});
|
||
|
||
// Копируем в буфер обмена
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text_content).then(function() {
|
||
frappe.show_alert({
|
||
message: __('Error log copied to clipboard'),
|
||
indicator: 'green'
|
||
}, 3);
|
||
}).catch(function(err) {
|
||
console.error('Could not copy text: ', err);
|
||
fallback_copy_to_clipboard(text_content);
|
||
});
|
||
} else {
|
||
fallback_copy_to_clipboard(text_content);
|
||
}
|
||
} catch (e) {
|
||
console.error('Copy error:', e);
|
||
frappe.show_alert({
|
||
message: __('Failed to copy to clipboard'),
|
||
indicator: 'red'
|
||
}, 3);
|
||
}
|
||
}
|
||
|
||
// Функция для очистки лога ошибок
|
||
function clear_error_log() {
|
||
frappe.confirm(
|
||
__('Are you sure you want to clear the error log?'),
|
||
function() {
|
||
loading_errors = [];
|
||
frappe.show_alert({
|
||
message: __('Error log cleared'),
|
||
indicator: 'green'
|
||
}, 2);
|
||
}
|
||
);
|
||
}
|
||
|
||
// Fallback функция для копирования в старых браузерах
|
||
function fallback_copy_to_clipboard(text) {
|
||
try {
|
||
const textArea = document.createElement("textarea");
|
||
textArea.value = text;
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
|
||
const successful = document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
|
||
if (successful) {
|
||
frappe.show_alert({
|
||
message: __('Error log copied to clipboard'),
|
||
indicator: 'green'
|
||
}, 3);
|
||
} else {
|
||
throw new Error('Copy command failed');
|
||
}
|
||
} catch (err) {
|
||
console.error('Fallback copy failed:', err);
|
||
frappe.show_alert({
|
||
message: __('Failed to copy to clipboard. Please copy manually.'),
|
||
indicator: 'red'
|
||
}, 3);
|
||
}
|
||
} |