invoice_az/invoice_az/client/sales_invoice.js

2228 lines
85 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 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() {
ETaxes.cancelLoading = true;
ETaxes.loadingDialog.$wrapper.find('.primary-action').prop('disabled', true);
ETaxes.loadingDialog.$wrapper.find('.primary-action').html(__('Cancelling...'));
$('#status_message p').text(__('Cancelling the operation.'));
}
});
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() {
if (ETaxes.loadingDialog) {
try {
ETaxes.loadingDialog.hide();
} catch (e) {
console.error("Error hiding loading dialog:", e);
}
ETaxes.loadingDialog = null;
}
ETaxes.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>
`;
}
};
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
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 (!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 style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered etaxes-invoices-table" style="width: 100%; table-layout: fixed;">';
invoiceTable += '<thead><tr>' +
'<th style="width: 6%;"><input type="checkbox" class="select-all-invoices"></th>' +
'<th style="width: 24%;">' + __('Number') + '</th>' +
'<th style="width: 16%;">' + __('Date') + '</th>' +
'<th style="width: 38%;">' + __('Supplier') + '</th>' +
'<th style="width: 16%;">' + __('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 infoMessage = '<div class="alert alert-info">' +
__('New invoices found: ') + invoices.length +
'</div>';
const d = new frappe.ui.Dialog({
title: __('Select invoice'),
size: 'large',
fields: [
{
fieldname: 'info_html',
fieldtype: 'HTML',
options: infoMessage
},
{
fieldname: 'invoices_html',
fieldtype: 'HTML',
options: invoiceTable
},
{
fieldname: 'warehouse_display',
fieldtype: 'HTML',
options: '<div class="row"><div class="col-xs-12"><div class="alert alert-info">' +
__('Selected warehouse: ') + '<strong>' + warehouse + '</strong></div></div></div>'
}
],
primary_action_label: __('Load selected'),
primary_action: function() {
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);
});
},
// Загрузка выбранных инвойсов
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) {
frappe.hide_progress();
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 = [];
frappe.hide_progress();
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);
frappe.hide_progress();
});
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') {
frappe.hide_progress();
$(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) {
frappe.hide_progress();
$(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) {
frappe.hide_progress();
}
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;
frappe.show_progress(
__('Importing Invoices'), 0, total,
__('Starting import of {0} invoices...', [total])
);
frappe.realtime.off('purchase_import_progress');
frappe.realtime.on('purchase_import_progress', function(data) {
frappe.show_progress(
__('Importing Invoices'), data.current, data.total,
__('Importing invoice {0} of {1}', [data.current, data.total])
);
});
frappe.realtime.off('purchase_import_complete');
frappe.realtime.on('purchase_import_complete', function(data) {
frappe.realtime.off('purchase_import_progress');
frappe.realtime.off('purchase_import_complete');
frappe.hide_progress();
(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');
frappe.hide_progress();
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');
frappe.hide_progress();
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');
// 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 Azerbaijan 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
});
}
/**
* List view settings
*/
frappe.listview_settings['Sales Invoice'] = {
onload: function(listview) {
// Add field as column (proper way)
listview.columns.push({
type: 'Check',
df: {
label: __('Tax Doc'),
fieldname: 'is_taxes_doc'
},
width: 120
});
// Add E-Taxes status column
listview.columns.push({
type: 'Select',
df: {
label: __('E-Taxes Status'),
fieldname: 'etaxes_send_status'
},
width: 150
});
// Force refresh list with new columns
listview.refresh();
},
// Format is_taxes_doc field display
formatters: {
is_taxes_doc: function(value) {
return value ?
`<span class="indicator-pill green" title="${__('Tax Document')}"></span>` :
`<span class="indicator-pill gray" title="${__('Regular Document')}"></span>`;
},
etaxes_send_status: function(value) {
if (value === 'Sent and Signed') {
return `<span class="indicator-pill green">${__('Sent and Signed')}</span>`;
} else if (value === 'Created, not signed') {
return `<span class="indicator-pill orange">${__('Draft Created')}</span>`;
} else if (value === 'Not Sent') {
return `<span class="indicator-pill gray">${__('Not Sent')}</span>`;
}
return value || '';
}
}
};