invoice_az/invoice_az/client/journal_entry.js

1449 lines
61 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
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() {
VATETaxes.cancelLoading = true;
VATETaxes.loadingDialog.$wrapper.find('.primary-action').prop('disabled', true);
VATETaxes.loadingDialog.$wrapper.find('.primary-action').html(__('Cancelling...'));
$('#status_message p').text(__('Cancelling the operation.'));
}
});
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());
}
},
// Скрыть диалог
hide: function() {
if (VATETaxes.loadingDialog) {
try {
VATETaxes.loadingDialog.hide();
} catch (e) {
console.error("Error hiding loading dialog:", e);
}
VATETaxes.loadingDialog = null;
}
VATETaxes.cancelLoading = false;
},
// Получить 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) {
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>';
const summaryDialog = frappe.msgprint({
title: title,
indicator: indicator,
message: message
});
// Добавляем обработчики событий
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) {
frappe.show_alert({
message: __('Fetching VAT operations from E-Taxes...'),
indicator: 'blue'
}, 3);
}
VATETaxes.utils.getDefaultLogin(function(loginResponse) {
if (VATETaxes.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 = 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.import.loadOperations(fromDate, toDate, company, allOperations, newOffset, createAsDraft, autoCreateCustomers);
} else {
if (allOperations.length > 0) {
frappe.show_alert({
message: __('Processing ') + allOperations.length + __(' VAT operations...'),
indicator: 'blue'
}, 3);
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft, autoCreateCustomers);
} else {
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
message: __('No VAT operations 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() {
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 {
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);
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(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 {
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);
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) {
let operationTable = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered vat-operations-table" style="width: 100%; table-layout: fixed;">';
operationTable += '<thead><tr>' +
'<th style="width: 3%;"><input type="checkbox" class="select-all-operations"></th>' +
'<th style="width: 9%;">' + __('Date') + '</th>' +
'<th style="width: 10%;">' + __('TIN') + '</th>' +
'<th style="width: 18%;">' + __('Name') + '</th>' +
'<th style="width: 18%;">' + __('Operation Type') + '</th>' +
'<th style="width: 11%;">' + __('Classification Code') + '</th>' +
'<th style="width: 13%;">' + __('Income') + '</th>' +
'<th style="width: 13%;">' + __('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;
}
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="text-align: right;">' + VATETaxes.utils.formatCurrency(income) + '</td>' +
'<td style="text-align: right;">' + VATETaxes.utils.formatCurrency(expense) + '</td>' +
'</tr>';
});
operationTable += '</tbody></table></div>';
const infoMessage = '<div class="alert alert-info">' +
__('New VAT operations found: ') + operations.length +
'</div>';
const d = new frappe.ui.Dialog({
title: __('Select VAT Operations'),
size: 'large',
fields: [
{
fieldname: 'info_html',
fieldtype: 'HTML',
options: infoMessage
},
{
fieldname: 'settings_section',
fieldtype: 'Section Break',
label: __('Import Settings')
},
{
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: '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',
label: __('Operations')
},
{
fieldname: 'operations_html',
fieldtype: 'HTML',
options: operationTable
},
{
fieldname: 'company_display',
fieldtype: 'HTML',
options: '<div class="row"><div class="col-xs-12"><div class="alert alert-info">' +
__('Selected company: ') + '<strong>' + company + '</strong></div></div></div>'
}
],
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.loadSelectedOperations(selectedOperations, token, company, 0, 0, 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);
}
};
// ======= ОСНОВНЫЕ ОБРАБОТЧИКИ СПИСКОВ =======
// 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'],
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="indicator-pill green">' + value + '</span>';
} else if (value === 'Expense') {
return '<span class="indicator-pill red">' + 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="indicator-pill blue" title="' + __('VAT Operation from E-Taxes') + '">ƏDV</span>';
}
return '';
}
},
onload: function(listview) {
listview.page.add_menu_item(__('Import VAT Operations from E-Taxes'), function() {
VATETaxes.import.showDialog();
});
listview.refresh();
}
};