invoice_az/invoice_az/client/purchase_order.js

1530 lines
76 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.

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">';
invoice_table += '<thead><tr>' +
'<th style="width: 30px;"><input type="checkbox" class="select-all-invoices"></th>' +
'<th>' + __('Number') + '</th>' +
'<th>' + __('Date') + '</th>' +
'<th>' + __('Supplier') + '</th>' +
'<th>' + __('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>' + serialNumber + '</td>' +
'<td>' + creationDate + '</td>' +
'<td>' + senderName + '</td>' +
'<td>' + 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'),
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();
}
});
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) {
// Удаляем существующие обработчики отмены, если они есть
$(document).off('progress-cancel.loading_invoices');
// Если все инвойсы обработаны или был запрос на отмену
if (invoice_ids.length === 0) {
// Скрываем прогресс-бар
frappe.hide_progress();
// Все успешно загружено или загрузка отменена
let message = cancel_loading ?
__('Loading cancelled. Loaded ' + processed_count + ' invoices.') :
__('Loading completed. Loaded ' + processed_count + ' invoices.');
let indicator = cancel_loading ? 'orange' : 'green';
frappe.msgprint({
title: __('Import Result'),
indicator: indicator,
message: message
});
// Обновляем форму или список
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);
return;
}
// Берем первый ID из списка
var invoice_id = invoice_ids.shift();
var is_last_invoice = invoice_ids.length === 0;
// Обновляем прогресс-бар
frappe.show_progress(__('Importing Invoices'), processed_count, processed_count + invoice_ids.length + 1,
__('Loading invoice ') + (processed_count + 1) + __(' of ') + (processed_count + invoice_ids.length + 1), 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'), processed_count, processed_count + invoice_ids.length + 1,
__('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);
// Получаем имя созданного Purchase Order
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) {
// Устанавливаем связь между Purchase Order и E-Taxes Purchase
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');
// Показываем сообщение о завершении
frappe.msgprint({
title: __('Import Result'),
indicator: 'green',
message: __('Loading completed. Loaded ' + processed_count + ' invoices.')
});
// Обновляем форму или список
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);
}, 300);
}
});
} else {
// Обрабатываем ошибку создания E-Taxes Purchase
frappe.show_alert({
message: __('Error creating E-Taxes Purchase'),
indicator: 'red'
}, 3);
// Всё равно увеличиваем счетчик и переходим к следующему
processed_count++;
// Если это был последний инвойс, явно скрываем прогресс-бар
if (is_last_invoice) {
frappe.hide_progress();
$(document).off('progress-cancel.loading_invoices');
// Показываем сообщение о завершении
frappe.msgprint({
title: __('Import Result'),
indicator: 'green',
message: __('Loading completed. Loaded ' + processed_count + ' invoices.')
});
// Обновляем форму или список
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);
}, 300);
}
}
});
} else if (import_r.message && (import_r.message.unmatched_items || import_r.message.unmatched_parties)) {
// Если есть несопоставленные элементы или контрагенты
frappe.hide_progress();
$(document).off('progress-cancel.loading_invoices');
if (import_r.message.unmatched_items) {
show_unmatched_items_dialog(r.message, import_r.message.unmatched_items);
} else if (import_r.message.unmatched_parties) {
show_unmatched_parties_dialog(r.message, import_r.message.unmatched_parties);
}
// Останавливаем загрузку, так как требуется вмешательство пользователя
frappe.msgprint({
title: __('Import stopped'),
indicator: 'red',
message: __('To continue, you need to create missing mappings')
});
} else {
frappe.show_alert({
message: __('Import error: ') + serialNumber + ' ' + import_r.message.message,
indicator: 'red'
}, 3);
// Если это был последний инвойс, явно скрываем прогресс-бар
if (is_last_invoice) {
frappe.hide_progress();
$(document).off('progress-cancel.loading_invoices');
// Показываем сообщение о завершении
frappe.msgprint({
title: __('Import Result'),
indicator: 'orange',
message: __('Loading completed with some errors. Loaded ' + processed_count + ' invoices.')
});
// Обновляем форму или список
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);
}, 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);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Failed to get token after authentication')
});
}
}
});
});
},
function() {
// Если пользователь отказался, показываем сообщение
frappe.show_alert({
message: __('Authentication canceled. Operation cannot be completed.'),
indicator: 'red'
}, 5);
}
);
} else {
frappe.show_alert({
message: __('Error loading invoice data'),
indicator: 'red'
}, 3);
// Если это был последний инвойс, явно скрываем прогресс-бар
if (is_last_invoice) {
frappe.hide_progress();
$(document).off('progress-cancel.loading_invoices');
// Показываем сообщение о завершении
frappe.msgprint({
title: __('Import Result'),
indicator: 'orange',
message: __('Loading completed with some errors. Loaded ' + processed_count + ' invoices.')
});
// Обновляем форму или список
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);
}, 300);
}
},
error: function(xhr, status, error) {
console.error("Error fetching invoice details:", error);
// Если это был последний инвойс, явно скрываем прогресс-бар
if (is_last_invoice) {
frappe.hide_progress();
$(document).off('progress-cancel.loading_invoices');
// Показываем сообщение о завершении
frappe.msgprint({
title: __('Import Result'),
indicator: 'orange',
message: __('Loading completed with some errors. Loaded ' + processed_count + ' invoices.')
});
// Обновляем форму или список
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);
}, 300);
}
});
}
// Функция для отображения диалога с несопоставленными элементами
function show_unmatched_items_dialog(invoice_data, unmatched_items) {
// Создаем таблицу с несопоставленными товарами
var items_table = '<div class="text-center mb-3">' +
'<p>' + __('To complete the import, you need to create mappings for the following items:') + '</p>' +
'</div>' +
'<div style="max-height: 300px; overflow-y: auto;"><table class="table table-bordered">';
items_table += '<thead><tr>' +
'<th>' + __('Name') + '</th>' +
'<th>' + __('Code') + '</th>' +
'</tr></thead><tbody>';
unmatched_items.forEach(function(item) {
items_table += '<tr>' +
'<td>' + item.name + '</td>' +
'<td>' + (item.code || '') + '</td>' +
'</tr>';
});
items_table += '</tbody></table></div>';
// Создаем диалог
var d = new frappe.ui.Dialog({
title: __('Unmapped items'),
fields: [
{
fieldname: 'items_html',
fieldtype: 'HTML',
options: items_table
}
],
primary_action_label: __('Go to settings'),
primary_action: function() {
d.hide();
// Переходим к настройкам E-Taxes
frappe.set_route('Form', 'E-Taxes Settings');
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
d.hide();
}
});
d.show();
}
// Функция для отображения диалога с несопоставленными контрагентами
function show_unmatched_parties_dialog(invoice_data, unmatched_parties) {
// Создаем таблицу с несопоставленными контрагентами
var parties_table = '<div class="text-center mb-3">' +
'<p>' + __('To complete the import, you need to create mappings for the following partners:') + '</p>' +
'</div>' +
'<div style="max-height: 300px; overflow-y: auto;"><table class="table table-bordered">';
parties_table += '<thead><tr>' +
'<th>' + __('Name') + '</th>' +
'<th>' + __('TIN/VOEN') + '</th>' +
'<th>' + __('Type') + '</th>' +
'</tr></thead><tbody>';
unmatched_parties.forEach(function(party) {
// Преобразуем тип контрагента для отображения
let partyTypeDisplay = '';
if (party.type === 'Sender') {
partyTypeDisplay = 'Supplier (Sender)';
} else if (party.type === 'Receiver') {
partyTypeDisplay = 'Customer (Receiver)';
} else {
partyTypeDisplay = party.type || '';
}
parties_table += '<tr>' +
'<td>' + party.name + '</td>' +
'<td>' + (party.tin || '') + '</td>' +
'<td>' + partyTypeDisplay + '</td>' +
'</tr>';
});
parties_table += '</tbody></table></div>';
// Создаем диалог
var d = new frappe.ui.Dialog({
title: __('Unmapped partners'),
fields: [
{
fieldname: 'parties_html',
fieldtype: 'HTML',
options: parties_table
}
],
primary_action_label: __('Go to settings'),
primary_action: function() {
d.hide();
// Переходим к настройкам E-Taxes
frappe.set_route('Form', 'E-Taxes Settings');
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
d.hide();
}
});
d.show();
}