Parties got separated to customers and suppliers

This commit is contained in:
Ali 2025-07-15 17:19:29 +04:00
parent 295dfb2267
commit 85f37a3f67
24 changed files with 2931 additions and 387 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Обновленные настройки списка E-Taxes Parties с прогресс баром
frappe.listview_settings['E-Taxes Parties'] = {
add_fields: ['status', 'mapped_party'],
// Настройки списка E-Taxes Customers с прогресс баром
frappe.listview_settings['E-Taxes Customers'] = {
add_fields: ['status', 'mapped_customer'],
get_indicator: function(doc) {
if (doc.status === 'Mapped') {
@ -13,48 +13,48 @@ frappe.listview_settings['E-Taxes Parties'] = {
},
onload: function(listview) {
// Add button to load parties from E-Taxes
// Add button to load customers from E-Taxes
listview.page.add_menu_item(__('Load from E-Taxes'), function() {
// Сначала проверяем актуальность токена перед загрузкой
check_and_process_token_parties(function() {
show_invoice_filter_dialog();
check_and_process_token_customers(function() {
show_invoice_filter_dialog_customers();
});
});
}
};
// Глобальная переменная для хранения диалога загрузки
var loading_dialog_parties = null;
var loading_dialog_customers = null;
// Очистка состояния при закрытии/перезагрузке страницы
$(window).on('beforeunload', function() {
cleanup_parties_loading_state();
cleanup_customers_loading_state();
});
// Очистка состояния при переходе на другую страницу
$(document).on('page:before-change', function() {
cleanup_parties_loading_state();
cleanup_customers_loading_state();
});
// НОВАЯ ФУНКЦИЯ: Очистка состояния загрузки контрагентов
function cleanup_parties_loading_state() {
// НОВАЯ ФУНКЦИЯ: Очистка состояния загрузки customers
function cleanup_customers_loading_state() {
try {
// Скрываем прогресс бар
frappe.hide_progress();
// Удаляем event listeners
$(document).off('progress-cancel.loading_parties');
$(document).off('progress-cancel.loading_customers');
// Очищаем глобальные переменные
if (typeof window.cancelPartiesLoading !== 'undefined') {
delete window.cancelPartiesLoading;
if (typeof window.cancelCustomersLoading !== 'undefined') {
delete window.cancelCustomersLoading;
}
if (typeof window.totalPartiesToProcess !== 'undefined') {
delete window.totalPartiesToProcess;
if (typeof window.totalCustomersToProcess !== 'undefined') {
delete window.totalCustomersToProcess;
}
// ВАЖНО: Скрываем все возможные диалоги загрузки
hide_loading_dialog_parties();
hide_loading_dialog_customers();
// Дополнительная очистка - удаляем все модальные окна, которые могут висеть
$('.modal-backdrop').remove();
@ -63,9 +63,9 @@ function cleanup_parties_loading_state() {
// Очищаем любые блокировки интерфейса
frappe.dom.unfreeze();
console.log('Parties loading state cleaned up');
console.log('Customers loading state cleaned up');
} catch (e) {
console.error('Error during parties cleanup:', e);
console.error('Error during customers cleanup:', e);
// В крайнем случае, принудительно разблокируем интерфейс
frappe.dom.unfreeze();
$('.modal-backdrop').remove();
@ -74,21 +74,21 @@ function cleanup_parties_loading_state() {
}
// Функция для отображения диалога загрузки с анимацией
function show_loading_dialog_parties(title, message, submessage) {
function show_loading_dialog_customers(title, message, submessage) {
// Если диалог уже открыт, обновляем только сообщение
if (loading_dialog_parties) {
$('#loading_title_parties').text(title);
$('#loading_message_parties').text(message);
if (loading_dialog_customers) {
$('#loading_title_customers').text(title);
$('#loading_message_customers').text(message);
if (submessage) {
$('#loading_submessage_parties').text(submessage).show();
$('#loading_submessage_customers').text(submessage).show();
} else {
$('#loading_submessage_parties').hide();
$('#loading_submessage_customers').hide();
}
return;
}
// Создаем диалог с анимацией загрузки
loading_dialog_parties = new frappe.ui.Dialog({
loading_dialog_customers = new frappe.ui.Dialog({
title: title,
fields: [
{
@ -106,13 +106,13 @@ function show_loading_dialog_parties(title, message, submessage) {
}
</style>
<div class="mt-3">
<h4 id="loading_title_parties">${title}</h4>
<p id="loading_message_parties">${message}</p>
<p id="loading_submessage_parties" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
<h4 id="loading_title_customers">${title}</h4>
<p id="loading_message_customers">${message}</p>
<p id="loading_submessage_customers" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
<!-- Блок для отображения кода верификации -->
<div id="verification_code_container_parties" style="display:none; margin-top: 15px;">
<div id="verification_code_container_customers" 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_parties">----</span>
<span id="verification_code_customers">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
@ -123,63 +123,63 @@ function show_loading_dialog_parties(title, message, submessage) {
]
});
loading_dialog_parties.show();
loading_dialog_parties.$wrapper.find('.modal-dialog').css('max-width', '450px');
loading_dialog_customers.show();
loading_dialog_customers.$wrapper.find('.modal-dialog').css('max-width', '450px');
}
// ИСПРАВЛЕННАЯ функция для скрытия диалога загрузки
function hide_loading_dialog_parties() {
if (loading_dialog_parties) {
function hide_loading_dialog_customers() {
if (loading_dialog_customers) {
try {
loading_dialog_parties.hide();
loading_dialog_parties = null;
loading_dialog_customers.hide();
loading_dialog_customers = null;
} catch (e) {
console.error('Error hiding loading dialog parties:', e);
loading_dialog_parties = null;
console.error('Error hiding loading dialog customers:', e);
loading_dialog_customers = null;
}
}
}
// Функция для обновления сообщения в диалоге загрузки
function update_loading_message_parties(message, submessage) {
if (loading_dialog_parties) {
$('#loading_message_parties').text(message);
function update_loading_message_customers(message, submessage) {
if (loading_dialog_customers) {
$('#loading_message_customers').text(message);
if (submessage !== undefined) {
$('#loading_submessage_parties').text(submessage);
$('#loading_submessage_parties').toggle(!!submessage);
$('#loading_submessage_customers').text(submessage);
$('#loading_submessage_customers').toggle(!!submessage);
}
}
}
// Функция для изменения статуса загрузки на успешный с автоматическим закрытием
function set_loading_success_parties(message, submessage, callback, delay = 2000) {
if (loading_dialog_parties) {
function set_loading_success_customers(message, submessage, callback, delay = 2000) {
if (loading_dialog_customers) {
// Меняем анимацию на галочку
loading_dialog_parties.$wrapper.find('.lds-dual-ring').html(`
loading_dialog_customers.$wrapper.find('.lds-dual-ring').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
// Обновляем сообщения
$('#loading_message_parties').text(message);
$('#loading_message_customers').text(message);
if (submessage !== undefined) {
$('#loading_submessage_parties').text(submessage);
$('#loading_submessage_parties').toggle(!!submessage);
$('#loading_submessage_customers').text(submessage);
$('#loading_submessage_customers').toggle(!!submessage);
}
// Скрываем код верификации при успешном завершении
$('#verification_code_container_parties').hide();
$('#verification_code_container_customers').hide();
// Удаляем все существующие кнопки действий
loading_dialog_parties.set_primary_action(null);
loading_dialog_parties.set_secondary_action(null);
loading_dialog_customers.set_primary_action(null);
loading_dialog_customers.set_secondary_action(null);
// Автоматически закрываем через указанную задержку
setTimeout(function() {
if (loading_dialog_parties) {
loading_dialog_parties.hide();
loading_dialog_parties = null;
if (loading_dialog_customers) {
loading_dialog_customers.hide();
loading_dialog_customers = null;
}
if (callback) callback();
}, delay);
@ -190,36 +190,36 @@ function set_loading_success_parties(message, submessage, callback, delay = 2000
}
// Функция для изменения статуса загрузки на ошибку
function set_loading_error_parties(message, submessage, show_close_button = true) {
if (loading_dialog_parties) {
function set_loading_error_customers(message, submessage, show_close_button = true) {
if (loading_dialog_customers) {
// Меняем анимацию на крестик
loading_dialog_parties.$wrapper.find('.lds-dual-ring').html(`
loading_dialog_customers.$wrapper.find('.lds-dual-ring').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
</div>
`);
// Обновляем сообщения
$('#loading_message_parties').text(message);
$('#loading_message_customers').text(message);
if (submessage !== undefined) {
$('#loading_submessage_parties').text(submessage);
$('#loading_submessage_parties').toggle(!!submessage);
$('#loading_submessage_customers').text(submessage);
$('#loading_submessage_customers').toggle(!!submessage);
}
// Скрываем код верификации при ошибке
$('#verification_code_container_parties').hide();
$('#verification_code_container_customers').hide();
// При необходимости добавляем кнопку закрытия
if (show_close_button) {
loading_dialog_parties.set_primary_action(__('Close'), function() {
hide_loading_dialog_parties();
loading_dialog_customers.set_primary_action(__('Close'), function() {
hide_loading_dialog_customers();
});
}
}
}
// Функция для проверки токена и обработки аутентификации при необходимости
function check_and_process_token_parties(callback) {
function check_and_process_token_customers(callback) {
// Проверяем, доступен ли новый модуль ETaxes
if (typeof ETaxes !== 'undefined' && ETaxes.auth) {
ETaxes.auth.checkAndProcess(callback);
@ -236,7 +236,7 @@ function check_and_process_token_parties(callback) {
frappe.confirm(
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
function() {
start_authentication_process_parties(callback);
start_authentication_process_customers(callback);
},
function() {
frappe.show_alert({
@ -258,8 +258,8 @@ function check_and_process_token_parties(callback) {
}
// Функция для запуска полного процесса аутентификации
function start_authentication_process_parties(callback) {
show_loading_dialog_parties(
function start_authentication_process_customers(callback) {
show_loading_dialog_customers(
__('Starting Authentication'),
__('Getting Asan Login settings...'),
__('Please wait')
@ -270,7 +270,7 @@ function start_authentication_process_parties(callback) {
callback: function(r) {
if (r.message && r.message.found) {
var asan_login_name = r.message.name;
update_loading_message_parties(
update_loading_message_customers(
__('Sending authentication request...'),
__('This will send a request to your Asan Imza mobile app')
);
@ -287,24 +287,24 @@ function start_authentication_process_parties(callback) {
// Отображаем код верификации, если он есть в ответе
if (r.message.verification_code) {
$('#verification_code_parties').text(r.message.verification_code);
$('#verification_code_container_parties').attr('style', 'display: block !important; margin-top: 15px;');
$('#verification_code_customers').text(r.message.verification_code);
$('#verification_code_container_customers').attr('style', 'display: block !important; margin-top: 15px;');
}
// Изменяем сообщение для ожидания подтверждения
update_loading_message_parties(
update_loading_message_customers(
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request')
);
// Опрашиваем статус аутентификации
poll_authentication_status_parties(asan_login_name, bearer_token, function(success) {
poll_authentication_status_customers(asan_login_name, bearer_token, function(success) {
if (success) {
// Если аутентификация успешна, продолжаем процесс
complete_authentication_parties(asan_login_name, callback);
complete_authentication_customers(asan_login_name, callback);
} else {
// Если аутентификация не удалась, показываем сообщение об ошибке
set_loading_error_parties(
set_loading_error_customers(
__('Authentication Failed'),
__('Could not authenticate with Asan Imza')
);
@ -312,7 +312,7 @@ function start_authentication_process_parties(callback) {
});
} else {
// Если запрос не удался, показываем сообщение об ошибке
set_loading_error_parties(
set_loading_error_customers(
__('Authentication Error'),
r.message ? r.message.message : __('Authentication request failed')
);
@ -321,7 +321,7 @@ function start_authentication_process_parties(callback) {
});
} else {
// Если настройки не найдены, показываем сообщение об ошибке
set_loading_error_parties(
set_loading_error_customers(
__('No Asan Login Settings'),
__('Please configure Asan Login settings first')
);
@ -331,7 +331,7 @@ function start_authentication_process_parties(callback) {
}
// Функция для опроса статуса аутентификации
function poll_authentication_status_parties(asan_login_name, bearer_token, callback) {
function poll_authentication_status_customers(asan_login_name, bearer_token, callback) {
// Счетчик попыток и флаг для остановки опроса
let attempts = 0;
const maxAttempts = 20; // 20 попыток с интервалом 6 секунд
@ -343,7 +343,7 @@ function poll_authentication_status_parties(asan_login_name, bearer_token, callb
if (attempts >= maxAttempts || stopPolling) {
if (attempts >= maxAttempts) {
// Показываем сообщение о таймауте
set_loading_error_parties(
set_loading_error_customers(
__('Authentication Timeout'),
__('The authentication request has timed out. Please try again.')
);
@ -355,7 +355,7 @@ function poll_authentication_status_parties(asan_login_name, bearer_token, callb
attempts++;
// Обновляем счетчик попыток в сообщении
update_loading_message_parties(
update_loading_message_customers(
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request') +
' (' + attempts + '/' + maxAttempts + ')'
@ -373,7 +373,7 @@ function poll_authentication_status_parties(asan_login_name, bearer_token, callb
if (r.message.authenticated) {
// Авторизация успешна
stopPolling = true;
set_loading_success_parties(
set_loading_success_customers(
__('Authentication Successful'),
__('You are now authenticated with Asan Imza'),
function() {
@ -387,7 +387,7 @@ function poll_authentication_status_parties(asan_login_name, bearer_token, callb
} else {
// Если ошибка, останавливаем опрос и показываем сообщение
stopPolling = true;
set_loading_error_parties(
set_loading_error_customers(
__('Authentication Error'),
r.message ? r.message.message : __('An unknown error occurred')
);
@ -407,8 +407,8 @@ function poll_authentication_status_parties(asan_login_name, bearer_token, callb
}
// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
function complete_authentication_parties(asan_login_name, callback) {
show_loading_dialog_parties(
function complete_authentication_customers(asan_login_name, callback) {
show_loading_dialog_customers(
__('Completing Authentication'),
__('Getting certificates...'),
__('Please wait')
@ -422,7 +422,7 @@ function complete_authentication_parties(asan_login_name, callback) {
},
callback: function(cert_r) {
if (cert_r.message && cert_r.message.success) {
update_loading_message_parties(
update_loading_message_customers(
__('Certificates retrieved'),
__('Selecting certificate and taxpayer')
);
@ -440,7 +440,7 @@ function complete_authentication_parties(asan_login_name, callback) {
const cert_data = JSON.parse(r.message.selected_certificate_json);
const cert_name = r.message.selected_certificate;
update_loading_message_parties(
update_loading_message_customers(
__('Selecting certificate'),
cert_name
);
@ -455,7 +455,7 @@ function complete_authentication_parties(asan_login_name, callback) {
},
callback: function(select_r) {
if (select_r.message && select_r.message.success) {
update_loading_message_parties(
update_loading_message_customers(
__('Selecting taxpayer'),
__('Using certificate: ') + cert_name
);
@ -468,37 +468,37 @@ function complete_authentication_parties(asan_login_name, callback) {
},
callback: function(tax_r) {
if (tax_r.message && tax_r.message.success) {
set_loading_success_parties(
set_loading_success_customers(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback,
2000 // Автоматически закроется через 2 секунды
);
} else {
hide_loading_dialog_parties();
show_certificate_selector_parties(cert_r.message.certificates, asan_login_name, callback);
hide_loading_dialog_customers();
show_certificate_selector_customers(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} else {
hide_loading_dialog_parties();
show_certificate_selector_parties(cert_r.message.certificates, asan_login_name, callback);
hide_loading_dialog_customers();
show_certificate_selector_customers(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} catch (e) {
console.error('Error parsing certificate:', e);
hide_loading_dialog_parties();
show_certificate_selector_parties(cert_r.message.certificates, asan_login_name, callback);
hide_loading_dialog_customers();
show_certificate_selector_customers(cert_r.message.certificates, asan_login_name, callback);
}
} else {
hide_loading_dialog_parties();
show_certificate_selector_parties(cert_r.message.certificates, asan_login_name, callback);
hide_loading_dialog_customers();
show_certificate_selector_customers(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} else {
set_loading_error_parties(
set_loading_error_customers(
__('Error'),
cert_r.message ? cert_r.message.message : __('Failed to get certificates')
);
@ -508,7 +508,7 @@ function complete_authentication_parties(asan_login_name, callback) {
}
// Функция для отображения селектора сертификатов
function show_certificate_selector_parties(certificates, asan_login_name, callback) {
function show_certificate_selector_customers(certificates, asan_login_name, callback) {
if (!certificates || !certificates.length) {
frappe.msgprint(__('No certificates available'));
return;
@ -536,7 +536,7 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
'<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-parties" data-index="' + index + '">Select</button></td>' +
'<td><button class="btn btn-xs btn-primary select-cert-customers" data-index="' + index + '">Select</button></td>' +
'</tr>';
});
@ -554,7 +554,7 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
dialog.show();
// Обработчик нажатия на кнопку выбора сертификата
dialog.$wrapper.find('.select-cert-parties').on('click', function() {
dialog.$wrapper.find('.select-cert-customers').on('click', function() {
var index = $(this).data('index');
var cert = certificates[index];
@ -571,7 +571,7 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
dialog.hide();
// Показываем прогресс
show_loading_dialog_parties(
show_loading_dialog_customers(
__('Selecting Certificate'),
__('Processing your selection...'),
certName
@ -587,7 +587,7 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
},
callback: function(r) {
if (r.message && r.message.success) {
update_loading_message_parties(
update_loading_message_customers(
__('Certificate selected'),
__('Now selecting taxpayer information')
);
@ -600,14 +600,14 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
},
callback: function(r) {
if (r.message && r.message.success) {
set_loading_success_parties(
set_loading_success_customers(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback,
2000 // Автоматически закроется через 2 секунды
);
} else {
set_loading_error_parties(
set_loading_error_customers(
__('Error'),
r.message ? r.message.message : __('Failed to select taxpayer')
);
@ -615,7 +615,7 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
}
});
} else {
set_loading_error_parties(
set_loading_error_customers(
__('Error'),
r.message ? r.message.message : __('Failed to select certificate')
);
@ -625,14 +625,14 @@ function show_certificate_selector_parties(certificates, asan_login_name, callba
});
}
function show_invoice_filter_dialog() {
function show_invoice_filter_dialog_customers() {
const minDate = moment("2020-01-01", "YYYY-MM-DD");
const prevYear = moment().subtract(1, 'year').year();
const startDate = prevYear + "-01-01";
const endDate = moment().format('YYYY-MM-DD');
var d = new frappe.ui.Dialog({
title: __('Party Filters'),
title: __('Customer Filters'),
fields: [
{
fieldname: 'date_range_section',
@ -652,7 +652,7 @@ function show_invoice_filter_dialog() {
default: endDate
}
],
primary_action_label: __('Load Parties'),
primary_action_label: __('Load Customers'),
primary_action: function() {
var values = d.get_values();
@ -676,29 +676,29 @@ function show_invoice_filter_dialog() {
let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59');
// Запускаем загрузку с прогресс-баром
load_parties_with_pagination(fromDate, toDate, 0, []);
load_customers_with_pagination(fromDate, toDate, 0, []);
}
});
d.show();
}
// ИСПРАВЛЕННАЯ функция загрузки контрагентов с пагинацией
function load_parties_with_pagination(fromDate, toDate, offset = 0, accumulated_data = null) {
// ИСПРАВЛЕННАЯ функция загрузки customers с пагинацией
function load_customers_with_pagination(fromDate, toDate, offset = 0, accumulated_data = null) {
const maxCount = 200;
// Инициализируем accumulated_data если это первый вызов
if (!accumulated_data) {
accumulated_data = {
created_count: 0,
customers_created: 0,
suppliers_created: 0,
skipped_count: 0,
failed_count: 0,
total_invoices: 0,
unique_parties: 0
total_invoices: 0
};
frappe.show_alert({
message: __('Fetching parties from E-Taxes...'),
message: __('Fetching customers from E-Taxes...'),
indicator: 'blue'
}, 3);
}
@ -727,18 +727,18 @@ function load_parties_with_pagination(fromDate, toDate, offset = 0, accumulated_
if (hasMore && currentInvoices.length > 0) {
// Если есть еще инвойсы, загружаем следующую партию
let newOffset = offset + currentInvoices.length;
load_parties_with_pagination(fromDate, toDate, newOffset, accumulated_data);
load_customers_with_pagination(fromDate, toDate, newOffset, accumulated_data);
} else {
// Если больше нет инвойсов, переходим к обработке
if (allInvoices.length > 0) {
frappe.show_alert({
message: __('Found {0} invoices. Processing parties...', [allInvoices.length]),
message: __('Found {0} invoices. Processing customers...', [allInvoices.length]),
indicator: 'green'
}, 3);
// Теперь обрабатываем инвойсы батчами с прогресс баром
setTimeout(() => {
process_invoices_for_parties_with_progress(allInvoices, token);
process_invoices_for_customers_with_progress(allInvoices, token);
}, 1000);
} else {
frappe.msgprint({
@ -752,8 +752,8 @@ function load_parties_with_pagination(fromDate, toDate, offset = 0, accumulated_
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
check_and_process_token_parties(function() {
show_invoice_filter_dialog();
check_and_process_token_customers(function() {
show_invoice_filter_dialog_customers();
});
},
function() {
@ -767,13 +767,13 @@ function load_parties_with_pagination(fromDate, toDate, offset = 0, accumulated_
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('An error occurred while loading parties')
message: r.message ? r.message.message : __('An error occurred while loading customers')
});
}
},
error: function(xhr, status, error) {
// ИСПРАВЛЕНО: Очищаем состояние при ошибке
cleanup_parties_loading_state();
cleanup_customers_loading_state();
frappe.msgprint({
title: __('Network Error'),
indicator: 'red',
@ -783,33 +783,33 @@ function load_parties_with_pagination(fromDate, toDate, offset = 0, accumulated_
});
}
// ИСПРАВЛЕННАЯ функция обработки инвойсов для контрагентов с прогресс баром
function process_invoices_for_parties_with_progress(invoices, token, processedCount = 0, currentIndex = 0, batchSize = 50) {
// ИСПРАВЛЕННАЯ функция обработки инвойсов для customers с прогресс баром
function process_invoices_for_customers_with_progress(invoices, token, processedCount = 0, currentIndex = 0, batchSize = 50) {
// Инициализация при первом вызове
if (processedCount === 0 && currentIndex === 0) {
window.cancelPartiesLoading = false;
window.totalPartiesToProcess = invoices.length;
window.cancelCustomersLoading = false;
window.totalCustomersToProcess = invoices.length;
// Показываем прогресс бар
frappe.show_progress(__('Processing Parties'), 0, invoices.length,
__('Extracting parties from invoices...'), null, true);
frappe.show_progress(__('Processing Customers'), 0, invoices.length,
__('Extracting customers from invoices...'), null, true);
// Добавляем обработчик отмены
$(document).off('progress-cancel.loading_parties');
$(document).on('progress-cancel.loading_parties', function() {
window.cancelPartiesLoading = true;
$(document).off('progress-cancel.loading_customers');
$(document).on('progress-cancel.loading_customers', function() {
window.cancelCustomersLoading = true;
frappe.show_alert({
message: __('Cancelling party extraction...'),
message: __('Cancelling customer extraction...'),
indicator: 'orange'
}, 3);
});
}
// Проверка отмены
if (window.cancelPartiesLoading) {
cleanup_parties_loading_state();
if (window.cancelCustomersLoading) {
cleanup_customers_loading_state();
frappe.show_alert({
message: __('Party extraction cancelled'),
message: __('Customer extraction cancelled'),
indicator: 'red'
}, 3);
return;
@ -817,14 +817,14 @@ function process_invoices_for_parties_with_progress(invoices, token, processedCo
// Если все инвойсы обработаны
if (currentIndex >= invoices.length) {
cleanup_parties_loading_state();
cleanup_customers_loading_state();
// Показываем итоговый результат
frappe.msgprint({
title: __('Parties Loaded Successfully'),
title: __('Customers Loaded Successfully'),
indicator: 'green',
message: __('Created {0} unique parties. Skipped {1} duplicates. Total invoices processed: {2}',
[processedCount, 0, invoices.length]) // skipped считается на сервере
message: __('Created {0} customers and {1} suppliers. Total invoices processed: {2}',
[processedCount, 0, invoices.length]) // suppliers считается на сервере
});
// ИСПРАВЛЕНО: Обновляем список с задержкой для уверенности в очистке состояния
@ -842,7 +842,7 @@ function process_invoices_for_parties_with_progress(invoices, token, processedCo
const currentBatch = invoices.slice(currentIndex, currentIndex + currentBatchSize);
// Обновляем прогресс
frappe.show_progress(__('Processing Parties'), currentIndex, invoices.length,
frappe.show_progress(__('Processing Customers'), currentIndex, invoices.length,
__('Processing batch {0}-{1} of {2} invoices', [currentIndex + 1, currentIndex + currentBatchSize, invoices.length]));
// Обрабатываем текущий батч
@ -855,29 +855,29 @@ function process_invoices_for_parties_with_progress(invoices, token, processedCo
callback: function(r) {
if (r.message && r.message.success) {
// Обновляем накопленные данные
processedCount += r.message.created_count || 0;
processedCount += (r.message.customers_created || 0) + (r.message.suppliers_created || 0);
// Переходим к следующему батчу с небольшой задержкой
setTimeout(function() {
if (!window.cancelPartiesLoading) { // Дополнительная проверка
process_invoices_for_parties_with_progress(invoices, token, processedCount,
if (!window.cancelCustomersLoading) { // Дополнительная проверка
process_invoices_for_customers_with_progress(invoices, token, processedCount,
currentIndex + currentBatchSize, batchSize);
}
}, 100); // 100ms задержка между батчами
} else if (r.message && r.message.error === 'unauthorized') {
cleanup_parties_loading_state();
cleanup_customers_loading_state();
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
check_and_process_token_parties(function() {
check_and_process_token_customers(function() {
// Получаем новый токен и продолжаем с текущего индекса
frappe.call({
method: 'invoice_az.auth.get_default_asan_login',
callback: function(login_r) {
if (login_r.message && login_r.message.found && login_r.message.main_token) {
process_invoices_for_parties_with_progress(invoices, login_r.message.main_token,
process_invoices_for_customers_with_progress(invoices, login_r.message.main_token,
processedCount, currentIndex, batchSize);
} else {
frappe.msgprint(__('Failed to get authentication token'));
@ -896,8 +896,8 @@ function process_invoices_for_parties_with_progress(invoices, token, processedCo
} else {
// Ошибка обработки батча - продолжаем со следующим
setTimeout(function() {
if (!window.cancelPartiesLoading) { // Дополнительная проверка
process_invoices_for_parties_with_progress(invoices, token, processedCount,
if (!window.cancelCustomersLoading) { // Дополнительная проверка
process_invoices_for_customers_with_progress(invoices, token, processedCount,
currentIndex + currentBatchSize, batchSize);
}
}, 200); // Увеличенная задержка при ошибках
@ -906,8 +906,8 @@ function process_invoices_for_parties_with_progress(invoices, token, processedCo
error: function(xhr, status, error) {
// Сетевая ошибка - продолжаем со следующим батчом
setTimeout(function() {
if (!window.cancelPartiesLoading) { // Дополнительная проверка
process_invoices_for_parties_with_progress(invoices, token, processedCount,
if (!window.cancelCustomersLoading) { // Дополнительная проверка
process_invoices_for_customers_with_progress(invoices, token, processedCount,
currentIndex + currentBatchSize, batchSize);
}
}, 500); // Еще большая задержка при сетевых ошибках

View File

@ -0,0 +1,916 @@
// Настройки списка E-Taxes Suppliers с прогресс баром
frappe.listview_settings['E-Taxes Suppliers'] = {
add_fields: ['status', 'mapped_supplier'],
get_indicator: function(doc) {
if (doc.status === 'Mapped') {
return [__('Mapped'), 'green', 'status,=,Mapped'];
} else if (doc.status === 'Processing') {
return [__('Processing'), 'orange', 'status,=,Processing'];
} else {
return [__('New'), 'red', 'status,=,New'];
}
},
onload: function(listview) {
// Add button to load suppliers from E-Taxes
listview.page.add_menu_item(__('Load from E-Taxes'), function() {
// Сначала проверяем актуальность токена перед загрузкой
check_and_process_token_suppliers(function() {
show_invoice_filter_dialog_suppliers();
});
});
}
};
// Глобальная переменная для хранения диалога загрузки
var loading_dialog_suppliers = null;
// Очистка состояния при закрытии/перезагрузке страницы
$(window).on('beforeunload', function() {
cleanup_suppliers_loading_state();
});
// Очистка состояния при переходе на другую страницу
$(document).on('page:before-change', function() {
cleanup_suppliers_loading_state();
});
// НОВАЯ ФУНКЦИЯ: Очистка состояния загрузки suppliers
function cleanup_suppliers_loading_state() {
try {
// Скрываем прогресс бар
frappe.hide_progress();
// Удаляем event listeners
$(document).off('progress-cancel.loading_suppliers');
// Очищаем глобальные переменные
if (typeof window.cancelSuppliersLoading !== 'undefined') {
delete window.cancelSuppliersLoading;
}
if (typeof window.totalSuppliersToProcess !== 'undefined') {
delete window.totalSuppliersToProcess;
}
// ВАЖНО: Скрываем все возможные диалоги загрузки
hide_loading_dialog_suppliers();
// Дополнительная очистка - удаляем все модальные окна, которые могут висеть
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
// Очищаем любые блокировки интерфейса
frappe.dom.unfreeze();
console.log('Suppliers loading state cleaned up');
} catch (e) {
console.error('Error during suppliers cleanup:', e);
// В крайнем случае, принудительно разблокируем интерфейс
frappe.dom.unfreeze();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
}
// Функция для отображения диалога загрузки с анимацией
function show_loading_dialog_suppliers(title, message, submessage) {
// Если диалог уже открыт, обновляем только сообщение
if (loading_dialog_suppliers) {
$('#loading_title_suppliers').text(title);
$('#loading_message_suppliers').text(message);
if (submessage) {
$('#loading_submessage_suppliers').text(submessage).show();
} else {
$('#loading_submessage_suppliers').hide();
}
return;
}
// Создаем диалог с анимацией загрузки
loading_dialog_suppliers = new frappe.ui.Dialog({
title: title,
fields: [
{
fieldname: 'loading_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="lds-dual-ring" 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">
<h4 id="loading_title_suppliers">${title}</h4>
<p id="loading_message_suppliers">${message}</p>
<p id="loading_submessage_suppliers" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
<!-- Блок для отображения кода верификации -->
<div id="verification_code_container_suppliers" 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_suppliers">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
</div>
</div>
`
}
]
});
loading_dialog_suppliers.show();
loading_dialog_suppliers.$wrapper.find('.modal-dialog').css('max-width', '450px');
}
// ИСПРАВЛЕННАЯ функция для скрытия диалога загрузки
function hide_loading_dialog_suppliers() {
if (loading_dialog_suppliers) {
try {
loading_dialog_suppliers.hide();
loading_dialog_suppliers = null;
} catch (e) {
console.error('Error hiding loading dialog suppliers:', e);
loading_dialog_suppliers = null;
}
}
}
// Функция для обновления сообщения в диалоге загрузки
function update_loading_message_suppliers(message, submessage) {
if (loading_dialog_suppliers) {
$('#loading_message_suppliers').text(message);
if (submessage !== undefined) {
$('#loading_submessage_suppliers').text(submessage);
$('#loading_submessage_suppliers').toggle(!!submessage);
}
}
}
// Функция для изменения статуса загрузки на успешный с автоматическим закрытием
function set_loading_success_suppliers(message, submessage, callback, delay = 2000) {
if (loading_dialog_suppliers) {
// Меняем анимацию на галочку
loading_dialog_suppliers.$wrapper.find('.lds-dual-ring').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
// Обновляем сообщения
$('#loading_message_suppliers').text(message);
if (submessage !== undefined) {
$('#loading_submessage_suppliers').text(submessage);
$('#loading_submessage_suppliers').toggle(!!submessage);
}
// Скрываем код верификации при успешном завершении
$('#verification_code_container_suppliers').hide();
// Удаляем все существующие кнопки действий
loading_dialog_suppliers.set_primary_action(null);
loading_dialog_suppliers.set_secondary_action(null);
// Автоматически закрываем через указанную задержку
setTimeout(function() {
if (loading_dialog_suppliers) {
loading_dialog_suppliers.hide();
loading_dialog_suppliers = null;
}
if (callback) callback();
}, delay);
} else if (callback) {
// Если диалог не открыт, но есть callback, вызываем его
callback();
}
}
// Функция для изменения статуса загрузки на ошибку
function set_loading_error_suppliers(message, submessage, show_close_button = true) {
if (loading_dialog_suppliers) {
// Меняем анимацию на крестик
loading_dialog_suppliers.$wrapper.find('.lds-dual-ring').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
</div>
`);
// Обновляем сообщения
$('#loading_message_suppliers').text(message);
if (submessage !== undefined) {
$('#loading_submessage_suppliers').text(submessage);
$('#loading_submessage_suppliers').toggle(!!submessage);
}
// Скрываем код верификации при ошибке
$('#verification_code_container_suppliers').hide();
// При необходимости добавляем кнопку закрытия
if (show_close_button) {
loading_dialog_suppliers.set_primary_action(__('Close'), function() {
hide_loading_dialog_suppliers();
});
}
}
}
// Функция для проверки токена и обработки аутентификации при необходимости
function check_and_process_token_suppliers(callback) {
// Проверяем, доступен ли новый модуль ETaxes
if (typeof ETaxes !== 'undefined' && ETaxes.auth) {
ETaxes.auth.checkAndProcess(callback);
return;
}
// Fallback на старый код
frappe.call({
method: 'invoice_az.auth.check_token_validity',
callback: function(r) {
if (r.message && r.message.valid) {
callback();
} else {
frappe.confirm(
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
function() {
start_authentication_process_suppliers(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_suppliers(callback) {
show_loading_dialog_suppliers(
__('Starting Authentication'),
__('Getting Asan Login settings...'),
__('Please wait')
);
frappe.call({
method: 'invoice_az.auth.get_default_asan_login',
callback: function(r) {
if (r.message && r.message.found) {
var asan_login_name = r.message.name;
update_loading_message_suppliers(
__('Sending authentication request...'),
__('This will send a request to your Asan Imza mobile app')
);
// Запускаем процесс аутентификации
frappe.call({
method: 'invoice_az.auth.handle_authentication',
args: {
'asan_login_name': asan_login_name
},
callback: function(r) {
if (r.message && r.message.success) {
const bearer_token = r.message.bearer_token;
// Отображаем код верификации, если он есть в ответе
if (r.message.verification_code) {
$('#verification_code_suppliers').text(r.message.verification_code);
$('#verification_code_container_suppliers').attr('style', 'display: block !important; margin-top: 15px;');
}
// Изменяем сообщение для ожидания подтверждения
update_loading_message_suppliers(
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request')
);
// Опрашиваем статус аутентификации
poll_authentication_status_suppliers(asan_login_name, bearer_token, function(success) {
if (success) {
// Если аутентификация успешна, продолжаем процесс
complete_authentication_suppliers(asan_login_name, callback);
} else {
// Если аутентификация не удалась, показываем сообщение об ошибке
set_loading_error_suppliers(
__('Authentication Failed'),
__('Could not authenticate with Asan Imza')
);
}
});
} else {
// Если запрос не удался, показываем сообщение об ошибке
set_loading_error_suppliers(
__('Authentication Error'),
r.message ? r.message.message : __('Authentication request failed')
);
}
}
});
} else {
// Если настройки не найдены, показываем сообщение об ошибке
set_loading_error_suppliers(
__('No Asan Login Settings'),
__('Please configure Asan Login settings first')
);
}
}
});
}
// Функция для опроса статуса аутентификации
function poll_authentication_status_suppliers(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_loading_error_suppliers(
__('Authentication Timeout'),
__('The authentication request has timed out. Please try again.')
);
callback(false);
}
return;
}
attempts++;
// Обновляем счетчик попыток в сообщении
update_loading_message_suppliers(
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request') +
' (' + attempts + '/' + maxAttempts + ')'
);
// Проверяем статус авторизации
frappe.call({
method: 'invoice_az.auth.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_loading_success_suppliers(
__('Authentication Successful'),
__('You are now authenticated with Asan Imza'),
function() {
callback(true);
}
);
} else {
// Продолжаем опрос
setTimeout(pollStatus, 6000);
}
} else {
// Если ошибка, останавливаем опрос и показываем сообщение
stopPolling = true;
set_loading_error_suppliers(
__('Authentication Error'),
r.message ? r.message.message : __('An unknown error occurred')
);
callback(false);
}
},
error: function(err) {
// В случае ошибки продолжаем опрос
console.error('Error during status check:', err);
setTimeout(pollStatus, 6000);
}
});
}
// Начинаем опрос
pollStatus();
}
// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
function complete_authentication_suppliers(asan_login_name, callback) {
show_loading_dialog_suppliers(
__('Completing Authentication'),
__('Getting certificates...'),
__('Please wait')
);
// Получаем сертификаты
frappe.call({
method: 'invoice_az.auth.get_auth_certificates',
args: {
'asan_login_name': asan_login_name
},
callback: function(cert_r) {
if (cert_r.message && cert_r.message.success) {
update_loading_message_suppliers(
__('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_loading_message_suppliers(
__('Selecting certificate'),
cert_name
);
// Выбираем сертификат
frappe.call({
method: 'invoice_az.auth.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_loading_message_suppliers(
__('Selecting taxpayer'),
__('Using certificate: ') + cert_name
);
// Выбираем налогоплательщика
frappe.call({
method: 'invoice_az.auth.select_taxpayer',
args: {
'asan_login_name': asan_login_name
},
callback: function(tax_r) {
if (tax_r.message && tax_r.message.success) {
set_loading_success_suppliers(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback,
2000 // Автоматически закроется через 2 секунды
);
} else {
hide_loading_dialog_suppliers();
show_certificate_selector_suppliers(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} else {
hide_loading_dialog_suppliers();
show_certificate_selector_suppliers(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} catch (e) {
console.error('Error parsing certificate:', e);
hide_loading_dialog_suppliers();
show_certificate_selector_suppliers(cert_r.message.certificates, asan_login_name, callback);
}
} else {
hide_loading_dialog_suppliers();
show_certificate_selector_suppliers(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} else {
set_loading_error_suppliers(
__('Error'),
cert_r.message ? cert_r.message.message : __('Failed to get certificates')
);
}
}
});
}
// Функция для отображения селектора сертификатов
function show_certificate_selector_suppliers(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-suppliers" 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-suppliers').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_loading_dialog_suppliers(
__('Selecting Certificate'),
__('Processing your selection...'),
certName
);
// Отправляем запрос на выбор сертификата
frappe.call({
method: 'invoice_az.auth.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_loading_message_suppliers(
__('Certificate selected'),
__('Now selecting taxpayer information')
);
// Выбираем налогоплательщика
frappe.call({
method: 'invoice_az.auth.select_taxpayer',
args: {
'asan_login_name': asan_login_name
},
callback: function(r) {
if (r.message && r.message.success) {
set_loading_success_suppliers(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback,
2000 // Автоматически закроется через 2 секунды
);
} else {
set_loading_error_suppliers(
__('Error'),
r.message ? r.message.message : __('Failed to select taxpayer')
);
}
}
});
} else {
set_loading_error_suppliers(
__('Error'),
r.message ? r.message.message : __('Failed to select certificate')
);
}
}
});
});
}
function show_invoice_filter_dialog_suppliers() {
const minDate = moment("2020-01-01", "YYYY-MM-DD");
const prevYear = moment().subtract(1, 'year').year();
const startDate = prevYear + "-01-01";
const endDate = moment().format('YYYY-MM-DD');
var d = new frappe.ui.Dialog({
title: __('Supplier Filters'),
fields: [
{
fieldname: 'date_range_section',
fieldtype: 'Section Break',
label: __('Date Range')
},
{
fieldname: 'creationDateFrom',
fieldtype: 'Date',
label: __('From Date'),
default: startDate
},
{
fieldname: 'creationDateTo',
fieldtype: 'Date',
label: __('To Date'),
default: endDate
}
],
primary_action_label: __('Load Suppliers'),
primary_action: function() {
var values = d.get_values();
const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
const todayMoment = moment();
if (fromDateMoment.isBefore(minDate)) {
frappe.msgprint(__('From Date cannot be earlier than January 1, 2020'));
return;
}
if (toDateMoment.isAfter(todayMoment)) {
frappe.msgprint(__('To Date cannot be later than today'));
return;
}
d.hide();
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00');
let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59');
// Запускаем загрузку с прогресс-баром
load_suppliers_with_pagination(fromDate, toDate, 0, []);
}
});
d.show();
}
// ИСПРАВЛЕННАЯ функция загрузки suppliers с пагинацией
function load_suppliers_with_pagination(fromDate, toDate, offset = 0, accumulated_data = null) {
const maxCount = 200;
// Инициализируем accumulated_data если это первый вызов
if (!accumulated_data) {
accumulated_data = {
customers_created: 0,
suppliers_created: 0,
skipped_count: 0,
failed_count: 0,
total_invoices: 0
};
frappe.show_alert({
message: __('Fetching suppliers from E-Taxes...'),
indicator: 'blue'
}, 3);
}
frappe.call({
method: 'invoice_az.api.load_parties_from_invoices',
args: {
'date_from': fromDate,
'date_to': toDate,
'max_count': maxCount,
'offset': offset,
'invoice_type': 'purchase'
},
callback: function(r) {
if (r.message && r.message.success) {
// Добавляем полученные инвойсы к уже накопленным
let currentInvoices = r.message.invoices || [];
let allInvoices = accumulated_data.invoices ? accumulated_data.invoices.concat(currentInvoices) : currentInvoices;
let hasMore = r.message.hasMore || false;
let token = r.message.token;
// Сохраняем накопленные инвойсы
accumulated_data.invoices = allInvoices;
accumulated_data.token = token;
if (hasMore && currentInvoices.length > 0) {
// Если есть еще инвойсы, загружаем следующую партию
let newOffset = offset + currentInvoices.length;
load_suppliers_with_pagination(fromDate, toDate, newOffset, accumulated_data);
} else {
// Если больше нет инвойсов, переходим к обработке
if (allInvoices.length > 0) {
frappe.show_alert({
message: __('Found {0} invoices. Processing suppliers...', [allInvoices.length]),
indicator: 'green'
}, 3);
// Теперь обрабатываем инвойсы батчами с прогресс баром
setTimeout(() => {
process_invoices_for_suppliers_with_progress(allInvoices, token);
}, 1000);
} 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() {
check_and_process_token_suppliers(function() {
show_invoice_filter_dialog_suppliers();
});
},
function() {
frappe.show_alert({
message: __('Authentication cancelled'),
indicator: 'red'
}, 5);
}
);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('An error occurred while loading suppliers')
});
}
},
error: function(xhr, status, error) {
// ИСПРАВЛЕНО: Очищаем состояние при ошибке
cleanup_suppliers_loading_state();
frappe.msgprint({
title: __('Network Error'),
indicator: 'red',
message: __('Failed to connect to server: ') + (error || 'Unknown error')
});
}
});
}
// ИСПРАВЛЕННАЯ функция обработки инвойсов для suppliers с прогресс баром
function process_invoices_for_suppliers_with_progress(invoices, token, processedCount = 0, currentIndex = 0, batchSize = 50) {
// Инициализация при первом вызове
if (processedCount === 0 && currentIndex === 0) {
window.cancelSuppliersLoading = false;
window.totalSuppliersToProcess = invoices.length;
// Показываем прогресс бар
frappe.show_progress(__('Processing Suppliers'), 0, invoices.length,
__('Extracting suppliers from invoices...'), null, true);
// Добавляем обработчик отмены
$(document).off('progress-cancel.loading_suppliers');
$(document).on('progress-cancel.loading_suppliers', function() {
window.cancelSuppliersLoading = true;
frappe.show_alert({
message: __('Cancelling supplier extraction...'),
indicator: 'orange'
}, 3);
});
}
// Проверка отмены
if (window.cancelSuppliersLoading) {
cleanup_suppliers_loading_state();
frappe.show_alert({
message: __('Supplier extraction cancelled'),
indicator: 'red'
}, 3);
return;
}
// Если все инвойсы обработаны
if (currentIndex >= invoices.length) {
cleanup_suppliers_loading_state();
// Показываем итоговый результат
frappe.msgprint({
title: __('Suppliers Loaded Successfully'),
indicator: 'green',
message: __('Created {0} suppliers and {1} customers. Total invoices processed: {2}',
[processedCount, 0, invoices.length]) // customers считается на сервере
});
// ИСПРАВЛЕНО: Обновляем список с задержкой для уверенности в очистке состояния
setTimeout(function() {
if (cur_list) {
cur_list.refresh();
}
}, 500);
return;
}
// Определяем размер текущего батча
const remainingInvoices = invoices.length - currentIndex;
const currentBatchSize = Math.min(batchSize, remainingInvoices);
const currentBatch = invoices.slice(currentIndex, currentIndex + currentBatchSize);
// Обновляем прогресс
frappe.show_progress(__('Processing Suppliers'), currentIndex, invoices.length,
__('Processing batch {0}-{1} of {2} invoices', [currentIndex + 1, currentIndex + currentBatchSize, invoices.length]));
// Обрабатываем текущий батч
frappe.call({
method: 'invoice_az.api.process_invoice_parties_from_list',
args: {
'invoice_list_data': currentBatch,
'token': token
},
callback: function(r) {
if (r.message && r.message.success) {
// Обновляем накопленные данные
processedCount += (r.message.customers_created || 0) + (r.message.suppliers_created || 0);
// Переходим к следующему батчу с небольшой задержкой
setTimeout(function() {
if (!window.cancelSuppliersLoading) { // Дополнительная проверка
process_invoices_for_suppliers_with_progress(invoices, token, processedCount,
currentIndex + currentBatchSize, batchSize);
}
}, 100); // 100ms задержка между батчами
} else if (r.message && r.message.error === 'unauthorized') {
cleanup_suppliers_loading_state();
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
check_and_process_token_suppliers(function() {
// Получаем новый токен и продолжаем с текущего индекса
frappe.call({
method: 'invoice_az.auth.get_default_asan_login',
callback: function(login_r) {
if (login_r.message && login_r.message.found && login_r.message.main_token) {
process_invoices_for_suppliers_with_progress(invoices, login_r.message.main_token,
processedCount, currentIndex, batchSize);
} else {
frappe.msgprint(__('Failed to get authentication token'));
}
}
});
});
},
function() {
frappe.show_alert({
message: __('Authentication cancelled'),
indicator: 'red'
}, 5);
}
);
} else {
// Ошибка обработки батча - продолжаем со следующим
setTimeout(function() {
if (!window.cancelSuppliersLoading) { // Дополнительная проверка
process_invoices_for_suppliers_with_progress(invoices, token, processedCount,
currentIndex + currentBatchSize, batchSize);
}
}, 200); // Увеличенная задержка при ошибках
}
},
error: function(xhr, status, error) {
// Сетевая ошибка - продолжаем со следующим батчом
setTimeout(function() {
if (!window.cancelSuppliersLoading) { // Дополнительная проверка
process_invoices_for_suppliers_with_progress(invoices, token, processedCount,
currentIndex + currentBatchSize, batchSize);
}
}, 500); // Еще большая задержка при сетевых ошибках
}
});
}

View File

@ -1329,17 +1329,24 @@ SalesETaxes.import = {
});
},
// Обработка результата импорта
// Обработка результата импорта - ОБНОВЛЕНО для Sales Order + Sales Invoice
_handleImportResult: function(importR, invoiceMessage, invoiceId, scheduleDate, receiverName, total,
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
if (importR.message && importR.message.success) {
frappe.show_alert({
message: __('Sales invoice successfully imported: ') + (invoiceMessage.serialNumber || ''),
indicator: 'green'
}, 3);
// ИЗМЕНЕНО: теперь получаем sales_order и sales_invoice из ответа
const salesOrderName = importR.message.sales_order;
const salesInvoiceName = importR.message.sales_invoice;
// ИЗМЕНЕНО: создание E-Taxes Sales и связывание с Sales Order происходит на сервере
// Поэтому просто увеличиваем счетчик и продолжаем
processedCount++;
SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
} else if (importR.message && (importR.message.unmatched_items || importR.message.unmatched_parties || importR.message.unmatched_units)) {
let errorType = '';
const additionalData = { invoice_items: invoiceMessage.items || [] };
@ -1490,9 +1497,8 @@ frappe.ui.form.on('Sales Order', {
}
});
// Sales Order List
frappe.listview_settings['Sales Order'] = {
add_fields: ['customer', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc'],
add_fields: ['customer', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
get_indicator: function(doc) {
if (doc.status === 'Closed') {
@ -1511,6 +1517,7 @@ frappe.listview_settings['Sales Order'] = {
},
onload: function(listview) {
// ДОБАВЛЕНО: кнопка импорта из E-Taxes для Sales Order
listview.page.add_menu_item(__('Import from E-Taxes'), function() {
SalesETaxes.import.showDialog();
});
@ -1550,9 +1557,13 @@ frappe.ui.form.on('Sales Invoice', {
}
});
// Sales Invoice List
// Sales Invoice List - УБРАНО: кнопка импорта (теперь только в Sales Order)
frappe.listview_settings['Sales Invoice'] = {
add_fields: ['customer', 'posting_date', 'status', 'docstatus', 'is_taxes_doc'],
onload: function(listview) {
// УБРАНО: кнопка импорта - теперь импорт только через Sales Order
listview.columns.push({
type: 'Check',
df: {

View File

@ -15,6 +15,8 @@ doctype_js = {
doctype_list_js = {
"E-Taxes Item": "client/e_taxes_items_list.js",
"E-Taxes Parties": "client/e_taxes_parties_list.js",
"E-Taxes Suppliers": "client/e_taxes_suppliers_list.js",
"E-Taxes Customers": "client/e_taxes_customers_list.js",
"E-Taxes Unit": "client/e_taxes_unit_list.js",
"Purchase Order": "client/purchase_order.js",
"Purchase Invoice": "client/purchase_invoice.js",
@ -28,6 +30,10 @@ doc_events = {
"on_trash": "invoice_az.api.on_delete_purchase_order",
"on_cancel": "invoice_az.api.on_delete_purchase_order"
},
"Sales Order": {
"on_trash": "invoice_az.sales_api.on_delete_sales_order",
"on_cancel": "invoice_az.sales_api.on_delete_sales_order"
},
"E-Taxes Settings": {
"on_update": "invoice_az.api.update_mapped_statuses"
}

View File

@ -0,0 +1,76 @@
{
"actions": [],
"creation": "2025-07-11 14:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_customer_name",
"etaxes_tax_id",
"erp_customer",
"customer_group",
"territory",
"payment_terms",
"mapping_type"
],
"fields": [
{
"fieldname": "etaxes_customer_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "E-Taxes Customer Name",
"reqd": 1
},
{
"fieldname": "etaxes_tax_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax ID"
},
{
"fieldname": "erp_customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "ERP Customer",
"options": "Customer"
},
{
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"options": "Customer Group"
},
{
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
"options": "Territory"
},
{
"fieldname": "payment_terms",
"fieldtype": "Link",
"label": "Payment Terms",
"options": "Payment Terms Template"
},
{
"default": "Manual",
"fieldname": "mapping_type",
"fieldtype": "Select",
"label": "Mapping Type",
"options": "Manual\nAutomatic",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-11 14:00:00",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Customer Mappings",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ETaxesCustomerMappings(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Jey ERP and contributors
// For license information, please see license.txt
// frappe.ui.form.on("E-Taxes Customers", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,139 @@
{
"actions": [],
"autoname": "field:etaxes_party_name",
"creation": "2025-07-11 14:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_party_name",
"etaxes_tax_id",
"etaxes_party_type",
"etaxes_address",
"source_invoice",
"status",
"customer_mapping_section",
"mapped_customer",
"customer_group",
"territory",
"payment_terms"
],
"fields": [
{
"fieldname": "etaxes_party_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Customer Name",
"length": 500,
"read_only": 1,
"reqd": 1,
"unique": 1
},
{
"fieldname": "etaxes_tax_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax ID",
"read_only": 1
},
{
"default": "Receiver",
"fieldname": "etaxes_party_type",
"fieldtype": "Select",
"label": "E-Taxes Type",
"options": "Receiver\nSender/Receiver",
"read_only": 1
},
{
"fieldname": "etaxes_address",
"fieldtype": "Data",
"label": "Address",
"read_only": 1
},
{
"description": "Source invoice number",
"fieldname": "source_invoice",
"fieldtype": "Data",
"label": "Source",
"read_only": 1
},
{
"default": "New",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "New\nMapped\nProcessing",
"read_only": 1
},
{
"fieldname": "customer_mapping_section",
"fieldtype": "Section Break",
"label": "Customer Mapping"
},
{
"fieldname": "mapped_customer",
"fieldtype": "Link",
"label": "Mapped Customer",
"options": "Customer",
"read_only": 1
},
{
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"options": "Customer Group",
"read_only": 1
},
{
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
"options": "Territory",
"read_only": 1
},
{
"fieldname": "payment_terms",
"fieldtype": "Link",
"label": "Payment Terms",
"options": "Payment Terms Template",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-11 14:00:00",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Customers",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ETaxesCustomers(Document):
pass

View File

@ -0,0 +1,30 @@
# Copyright (c) 2025, Jey ERP and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestETaxesCustomers(UnitTestCase):
"""
Unit tests for ETaxesCustomers.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestETaxesCustomers(IntegrationTestCase):
"""
Integration tests for ETaxesCustomers.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -501,6 +501,381 @@ frappe.ui.form.on('E-Taxes Settings', {
}
}, __('Partners'));
// =================================================================
// КНОПКИ ДЛЯ CUSTOMERS
// =================================================================
frm.add_custom_button(__('Match customers by similar name'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
frm.save().then(function() {
match_similar_customers();
});
},
function() {
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
match_similar_customers();
}
function match_similar_customers() {
frappe.confirm(
__('This operation will automatically match unmapped customers with similar names. Continue?'),
function() {
frappe.show_alert({
message: __('Matching customers...'),
indicator: 'blue'
});
frappe.call({
method: 'invoice_az.api.match_similar_customers',
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Matched {0} of {1} customers',
[r.message.matched_count, r.message.total_processed]),
indicator: 'green'
}, 5);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error matching customers')
});
}
}
});
}
);
}
}, __('Customers'));
frm.add_custom_button(__('Create matching customers'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
frm.save().then(function() {
create_unmapped_customers();
});
},
function() {
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
create_unmapped_customers();
}
function create_unmapped_customers() {
frappe.confirm(
__('<div style="color: red; font-weight: bold;">WARNING! This action will create new customers in the system for all unmapped elements!</div><p>This action is irreversible. Are you sure?</p>'),
function() {
frappe.confirm(
__('Are you really sure you want to create new customers? This cannot be undone.'),
function() {
frappe.call({
method: 'invoice_az.api.create_unmapped_customers',
args: {
'settings_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Created {0} new customers', [r.message.created_count]),
indicator: 'green'
}, 5);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error creating customers')
});
}
}
});
}
);
}
);
}
}, __('Customers'));
frm.add_custom_button(__('Add unmapped customers'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
frm.save().then(function() {
add_unmapped_customers();
});
},
function() {
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
add_unmapped_customers();
}
function add_unmapped_customers() {
frappe.call({
method: 'invoice_az.api.get_unmapped_customers',
callback: function(r) {
if (r.message && r.message.success) {
if (r.message.customers && r.message.customers.length > 0) {
r.message.customers.forEach(function(customer) {
let row = frm.add_child('customer_mappings');
row.etaxes_customer_name = customer.etaxes_party_name;
row.etaxes_tax_id = customer.etaxes_tax_id;
if (customer.customer_group)
row.customer_group = customer.customer_group;
if (customer.territory)
row.territory = customer.territory;
if (customer.payment_terms)
row.payment_terms = customer.payment_terms;
row.mapping_type = 'Manual';
});
frm.refresh_field('customer_mappings');
frm.save().then(function() {
frappe.show_alert({
message: __('Added {0} unmapped customers', [r.message.customers.length]),
indicator: 'green'
}, 5);
});
} else {
frappe.show_alert({
message: __('No unmapped customers'),
indicator: 'blue'
}, 5);
}
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error retrieving customers')
});
}
}
});
}
}, __('Customers'));
// =================================================================
// КНОПКИ ДЛЯ SUPPLIERS
// =================================================================
frm.add_custom_button(__('Match suppliers by similar name'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
frm.save().then(function() {
match_similar_suppliers();
});
},
function() {
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
match_similar_suppliers();
}
function match_similar_suppliers() {
frappe.confirm(
__('This operation will automatically match unmapped suppliers with similar names. Continue?'),
function() {
frappe.show_alert({
message: __('Matching suppliers...'),
indicator: 'blue'
});
frappe.call({
method: 'invoice_az.api.match_similar_suppliers',
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Matched {0} of {1} suppliers',
[r.message.matched_count, r.message.total_processed]),
indicator: 'green'
}, 5);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error matching suppliers')
});
}
}
});
}
);
}
}, __('Suppliers'));
frm.add_custom_button(__('Create matching suppliers'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
frm.save().then(function() {
create_unmapped_suppliers();
});
},
function() {
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
create_unmapped_suppliers();
}
function create_unmapped_suppliers() {
frappe.confirm(
__('<div style="color: red; font-weight: bold;">WARNING! This action will create new suppliers in the system for all unmapped elements!</div><p>This action is irreversible. Are you sure?</p>'),
function() {
frappe.confirm(
__('Are you really sure you want to create new suppliers? This cannot be undone.'),
function() {
frappe.call({
method: 'invoice_az.api.create_unmapped_suppliers',
args: {
'settings_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Created {0} new suppliers', [r.message.created_count]),
indicator: 'green'
}, 5);
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error creating suppliers')
});
}
}
});
}
);
}
);
}
}, __('Suppliers'));
frm.add_custom_button(__('Add unmapped suppliers'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
frm.save().then(function() {
add_unmapped_suppliers();
});
},
function() {
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
add_unmapped_suppliers();
}
function add_unmapped_suppliers() {
frappe.call({
method: 'invoice_az.api.get_unmapped_suppliers',
callback: function(r) {
if (r.message && r.message.success) {
if (r.message.suppliers && r.message.suppliers.length > 0) {
r.message.suppliers.forEach(function(supplier) {
let row = frm.add_child('supplier_mappings');
row.etaxes_supplier_name = supplier.etaxes_party_name;
row.etaxes_tax_id = supplier.etaxes_tax_id;
if (supplier.supplier_group)
row.supplier_group = supplier.supplier_group;
if (supplier.payment_terms)
row.payment_terms = supplier.payment_terms;
row.mapping_type = 'Manual';
});
frm.refresh_field('supplier_mappings');
frm.save().then(function() {
frappe.show_alert({
message: __('Added {0} unmapped suppliers', [r.message.suppliers.length]),
indicator: 'green'
}, 5);
});
} else {
frappe.show_alert({
message: __('No unmapped suppliers'),
indicator: 'blue'
}, 5);
}
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error retrieving suppliers')
});
}
}
});
}
}, __('Suppliers'));
// Кнопки для единиц измерения
frm.add_custom_button(__('Match units by similar name'), function() {
if(frm.is_dirty()) {

View File

@ -27,6 +27,10 @@
"mappings_tab",
"item_mappings_section",
"item_mappings",
"customer_mappings_section",
"customer_mappings",
"supplier_mappings_section",
"supplier_mappings",
"party_mappings_section",
"party_mappings",
"unit_mappings_section",
@ -157,10 +161,32 @@
"label": "Item Mappings",
"options": "E-Taxes Item Mapping"
},
{
"fieldname": "customer_mappings_section",
"fieldtype": "Section Break",
"label": "Customer Mappings"
},
{
"fieldname": "customer_mappings",
"fieldtype": "Table",
"label": "Customer Mappings",
"options": "E-Taxes Customer Mappings"
},
{
"fieldname": "supplier_mappings_section",
"fieldtype": "Section Break",
"label": "Supplier Mappings"
},
{
"fieldname": "supplier_mappings",
"fieldtype": "Table",
"label": "Supplier Mappings",
"options": "E-Taxes Supplier Mappings"
},
{
"fieldname": "party_mappings_section",
"fieldtype": "Section Break",
"label": "Business Partner Mappings"
"label": "Business Partner Mappings (Legacy)"
},
{
"fieldname": "party_mappings",
@ -182,7 +208,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-21 15:00:00.000000",
"modified": "2025-07-11 14:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Settings",

View File

@ -0,0 +1,69 @@
{
"actions": [],
"creation": "2025-07-11 14:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_supplier_name",
"etaxes_tax_id",
"erp_supplier",
"supplier_group",
"payment_terms",
"mapping_type"
],
"fields": [
{
"fieldname": "etaxes_supplier_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "E-Taxes Supplier Name",
"reqd": 1
},
{
"fieldname": "etaxes_tax_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax ID"
},
{
"fieldname": "erp_supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "ERP Supplier",
"options": "Supplier"
},
{
"fieldname": "supplier_group",
"fieldtype": "Link",
"label": "Supplier Group",
"options": "Supplier Group"
},
{
"fieldname": "payment_terms",
"fieldtype": "Link",
"label": "Payment Terms",
"options": "Payment Terms Template"
},
{
"default": "Manual",
"fieldname": "mapping_type",
"fieldtype": "Select",
"label": "Mapping Type",
"options": "Manual\nAutomatic",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-11 14:00:00",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Supplier Mappings",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ETaxesSupplierMappings(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Jey ERP and contributors
// For license information, please see license.txt
// frappe.ui.form.on("E-Taxes Suppliers", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,131 @@
{
"actions": [],
"autoname": "field:etaxes_party_name",
"creation": "2025-07-11 14:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_party_name",
"etaxes_tax_id",
"etaxes_party_type",
"etaxes_address",
"source_invoice",
"status",
"supplier_mapping_section",
"mapped_supplier",
"supplier_group",
"payment_terms"
],
"fields": [
{
"fieldname": "etaxes_party_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Supplier Name",
"length": 500,
"read_only": 1,
"reqd": 1,
"unique": 1
},
{
"fieldname": "etaxes_tax_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax ID",
"read_only": 1
},
{
"default": "Sender",
"fieldname": "etaxes_party_type",
"fieldtype": "Select",
"label": "E-Taxes Type",
"options": "Sender\nSender/Receiver",
"read_only": 1
},
{
"fieldname": "etaxes_address",
"fieldtype": "Data",
"label": "Address",
"read_only": 1
},
{
"description": "Source invoice number",
"fieldname": "source_invoice",
"fieldtype": "Data",
"label": "Source",
"read_only": 1
},
{
"default": "New",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "New\nMapped\nProcessing",
"read_only": 1
},
{
"fieldname": "supplier_mapping_section",
"fieldtype": "Section Break",
"label": "Supplier Mapping"
},
{
"fieldname": "mapped_supplier",
"fieldtype": "Link",
"label": "Mapped Supplier",
"options": "Supplier",
"read_only": 1
},
{
"fieldname": "supplier_group",
"fieldtype": "Link",
"label": "Supplier Group",
"options": "Supplier Group",
"read_only": 1
},
{
"fieldname": "payment_terms",
"fieldtype": "Link",
"label": "Payment Terms",
"options": "Payment Terms Template",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-11 14:00:00",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Suppliers",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ETaxesSuppliers(Document):
pass

View File

@ -0,0 +1,30 @@
# Copyright (c) 2025, Jey ERP and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestETaxesSuppliers(UnitTestCase):
"""
Unit tests for ETaxesSuppliers.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestETaxesSuppliers(IntegrationTestCase):
"""
Integration tests for ETaxesSuppliers.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -214,8 +214,8 @@ def get_sales_invoice_details(token, invoice_id):
}
@frappe.whitelist()
def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehouse=None):
"""Import sales invoice taking into account mappings and processing unmapped elements"""
def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, schedule_date=None, warehouse=None):
"""Import sales invoice taking into account NEW customer mappings"""
# Записываем активность
record_etaxes_activity()
@ -240,26 +240,24 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
if mapping.etaxes_item_name and mapping.erp_item:
item_mappings[mapping.etaxes_item_name] = mapping.erp_item
party_mappings = {}
for mapping in settings.party_mappings:
if mapping.etaxes_party_name and mapping.erp_party:
key = f"{mapping.etaxes_party_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}"
party_mappings[key] = (mapping.erp_party, mapping.party_type)
# ИЗМЕНЕНО: Используем customer_mappings вместо party_mappings
customer_mappings = {}
for mapping in settings.customer_mappings:
if mapping.etaxes_customer_name and mapping.erp_customer:
key = f"{mapping.etaxes_customer_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}"
customer_mappings[key] = mapping.erp_customer
# Get default warehouse
default_warehouse = warehouse
if not default_warehouse:
# If warehouse not specified, try to get from settings
if hasattr(settings, 'default_warehouse') and settings.default_warehouse:
default_warehouse = settings.default_warehouse
else:
# If warehouse not specified in settings, try to get default warehouse for company
company = frappe.defaults.get_user_default('Company')
if company:
default_warehouse = frappe.db.get_value('Company', company, 'default_warehouse')
# If still no warehouse, take first active warehouse
if not default_warehouse:
warehouses = frappe.get_all('Warehouse',
filters={'is_group': 0, 'disabled': 0},
@ -268,56 +266,54 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
if warehouses:
default_warehouse = warehouses[0].name
# Check warehouse existence
if not default_warehouse:
return {
'success': False,
'message': 'Default warehouse not found. Please specify warehouse in E-Taxes settings.'
}
# Create new Sales Invoice
si = frappe.new_doc('Sales Invoice')
# Set customer
receiver = invoice_data.get('receiver', {})
receiver_key = f"{receiver.get('name', '').lower()}|{receiver.get('tin', '').lower()}"
if receiver_key in party_mappings and party_mappings[receiver_key][0]:
# Check if it's a customer
if party_mappings[receiver_key][1] == 'Customer':
si.customer = party_mappings[receiver_key][0]
# ИЗМЕНЕНО: Создаем Sales Order сначала (аналогично Purchase Order логике)
if sales_order_name:
so = frappe.get_doc('Sales Order', sales_order_name)
else:
so = frappe.new_doc('Sales Order')
# ИЗМЕНЕНО: Set customer используя customer_mappings
receiver = invoice_data.get('receiver', {})
receiver_key = f"{receiver.get('name', '').lower()}|{receiver.get('tin', '').lower()}"
if receiver_key in customer_mappings and customer_mappings[receiver_key]:
so.customer = customer_mappings[receiver_key]
else:
return {
'success': False,
'message': f'Receiver is mapped as {party_mappings[receiver_key][1]}, but Sales Invoice requires a Customer'
'unmatched_parties': [{
'name': receiver.get('name', ''),
'tin': receiver.get('tin', ''),
'type': 'Receiver'
}],
'message': f'No mapping found for customer: {receiver.get("name", "")}'
}
else:
return {
'success': False,
'unmatched_parties': [{
'name': receiver.get('name', ''),
'tin': receiver.get('tin', ''),
'type': 'Receiver'
}],
'message': f'No mapping found for customer: {receiver.get("name", "")}'
}
# Set dates
si.posting_date = frappe.utils.today()
# Get date from invoice or parameter if specified
if schedule_date:
si.due_date = schedule_date
elif invoice_data.get("creationDate"):
si.due_date = frappe.utils.getdate(invoice_data.get("creationDate"))
else:
si.due_date = frappe.utils.add_days(frappe.utils.today(), 30)
si.company = frappe.defaults.get_user_default('Company')
# Set dates
so.transaction_date = frappe.utils.today()
if schedule_date:
so.delivery_date = schedule_date
elif invoice_data.get("creationDate"):
so.delivery_date = frappe.utils.getdate(invoice_data.get("creationDate"))
else:
so.delivery_date = frappe.utils.today()
so.company = frappe.defaults.get_user_default('Company')
# Add additional fields from invoice
si.title = f"Sales Invoice {invoice_data.get('serialNumber', '')}"
si.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
# Add/update additional fields from invoice
so.title = f"Sales Order {invoice_data.get('serialNumber', '')}"
so.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
# Clear existing items if need to update them completely
if sales_order_name and invoice_data.get("items"):
so.items = []
date_to_use = None
if schedule_date:
@ -360,7 +356,6 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
break
if not mapped_item:
# Если соответствие не найдено, добавляем в список несопоставленных
unmatched_items.append({
'name': item_name,
'code': item_code
@ -372,7 +367,6 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
mapped_uom = None
if unit_name:
# Ищем существующую запись E-Taxes Unit и берем mapped_unit
try:
mapped_uom = frappe.db.get_value('E-Taxes Unit',
filters={'etaxes_unit_name': unit_name},
@ -383,46 +377,41 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
# Если соответствие единицы не найдено, используем UOM из товара или настроек
if not mapped_uom:
if unit_name:
# Добавляем в список несопоставленных единиц
unmatched_units.append({
'name': unit_name
})
# Используем UOM из настроек товара или default
try:
item_doc = frappe.get_doc('Item', mapped_item)
mapped_uom = item_doc.stock_uom
except:
# Fallback на default_uom из настроек
mapped_uom = settings.default_uom if settings.default_uom else "Nos"
# Add position to Sales Invoice
si_item = frappe.new_doc("Sales Invoice Item")
si_item.parent = si.name
si_item.parenttype = "Sales Invoice"
si_item.parentfield = "items"
# ИЗМЕНЕНО: Add position to Sales Order (не Sales Invoice)
so_item = frappe.new_doc("Sales Order Item")
so_item.parent = so.name
so_item.parenttype = "Sales Order"
so_item.parentfield = "items"
si_item.item_code = mapped_item
so_item.item_code = mapped_item
# Получаем название и описание из базы данных Item
try:
item_doc = frappe.get_doc('Item', mapped_item)
si_item.item_name = item_doc.item_name
si_item.description = item_doc.description or item_doc.item_name
so_item.item_name = item_doc.item_name
so_item.description = item_doc.description or item_doc.item_name
except:
# Fallback на название из E-Taxes если не удалось получить из базы
si_item.item_name = item.get("productName", "")
si_item.description = item.get("productName", "")
so_item.item_name = item.get("productName", "")
so_item.description = item.get("productName", "")
si_item.qty = item.get("quantity", 0)
si_item.rate = item.get("pricePerUnit", 0)
si_item.amount = item.get("cost", 0)
si_item.uom = mapped_uom
# IMPORTANT: Set default warehouse
si_item.warehouse = default_warehouse
so_item.qty = item.get("quantity", 0)
so_item.rate = item.get("pricePerUnit", 0)
so_item.amount = item.get("cost", 0)
so_item.uom = mapped_uom
so_item.delivery_date = date_to_use # ИЗМЕНЕНО: delivery_date вместо schedule_date
so_item.warehouse = default_warehouse
si.append("items", si_item)
so.append("items", so_item)
added_items_count += 1
# Check if there are unmapped items
@ -444,31 +433,28 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
# Check that there is at least one element in table
if added_items_count == 0:
frappe.log_error(
f"No items were added to Sales Invoice. Invoice data: {invoice_data}",
f"No items were added to Sales Order. Invoice data: {invoice_data}",
"Import Sales Invoice Error"
)
return {
'success': False,
'message': 'Could not add any items to sales invoice'
'message': 'Could not add any items to sales order'
}
# ДОБАВЛЕНО: Создание строки ƏDV в Sales Taxes and Charges ТОЛЬКО если есть vat в данных
try:
# Получаем VAT из E-Taxes данных
vat_amount = invoice_data.get('vat', 0)
# Создаем строку ƏDV ТОЛЬКО если есть vat в данных и он больше 0
if vat_amount and vat_amount > 0:
vat_account = frappe.db.get_value('Account',
filters={'account_number': '531.1'},
filters={'account_number': '521.3'},
fieldname='name')
if vat_account:
# Рассчитываем net_total для определения ставки
net_total = sum(item.get('cost', 0) for item in invoice_data.get('items', []))
vat_rate = (vat_amount / net_total * 100) if net_total > 0 else 0
si.append("taxes", {
so.append("taxes", {
"account_head": vat_account,
"charge_type": "On Net Total",
"rate": vat_rate,
@ -477,17 +463,27 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
"add_deduct_tax": "Add"
})
frappe.log_error(f"Added ƏDV tax: Amount={vat_amount}, Rate={vat_rate}%", "Sales Invoice Tax")
frappe.log_error(f"Added ƏDV tax: Amount={vat_amount}, Rate={vat_rate}%", "Sales Order Tax")
else:
frappe.log_error("ƏDV account (531.1) not found", "Sales Invoice Tax Warning")
frappe.log_error("ƏDV account (521.3) not found", "Sales Order Tax Warning")
else:
frappe.log_error("No VAT amount in E-Taxes data, skipping ƏDV tax creation", "Sales Invoice Tax Info")
frappe.log_error("No VAT amount in E-Taxes data, skipping ƏDV tax creation", "Sales Order Tax Info")
except Exception as e:
frappe.log_error(f"Error adding ƏDV tax: {str(e)}", "Sales Invoice Tax Error")
frappe.log_error(f"Error adding ƏDV tax: {str(e)}", "Sales Order Tax Error")
# Save document
si.save()
so.save()
# Additional check that all delivery_date and warehouse are set
for item in so.items:
if not item.delivery_date:
item.delivery_date = date_to_use
if not item.warehouse:
item.warehouse = default_warehouse
# Save again to ensure changes are applied
so.save()
# ИЗМЕНЕНИЕ: Создаем E-Taxes Sales и устанавливаем связь ДО submit'а
invoice_id = invoice_data.get('id', '')
@ -499,27 +495,50 @@ def import_sales_invoice_with_mapping(invoice_data, schedule_date=None, warehous
etaxes_sales_result = create_etaxes_sales(invoice_id, date_to_use, receiver_name, total)
if etaxes_sales_result and etaxes_sales_result.get('success'):
# Устанавливаем поля E-Taxes ДО submit'а
si.is_taxes_doc = 1
si.taxes_doc = etaxes_sales_result.get('name')
so.is_taxes_doc = 1
so.taxes_doc = etaxes_sales_result.get('name')
# Сохраняем изменения
si.save()
so.save()
# ИЗМЕНЕНИЕ: Делаем Submit для Sales Invoice
# ИЗМЕНЕНИЕ: Делаем Submit для Sales Order
try:
si.submit()
frappe.log_error(f"Sales Invoice {si.name} submitted successfully", "Import Sales Invoice Success")
so.submit()
frappe.log_error(f"Sales Order {so.name} submitted successfully", "Import Sales Invoice Success")
except Exception as e:
frappe.log_error(f"Error submitting Sales Invoice {si.name}: {str(e)}", "Submit SI Error")
frappe.log_error(f"Error submitting Sales Order {so.name}: {str(e)}", "Submit SO Error")
return {
'success': False,
'message': f'Failed to submit Sales Invoice: {str(e)}'
'message': f'Failed to submit Sales Order: {str(e)}'
}
return {
'success': True,
'message': 'Sales invoice data imported successfully.',
'sales_invoice': si.name
}
# ИЗМЕНЕНИЕ: Создаем Sales Invoice из Sales Order
try:
si_name = create_sales_invoice_from_order(so.name)
if si_name:
# Делаем Submit для Sales Invoice
si = frappe.get_doc("Sales Invoice", si_name)
si.submit()
frappe.log_error(f"Sales Invoice {si_name} created and submitted successfully", "Import Sales Invoice Success")
return {
'success': True,
'message': 'Sales invoice data imported successfully. Sales Order and Sales Invoice created.',
'sales_order': so.name,
'sales_invoice': si_name
}
else:
return {
'success': True,
'message': 'Sales invoice data imported successfully. Sales Order created, but Sales Invoice creation failed.',
'sales_order': so.name
}
except Exception as e:
frappe.log_error(f"Error creating or submitting Sales Invoice: {str(e)}", "Submit SI Error")
return {
'success': True,
'message': 'Sales invoice data imported successfully. Sales Order created, but Sales Invoice creation failed.',
'sales_order': so.name
}
except Exception as e:
frappe.log_error(f"Error in import_sales_invoice_with_mapping: {str(e)}\n{frappe.get_traceback()}", "Import Sales Invoice Error")
@ -683,3 +702,19 @@ def normalize_string(text, consider_azeri=True):
text = ' '.join(text.split())
return text
@frappe.whitelist()
def on_delete_sales_order(doc, method):
"""Deletes related E-Taxes Sales record when Sales Order is deleted"""
if doc.is_taxes_doc and doc.taxes_doc:
try:
frappe.logger().info(f"Deleting E-Taxes Sales {doc.taxes_doc} due to Sales Order {doc.name} deletion")
# Check if E-Taxes Sales record exists
if frappe.db.exists("E-Taxes Sales", doc.taxes_doc):
frappe.delete_doc('E-Taxes Sales', doc.taxes_doc, ignore_permissions=True, force=True)
frappe.db.commit()
frappe.logger().info(f"Successfully deleted E-Taxes Sales {doc.taxes_doc}")
except Exception as e:
frappe.log_error(f"Error deleting E-Taxes Sales {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
"Sales Order Delete Error")