invoice_az/invoice_az/client/purchase_order.js

1874 lines
91 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// Глобальный массив для хранения ошибок загрузки
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);
}
}