1598 lines
70 KiB
JavaScript
1598 lines
70 KiB
JavaScript
// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
|
||
const VATETaxes = {
|
||
// Константы
|
||
PROGRESS_UPDATE_DELAY: 50,
|
||
AUTH_POLL_INTERVAL: 6000,
|
||
AUTH_MAX_ATTEMPTS: 20,
|
||
|
||
// Глобальные переменные
|
||
loadingDialog: null,
|
||
cancelLoading: false,
|
||
loadingErrors: [],
|
||
|
||
// Кэш
|
||
cache: {
|
||
defaultLogin: null,
|
||
cacheTime: null,
|
||
cacheDuration: 300000 // 5 минут
|
||
}
|
||
};
|
||
|
||
// ======= БАЗОВЫЕ УТИЛИТЫ =======
|
||
VATETaxes.utils = {
|
||
// Кэшированное получение настроек входа
|
||
getDefaultLogin: function(callback, useCache = true) {
|
||
const now = Date.now();
|
||
|
||
if (useCache && VATETaxes.cache.defaultLogin && VATETaxes.cache.cacheTime &&
|
||
(now - VATETaxes.cache.cacheTime) < VATETaxes.cache.cacheDuration) {
|
||
callback(VATETaxes.cache.defaultLogin);
|
||
return;
|
||
}
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.get_default_asan_login',
|
||
callback: function(r) {
|
||
if (r.message) {
|
||
VATETaxes.cache.defaultLogin = r.message;
|
||
VATETaxes.cache.cacheTime = now;
|
||
}
|
||
callback(r.message);
|
||
},
|
||
error: function() {
|
||
callback({found: false, error: 'Network error'});
|
||
}
|
||
});
|
||
},
|
||
|
||
// Проверка валидности токена
|
||
checkTokenValidity: function(callback) {
|
||
frappe.call({
|
||
method: 'invoice_az.auth.check_token_validity',
|
||
callback: function(r) {
|
||
callback(r.message && r.message.valid);
|
||
},
|
||
error: function() {
|
||
callback(false);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Форматирование валюты
|
||
formatCurrency: function(amount) {
|
||
return new Intl.NumberFormat('az-AZ', {
|
||
style: 'currency',
|
||
currency: 'AZN',
|
||
minimumFractionDigits: 2
|
||
}).format(amount || 0);
|
||
},
|
||
|
||
// Парсинг даты операции из формата "20250319154646"
|
||
parseOperationDate: function(dateStr) {
|
||
if (!dateStr || dateStr.length < 8) {
|
||
return '';
|
||
}
|
||
const year = dateStr.substring(0, 4);
|
||
const month = dateStr.substring(4, 6);
|
||
const day = dateStr.substring(6, 8);
|
||
return `${day}.${month}.${year}`;
|
||
},
|
||
|
||
// Гуманизация типа ошибки
|
||
humanizeErrorType: function(errorType) {
|
||
const errorTypeMap = {
|
||
'customer_not_found': __('Customer Not Found'),
|
||
'customer_creation_failed': __('Customer Creation Failed'),
|
||
'account_not_found': __('Account Not Found'),
|
||
'mapping_not_found': __('VAT Account Mapping Not Found'),
|
||
'import_error': __('Import Error'),
|
||
'network_error': __('Network Error'),
|
||
'validation_error': __('Validation Error'),
|
||
'duplicate': __('Duplicate'),
|
||
'permission_error': __('Permission Error')
|
||
};
|
||
return errorTypeMap[errorType] || errorType;
|
||
},
|
||
|
||
// Очистка кэша
|
||
clearCache: function() {
|
||
VATETaxes.cache.defaultLogin = null;
|
||
VATETaxes.cache.cacheTime = null;
|
||
},
|
||
|
||
// Получить красивое название типа операции
|
||
getOperationTypeLabel: function(operationType) {
|
||
return operationType || '-';
|
||
}
|
||
};
|
||
|
||
// ======= ДИАЛОГИ ЗАГРУЗКИ =======
|
||
VATETaxes.dialogs = {
|
||
// Показать диалог загрузки
|
||
showLoading: function(title, message, submessage) {
|
||
if (VATETaxes.loadingDialog) {
|
||
this.updateLoading(title, message, submessage);
|
||
return;
|
||
}
|
||
|
||
VATETaxes.loadingDialog = new frappe.ui.Dialog({
|
||
title: title,
|
||
fields: [{
|
||
fieldname: 'loading_html',
|
||
fieldtype: 'HTML',
|
||
options: this._getLoadingHTML(title, message, submessage)
|
||
}],
|
||
primary_action_label: __('Cancel'),
|
||
primary_action: function() {
|
||
// Флаг ставим ПЕРЕД закрытием: hide() его больше не сбрасывает,
|
||
// поэтому in-flight callback'и пагинации увидят отмену и выйдут.
|
||
VATETaxes.cancelLoading = true;
|
||
VATETaxes.dialogs.hide();
|
||
}
|
||
});
|
||
|
||
VATETaxes.cancelLoading = false;
|
||
VATETaxes.loadingDialog.show();
|
||
VATETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
|
||
},
|
||
|
||
// Обновить сообщение в диалоге
|
||
updateLoading: function(title, message, submessage) {
|
||
if (!VATETaxes.loadingDialog) return;
|
||
|
||
if (title) $('#loading_title').text(title);
|
||
if (message) $('#loading_message').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#loading_submessage').text(submessage);
|
||
$('#loading_submessage').toggle(!!submessage);
|
||
}
|
||
},
|
||
|
||
// Показать код верификации
|
||
showVerificationCode: function(code) {
|
||
if (VATETaxes.loadingDialog && code) {
|
||
$('#verification_code').text(code);
|
||
$('#verification_code_container').show();
|
||
}
|
||
},
|
||
|
||
// Установить статус успеха
|
||
setSuccess: function(message, submessage, callback, delay = 2000) {
|
||
if (!VATETaxes.loadingDialog) {
|
||
if (callback) callback();
|
||
return;
|
||
}
|
||
|
||
VATETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
|
||
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
|
||
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
|
||
</div>
|
||
`);
|
||
|
||
$('#loading_message').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#loading_submessage').text(submessage);
|
||
$('#loading_submessage').toggle(!!submessage);
|
||
}
|
||
|
||
$('#verification_code_container').hide();
|
||
VATETaxes.loadingDialog.set_primary_action(null);
|
||
VATETaxes.loadingDialog.set_secondary_action(null);
|
||
|
||
setTimeout(() => {
|
||
this.hide();
|
||
if (callback) callback();
|
||
}, delay);
|
||
},
|
||
|
||
// Установить статус ошибки
|
||
setError: function(message, submessage, showCloseButton = true) {
|
||
if (!VATETaxes.loadingDialog) return;
|
||
|
||
VATETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
|
||
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
|
||
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
|
||
</div>
|
||
`);
|
||
|
||
$('#loading_message').text(message);
|
||
if (submessage !== undefined) {
|
||
$('#loading_submessage').text(submessage);
|
||
$('#loading_submessage').toggle(!!submessage);
|
||
}
|
||
|
||
$('#verification_code_container').hide();
|
||
|
||
if (showCloseButton) {
|
||
VATETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
|
||
}
|
||
},
|
||
|
||
// Скрыть диалог. Мягкий loadingDialog.hide() в jey_theme/Bootstrap НЕ всегда
|
||
// убирает модал (он зависает и следующий диалог накладывается сверху). Поэтому
|
||
// принудительно: modal('hide') + remove() + чистка осиротевшего backdrop —
|
||
// тот же приём, что и в _closeProgress.
|
||
// cancelLoading здесь НЕ сбрасываем: иначе отмена потеряется для in-flight
|
||
// callback'ов. Флаг сбрасывается в начале операции (loadOperations/showLoading).
|
||
hide: function() {
|
||
if (VATETaxes.loadingDialog) {
|
||
try {
|
||
VATETaxes.loadingDialog.$wrapper.modal('hide');
|
||
VATETaxes.loadingDialog.$wrapper.remove();
|
||
} catch (e) {
|
||
console.error("Error hiding loading dialog:", e);
|
||
}
|
||
VATETaxes.loadingDialog = null;
|
||
}
|
||
// backdrop/scroll-lock убираем только если не осталось видимых модалов
|
||
// (иначе сломаем диалог, который показывается следом, напр. выбор даты).
|
||
if ($('.modal:visible').length === 0) {
|
||
$('.modal-backdrop').remove();
|
||
$('body').removeClass('modal-open');
|
||
}
|
||
},
|
||
|
||
// Получить HTML для диалога загрузки
|
||
_getLoadingHTML: function(title, message, submessage) {
|
||
return `
|
||
<div class="text-center">
|
||
<div class="loading-animation" style="display: inline-block; width: 64px; height: 64px;">
|
||
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
|
||
</div>
|
||
<style>
|
||
@keyframes lds-dual-ring {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
<div class="mt-3" id="status_message">
|
||
<h4 id="loading_title">${title || __('Please wait...')}</h4>
|
||
<p id="loading_message">${message || __('The operation may take some time')}</p>
|
||
<p id="loading_submessage" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
|
||
</div>
|
||
<div id="verification_code_container" style="display:none; margin-top: 15px;">
|
||
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
|
||
<span id="verification_code">----</span>
|
||
</div>
|
||
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
};
|
||
|
||
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
|
||
VATETaxes.auth = {
|
||
// Проверить токен и обработать аутентификацию при необходимости
|
||
checkAndProcess: function(callback) {
|
||
VATETaxes.utils.checkTokenValidity(function(isValid) {
|
||
if (isValid) {
|
||
callback();
|
||
} else {
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
|
||
function() {
|
||
VATETaxes.auth.startProcess(callback);
|
||
},
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Начать процесс аутентификации
|
||
startProcess: function(callback) {
|
||
VATETaxes.dialogs.showLoading(
|
||
__('Starting Authentication'),
|
||
__('Getting Asan Login settings...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
VATETaxes.utils.getDefaultLogin(function(loginData) {
|
||
if (loginData && loginData.found) {
|
||
const asanLoginName = loginData.name;
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Sending authentication request...'),
|
||
__('This will send a request to your Asan Imza mobile app')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.handle_authentication',
|
||
args: { 'asan_login_name': asanLoginName },
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
const bearerToken = r.message.bearer_token;
|
||
|
||
if (r.message.verification_code) {
|
||
VATETaxes.dialogs.showVerificationCode(r.message.verification_code);
|
||
}
|
||
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request')
|
||
);
|
||
|
||
VATETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
|
||
if (success) {
|
||
VATETaxes.auth.complete(asanLoginName, callback);
|
||
} else {
|
||
VATETaxes.dialogs.setError(
|
||
__('Authentication Failed'),
|
||
__('Could not authenticate with Asan Imza')
|
||
);
|
||
}
|
||
});
|
||
} else {
|
||
VATETaxes.dialogs.setError(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('Authentication request failed')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
VATETaxes.dialogs.setError(
|
||
__('No Asan Login Settings'),
|
||
__('Please configure Asan Login settings first')
|
||
);
|
||
}
|
||
}, false); // Не использовать кэш для аутентификации
|
||
},
|
||
|
||
// Опрос статуса аутентификации
|
||
pollStatus: function(asanLoginName, bearerToken, callback) {
|
||
let attempts = 0;
|
||
let stopPolling = false;
|
||
|
||
function pollStatusStep() {
|
||
if (attempts >= VATETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
|
||
if (attempts >= VATETaxes.AUTH_MAX_ATTEMPTS) {
|
||
VATETaxes.dialogs.setError(
|
||
__('Authentication Timeout'),
|
||
__('The authentication request has timed out. Please try again.')
|
||
);
|
||
callback(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request') +
|
||
' (' + attempts + '/' + VATETaxes.AUTH_MAX_ATTEMPTS + ')'
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.poll_auth_status',
|
||
args: {
|
||
'asan_login_name': asanLoginName,
|
||
'bearer_token': bearerToken
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
if (r.message.authenticated) {
|
||
stopPolling = true;
|
||
VATETaxes.dialogs.setSuccess(
|
||
__('Authentication Successful'),
|
||
__('You are now authenticated with Asan Imza'),
|
||
function() {
|
||
callback(true);
|
||
}
|
||
);
|
||
} else {
|
||
setTimeout(pollStatusStep, VATETaxes.AUTH_POLL_INTERVAL);
|
||
}
|
||
} else {
|
||
stopPolling = true;
|
||
VATETaxes.dialogs.setError(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('An unknown error occurred')
|
||
);
|
||
callback(false);
|
||
}
|
||
},
|
||
error: function(err) {
|
||
console.error('Error during status check:', err);
|
||
setTimeout(pollStatusStep, VATETaxes.AUTH_POLL_INTERVAL);
|
||
}
|
||
});
|
||
}
|
||
|
||
pollStatusStep();
|
||
},
|
||
|
||
// Завершение аутентификации
|
||
complete: function(asanLoginName, callback) {
|
||
VATETaxes.dialogs.showLoading(
|
||
__('Completing Authentication'),
|
||
__('Getting certificates...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.get_auth_certificates',
|
||
args: { 'asan_login_name': asanLoginName },
|
||
callback: function(certR) {
|
||
if (certR.message && certR.message.success) {
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Certificates retrieved'),
|
||
__('Selecting certificate and taxpayer')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'frappe.client.get',
|
||
args: {
|
||
doctype: 'Asan Login',
|
||
name: asanLoginName
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.selected_certificate_json) {
|
||
try {
|
||
const certData = JSON.parse(r.message.selected_certificate_json);
|
||
const certName = r.message.selected_certificate;
|
||
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Selecting certificate'),
|
||
certName
|
||
);
|
||
|
||
VATETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
|
||
} catch (e) {
|
||
console.error('Error parsing certificate:', e);
|
||
VATETaxes.dialogs.hide();
|
||
VATETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||
}
|
||
} else {
|
||
VATETaxes.dialogs.hide();
|
||
VATETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
VATETaxes.dialogs.setError(
|
||
__('Error'),
|
||
certR.message ? certR.message.message : __('Failed to get certificates')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// Выбор сертификата
|
||
_selectCertificate: function(asanLoginName, certData, certName, callback) {
|
||
frappe.call({
|
||
method: 'invoice_az.auth.select_certificate',
|
||
args: {
|
||
'asan_login_name': asanLoginName,
|
||
'certificate_data': certData,
|
||
'certificate_name': certName
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Selecting taxpayer'),
|
||
__('Using certificate: ') + certName
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.select_taxpayer',
|
||
args: { 'asan_login_name': asanLoginName },
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
VATETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации
|
||
VATETaxes.dialogs.setSuccess(
|
||
__('Authentication Complete'),
|
||
__('You can now access E-Taxes services'),
|
||
callback
|
||
);
|
||
} else {
|
||
VATETaxes.dialogs.setError(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select taxpayer')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
VATETaxes.dialogs.setError(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select certificate')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// Показать селектор сертификатов
|
||
_showCertificateSelector: function(certificates, asanLoginName, callback) {
|
||
if (!certificates || !certificates.length) {
|
||
frappe.msgprint(__('No certificates available'));
|
||
return;
|
||
}
|
||
|
||
let certHtml = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
|
||
certHtml += '<thead><tr><th>Type</th><th>Name</th><th>ID</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>';
|
||
|
||
certificates.forEach(function(cert, index) {
|
||
let name = '';
|
||
let id = '';
|
||
|
||
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
|
||
name = cert.individualInfo.name || '';
|
||
id = cert.individualInfo.fin || '';
|
||
} else if (cert.legalInfo) {
|
||
name = cert.legalInfo.name || '';
|
||
id = cert.legalInfo.tin || cert.legalInfo.voen || '';
|
||
}
|
||
|
||
certHtml += '<tr>' +
|
||
'<td>' + (cert.taxpayerType || '') + '</td>' +
|
||
'<td>' + name + '</td>' +
|
||
'<td>' + id + '</td>' +
|
||
'<td>' + (cert.position || '') + '</td>' +
|
||
'<td>' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + '</td>' +
|
||
'<td><button class="btn btn-xs btn-primary select-cert" data-index="' + index + '">Select</button></td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
certHtml += '</tbody></table></div>';
|
||
|
||
const dialog = new frappe.ui.Dialog({
|
||
title: __('Select Certificate'),
|
||
fields: [{
|
||
fieldtype: 'HTML',
|
||
fieldname: 'certificates',
|
||
options: certHtml
|
||
}]
|
||
});
|
||
|
||
dialog.show();
|
||
|
||
dialog.$wrapper.find('.select-cert').on('click', function() {
|
||
const index = $(this).data('index');
|
||
const cert = certificates[index];
|
||
|
||
let certName = '';
|
||
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
|
||
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
|
||
} else if (cert.legalInfo) {
|
||
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
|
||
} else {
|
||
certName = `Certificate ${index + 1}`;
|
||
}
|
||
|
||
dialog.hide();
|
||
|
||
VATETaxes.dialogs.showLoading(
|
||
__('Selecting Certificate'),
|
||
__('Processing your selection...'),
|
||
certName
|
||
);
|
||
|
||
VATETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
|
||
});
|
||
}
|
||
};
|
||
|
||
// ======= УПРАВЛЕНИЕ ОШИБКАМИ =======
|
||
VATETaxes.errors = {
|
||
// Добавить ошибку в лог
|
||
add: function(operationData, errorType, errorMessage, additionalData = {}) {
|
||
const errorEntry = {
|
||
operation_id: operationData.id || 'Unknown',
|
||
tin: operationData.tin || 'Unknown',
|
||
name: operationData.name || 'Unknown',
|
||
operation_date: operationData.operationDate || 'Unknown',
|
||
error_type: errorType,
|
||
error_message: errorMessage,
|
||
timestamp: new Date().toISOString(),
|
||
...additionalData
|
||
};
|
||
|
||
VATETaxes.loadingErrors.push(errorEntry);
|
||
console.error('VAT E-Taxes Import Error:', errorEntry);
|
||
},
|
||
|
||
// Очистить лог ошибок
|
||
clear: function() {
|
||
frappe.confirm(
|
||
__('Are you sure you want to clear the error log?'),
|
||
function() {
|
||
VATETaxes.loadingErrors = [];
|
||
frappe.show_alert({
|
||
message: __('Error log cleared'),
|
||
indicator: 'green'
|
||
}, 2);
|
||
}
|
||
);
|
||
},
|
||
|
||
// Показать детальный лог ошибок
|
||
showDetails: function() {
|
||
if (VATETaxes.loadingErrors.length === 0) {
|
||
frappe.msgprint(__('No errors to display'));
|
||
return;
|
||
}
|
||
|
||
let errorsHtml = '<div class="error-log-container">';
|
||
|
||
VATETaxes.loadingErrors.forEach(function(error, index) {
|
||
let documentDate = 'No date';
|
||
if (error.operation_date && error.operation_date !== 'Unknown') {
|
||
try {
|
||
documentDate = VATETaxes.utils.parseOperationDate(error.operation_date);
|
||
} catch (e) {
|
||
documentDate = 'Invalid date';
|
||
}
|
||
}
|
||
|
||
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
|
||
|
||
// Заголовок ошибки
|
||
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||
errorsHtml += '<span class="label label-danger">' + VATETaxes.utils.humanizeErrorType(error.error_type) + '</span>';
|
||
errorsHtml += '</div>';
|
||
errorsHtml += '<small style="color: var(--text-muted);">' + error.name + ' (TIN: ' + error.tin + ')</small>';
|
||
errorsHtml += '</div>';
|
||
|
||
// Сообщение об ошибке
|
||
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
|
||
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
|
||
errorsHtml += '</div>';
|
||
|
||
// Дополнительная информация
|
||
if (error.income || error.account_number || error.customer_name || error.operation_type) {
|
||
errorsHtml += '<div class="error-details" style="margin-top: 10px; padding: 10px; background: var(--bg-light); border-radius: 4px; font-size: 0.9em;">';
|
||
|
||
if (error.income) {
|
||
errorsHtml += '<div><strong>' + __('Amount:') + '</strong> ' + VATETaxes.utils.formatCurrency(error.income) + '</div>';
|
||
}
|
||
|
||
if (error.customer_name) {
|
||
errorsHtml += '<div><strong>' + __('Customer Name:') + '</strong> ' + error.customer_name + '</div>';
|
||
}
|
||
|
||
if (error.account_number) {
|
||
errorsHtml += '<div><strong>' + __('Missing Account:') + '</strong> ' + error.account_number + '</div>';
|
||
}
|
||
|
||
if (error.operation_type) {
|
||
let opTypeDisplay = error.operation_type;
|
||
if (error.expense_income_type) {
|
||
opTypeDisplay += ' (' + error.expense_income_type.toLowerCase() + ')';
|
||
}
|
||
errorsHtml += '<div><strong>' + __('Operation Type:') + '</strong> ' + opTypeDisplay + '</div>';
|
||
}
|
||
|
||
if (error.classification_code) {
|
||
errorsHtml += '<div><strong>' + __('Classification Code:') + '</strong> ' + error.classification_code + '</div>';
|
||
}
|
||
|
||
if (error.missing_tin) {
|
||
errorsHtml += '<div><strong>' + __('Missing TIN:') + '</strong> ' + error.missing_tin + '</div>';
|
||
}
|
||
|
||
errorsHtml += '</div>';
|
||
}
|
||
|
||
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
||
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
||
errorsHtml += '</div>';
|
||
|
||
errorsHtml += '</div>';
|
||
});
|
||
|
||
errorsHtml += '</div>';
|
||
|
||
// Статистика
|
||
const errorTypes = {};
|
||
VATETaxes.loadingErrors.forEach(function(error) {
|
||
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
|
||
});
|
||
|
||
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
|
||
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||
statsHtml += '<div>';
|
||
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + VATETaxes.loadingErrors.length + '<br>';
|
||
statsHtml += '<span style="color: var(--text-muted);">';
|
||
Object.keys(errorTypes).forEach(function(type, index) {
|
||
if (index > 0) statsHtml += ' • ';
|
||
statsHtml += VATETaxes.utils.humanizeErrorType(type) + ': ' + errorTypes[type];
|
||
});
|
||
statsHtml += '</span>';
|
||
statsHtml += '</div>';
|
||
statsHtml += '<div>';
|
||
statsHtml += '<button class="btn btn-default btn-sm" id="export-csv-dialog-btn" style="margin-right: 5px;">' +
|
||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
|
||
statsHtml += '<button class="btn btn-warning btn-sm" id="clear-log-dialog-btn">' +
|
||
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
|
||
statsHtml += '</div></div>';
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Error Details') + ' (' + VATETaxes.loadingErrors.length + ')',
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'stats_html',
|
||
fieldtype: 'HTML',
|
||
options: statsHtml
|
||
},
|
||
{
|
||
fieldname: 'errors_html',
|
||
fieldtype: 'HTML',
|
||
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
|
||
}
|
||
],
|
||
primary_action_label: __('Close'),
|
||
primary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
|
||
// Добавляем обработчики событий
|
||
setTimeout(function() {
|
||
$('#export-csv-dialog-btn').off('click').on('click', function() {
|
||
VATETaxes.errors.exportCSV();
|
||
});
|
||
|
||
$('#clear-log-dialog-btn').off('click').on('click', function() {
|
||
VATETaxes.errors.clear();
|
||
d.hide();
|
||
});
|
||
}, 200);
|
||
},
|
||
|
||
// Экспорт в CSV
|
||
exportCSV: function() {
|
||
if (VATETaxes.loadingErrors.length === 0) {
|
||
frappe.msgprint(__('No errors to export'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let csvContent = "data:text/csv;charset=utf-8,";
|
||
csvContent += "Operation ID,Date,TIN,Name,Error Type,Error Message,Timestamp\n";
|
||
|
||
VATETaxes.loadingErrors.forEach(function(error) {
|
||
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||
const cleanName = error.name.replace(/"/g, '""');
|
||
|
||
const row = [
|
||
error.operation_id,
|
||
error.operation_date,
|
||
error.tin,
|
||
cleanName,
|
||
error.error_type,
|
||
cleanMessage,
|
||
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
|
||
].map(function(field) {
|
||
return '"' + (field || '') + '"';
|
||
}).join(',');
|
||
|
||
csvContent += row + "\n";
|
||
});
|
||
|
||
const encodedUri = encodeURI(csvContent);
|
||
const link = document.createElement("a");
|
||
link.setAttribute("href", encodedUri);
|
||
link.setAttribute("download", "vat_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
frappe.show_alert({
|
||
message: __('Error log exported successfully'),
|
||
indicator: 'green'
|
||
}, 3);
|
||
} catch (e) {
|
||
console.error('Export error:', e);
|
||
frappe.show_alert({
|
||
message: __('Failed to export error log'),
|
||
indicator: 'red'
|
||
}, 3);
|
||
}
|
||
},
|
||
|
||
// Показать сводку результатов
|
||
showSummary: function(totalOperations, processedCount, errorsCount, createAsDraft = 0) {
|
||
let title = '';
|
||
let indicator = '';
|
||
let message = '';
|
||
|
||
if (errorsCount === 0) {
|
||
// Прогресс-бар к этому моменту уже надёжно закрыт через _closeProgress,
|
||
// поэтому показываем нормальное окно-итог (а не тост).
|
||
title = __('Import Completed Successfully');
|
||
indicator = 'green';
|
||
message = __('All ') + totalOperations + __(' VAT operations were imported successfully.');
|
||
if (createAsDraft) {
|
||
message += ' ' + __('(created as Draft)');
|
||
}
|
||
|
||
frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
} else {
|
||
title = errorsCount === totalOperations ? __('Import Failed') : __('Import Completed with Errors');
|
||
indicator = processedCount > 0 ? 'orange' : 'red';
|
||
|
||
message = '<div style="margin-bottom: 15px;">';
|
||
if (processedCount > 0) {
|
||
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' operations') + '<br>';
|
||
}
|
||
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' operations');
|
||
message += '</div>';
|
||
|
||
message += '<div style="text-align: center;">' +
|
||
'<button class="btn btn-primary btn-sm" id="view-error-details-btn" style="margin-right: 10px;">' +
|
||
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
|
||
'<button class="btn btn-default btn-sm" id="export-error-log-btn">' +
|
||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
||
'</div>';
|
||
|
||
// Отдельный frappe.ui.Dialog (НЕ общий frappe.msg_dialog), чтобы остаточный
|
||
// прогресс-бар из show_progress не протекал в это окно.
|
||
const summaryDialog = new frappe.ui.Dialog({
|
||
title: title,
|
||
indicator: indicator,
|
||
fields: [{ fieldtype: 'HTML', fieldname: 'summary_html', options: message }]
|
||
});
|
||
summaryDialog.show();
|
||
|
||
// Добавляем обработчики событий
|
||
setTimeout(function() {
|
||
$('#view-error-details-btn').off('click').on('click', function() {
|
||
summaryDialog.hide();
|
||
VATETaxes.errors.showDetails();
|
||
});
|
||
|
||
$('#export-error-log-btn').off('click').on('click', function() {
|
||
VATETaxes.errors.exportCSV();
|
||
});
|
||
}, 200);
|
||
}
|
||
}
|
||
};
|
||
|
||
// ======= МОДУЛЬ ИМПОРТА VAT ОПЕРАЦИЙ =======
|
||
VATETaxes.import = {
|
||
// Показать диалог импорта из E-Taxes
|
||
showDialog: function() {
|
||
VATETaxes.auth.checkAndProcess(function() {
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Import VAT Operations from E-Taxes'),
|
||
fields: [
|
||
{
|
||
fieldname: 'date_range_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Period')
|
||
},
|
||
{
|
||
fieldname: 'creationDateFrom',
|
||
fieldtype: 'Date',
|
||
label: __('Date from'),
|
||
default: moment().subtract(1, 'month').format('YYYY-MM-DD')
|
||
},
|
||
{
|
||
fieldname: 'creationDateTo',
|
||
fieldtype: 'Date',
|
||
label: __('Date to'),
|
||
default: moment().format('YYYY-MM-DD')
|
||
},
|
||
{
|
||
fieldname: 'settings_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Settings')
|
||
},
|
||
{
|
||
fieldname: 'company',
|
||
fieldtype: 'Link',
|
||
label: __('Company'),
|
||
options: 'Company',
|
||
reqd: true,
|
||
default: frappe.defaults.get_user_default('Company')
|
||
}
|
||
],
|
||
primary_action_label: __('Search'),
|
||
primary_action: function() {
|
||
const values = d.get_values();
|
||
|
||
// Validate date range
|
||
const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
|
||
const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
|
||
const todayMoment = moment();
|
||
|
||
if (toDateMoment.isAfter(todayMoment)) {
|
||
frappe.msgprint(__('To Date cannot be later than today'));
|
||
return;
|
||
}
|
||
|
||
if (fromDateMoment.isAfter(toDateMoment)) {
|
||
frappe.msgprint(__('From Date cannot be later than To Date'));
|
||
return;
|
||
}
|
||
|
||
if (!values.company) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('Please select a company')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const fromDate = values.creationDateFrom;
|
||
const toDate = values.creationDateTo;
|
||
const createAsDraft = values.create_as_draft || 0;
|
||
|
||
d.hide();
|
||
VATETaxes.import.loadOperations(fromDate, toDate, values.company, [], 0, createAsDraft, 0);
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
});
|
||
},
|
||
|
||
// Загрузить операции с фильтрацией дубликатов
|
||
loadOperations: function(fromDate, toDate, company, accumulatedOperations = [], offset = 0, createAsDraft = 0, autoCreateCustomers = 0) {
|
||
VATETaxes.cancelLoading = false;
|
||
$(document).off('progress-cancel.vat_import');
|
||
|
||
if (offset === 0) {
|
||
VATETaxes.dialogs.showLoading(
|
||
__('Loading E-Taxes Documents'),
|
||
__('Fetching VAT operations from E-Taxes...'),
|
||
__('Please wait')
|
||
);
|
||
}
|
||
|
||
VATETaxes.utils.getDefaultLogin(function(loginResponse) {
|
||
if (VATETaxes.cancelLoading) return;
|
||
|
||
if (loginResponse && loginResponse.found) {
|
||
const mainToken = loginResponse.main_token;
|
||
|
||
if (!mainToken) {
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Authentication Error'),
|
||
indicator: 'red',
|
||
message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const maxCount = 50;
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.vat_api.get_vat_operations',
|
||
args: {
|
||
'token': mainToken,
|
||
'date_from': fromDate,
|
||
'date_to': toDate,
|
||
'max_count': maxCount,
|
||
'offset': offset
|
||
},
|
||
callback: function(r) {
|
||
if (VATETaxes.cancelLoading) return;
|
||
|
||
if (r.message && !r.message.error) {
|
||
const currentOperations = r.message.operations || [];
|
||
const hasMore = r.message.hasMore || false;
|
||
const allOperations = accumulatedOperations.concat(currentOperations);
|
||
|
||
if (hasMore && currentOperations.length > 0) {
|
||
const newOffset = offset + currentOperations.length;
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Fetching VAT operations from E-Taxes...'),
|
||
__('Loaded ') + allOperations.length + __(' so far...')
|
||
);
|
||
VATETaxes.import.loadOperations(fromDate, toDate, company, allOperations, newOffset, createAsDraft, autoCreateCustomers);
|
||
} else {
|
||
if (allOperations.length > 0) {
|
||
VATETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Processing ') + allOperations.length + __(' VAT operations...'),
|
||
__('Checking for already imported documents')
|
||
);
|
||
|
||
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft, autoCreateCustomers);
|
||
} else {
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Information'),
|
||
indicator: 'blue',
|
||
message: __('No VAT operations found for the specified period')
|
||
});
|
||
}
|
||
}
|
||
} else if (r.message && r.message.error === 'unauthorized') {
|
||
VATETaxes.dialogs.hide();
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||
function() {
|
||
VATETaxes.auth.startProcess(function() {
|
||
VATETaxes.import.loadOperations(fromDate, toDate, company, [], 0, createAsDraft, autoCreateCustomers);
|
||
});
|
||
},
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
} else {
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: r.message ? r.message.message : __('Error loading VAT operations')
|
||
});
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error("Error fetching VAT operations:", error);
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Network Error'),
|
||
indicator: 'red',
|
||
message: __('Error fetching data from server: ') + (error || 'Unknown error')
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('E-Taxes authentication settings not found')
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// Фильтрация дубликатов
|
||
filterDuplicates: function(allOperations, token, company, createAsDraft = 0, autoCreateCustomers = 0) {
|
||
frappe.call({
|
||
method: 'frappe.client.get_list',
|
||
args: {
|
||
doctype: 'E-Taxes VAT Operations',
|
||
fields: ['operation_id'],
|
||
limit_page_length: 0
|
||
},
|
||
callback: function(r) {
|
||
try {
|
||
if (VATETaxes.cancelLoading) return;
|
||
|
||
const importedOperations = {};
|
||
|
||
if (r.message) {
|
||
r.message.forEach(function(op) {
|
||
if (op.operation_id) {
|
||
const normId = String(op.operation_id).trim();
|
||
importedOperations[normId] = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
const filteredOperations = [];
|
||
|
||
for (let i = 0; i < allOperations.length; i++) {
|
||
const operation = allOperations[i];
|
||
const operationId = String(operation.id).trim();
|
||
|
||
if (!importedOperations.hasOwnProperty(operationId)) {
|
||
filteredOperations.push(operation);
|
||
}
|
||
}
|
||
|
||
if (filteredOperations.length > 0) {
|
||
VATETaxes.import.showOperationSelection(filteredOperations, token, company, createAsDraft, autoCreateCustomers);
|
||
} else {
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Information'),
|
||
indicator: 'blue',
|
||
message: __('No new VAT operations found for the specified period (all ' +
|
||
allOperations.length + ' are already imported)')
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error("Error processing VAT operations data:", e);
|
||
VATETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('An error occurred while processing data: ') + e.message
|
||
});
|
||
}
|
||
},
|
||
error: function(err) {
|
||
console.error("Error fetching VAT operations:", err);
|
||
VATETaxes.import.showOperationSelection(allOperations, token, company, createAsDraft, autoCreateCustomers);
|
||
frappe.show_alert({
|
||
message: __('Failed to check for duplicates. Showing all operations.'),
|
||
indicator: 'orange'
|
||
}, 5);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Показать диалог выбора операций
|
||
showOperationSelection: function(operations, token, company, createAsDraft = 0) {
|
||
VATETaxes.dialogs.hide();
|
||
// Таблица операций — единственная прокручиваемая область. Высота привязана к
|
||
// вьюпорту (calc), чтобы модал целиком помещался в экран без внешнего скролла.
|
||
// Шапка таблицы «липкая» (sticky), чтобы заголовки колонок оставались видны
|
||
// при прокрутке списка.
|
||
let operationTable = '<div class="vat-operations-scroll" style="max-height: calc(100vh - 360px); min-height: 220px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 6px;"><table class="table table-bordered vat-operations-table" style="width: 100%; table-layout: fixed; margin-bottom: 0;">';
|
||
operationTable += '<thead style="position: sticky; top: 0; z-index: 1;"><tr style="background: var(--control-bg, var(--bg-color));">' +
|
||
'<th style="width: 3%; background: var(--control-bg, var(--bg-color));"><input type="checkbox" class="select-all-operations"></th>' +
|
||
'<th style="width: 8%; background: var(--control-bg, var(--bg-color));">' + __('Date') + '</th>' +
|
||
'<th style="width: 9%; background: var(--control-bg, var(--bg-color));">' + __('TIN') + '</th>' +
|
||
'<th style="width: 15%; background: var(--control-bg, var(--bg-color));">' + __('Name') + '</th>' +
|
||
'<th style="width: 15%; background: var(--control-bg, var(--bg-color));">' + __('Operation Type') + '</th>' +
|
||
'<th style="width: 9%; background: var(--control-bg, var(--bg-color));">' + __('Classification Code') + '</th>' +
|
||
'<th style="width: 17%; background: var(--control-bg, var(--bg-color));">' + __('Explanation') + '</th>' +
|
||
'<th style="width: 12%; background: var(--control-bg, var(--bg-color));">' + __('Income') + '</th>' +
|
||
'<th style="width: 12%; background: var(--control-bg, var(--bg-color));">' + __('Expense') + '</th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
operations.forEach(function(operation) {
|
||
const operationDate = VATETaxes.utils.parseOperationDate(operation.operationDate);
|
||
const tin = operation.tin || '';
|
||
const name = operation.name || '';
|
||
// Разделяем income и expense
|
||
const income = operation.income || 0;
|
||
const expense = operation.expense || 0;
|
||
const operationType = VATETaxes.utils.getOperationTypeLabel(operation.operationType);
|
||
|
||
// Извлекаем classification code из taxCodeInfo
|
||
let classificationCode = '-';
|
||
if (operation.taxCodeInfo && operation.taxCodeInfo.code) {
|
||
classificationCode = operation.taxCodeInfo.code;
|
||
}
|
||
|
||
// İzahat (свободный текст) — экранируем, чтобы спецсимволы не сломали таблицу
|
||
const explanation = frappe.utils.escape_html(operation.explanation || '');
|
||
|
||
operationTable += '<tr>' +
|
||
'<td><input type="checkbox" class="select-operation" data-id="' + operation.id + '"></td>' +
|
||
'<td>' + operationDate + '</td>' +
|
||
'<td style="word-break: break-word;">' + tin + '</td>' +
|
||
'<td style="word-break: break-word;">' + name + '</td>' +
|
||
'<td style="word-break: break-word;">' + operationType + '</td>' +
|
||
'<td style="text-align: center;">' + classificationCode + '</td>' +
|
||
'<td style="word-break: break-word;">' + explanation + '</td>' +
|
||
'<td style="text-align: right;">' + VATETaxes.utils.formatCurrency(income) + '</td>' +
|
||
'<td style="text-align: right;">' + VATETaxes.utils.formatCurrency(expense) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
operationTable += '</tbody></table></div>';
|
||
|
||
// Компактная шапка: количество операций + компания в одной строке.
|
||
// Заменяет два отдельных alert'а (верхний со счётчиком и нижний с компанией,
|
||
// который раньше уезжал под таблицу и требовал скролла).
|
||
const headerHtml =
|
||
'<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:8px;">' +
|
||
'<span class="indicator-pill blue" style="font-size:13px;">' +
|
||
__('New VAT operations found: ') + '<strong>' + operations.length + '</strong>' +
|
||
'</span>' +
|
||
'<span style="color: var(--text-muted);">' +
|
||
__('Company: ') + '<strong>' + frappe.utils.escape_html(company) + '</strong>' +
|
||
'</span>' +
|
||
'</div>';
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Select VAT Operations'),
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'header_html',
|
||
fieldtype: 'HTML',
|
||
options: headerHtml
|
||
},
|
||
{
|
||
fieldname: 'create_as_draft',
|
||
fieldtype: 'Check',
|
||
label: __('Create as Draft'),
|
||
default: createAsDraft,
|
||
description: __('If checked, Journal Entries will be created in Draft status instead of being submitted automatically')
|
||
},
|
||
{
|
||
fieldname: 'settings_col_break',
|
||
fieldtype: 'Column Break'
|
||
},
|
||
{
|
||
fieldname: 'auto_create_customers',
|
||
fieldtype: 'Check',
|
||
label: __('Auto-create missing customers'),
|
||
default: 0,
|
||
description: __('If checked, will automatically create Customer records and mappings for TINs not found in the system')
|
||
},
|
||
{
|
||
fieldname: 'operations_section',
|
||
fieldtype: 'Section Break'
|
||
},
|
||
{
|
||
fieldname: 'operations_html',
|
||
fieldtype: 'HTML',
|
||
options: operationTable
|
||
}
|
||
],
|
||
primary_action_label: __('Load Selected'),
|
||
primary_action: function() {
|
||
const selectedOperationIds = [];
|
||
d.$wrapper.find('.select-operation:checked').each(function() {
|
||
selectedOperationIds.push($(this).data('id'));
|
||
});
|
||
|
||
if (selectedOperationIds.length === 0) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('No operations selected')
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Получить значения настроек из диалога
|
||
const values = d.get_values();
|
||
const createAsDraftValue = values.create_as_draft || 0;
|
||
const autoCreateCustomers = values.auto_create_customers || 0;
|
||
|
||
// Найти полные данные операций
|
||
const selectedOperations = [];
|
||
selectedOperationIds.forEach(function(id) {
|
||
const operation = operations.find(op => op.id === id);
|
||
if (operation) {
|
||
selectedOperations.push(operation);
|
||
}
|
||
});
|
||
|
||
d.hide();
|
||
VATETaxes.cancelLoading = false;
|
||
VATETaxes.import.loadSelectedOperationsSocketIO(selectedOperations, token, company, createAsDraftValue, autoCreateCustomers);
|
||
},
|
||
secondary_action_label: __('Cancel'),
|
||
secondary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
d.$wrapper.find('.modal-dialog').css({
|
||
'max-width': '80%',
|
||
'width': '80%',
|
||
'margin': '30px auto'
|
||
});
|
||
|
||
d.$wrapper.find('.modal-body').css({
|
||
'padding': '15px'
|
||
});
|
||
|
||
d.show();
|
||
|
||
// Обработчик для чекбокса "выбрать все"
|
||
d.$wrapper.find('.select-all-operations').on('change', function() {
|
||
const isChecked = $(this).prop('checked');
|
||
d.$wrapper.find('.select-operation').prop('checked', isChecked);
|
||
});
|
||
},
|
||
|
||
// Загрузка выбранных операций
|
||
loadSelectedOperations: function(operations, token, company, processedCount = 0, currentIndex = 0, createAsDraft = 0, autoCreateCustomers = 0) {
|
||
if (processedCount === 0 && currentIndex === 0) {
|
||
VATETaxes.loadingErrors = [];
|
||
window.totalOperationsToProcess = operations.length;
|
||
}
|
||
|
||
$(document).off('progress-cancel.loading_operations');
|
||
|
||
if (operations.length === 0) {
|
||
const errorsCount = VATETaxes.loadingErrors.length;
|
||
const totalOperations = window.totalOperationsToProcess || processedCount;
|
||
|
||
// Искусственная задержка перед показом summary, чтобы прогресс-бар успел закрыться
|
||
setTimeout(function() {
|
||
// Закрываем прогресс-бар прямо перед показом summary
|
||
frappe.hide_progress();
|
||
|
||
// Дополнительная микрозадержка для гарантии
|
||
setTimeout(function() {
|
||
VATETaxes.errors.showSummary(totalOperations, processedCount, errorsCount, createAsDraft);
|
||
window.totalOperationsToProcess = null;
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
}, 100);
|
||
}, 500);
|
||
|
||
return;
|
||
}
|
||
|
||
if (VATETaxes.cancelLoading) {
|
||
operations = [];
|
||
frappe.hide_progress();
|
||
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers);
|
||
return;
|
||
}
|
||
|
||
const operation = operations.shift();
|
||
const isLastOperation = operations.length === 0;
|
||
const totalToProcess = window.totalOperationsToProcess || (processedCount + operations.length + 1);
|
||
|
||
currentIndex++;
|
||
|
||
frappe.show_progress(__('Importing VAT Operations'), currentIndex, totalToProcess,
|
||
__('Processing operation ') + currentIndex + __(' of ') + totalToProcess, null, true);
|
||
|
||
$(document).on('progress-cancel.loading_operations', function() {
|
||
VATETaxes.cancelLoading = true;
|
||
frappe.show_alert({
|
||
message: __('Cancelling import... Finishing current operation.'),
|
||
indicator: 'orange'
|
||
}, 3);
|
||
frappe.hide_progress();
|
||
});
|
||
|
||
// Импорт одной операции
|
||
frappe.call({
|
||
method: 'invoice_az.vat_api.import_vat_operations',
|
||
args: {
|
||
'operations_data': JSON.stringify([operation]),
|
||
'company': company,
|
||
'create_as_draft': createAsDraft,
|
||
'auto_create_customers': autoCreateCustomers
|
||
},
|
||
callback: function(r) {
|
||
VATETaxes.import._handleImportResult(r, operation,
|
||
operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
|
||
},
|
||
error: function(xhr, status, error) {
|
||
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
|
||
VATETaxes.errors.add(operation, 'Network Error', errorMessage, {
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
});
|
||
|
||
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Обработка результата импорта
|
||
_handleImportResult: function(importR, operation, operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft = 0, autoCreateCustomers = 0) {
|
||
if (importR.message && importR.message.success) {
|
||
if (importR.message.imported > 0) {
|
||
// НЕ показываем индивидуальные алерты во время массового импорта
|
||
// Только счетчик в прогресс-баре
|
||
processedCount++;
|
||
} else if (importR.message.failed > 0 && importR.message.errors && importR.message.errors.length > 0) {
|
||
const error = importR.message.errors[0];
|
||
// Передаем ВСЕ данные ошибки включая tin, customer_name, income, account_number
|
||
VATETaxes.errors.add(operation, error.error_type || 'Import Error', error.message, error);
|
||
}
|
||
|
||
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
|
||
} else {
|
||
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
|
||
VATETaxes.errors.add(operation, 'Import Error', errorMessage, {
|
||
server_response: importR.message
|
||
});
|
||
|
||
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
|
||
}
|
||
},
|
||
|
||
// Продолжить или завершить обработку
|
||
_continueOrFinish: function(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft = 0, autoCreateCustomers = 0) {
|
||
if (isLastOperation) {
|
||
$(document).off('progress-cancel.loading_operations');
|
||
|
||
const totalToProcess = window.totalOperationsToProcess || processedCount;
|
||
const errorsCount = VATETaxes.loadingErrors.length;
|
||
|
||
// Искусственная задержка перед показом summary, чтобы прогресс-бар успел закрыться
|
||
setTimeout(function() {
|
||
// Закрываем прогресс-бар прямо перед показом summary
|
||
frappe.hide_progress();
|
||
|
||
// Дополнительная микрозадержка для гарантии
|
||
setTimeout(function() {
|
||
VATETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount, createAsDraft);
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
}, 100);
|
||
}, 500);
|
||
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (operations.length === 0) {
|
||
frappe.hide_progress();
|
||
}
|
||
|
||
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers);
|
||
}, VATETaxes.PROGRESS_UPDATE_DELAY);
|
||
},
|
||
|
||
// Надёжное закрытие прогресс-бара. frappe.hide_progress()/dialog.hide() в этом
|
||
// окружении (jey_theme/Bootstrap) НЕ всегда убирает модал — он зависает. Поэтому
|
||
// принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
|
||
_closeProgress: function() {
|
||
if (frappe.cur_progress) {
|
||
try {
|
||
frappe.cur_progress.$wrapper.modal('hide');
|
||
frappe.cur_progress.$wrapper.remove();
|
||
} catch (e) {}
|
||
frappe.cur_progress = null;
|
||
}
|
||
// backdrop/scroll-lock убираем только если не осталось видимых модалов
|
||
// (иначе сломаем окно summary, которое показывается следом)
|
||
if ($('.modal:visible').length === 0) {
|
||
$('.modal-backdrop').remove();
|
||
$('body').removeClass('modal-open');
|
||
}
|
||
},
|
||
|
||
// Socket.IO bulk import
|
||
loadSelectedOperationsSocketIO: function(operations, token, company, createAsDraft, autoCreateCustomers) {
|
||
VATETaxes.loadingErrors = [];
|
||
const total = operations.length;
|
||
|
||
// Защита от гонки realtime: 'vat_import_complete' вызывает hide_progress()
|
||
// (он обнуляет frappe.cur_progress). Если 'vat_import_progress' приходит ПОСЛЕ
|
||
// complete, frappe.show_progress создаёт НОВЫЙ диалог, который уже никто не
|
||
// закрывает — он зависает поверх окна результата. Флаг делает обработчик
|
||
// прогресса no-op после завершения.
|
||
let importFinished = false;
|
||
|
||
frappe.show_progress(
|
||
__('Importing VAT Operations'), 0, total,
|
||
__('Starting import of {0} operations...', [total])
|
||
);
|
||
|
||
frappe.realtime.off('vat_import_progress');
|
||
frappe.realtime.on('vat_import_progress', function(data) {
|
||
if (importFinished) return;
|
||
frappe.show_progress(
|
||
__('Importing VAT Operations'), data.current, data.total,
|
||
__('Processing operation {0} of {1}', [data.current, data.total])
|
||
);
|
||
});
|
||
|
||
frappe.realtime.off('vat_import_complete');
|
||
frappe.realtime.on('vat_import_complete', function(data) {
|
||
importFinished = true;
|
||
frappe.realtime.off('vat_import_progress');
|
||
frappe.realtime.off('vat_import_complete');
|
||
VATETaxes.import._closeProgress();
|
||
|
||
(data.errors || []).forEach(function(err) {
|
||
VATETaxes.loadingErrors.push({
|
||
operation_id: err.operation_id || 'Unknown',
|
||
error_type: err.error_type || 'Import Error',
|
||
error_message: err.error || 'Unknown error',
|
||
tin: err.tin,
|
||
customer_name: err.customer_name
|
||
});
|
||
});
|
||
|
||
const errorsCount = VATETaxes.loadingErrors.length;
|
||
VATETaxes.errors.showSummary(data.total, data.imported, errorsCount, data.create_as_draft);
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
});
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.vat_api.import_bulk_vat_operations',
|
||
args: {
|
||
operations_data: JSON.stringify(operations),
|
||
company: company,
|
||
create_as_draft: createAsDraft,
|
||
auto_create_customers: autoCreateCustomers
|
||
},
|
||
callback: function(r) {
|
||
if (!r.message || !r.message.enqueued) {
|
||
importFinished = true;
|
||
frappe.realtime.off('vat_import_progress');
|
||
frappe.realtime.off('vat_import_complete');
|
||
VATETaxes.import._closeProgress();
|
||
frappe.show_alert({
|
||
message: __('Failed to start import job'),
|
||
indicator: 'red'
|
||
}, 7);
|
||
}
|
||
},
|
||
error: function() {
|
||
importFinished = true;
|
||
frappe.realtime.off('vat_import_progress');
|
||
frappe.realtime.off('vat_import_complete');
|
||
VATETaxes.import._closeProgress();
|
||
frappe.show_alert({
|
||
message: __('Network error starting import'),
|
||
indicator: 'red'
|
||
}, 7);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
// ======= ОСНОВНЫЕ ОБРАБОТЧИКИ СПИСКОВ =======
|
||
|
||
// Journal Entry List
|
||
frappe.listview_settings['Journal Entry'] = {
|
||
add_fields: ['posting_date', 'voucher_type', 'total_debit', 'total_credit', 'docstatus', 'expense_income_type', 'etaxes_document_type', 'loaded_from_etaxes', 'etaxes_explanation'],
|
||
|
||
hide_name_column: false,
|
||
|
||
get_indicator: function(doc) {
|
||
if (doc.docstatus == 1) {
|
||
return [__('Submitted'), 'blue', 'docstatus,=,1'];
|
||
} else if (doc.docstatus == 0) {
|
||
return [__('Draft'), 'red', 'docstatus,=,0'];
|
||
} else if (doc.docstatus == 2) {
|
||
return [__('Cancelled'), 'grey', 'docstatus,=,2'];
|
||
}
|
||
},
|
||
|
||
formatters: {
|
||
expense_income_type: function(value) {
|
||
if (value === 'Income') {
|
||
return '<span class="filterable indicator-pill green" data-filter="expense_income_type,=,Income">' + value + '</span>';
|
||
} else if (value === 'Expense') {
|
||
return '<span class="filterable indicator-pill red" data-filter="expense_income_type,=,Expense">' + value + '</span>';
|
||
}
|
||
return value || '';
|
||
},
|
||
|
||
etaxes_document_type: function(value, field, doc) {
|
||
// Show blue badge "ƏDV" only for documents loaded from e-taxes
|
||
if (value === 'ədv' && (doc.loaded_from_etaxes === 1 || doc.loaded_from_etaxes === '1')) {
|
||
return '<span class="filterable indicator-pill blue" data-filter="etaxes_document_type,=,ədv" title="' + __('VAT Operation from E-Taxes') + '">ƏDV</span>';
|
||
}
|
||
return '';
|
||
}
|
||
},
|
||
|
||
onload: function(listview) {
|
||
listview.page.add_inner_button(__('Import VAT Operations from E-Taxes'), function() {
|
||
VATETaxes.import.showDialog();
|
||
}, __('E-Taxes'));
|
||
|
||
listview.page.add_inner_button(__('Open Settings'), function() {
|
||
frappe.set_route('Form', 'E-Taxes Settings');
|
||
}, __('E-Taxes'));
|
||
|
||
listview.refresh();
|
||
}
|
||
};
|