2221 lines
87 KiB
JavaScript
2221 lines
87 KiB
JavaScript
// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
|
||
const ETaxes = {
|
||
// Константы
|
||
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 минут
|
||
}
|
||
};
|
||
|
||
// ======= БАЗОВЫЕ УТИЛИТЫ =======
|
||
ETaxes.utils = {
|
||
// Кэшированное получение настроек входа
|
||
getDefaultLogin: function(callback, useCache = true) {
|
||
const now = Date.now();
|
||
|
||
if (useCache && ETaxes.cache.defaultLogin && ETaxes.cache.cacheTime &&
|
||
(now - ETaxes.cache.cacheTime) < ETaxes.cache.cacheDuration) {
|
||
callback(ETaxes.cache.defaultLogin);
|
||
return;
|
||
}
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.auth.get_default_asan_login',
|
||
callback: function(r) {
|
||
if (r.message) {
|
||
ETaxes.cache.defaultLogin = r.message;
|
||
ETaxes.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);
|
||
},
|
||
|
||
// Очистка кэша
|
||
clearCache: function() {
|
||
ETaxes.cache.defaultLogin = null;
|
||
ETaxes.cache.cacheTime = null;
|
||
}
|
||
};
|
||
|
||
// ======= ДИАЛОГИ ЗАГРУЗКИ =======
|
||
ETaxes.dialogs = {
|
||
// Показать диалог загрузки
|
||
showLoading: function(title, message, submessage) {
|
||
if (ETaxes.loadingDialog) {
|
||
this.updateLoading(title, message, submessage);
|
||
return;
|
||
}
|
||
|
||
ETaxes.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'и пагинации увидят отмену и выйдут.
|
||
ETaxes.cancelLoading = true;
|
||
ETaxes.dialogs.hide();
|
||
}
|
||
});
|
||
|
||
ETaxes.cancelLoading = false;
|
||
ETaxes.loadingDialog.show();
|
||
ETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
|
||
},
|
||
|
||
// Обновить сообщение в диалоге
|
||
updateLoading: function(title, message, submessage) {
|
||
if (!ETaxes.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 (ETaxes.loadingDialog && code) {
|
||
$('#verification_code').text(code);
|
||
$('#verification_code_container').show();
|
||
}
|
||
},
|
||
|
||
// Установить статус успеха
|
||
setSuccess: function(message, submessage, callback, delay = 2000) {
|
||
if (!ETaxes.loadingDialog) {
|
||
if (callback) callback();
|
||
return;
|
||
}
|
||
|
||
ETaxes.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();
|
||
ETaxes.loadingDialog.set_primary_action(null);
|
||
ETaxes.loadingDialog.set_secondary_action(null);
|
||
|
||
setTimeout(() => {
|
||
this.hide();
|
||
if (callback) callback();
|
||
}, delay);
|
||
},
|
||
|
||
// Установить статус ошибки
|
||
setError: function(message, submessage, showCloseButton = true) {
|
||
if (!ETaxes.loadingDialog) return;
|
||
|
||
// Update dialog title
|
||
ETaxes.loadingDialog.set_title(__('Error'));
|
||
|
||
// Change animation to error icon
|
||
ETaxes.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>
|
||
`);
|
||
|
||
// Clear all existing text elements
|
||
ETaxes.loadingDialog.$wrapper.find('#loading_title').text('');
|
||
ETaxes.loadingDialog.$wrapper.find('#loading_submessage').remove();
|
||
|
||
// Set error message
|
||
const $msgElement = ETaxes.loadingDialog.$wrapper.find('#loading_message');
|
||
if ($msgElement.length) {
|
||
$msgElement.css({
|
||
'color': '#d9534f',
|
||
'font-weight': '500',
|
||
'font-size': '14px',
|
||
'margin-top': '15px'
|
||
});
|
||
|
||
if (submessage) {
|
||
$msgElement.html(submessage.replace(/\n/g, '<br>'));
|
||
} else {
|
||
$msgElement.text(message);
|
||
}
|
||
}
|
||
|
||
$('#verification_code_container').hide();
|
||
|
||
if (showCloseButton) {
|
||
ETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
|
||
}
|
||
},
|
||
|
||
// Скрыть диалог
|
||
hide: function() {
|
||
// Мягкий loadingDialog.hide() в jey_theme/Bootstrap НЕ всегда убирает модал
|
||
// (он зависает и следующий диалог накладывается сверху). Принудительно:
|
||
// modal('hide') + remove() + чистка осиротевшего backdrop.
|
||
// cancelLoading здесь НЕ сбрасываем — иначе отмена потеряется для in-flight
|
||
// callback'ов; флаг сбрасывается в начале операции / при создании диалога.
|
||
if (ETaxes.loadingDialog) {
|
||
try {
|
||
ETaxes.loadingDialog.$wrapper.modal('hide');
|
||
ETaxes.loadingDialog.$wrapper.remove();
|
||
} catch (e) {
|
||
console.error("Error hiding loading dialog:", e);
|
||
}
|
||
ETaxes.loadingDialog = null;
|
||
}
|
||
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>
|
||
`;
|
||
}
|
||
};
|
||
|
||
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
|
||
ETaxes.auth = {
|
||
// Проверить токен и обработать аутентификацию при необходимости
|
||
checkAndProcess: function(callback) {
|
||
ETaxes.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() {
|
||
ETaxes.auth.startProcess(callback);
|
||
},
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Начать процесс аутентификации
|
||
startProcess: function(callback) {
|
||
ETaxes.dialogs.showLoading(
|
||
__('Starting Authentication'),
|
||
__('Getting Asan Login settings...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
ETaxes.utils.getDefaultLogin(function(loginData) {
|
||
if (loginData && loginData.found) {
|
||
const asanLoginName = loginData.name;
|
||
ETaxes.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) {
|
||
ETaxes.dialogs.showVerificationCode(r.message.verification_code);
|
||
}
|
||
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request')
|
||
);
|
||
|
||
ETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
|
||
if (success) {
|
||
ETaxes.auth.complete(asanLoginName, callback);
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Failed'),
|
||
__('Could not authenticate with Asan Imza')
|
||
);
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Error'),
|
||
r.message ? r.message.message : __('Authentication request failed')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.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 >= ETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
|
||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS) {
|
||
ETaxes.dialogs.setError(
|
||
__('Authentication Timeout'),
|
||
__('The authentication request has timed out. Please try again.')
|
||
);
|
||
callback(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Waiting for confirmation on your phone'),
|
||
__('Please check your phone and confirm the authentication request') +
|
||
' (' + attempts + '/' + ETaxes.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;
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Authentication Successful'),
|
||
__('You are now authenticated with Asan Imza'),
|
||
function() {
|
||
callback(true);
|
||
}
|
||
);
|
||
} else {
|
||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||
}
|
||
} else {
|
||
stopPolling = true;
|
||
ETaxes.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, ETaxes.AUTH_POLL_INTERVAL);
|
||
}
|
||
});
|
||
}
|
||
|
||
pollStatusStep();
|
||
},
|
||
|
||
// Завершение аутентификации
|
||
complete: function(asanLoginName, callback) {
|
||
ETaxes.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) {
|
||
ETaxes.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;
|
||
|
||
ETaxes.dialogs.updateLoading(
|
||
null,
|
||
__('Selecting certificate'),
|
||
certName
|
||
);
|
||
|
||
ETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
|
||
} catch (e) {
|
||
console.error('Error parsing certificate:', e);
|
||
ETaxes.dialogs.hide();
|
||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||
}
|
||
} else {
|
||
ETaxes.dialogs.hide();
|
||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.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) {
|
||
ETaxes.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) {
|
||
ETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Authentication Complete'),
|
||
__('You can now access E-Taxes services'),
|
||
callback
|
||
);
|
||
} else {
|
||
ETaxes.dialogs.setError(
|
||
__('Error'),
|
||
r.message ? r.message.message : __('Failed to select taxpayer')
|
||
);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.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();
|
||
|
||
ETaxes.dialogs.showLoading(
|
||
__('Selecting Certificate'),
|
||
__('Processing your selection...'),
|
||
certName
|
||
);
|
||
|
||
ETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
|
||
});
|
||
}
|
||
};
|
||
|
||
// ======= УПРАВЛЕНИЕ ОШИБКАМИ =======
|
||
ETaxes.errors = {
|
||
// Добавить ошибку в лог
|
||
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
|
||
const errorEntry = {
|
||
invoice_id: invoiceData.id || 'Unknown',
|
||
serial_number: invoiceData.serialNumber || 'Unknown',
|
||
sender_name: invoiceData.sender ? invoiceData.sender.name : 'Unknown',
|
||
creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown',
|
||
error_type: errorType,
|
||
error_message: errorMessage,
|
||
timestamp: new Date().toISOString(),
|
||
...additionalData
|
||
};
|
||
|
||
ETaxes.loadingErrors.push(errorEntry);
|
||
console.error('E-Taxes Import Error:', errorEntry);
|
||
},
|
||
|
||
// Очистить лог ошибок
|
||
clear: function() {
|
||
frappe.confirm(
|
||
__('Are you sure you want to clear the error log?'),
|
||
function() {
|
||
ETaxes.loadingErrors = [];
|
||
frappe.show_alert({
|
||
message: __('Error log cleared'),
|
||
indicator: 'green'
|
||
}, 2);
|
||
}
|
||
);
|
||
},
|
||
|
||
// Показать детальный лог ошибок
|
||
showDetails: function() {
|
||
if (ETaxes.loadingErrors.length === 0) {
|
||
frappe.msgprint(__('No errors to display'));
|
||
return;
|
||
}
|
||
|
||
let errorsHtml = '<div class="error-log-container">';
|
||
|
||
ETaxes.loadingErrors.forEach(function(error, index) {
|
||
let documentDate = 'No date';
|
||
if (error.creation_date && error.creation_date !== 'Unknown') {
|
||
try {
|
||
documentDate = moment(error.creation_date).format('DD.MM.YYYY');
|
||
} 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 += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||
'<strong>' + (error.serial_number || error.invoice_id) + '</strong></h5>';
|
||
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
||
errorsHtml += '</div>';
|
||
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</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.unmatched_items && error.unmatched_items.length > 0) {
|
||
errorsHtml += '<div class="error-details">';
|
||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped items:') + '</div>';
|
||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_items.forEach(function(item, idx) {
|
||
errorsHtml += '<li><strong>Row ' + (idx + 1) + ':</strong> ' + item.name;
|
||
if (item.code) {
|
||
errorsHtml += ' <em>(Code: ' + item.code + ')</em>';
|
||
}
|
||
errorsHtml += '</li>';
|
||
});
|
||
errorsHtml += '</ul></div>';
|
||
} else if (error.unmatched_parties && error.unmatched_parties.length > 0) {
|
||
errorsHtml += '<div class="error-details">';
|
||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped parties:') + '</div>';
|
||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_parties.forEach(function(party, idx) {
|
||
errorsHtml += '<li><strong>Party ' + (idx + 1) + ':</strong> ' + party.name;
|
||
if (party.tin) {
|
||
errorsHtml += ' <em>(TIN: ' + party.tin + ')</em>';
|
||
}
|
||
if (party.type) {
|
||
errorsHtml += ' <span class="label label-info" style="font-size: 10px;">' + party.type + '</span>';
|
||
}
|
||
errorsHtml += '</li>';
|
||
});
|
||
errorsHtml += '</ul></div>';
|
||
} else if (error.unmatched_units && error.unmatched_units.length > 0) {
|
||
errorsHtml += '<div class="error-details">';
|
||
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped units:') + '</div>';
|
||
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
|
||
error.unmatched_units.forEach(function(unit, idx) {
|
||
errorsHtml += '<li><strong>Unit ' + (idx + 1) + ':</strong> ' + unit.name + '</li>';
|
||
});
|
||
errorsHtml += '</ul></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 = {};
|
||
ETaxes.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> ' + ETaxes.loadingErrors.length + '<br>';
|
||
statsHtml += '<span style="color: var(--text-muted);">';
|
||
Object.keys(errorTypes).forEach(function(type, index) {
|
||
if (index > 0) statsHtml += ' • ';
|
||
statsHtml += 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') + ' (' + ETaxes.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() {
|
||
ETaxes.errors.exportCSV();
|
||
});
|
||
|
||
$('#clear-log-dialog-btn').off('click').on('click', function() {
|
||
ETaxes.errors.clear();
|
||
d.hide();
|
||
});
|
||
}, 200);
|
||
},
|
||
|
||
// Экспорт в CSV
|
||
exportCSV: function() {
|
||
if (ETaxes.loadingErrors.length === 0) {
|
||
frappe.msgprint(__('No errors to export'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let csvContent = "data:text/csv;charset=utf-8,";
|
||
csvContent += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
|
||
|
||
ETaxes.loadingErrors.forEach(function(error) {
|
||
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||
const cleanSender = error.sender_name.replace(/"/g, '""');
|
||
|
||
const row = [
|
||
error.invoice_id,
|
||
error.serial_number,
|
||
error.creation_date,
|
||
cleanSender,
|
||
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", "etaxes_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
frappe.show_alert({
|
||
message: __('Error log exported successfully'),
|
||
indicator: 'green'
|
||
}, 3);
|
||
} catch (e) {
|
||
console.error('Export error:', e);
|
||
frappe.show_alert({
|
||
message: __('Failed to export error log'),
|
||
indicator: 'red'
|
||
}, 3);
|
||
}
|
||
},
|
||
|
||
// Показать сводку результатов
|
||
showSummary: function(totalInvoices, processedCount, errorsCount) {
|
||
let title = '';
|
||
let indicator = '';
|
||
let message = '';
|
||
|
||
if (errorsCount === 0) {
|
||
title = __('Import Completed Successfully');
|
||
indicator = 'green';
|
||
message = __('All ') + totalInvoices + __(' invoices were imported successfully.');
|
||
|
||
frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
} else {
|
||
title = errorsCount === totalInvoices ? __('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>' + __(' invoices') + '<br>';
|
||
}
|
||
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' invoices');
|
||
message += '</div>';
|
||
|
||
message += '<div style="text-align: center;">' +
|
||
'<button class="btn btn-primary btn-sm" id="view-error-details-btn" style="margin-right: 10px;">' +
|
||
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
|
||
'<button class="btn btn-default btn-sm" id="export-error-log-btn">' +
|
||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
||
'</div>';
|
||
|
||
const summaryDialog = frappe.msgprint({
|
||
title: title,
|
||
indicator: indicator,
|
||
message: message
|
||
});
|
||
|
||
// Добавляем обработчики событий
|
||
setTimeout(function() {
|
||
$('#view-error-details-btn').off('click').on('click', function() {
|
||
summaryDialog.hide();
|
||
ETaxes.errors.showDetails();
|
||
});
|
||
|
||
$('#export-error-log-btn').off('click').on('click', function() {
|
||
ETaxes.errors.exportCSV();
|
||
});
|
||
}, 200);
|
||
}
|
||
}
|
||
};
|
||
|
||
// ======= МОДУЛЬ ИМПОРТА ИНВОЙСОВ =======
|
||
ETaxes.import = {
|
||
// Показать диалог импорта из E-Taxes
|
||
showDialog: function() {
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
const minDate = moment("2020-01-01", "YYYY-MM-DD");
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Import invoice from E-Taxes'),
|
||
fields: [
|
||
{
|
||
fieldname: 'date_range_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Period')
|
||
},
|
||
{
|
||
fieldname: 'creationDateFrom',
|
||
fieldtype: 'Date',
|
||
label: __('Date from'),
|
||
default: moment().subtract(1, 'month').format('YYYY-MM-DD')
|
||
},
|
||
{
|
||
fieldname: 'creationDateTo',
|
||
fieldtype: 'Date',
|
||
label: __('Date to'),
|
||
default: moment().format('YYYY-MM-DD')
|
||
},
|
||
{
|
||
fieldname: 'settings_section',
|
||
fieldtype: 'Section Break',
|
||
label: __('Settings')
|
||
},
|
||
{
|
||
fieldname: 'warehouse',
|
||
fieldtype: 'Link',
|
||
label: __('Default warehouse'),
|
||
options: 'Warehouse',
|
||
reqd: true,
|
||
get_query: function() {
|
||
return {
|
||
filters: {
|
||
'is_group': 0,
|
||
'disabled': 0
|
||
}
|
||
};
|
||
}
|
||
}
|
||
],
|
||
primary_action_label: __('Search'),
|
||
primary_action: function() {
|
||
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 (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;
|
||
}
|
||
|
||
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
|
||
frappe.msgprint(__('The date range cannot exceed one year.'));
|
||
return;
|
||
}
|
||
|
||
if (!values.warehouse) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('Please select a default warehouse')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const fromDate = moment(values.creationDateFrom).format('DD-MM-YYYY 00:00');
|
||
const toDate = moment(values.creationDateTo).format('DD-MM-YYYY 23:59');
|
||
|
||
d.hide();
|
||
ETaxes.import.loadInvoices(fromDate, toDate, values.warehouse);
|
||
}
|
||
});
|
||
|
||
d.show();
|
||
});
|
||
},
|
||
|
||
// Загрузить инвойсы с фильтрацией дубликатов
|
||
loadInvoices: function(fromDate, toDate, warehouse, accumulatedInvoices = [], offset = 0) {
|
||
ETaxes.cancelLoading = false;
|
||
$(document).off('progress-cancel.etaxes_import');
|
||
|
||
if (offset === 0) {
|
||
frappe.show_alert({
|
||
message: __('Fetching E-Taxes invoices...'),
|
||
indicator: 'blue'
|
||
}, 3);
|
||
}
|
||
|
||
ETaxes.utils.getDefaultLogin(function(loginResponse) {
|
||
if (ETaxes.cancelLoading) return;
|
||
|
||
if (loginResponse && loginResponse.found) {
|
||
const mainToken = loginResponse.main_token;
|
||
|
||
if (!mainToken) {
|
||
frappe.msgprint({
|
||
title: __('Authentication Error'),
|
||
indicator: 'red',
|
||
message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const maxCount = 200;
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_invoices',
|
||
args: {
|
||
'token': mainToken,
|
||
'filters': JSON.stringify({
|
||
"creationDateFrom": fromDate,
|
||
"creationDateTo": toDate,
|
||
"maxCount": maxCount,
|
||
"offset": offset
|
||
})
|
||
},
|
||
callback: function(r) {
|
||
if (ETaxes.cancelLoading) return;
|
||
|
||
if (r.message && !r.message.error) {
|
||
const currentInvoices = r.message.data || r.message.invoices || [];
|
||
const hasMore = r.message.hasMore || false;
|
||
const allInvoices = accumulatedInvoices.concat(currentInvoices);
|
||
|
||
if (hasMore && currentInvoices.length > 0) {
|
||
const newOffset = offset + currentInvoices.length;
|
||
ETaxes.import.loadInvoices(fromDate, toDate, warehouse, allInvoices, newOffset);
|
||
} else {
|
||
if (allInvoices.length > 0) {
|
||
frappe.show_alert({
|
||
message: __('Processing ') + allInvoices.length + __(' invoices...'),
|
||
indicator: 'blue'
|
||
}, 3);
|
||
|
||
ETaxes.import.filterDuplicates(allInvoices, mainToken, warehouse);
|
||
} 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() {
|
||
ETaxes.auth.startProcess(function() {
|
||
ETaxes.import.loadInvoices(fromDate, toDate, warehouse);
|
||
});
|
||
},
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||
indicator: 'red'
|
||
}, 5);
|
||
}
|
||
);
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: r.message ? r.message.message : __('Error loading invoices')
|
||
});
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error("Error fetching invoices:", error);
|
||
frappe.msgprint({
|
||
title: __('Network Error'),
|
||
indicator: 'red',
|
||
message: __('Error fetching data from server: ') + (error || 'Unknown error')
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('E-Taxes authentication settings not found')
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// Фильтрация дубликатов
|
||
filterDuplicates: function(allInvoices, token, warehouse) {
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_etaxes_purchases',
|
||
callback: function(etaxesR) {
|
||
try {
|
||
if (ETaxes.cancelLoading) return;
|
||
|
||
const importedInvoices = {};
|
||
|
||
if (etaxesR.message && etaxesR.message.success && etaxesR.message.purchases) {
|
||
etaxesR.message.purchases.forEach(function(purchase) {
|
||
if (purchase.etaxes_id) {
|
||
const normId = String(purchase.etaxes_id).trim();
|
||
importedInvoices[normId] = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
const filteredInvoices = [];
|
||
|
||
for (let i = 0; i < allInvoices.length; i++) {
|
||
const invoice = allInvoices[i];
|
||
const invoiceId = String(invoice.id).trim();
|
||
|
||
if (!importedInvoices.hasOwnProperty(invoiceId)) {
|
||
filteredInvoices.push(invoice);
|
||
}
|
||
}
|
||
|
||
if (filteredInvoices.length > 0) {
|
||
ETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
|
||
} else {
|
||
frappe.msgprint({
|
||
title: __('Information'),
|
||
indicator: 'blue',
|
||
message: __('No new invoices found for the specified period (all ' +
|
||
allInvoices.length + ' are already imported)')
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error("Error processing purchase data:", e);
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('An error occurred while processing data: ') + e.message
|
||
});
|
||
}
|
||
},
|
||
error: function(err) {
|
||
console.error("Error fetching purchases:", err);
|
||
ETaxes.import.showInvoiceSelection(allInvoices, token, warehouse);
|
||
frappe.show_alert({
|
||
message: __('Failed to check for duplicates. Showing all invoices.'),
|
||
indicator: 'orange'
|
||
}, 5);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Показать диалог выбора инвойсов
|
||
showInvoiceSelection: function(invoices, token, warehouse) {
|
||
let invoiceTable = '<div class="etaxes-sales-inv-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 etaxes-invoices-table" style="width: 100%; table-layout: fixed; margin-bottom: 0;">';
|
||
invoiceTable += '<thead style="position: sticky; top: 0; z-index: 1;"><tr>' +
|
||
'<th style="width: 6%; background: var(--control-bg, var(--bg-color));"><input type="checkbox" class="select-all-invoices"></th>' +
|
||
'<th style="width: 24%; background: var(--control-bg, var(--bg-color));">' + __('Number') + '</th>' +
|
||
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Date') + '</th>' +
|
||
'<th style="width: 38%; background: var(--control-bg, var(--bg-color));">' + __('Supplier') + '</th>' +
|
||
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Amount') + '</th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
invoices.forEach(function(invoice) {
|
||
const serialNumber = invoice.serialNumber || '';
|
||
|
||
let creationDate = '';
|
||
if (invoice.createdAt) {
|
||
creationDate = moment(invoice.createdAt).format('DD.MM.YYYY');
|
||
} else if (invoice.creationDate) {
|
||
creationDate = moment(invoice.creationDate).format('DD.MM.YYYY');
|
||
}
|
||
|
||
const senderName = invoice.sender ? invoice.sender.name : (invoice.senderName || '');
|
||
const amount = invoice.totalAmount || invoice.amount || 0;
|
||
|
||
invoiceTable += '<tr>' +
|
||
'<td><input type="checkbox" class="select-invoice" data-id="' + invoice.id + '"></td>' +
|
||
'<td style="word-break: break-word;">' + serialNumber + '</td>' +
|
||
'<td>' + creationDate + '</td>' +
|
||
'<td style="word-break: break-word;">' + senderName + '</td>' +
|
||
'<td style="text-align: right;">' + ETaxes.utils.formatCurrency(amount) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
invoiceTable += '</tbody></table></div>';
|
||
|
||
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 invoices found: ') + '<strong>' + invoices.length + '</strong>' +
|
||
'</span>' +
|
||
'<span style="color: var(--text-muted);">' +
|
||
__('Selected warehouse: ') + '<strong>' + frappe.utils.escape_html(warehouse) + '</strong>' +
|
||
'</span>' +
|
||
'</div>';
|
||
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Select invoice'),
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'header_html',
|
||
fieldtype: 'HTML',
|
||
options: headerHtml
|
||
},
|
||
{
|
||
fieldname: 'invoices_html',
|
||
fieldtype: 'HTML',
|
||
options: invoiceTable
|
||
}
|
||
],
|
||
primary_action_label: __('Load selected'),
|
||
primary_action: function() {
|
||
const selectedInvoiceIds = [];
|
||
d.$wrapper.find('.select-invoice:checked').each(function() {
|
||
selectedInvoiceIds.push($(this).data('id'));
|
||
});
|
||
|
||
if (selectedInvoiceIds.length === 0) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('No invoices selected')
|
||
});
|
||
return;
|
||
}
|
||
|
||
d.hide();
|
||
ETaxes.cancelLoading = false;
|
||
ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
|
||
},
|
||
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-invoices').on('change', function() {
|
||
const isChecked = $(this).prop('checked');
|
||
d.$wrapper.find('.select-invoice').prop('checked', isChecked);
|
||
});
|
||
},
|
||
|
||
// Загрузка выбранных инвойсов
|
||
// Надёжное закрытие прогресс-бара. frappe.hide_progress() в jey_theme/Bootstrap
|
||
// не всегда убирает модал (он зависает, и summary/диалог накладывается сверху).
|
||
// Принудительно: 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;
|
||
}
|
||
if ($('.modal:visible').length === 0) {
|
||
$('.modal-backdrop').remove();
|
||
$('body').removeClass('modal-open');
|
||
}
|
||
},
|
||
|
||
loadSelectedInvoices: function(invoiceIds, token, warehouse, processedCount = 0, currentIndex = 0) {
|
||
if (processedCount === 0 && currentIndex === 0) {
|
||
ETaxes.loadingErrors = [];
|
||
window.totalInvoicesToProcess = invoiceIds.length;
|
||
}
|
||
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
if (invoiceIds.length === 0) {
|
||
ETaxes.import._closeProgress();
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
const totalInvoices = window.totalInvoicesToProcess || processedCount;
|
||
|
||
ETaxes.errors.showSummary(totalInvoices, processedCount, errorsCount);
|
||
window.totalInvoicesToProcess = null;
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (ETaxes.cancelLoading) {
|
||
invoiceIds = [];
|
||
ETaxes.import._closeProgress();
|
||
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
|
||
return;
|
||
}
|
||
|
||
const invoiceId = invoiceIds.shift();
|
||
const isLastInvoice = invoiceIds.length === 0;
|
||
const totalToProcess = window.totalInvoicesToProcess || (processedCount + invoiceIds.length + 1);
|
||
|
||
currentIndex++;
|
||
|
||
frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
|
||
__('Loading invoice ') + currentIndex + __(' of ') + totalToProcess, null, true);
|
||
|
||
$(document).on('progress-cancel.loading_invoices', function() {
|
||
ETaxes.cancelLoading = true;
|
||
frappe.show_alert({
|
||
message: __('Cancelling import... Finishing current invoice.'),
|
||
indicator: 'orange'
|
||
}, 3);
|
||
ETaxes.import._closeProgress();
|
||
});
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.get_invoice_details',
|
||
args: {
|
||
'token': token,
|
||
'invoice_id': invoiceId
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && !r.message.error) {
|
||
const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
|
||
const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
|
||
const serialNumber = r.message.serialNumber || '';
|
||
const senderName = r.message.sender ? r.message.sender.name : '';
|
||
const total = r.message.totalAmount || r.message.amount || 0;
|
||
|
||
frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
|
||
__('Importing invoice: ') + serialNumber);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.import_invoice_with_mapping',
|
||
args: {
|
||
'invoice_data': r.message,
|
||
'purchase_order_name': null,
|
||
'schedule_date': scheduleDate,
|
||
'warehouse': warehouse
|
||
},
|
||
callback: function(importR) {
|
||
ETaxes.import._handleImportResult(importR, r.message, invoiceId, scheduleDate, senderName, total,
|
||
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
},
|
||
error: function(xhr, status, error) {
|
||
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
|
||
ETaxes.errors.add(r.message, 'Network Error', errorMessage, {
|
||
invoice_items: r.message.items || [],
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
});
|
||
} else if (r.message && r.message.error === 'unauthorized') {
|
||
ETaxes.import._closeProgress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
frappe.confirm(
|
||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||
function() {
|
||
ETaxes.auth.startProcess(function() {
|
||
ETaxes.utils.getDefaultLogin(function(loginR) {
|
||
if (loginR && loginR.found && loginR.main_token) {
|
||
invoiceIds.unshift(invoiceId);
|
||
ETaxes.import.loadSelectedInvoices(invoiceIds, loginR.main_token, warehouse, processedCount, currentIndex - 1);
|
||
} else {
|
||
ETaxes.errors.add({id: invoiceId}, 'Authentication Error',
|
||
'Failed to get token after authentication');
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||
}
|
||
}, false);
|
||
});
|
||
},
|
||
function() {
|
||
ETaxes.errors.add({id: invoiceId}, 'Authentication Cancelled',
|
||
'User cancelled authentication process');
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||
}
|
||
);
|
||
} else {
|
||
const errorMessage = r.message ? r.message.message : 'Failed to load invoice details';
|
||
ETaxes.errors.add({id: invoiceId}, 'Invoice Data Error', errorMessage, {
|
||
server_response: r.message
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
const errorMessage = 'Network error loading invoice details: ' + (error || 'Unknown network error');
|
||
ETaxes.errors.add({id: invoiceId}, 'Network Error', errorMessage, {
|
||
xhr_status: xhr.status,
|
||
xhr_response: xhr.responseText
|
||
});
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Обработка результата импорта
|
||
_handleImportResult: function(importR, invoiceMessage, invoiceId, scheduleDate, senderName, total,
|
||
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
|
||
if (importR.message && importR.message.success) {
|
||
frappe.show_alert({
|
||
message: __('Invoice successfully imported: ') + (invoiceMessage.serialNumber || ''),
|
||
indicator: 'green'
|
||
}, 3);
|
||
|
||
const purchaseOrderName = importR.message.purchase_order;
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.create_etaxes_purchase',
|
||
args: {
|
||
'etaxes_id': invoiceId,
|
||
'date': scheduleDate,
|
||
'party': senderName,
|
||
'total': total
|
||
},
|
||
callback: function(etaxesR) {
|
||
if (etaxesR.message && etaxesR.message.success) {
|
||
frappe.call({
|
||
method: 'invoice_az.api.link_purchase_order_to_etaxes',
|
||
args: {
|
||
'purchase_order': purchaseOrderName,
|
||
'etaxes_purchase': etaxesR.message.name
|
||
},
|
||
callback: function(linkR) {
|
||
processedCount++;
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
});
|
||
} else {
|
||
ETaxes.errors.add(invoiceMessage, 'E-Taxes Purchase Creation Error',
|
||
etaxesR.message ? etaxesR.message.message : 'Failed to create E-Taxes Purchase record', {
|
||
server_response: etaxesR.message
|
||
});
|
||
|
||
ETaxes.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 || [] };
|
||
|
||
if (importR.message.unmatched_items) {
|
||
errorType = 'Unmapped Items';
|
||
additionalData.unmatched_items = importR.message.unmatched_items;
|
||
} else if (importR.message.unmatched_parties) {
|
||
errorType = 'Unmapped Parties';
|
||
additionalData.unmatched_parties = importR.message.unmatched_parties;
|
||
} else if (importR.message.unmatched_units) {
|
||
errorType = 'Unmapped Units';
|
||
additionalData.unmatched_units = importR.message.unmatched_units;
|
||
}
|
||
|
||
additionalData.server_response = importR.message;
|
||
ETaxes.errors.add(invoiceMessage, errorType, importR.message.message, additionalData);
|
||
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
} else {
|
||
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
|
||
const additionalData = {
|
||
invoice_items: invoiceMessage.items || [],
|
||
server_response: importR.message
|
||
};
|
||
|
||
if (importR.exc) {
|
||
additionalData.stack_trace = importR.exc;
|
||
}
|
||
|
||
ETaxes.errors.add(invoiceMessage, 'Import Error', errorMessage, additionalData);
|
||
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
|
||
}
|
||
},
|
||
|
||
// Продолжить или завершить обработку
|
||
_continueOrFinish: function(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
|
||
if (isLastInvoice) {
|
||
ETaxes.import._closeProgress();
|
||
$(document).off('progress-cancel.loading_invoices');
|
||
|
||
const totalToProcess = window.totalInvoicesToProcess || processedCount;
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
if (invoiceIds.length === 0) {
|
||
ETaxes.import._closeProgress();
|
||
}
|
||
|
||
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
|
||
}, ETaxes.PROGRESS_UPDATE_DELAY);
|
||
},
|
||
|
||
// Socket.IO bulk import
|
||
loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
|
||
ETaxes.loadingErrors = [];
|
||
const total = invoiceIds.length;
|
||
// Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
|
||
// frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
|
||
// уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
|
||
let importFinished = false;
|
||
// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
|
||
// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
|
||
const closeProgress = function() {
|
||
if (frappe.cur_progress) {
|
||
try {
|
||
frappe.cur_progress.$wrapper.modal('hide');
|
||
frappe.cur_progress.$wrapper.remove();
|
||
} catch (e) {}
|
||
frappe.cur_progress = null;
|
||
}
|
||
if ($('.modal:visible').length === 0) {
|
||
$('.modal-backdrop').remove();
|
||
$('body').removeClass('modal-open');
|
||
}
|
||
};
|
||
|
||
// Progress + cancel are shown by the global Background Tasks widget
|
||
// (bottom-right). No blocking modal here; we keep only the complete
|
||
// subscription to show the final import summary.
|
||
frappe.realtime.off('purchase_import_complete');
|
||
frappe.realtime.on('purchase_import_complete', function(data) {
|
||
importFinished = true;
|
||
frappe.realtime.off('purchase_import_progress');
|
||
frappe.realtime.off('purchase_import_complete');
|
||
closeProgress();
|
||
|
||
(data.errors || []).forEach(function(err) {
|
||
ETaxes.loadingErrors.push({
|
||
invoice_id: err.invoice_id || 'Unknown',
|
||
error_type: err.unmatched_items ? 'Unmapped Items'
|
||
: err.unmatched_parties ? 'Unmapped Parties'
|
||
: err.unmatched_units ? 'Unmapped Units'
|
||
: 'Import Error',
|
||
error_message: err.error || 'Unknown error',
|
||
unmatched_items: err.unmatched_items,
|
||
unmatched_parties: err.unmatched_parties,
|
||
unmatched_units: err.unmatched_units
|
||
});
|
||
});
|
||
|
||
const errorsCount = ETaxes.loadingErrors.length;
|
||
ETaxes.errors.showSummary(data.total, data.imported, errorsCount);
|
||
|
||
if (cur_list) {
|
||
cur_list.refresh();
|
||
}
|
||
});
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.api.import_bulk_purchase_invoices',
|
||
args: {
|
||
invoice_ids: JSON.stringify(invoiceIds),
|
||
token: token,
|
||
warehouse: warehouse
|
||
},
|
||
callback: function(r) {
|
||
if (!r.message || !r.message.enqueued) {
|
||
frappe.realtime.off('purchase_import_progress');
|
||
frappe.realtime.off('purchase_import_complete');
|
||
closeProgress();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('Failed to start import job')
|
||
});
|
||
}
|
||
},
|
||
error: function() {
|
||
frappe.realtime.off('purchase_import_progress');
|
||
frappe.realtime.off('purchase_import_complete');
|
||
closeProgress();
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('Network error starting import')
|
||
});
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
// ======= SALES INVOICE SPECIFIC CODE =======
|
||
/**
|
||
* Sales Invoice Form and List Configuration
|
||
* Uses ETaxes common module patterns for consistency
|
||
*/
|
||
|
||
frappe.ui.form.on('Sales Invoice', {
|
||
refresh: function(frm) {
|
||
// Check if is_taxes_doc flag is set
|
||
if (frm.doc.is_taxes_doc) {
|
||
// Make all fields read-only
|
||
frm.set_read_only(true);
|
||
|
||
// Add visual indicator
|
||
frm.page.set_indicator(__('Tax Document'), 'blue');
|
||
}
|
||
|
||
// Add E-Taxes buttons only for submitted invoices
|
||
if (frm.doc.docstatus === 1) {
|
||
add_etaxes_buttons(frm);
|
||
}
|
||
|
||
// Add cancel button for cancelled invoices that were sent to E-Taxes
|
||
if (frm.doc.docstatus === 2 && frm.doc.etaxes_invoice_id) {
|
||
add_cancel_etaxes_button(frm);
|
||
}
|
||
},
|
||
|
||
// Make is_taxes_doc field read-only when document loads
|
||
onload: function(frm) {
|
||
frm.set_df_property('is_taxes_doc', 'read_only', 1);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Add E-Taxes buttons based on current status
|
||
*/
|
||
function add_etaxes_buttons(frm) {
|
||
// Status: Sent and Signed - show indicator and details button
|
||
if (frm.doc.etaxes_send_status === 'Sent and Signed') {
|
||
frm.page.set_indicator(__('Sent to E-Taxes'), 'green');
|
||
|
||
// Add button to view details
|
||
frm.add_custom_button(__('View E-Taxes Details'), function() {
|
||
show_etaxes_details(frm);
|
||
}, __('E-Taxes'));
|
||
|
||
return;
|
||
}
|
||
|
||
// Status: Created, not signed - show warning and sign button
|
||
if (frm.doc.etaxes_send_status === 'Created, not signed') {
|
||
frm.page.set_indicator(__('Draft Created'), 'orange');
|
||
|
||
// TEMPORARILY DISABLED: keep document as draft only for now.
|
||
// Add button to sign document
|
||
// frm.add_custom_button(__('Sign Document'), function() {
|
||
// sign_document(frm);
|
||
// }, __('E-Taxes'));
|
||
|
||
// Add button to view details
|
||
frm.add_custom_button(__('View Details'), function() {
|
||
show_etaxes_details(frm);
|
||
}, __('E-Taxes'));
|
||
|
||
return;
|
||
}
|
||
|
||
// Status: Not sent - show send button
|
||
frm.add_custom_button(__('Send to E-Taxes'), function() {
|
||
send_to_etaxes(frm);
|
||
}, __('E-Taxes'));
|
||
}
|
||
|
||
/**
|
||
* Add cancel button for cancelled Sales Invoices that were sent to E-Taxes
|
||
*/
|
||
function add_cancel_etaxes_button(frm) {
|
||
// Show indicator that invoice needs to be cancelled on E-Taxes
|
||
frm.page.set_indicator(__('Cancelled - Remove from E-Taxes'), 'red');
|
||
|
||
// Add button to cancel on E-Taxes
|
||
frm.add_custom_button(__('Cancel on E-Taxes'), function() {
|
||
cancel_invoice_on_etaxes(frm);
|
||
}, __('E-Taxes'));
|
||
}
|
||
|
||
/**
|
||
* Cancel invoice on E-Taxes system
|
||
*/
|
||
function cancel_invoice_on_etaxes(frm) {
|
||
// Check authentication first
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
// Show confirmation dialog
|
||
frappe.confirm(
|
||
__('Cancel this invoice on E-Taxes Government Portal?') + '<br><br>' +
|
||
__('Invoice: {0}', [frm.doc.name]) + '<br>' +
|
||
__('E-Taxes ID: {0}', [frm.doc.etaxes_invoice_id]) + '<br>' +
|
||
__('Status: {0}', [frm.doc.etaxes_send_status]) + '<br><br>' +
|
||
__('<strong>Warning:</strong> This will permanently remove the invoice from E-Taxes system.'),
|
||
function() {
|
||
// User confirmed - proceed with cancellation
|
||
frappe.call({
|
||
method: 'invoice_az.send_sales_api.cancel_invoice_on_etaxes',
|
||
args: {
|
||
sales_invoice_name: frm.doc.name
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
// Success
|
||
frappe.show_alert({
|
||
message: __('Invoice successfully removed from E-Taxes'),
|
||
indicator: 'green'
|
||
}, 5);
|
||
|
||
// Reload document to show updated status
|
||
frm.reload_doc();
|
||
} else {
|
||
// Error
|
||
const errorMsg = r.message ? r.message.message : __('Failed to cancel invoice on E-Taxes');
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: errorMsg
|
||
});
|
||
}
|
||
},
|
||
error: function(r) {
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: __('Failed to communicate with E-Taxes service.')
|
||
});
|
||
}
|
||
});
|
||
},
|
||
function() {
|
||
// User cancelled - do nothing
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Main function to send invoice to E-Taxes (create draft)
|
||
*/
|
||
function send_to_etaxes(frm) {
|
||
// Check authentication first
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
// НОВОЕ: Сначала показываем диалог выбора объекта
|
||
show_object_selection_dialog(frm);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show object selection dialog for customer
|
||
* This is the first step before sending to E-Taxes
|
||
*/
|
||
function show_object_selection_dialog(frm) {
|
||
// Show loading
|
||
ETaxes.dialogs.showLoading(
|
||
__('Loading Customer Objects'),
|
||
__('Fetching available objects from E-Taxes...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
// Call backend to get customer objects
|
||
frappe.call({
|
||
method: 'invoice_az.send_sales_api.get_customer_objects',
|
||
args: {
|
||
sales_invoice_name: frm.doc.name
|
||
},
|
||
callback: function(r) {
|
||
// Add small delay to ensure loading dialog is fully rendered before hiding
|
||
setTimeout(function() {
|
||
// Hide loading dialog
|
||
ETaxes.dialogs.hide();
|
||
|
||
if (r.message && r.message.success) {
|
||
const objects = r.message.objects || [];
|
||
const customerName = r.message.customer_name || frm.doc.customer;
|
||
const taxId = r.message.tax_id || '';
|
||
|
||
if (objects.length === 0) {
|
||
// No objects available
|
||
frappe.msgprint({
|
||
title: __('No Objects Available'),
|
||
indicator: 'orange',
|
||
message: __('Customer "{0}" (TIN: {1}) has no available objects in E-Taxes. Please contact the customer or E-Taxes support.', [customerName, taxId])
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Show object selection dialog
|
||
show_object_picker_dialog(frm, objects, customerName, taxId);
|
||
} else {
|
||
// Error getting objects
|
||
const errorMsg = r.message ? r.message.message : __('Failed to load customer objects');
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: errorMsg
|
||
});
|
||
}
|
||
}, 150); // Small delay to prevent race condition with fast API responses
|
||
},
|
||
error: function(r) {
|
||
// Add small delay for error case too
|
||
setTimeout(function() {
|
||
ETaxes.dialogs.hide();
|
||
|
||
let errorMsg = __('Failed to communicate with E-Taxes service.');
|
||
|
||
// Try to extract actual error message
|
||
if (r && r.responseText) {
|
||
try {
|
||
const errorData = JSON.parse(r.responseText);
|
||
if (errorData.exc) {
|
||
const excMatch = errorData.exc.match(/Exception: (.+)/);
|
||
if (excMatch) {
|
||
errorMsg = excMatch[1];
|
||
}
|
||
} else if (errorData.message) {
|
||
errorMsg = errorData.message;
|
||
}
|
||
} catch (e) {
|
||
// Ignore parsing errors
|
||
}
|
||
}
|
||
|
||
frappe.msgprint({
|
||
title: __('Error'),
|
||
indicator: 'red',
|
||
message: errorMsg
|
||
});
|
||
}, 150); // Small delay to prevent race condition
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show dialog with object picker
|
||
* User can select one object from the list
|
||
*/
|
||
function show_object_picker_dialog(frm, objects, customerName, taxId) {
|
||
// Build objects table HTML
|
||
let objectsHtml = '<div style="max-height: 400px; overflow-y: auto;">';
|
||
objectsHtml += '<table class="table table-bordered">';
|
||
objectsHtml += '<thead><tr>';
|
||
objectsHtml += '<th style="width: 5%;"></th>';
|
||
objectsHtml += '<th style="width: 35%;">' + __('Object Name') + '</th>';
|
||
objectsHtml += '<th style="width: 20%;">' + __('Code') + '</th>';
|
||
objectsHtml += '<th style="width: 40%;">' + __('Address') + '</th>';
|
||
objectsHtml += '</tr></thead><tbody>';
|
||
|
||
// Empty object option
|
||
objectsHtml += '<tr class="object-row" data-index="empty" style="cursor: pointer;">';
|
||
objectsHtml += '<td><input type="radio" name="object_selection" value="empty" class="object-radio"></td>';
|
||
objectsHtml += '<td>—</td>';
|
||
objectsHtml += '<td>—</td>';
|
||
objectsHtml += '<td style="color: var(--text-muted);">' + __('Without object') + '</td>';
|
||
objectsHtml += '</tr>';
|
||
|
||
objects.forEach(function(obj, index) {
|
||
const objectName = obj.name || '-';
|
||
const objectCode = obj.code || '-';
|
||
const objectAddress = obj.address || '-';
|
||
|
||
objectsHtml += '<tr class="object-row" data-index="' + index + '" style="cursor: pointer;">';
|
||
objectsHtml += '<td><input type="radio" name="object_selection" value="' + index + '" class="object-radio"></td>';
|
||
objectsHtml += '<td>' + objectName + '</td>';
|
||
objectsHtml += '<td>' + objectCode + '</td>';
|
||
objectsHtml += '<td style="word-break: break-word;">' + objectAddress + '</td>';
|
||
objectsHtml += '</tr>';
|
||
});
|
||
|
||
objectsHtml += '</tbody></table></div>';
|
||
|
||
// Info message
|
||
const infoHtml = '<div class="alert alert-info" style="margin-bottom: 15px;">' +
|
||
'<strong>' + __('Customer:') + '</strong> ' + customerName + '<br>' +
|
||
'<strong>' + __('TIN:') + '</strong> ' + taxId + '<br>' +
|
||
'<strong>' + __('Available Objects:') + '</strong> ' + objects.length +
|
||
'</div>';
|
||
|
||
// Create dialog
|
||
const d = new frappe.ui.Dialog({
|
||
title: __('Select Customer Object'),
|
||
size: 'large',
|
||
fields: [
|
||
{
|
||
fieldname: 'info_html',
|
||
fieldtype: 'HTML',
|
||
options: infoHtml
|
||
},
|
||
{
|
||
fieldname: 'objects_html',
|
||
fieldtype: 'HTML',
|
||
options: objectsHtml
|
||
}
|
||
],
|
||
primary_action_label: __('Select and Continue'),
|
||
primary_action: function() {
|
||
// Get selected object
|
||
const selectedIndex = d.$wrapper.find('input[name="object_selection"]:checked').val();
|
||
|
||
if (selectedIndex === undefined) {
|
||
frappe.msgprint({
|
||
title: __('Warning'),
|
||
indicator: 'orange',
|
||
message: __('Please select an object')
|
||
});
|
||
return;
|
||
}
|
||
|
||
const objectCode = selectedIndex === 'empty' ? '' : objects[parseInt(selectedIndex)].code;
|
||
|
||
// Hide dialog
|
||
d.hide();
|
||
|
||
// Save selected object code to customer_object_name
|
||
// We use frappe.client.set_value to bypass read_only restriction
|
||
frappe.call({
|
||
method: 'frappe.client.set_value',
|
||
args: {
|
||
doctype: 'Sales Invoice',
|
||
name: frm.doc.name,
|
||
fieldname: 'customer_object_name',
|
||
value: objectCode
|
||
},
|
||
callback: function(r) {
|
||
if (r.message) {
|
||
// Make sure all dialogs are hidden
|
||
ETaxes.dialogs.hide();
|
||
|
||
// Refresh form to show updated value
|
||
frm.reload_doc();
|
||
|
||
// Wait for reload to complete before showing confirmation
|
||
setTimeout(function() {
|
||
// Show first confirmation dialog
|
||
frappe.confirm(
|
||
__('Send this Sales Invoice to Azərbaycan E-Taxes system?') + '<br><br>' +
|
||
__('Invoice: {0}', [frm.doc.name]) + '<br>' +
|
||
__('Customer: {0}', [frm.doc.customer]) + '<br>' +
|
||
__('Object: {0}', [selectedObject.name]) + '<br>' +
|
||
__('Total: {0}', [ETaxes.utils.formatCurrency(frm.doc.grand_total)]),
|
||
function() {
|
||
// First confirmation accepted - show second confirmation
|
||
show_second_confirmation(frm);
|
||
}
|
||
);
|
||
}, 300); // Small delay to ensure reload completes
|
||
}
|
||
}
|
||
});
|
||
},
|
||
secondary_action_label: __('Cancel'),
|
||
secondary_action: function() {
|
||
d.hide();
|
||
}
|
||
});
|
||
|
||
// Show dialog
|
||
d.show();
|
||
|
||
// Make dialog wider
|
||
d.$wrapper.find('.modal-dialog').css({
|
||
'max-width': '900px',
|
||
'width': '90%'
|
||
});
|
||
|
||
// Add click handler for table rows
|
||
d.$wrapper.find('.object-row').on('click', function() {
|
||
const index = $(this).data('index');
|
||
$(this).find('.object-radio').prop('checked', true);
|
||
});
|
||
|
||
// Automatically select first object
|
||
d.$wrapper.find('.object-radio').first().prop('checked', true);
|
||
}
|
||
|
||
/**
|
||
* Show second confirmation dialog
|
||
*/
|
||
function show_second_confirmation(frm) {
|
||
frappe.confirm(
|
||
__('Are you sure you want to send this invoice to E-Taxes?') + '<br><br>' +
|
||
__('<strong>A draft will be created on E-Taxes system.</strong>') + '<br>' +
|
||
__('You will need to sign it separately after creation.'),
|
||
function() {
|
||
// Second confirmation accepted - proceed with sending
|
||
execute_send_to_etaxes(frm);
|
||
}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Execute the actual sending to E-Taxes (create draft only)
|
||
*/
|
||
function execute_send_to_etaxes(frm) {
|
||
// Show loading dialog using ETaxes common module
|
||
ETaxes.dialogs.showLoading(
|
||
__('Sending to E-Taxes'),
|
||
__('Creating draft invoice...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
// Call backend API
|
||
frappe.call({
|
||
method: 'invoice_az.send_sales_api.send_sales_invoice_to_etaxes',
|
||
args: {
|
||
sales_invoice_name: frm.doc.name
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
// Success
|
||
const status = r.message.status;
|
||
const serialNumber = r.message.serial_number;
|
||
|
||
if (status === 'Sent and Signed') {
|
||
// Fully completed (shouldn't happen with new workflow, but handle it)
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Invoice Sent and Signed'),
|
||
__('Serial: {0}', [serialNumber]),
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Successfully sent and signed!'),
|
||
indicator: 'green'
|
||
}, 5);
|
||
frm.reload_doc();
|
||
}
|
||
);
|
||
} else if (status === 'Created, not signed') {
|
||
// Draft created successfully
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Draft Created Successfully'),
|
||
__('Serial: {0}', [serialNumber]),
|
||
function() {
|
||
frappe.msgprint({
|
||
title: __('Draft Created'),
|
||
indicator: 'blue',
|
||
message: __('Invoice draft has been created on E-Taxes.') + '<br><br>' +
|
||
__('Serial Number: {0}', ['<strong>' + serialNumber + '</strong>']) + '<br><br>' +
|
||
__('Please use "Sign Document" button to complete the process.')
|
||
});
|
||
frm.reload_doc();
|
||
}
|
||
);
|
||
} else {
|
||
// Unknown status
|
||
ETaxes.dialogs.hide();
|
||
frappe.msgprint({
|
||
title: __('Sent'),
|
||
indicator: 'blue',
|
||
message: r.message.message || __('Invoice sent to E-Taxes')
|
||
});
|
||
frm.reload_doc();
|
||
}
|
||
} else {
|
||
// Error
|
||
const errorMsg = r.message ? r.message.message : __('Unknown error occurred');
|
||
ETaxes.dialogs.setError(
|
||
__('Failed to Send'),
|
||
errorMsg
|
||
);
|
||
}
|
||
},
|
||
error: function(r) {
|
||
let errorMsg = __('Failed to communicate with E-Taxes service.');
|
||
|
||
// Try to extract actual error message
|
||
if (r && r.responseText) {
|
||
try {
|
||
const errorData = JSON.parse(r.responseText);
|
||
if (errorData.exc) {
|
||
// Extract error from exception
|
||
const excMatch = errorData.exc.match(/Exception: (.+)/);
|
||
if (excMatch) {
|
||
errorMsg = excMatch[1];
|
||
} else {
|
||
// If no match, try to get last line
|
||
const lines = errorData.exc.split('\n');
|
||
errorMsg = lines[lines.length - 1] || errorData.exc.substring(0, 200);
|
||
}
|
||
} else if (errorData.message) {
|
||
errorMsg = errorData.message;
|
||
} else if (errorData._server_messages) {
|
||
try {
|
||
const messages = JSON.parse(errorData._server_messages);
|
||
if (Array.isArray(messages) && messages.length > 0) {
|
||
const msg = JSON.parse(messages[0]);
|
||
errorMsg = msg.message || errorMsg;
|
||
}
|
||
} catch (e) {
|
||
// Ignore parsing errors
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// If parsing fails, use response text
|
||
errorMsg = r.responseText.substring(0, 300);
|
||
}
|
||
} else if (r && r.statusText) {
|
||
errorMsg = r.statusText;
|
||
}
|
||
|
||
ETaxes.dialogs.setError(
|
||
__('Error'),
|
||
errorMsg
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Sign document on E-Taxes
|
||
*/
|
||
function sign_document(frm) {
|
||
// Check authentication first
|
||
ETaxes.auth.checkAndProcess(function() {
|
||
frappe.confirm(
|
||
__('Sign this invoice on E-Taxes?') + '<br><br>' +
|
||
__('Serial: {0}', [frm.doc.etaxes_serial_number || '-']) + '<br><br>' +
|
||
__('This will complete the sending process.'),
|
||
function() {
|
||
execute_signing(frm);
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Execute the signing process
|
||
*/
|
||
function execute_signing(frm) {
|
||
// Show loading dialog
|
||
ETaxes.dialogs.showLoading(
|
||
__('Signing Document'),
|
||
__('Sending signature request...'),
|
||
__('Please wait')
|
||
);
|
||
|
||
frappe.call({
|
||
method: 'invoice_az.send_sales_api.retry_signing_invoice',
|
||
args: {
|
||
sales_invoice_name: frm.doc.name
|
||
},
|
||
callback: function(r) {
|
||
if (r.message && r.message.success) {
|
||
// Success
|
||
const verificationCode = r.message.verification_code;
|
||
|
||
ETaxes.dialogs.setSuccess(
|
||
__('Document Signed Successfully'),
|
||
__('Verification Code: {0}', [verificationCode || '-']),
|
||
function() {
|
||
frappe.show_alert({
|
||
message: __('Invoice signed successfully!'),
|
||
indicator: 'green'
|
||
}, 5);
|
||
frm.reload_doc();
|
||
}
|
||
);
|
||
} else {
|
||
// Error
|
||
const errorMsg = r.message ? r.message.message : __('Signing failed');
|
||
ETaxes.dialogs.setError(
|
||
__('Signing Failed'),
|
||
errorMsg
|
||
);
|
||
}
|
||
},
|
||
error: function(r) {
|
||
let errorMsg = __('Failed to communicate with E-Taxes service.');
|
||
|
||
// Try to extract actual error message
|
||
if (r && r.responseText) {
|
||
try {
|
||
const errorData = JSON.parse(r.responseText);
|
||
if (errorData.exc) {
|
||
// Extract error from exception
|
||
const excMatch = errorData.exc.match(/Exception: (.+)/);
|
||
if (excMatch) {
|
||
errorMsg = excMatch[1];
|
||
} else {
|
||
// If no match, try to get last line
|
||
const lines = errorData.exc.split('\n');
|
||
errorMsg = lines[lines.length - 1] || errorData.exc.substring(0, 200);
|
||
}
|
||
} else if (errorData.message) {
|
||
errorMsg = errorData.message;
|
||
} else if (errorData._server_messages) {
|
||
try {
|
||
const messages = JSON.parse(errorData._server_messages);
|
||
if (Array.isArray(messages) && messages.length > 0) {
|
||
const msg = JSON.parse(messages[0]);
|
||
errorMsg = msg.message || errorMsg;
|
||
}
|
||
} catch (e) {
|
||
// Ignore parsing errors
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// If parsing fails, use response text
|
||
errorMsg = r.responseText.substring(0, 300);
|
||
}
|
||
} else if (r && r.statusText) {
|
||
errorMsg = r.statusText;
|
||
}
|
||
|
||
ETaxes.dialogs.setError(
|
||
__('Error'),
|
||
errorMsg
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show E-Taxes details for sent invoice
|
||
* Hides technical information (invoice ID) from client
|
||
*/
|
||
function show_etaxes_details(frm) {
|
||
let details_html = `
|
||
<table class="table table-bordered">
|
||
<tr>
|
||
<th style="width: 40%;">${__('Serial Number')}</th>
|
||
<td><strong>${frm.doc.etaxes_serial_number || '-'}</strong></td>
|
||
</tr>
|
||
<tr>
|
||
<th>${__('Status')}</th>
|
||
<td><strong>${frm.doc.etaxes_send_status || 'Not Sent'}</strong></td>
|
||
</tr>
|
||
</table>
|
||
<div style="margin-top: 15px; padding: 10px; background-color: #f8f9fa; border-radius: 4px;">
|
||
<p style="margin: 0; font-size: 12px; color: #6c757d;">
|
||
${__('You can verify this invoice on the E-Taxes portal using the serial number above.')}
|
||
</p>
|
||
</div>
|
||
`;
|
||
|
||
frappe.msgprint({
|
||
title: __('E-Taxes Details'),
|
||
indicator: 'blue',
|
||
message: details_html
|
||
});
|
||
}
|