invoice_az/invoice_az/client/purchase_order.js

1656 lines
72 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() {
// Флаг ставим ПЕРЕД закрытием: hide() его больше не сбрасывает,
// поэтому in-flight callback'и пагинации увидят отмену и выйдут.
ETaxes.cancelLoading = true;
ETaxes.dialogs.hide();
}
});
ETaxes.cancelLoading = false;
ETaxes.loadingDialog.show();
ETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
},
// Обновить сообщение в диалоге
updateLoading: function(title, message, submessage) {
if (!ETaxes.loadingDialog) return;
if (title) $('#loading_title').text(title);
if (message) $('#loading_message').text(message);
if (submessage !== undefined) {
$('#loading_submessage').text(submessage);
$('#loading_submessage').toggle(!!submessage);
}
},
// Показать код верификации
showVerificationCode: function(code) {
if (ETaxes.loadingDialog && code) {
$('#verification_code').text(code);
$('#verification_code_container').show();
}
},
// Установить статус успеха
setSuccess: function(message, submessage, callback, delay = 2000) {
if (!ETaxes.loadingDialog) {
if (callback) callback();
return;
}
ETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
$('#loading_message').text(message);
if (submessage !== undefined) {
$('#loading_submessage').text(submessage);
$('#loading_submessage').toggle(!!submessage);
}
$('#verification_code_container').hide();
ETaxes.loadingDialog.set_primary_action(null);
ETaxes.loadingDialog.set_secondary_action(null);
setTimeout(() => {
this.hide();
if (callback) callback();
}, delay);
},
// Установить статус ошибки
setError: function(message, submessage, showCloseButton = true) {
if (!ETaxes.loadingDialog) return;
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>
`);
$('#loading_message').text(message);
if (submessage !== undefined) {
$('#loading_submessage').text(submessage);
$('#loading_submessage').toggle(!!submessage);
}
$('#verification_code_container').hide();
if (showCloseButton) {
ETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
}
},
// Скрыть диалог
hide: function() {
// Мягкий loadingDialog.hide() в jey_theme/Bootstrap НЕ всегда убирает модал
// (он зависает и следующий диалог накладывается сверху). Принудительно:
// modal('hide') + remove() + чистка осиротевшего backdrop.
// cancelLoading здесь НЕ сбрасываем — иначе отмена потеряется для in-flight
// callback'ов; флаг сбрасывается в начале операции / при создании диалога.
if (ETaxes.loadingDialog) {
try {
ETaxes.loadingDialog.$wrapper.modal('hide');
ETaxes.loadingDialog.$wrapper.remove();
} catch (e) {
console.error("Error hiding loading dialog:", e);
}
ETaxes.loadingDialog = null;
}
if ($('.modal:visible').length === 0) {
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
},
// Получить HTML для диалога загрузки
_getLoadingHTML: function(title, message, submessage) {
return `
<div class="text-center">
<div class="loading-animation" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
</div>
<style>
@keyframes lds-dual-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="status_message">
<h4 id="loading_title">${title || __('Please wait...')}</h4>
<p id="loading_message">${message || __('The operation may take some time')}</p>
<p id="loading_submessage" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
</div>
<div id="verification_code_container" style="display:none; margin-top: 15px;">
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
<span id="verification_code">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
</div>
`;
}
};
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
ETaxes.auth = {
// Проверить токен и обработать аутентификацию при необходимости
checkAndProcess: function(callback) {
ETaxes.utils.checkTokenValidity(function(isValid) {
if (isValid) {
callback();
} else {
frappe.confirm(
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
function() {
ETaxes.auth.startProcess(callback);
},
function() {
frappe.show_alert({
message: __('Authentication canceled. Operation cannot be completed.'),
indicator: 'red'
}, 5);
}
);
}
});
},
// Начать процесс аутентификации
startProcess: function(callback) {
ETaxes.dialogs.showLoading(
__('Starting Authentication'),
__('Getting Asan Login settings...'),
__('Please wait')
);
ETaxes.utils.getDefaultLogin(function(loginData) {
if (loginData && loginData.found) {
const asanLoginName = loginData.name;
ETaxes.dialogs.updateLoading(
null,
__('Sending authentication request...'),
__('This will send a request to your Asan Imza mobile app')
);
frappe.call({
method: 'invoice_az.auth.handle_authentication',
args: { 'asan_login_name': asanLoginName },
callback: function(r) {
if (r.message && r.message.success) {
const bearerToken = r.message.bearer_token;
if (r.message.verification_code) {
ETaxes.dialogs.showVerificationCode(r.message.verification_code);
}
ETaxes.dialogs.updateLoading(
null,
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request')
);
ETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
if (success) {
ETaxes.auth.complete(asanLoginName, callback);
} else {
ETaxes.dialogs.setError(
__('Authentication Failed'),
__('Could not authenticate with Asan Imza')
);
}
});
} else {
ETaxes.dialogs.setError(
__('Authentication Error'),
r.message ? r.message.message : __('Authentication request failed')
);
}
}
});
} else {
ETaxes.dialogs.setError(
__('No Asan Login Settings'),
__('Please configure Asan Login settings first')
);
}
}, false); // Не использовать кэш для аутентификации
},
// Опрос статуса аутентификации
pollStatus: function(asanLoginName, bearerToken, callback) {
let attempts = 0;
let stopPolling = false;
function pollStatusStep() {
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS) {
ETaxes.dialogs.setError(
__('Authentication Timeout'),
__('The authentication request has timed out. Please try again.')
);
callback(false);
}
return;
}
attempts++;
ETaxes.dialogs.updateLoading(
null,
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request') +
' (' + attempts + '/' + ETaxes.AUTH_MAX_ATTEMPTS + ')'
);
frappe.call({
method: 'invoice_az.auth.poll_auth_status',
args: {
'asan_login_name': asanLoginName,
'bearer_token': bearerToken
},
callback: function(r) {
if (r.message && r.message.success) {
if (r.message.authenticated) {
stopPolling = true;
ETaxes.dialogs.setSuccess(
__('Authentication Successful'),
__('You are now authenticated with Asan Imza'),
function() {
callback(true);
}
);
} else {
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
}
} else {
stopPolling = true;
ETaxes.dialogs.setError(
__('Authentication Error'),
r.message ? r.message.message : __('An unknown error occurred')
);
callback(false);
}
},
error: function(err) {
console.error('Error during status check:', err);
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
}
});
}
pollStatusStep();
},
// Завершение аутентификации
complete: function(asanLoginName, callback) {
ETaxes.dialogs.showLoading(
__('Completing Authentication'),
__('Getting certificates...'),
__('Please wait')
);
frappe.call({
method: 'invoice_az.auth.get_auth_certificates',
args: { 'asan_login_name': asanLoginName },
callback: function(certR) {
if (certR.message && certR.message.success) {
ETaxes.dialogs.updateLoading(
null,
__('Certificates retrieved'),
__('Selecting certificate and taxpayer')
);
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Asan Login',
name: asanLoginName
},
callback: function(r) {
if (r.message && r.message.selected_certificate_json) {
try {
const certData = JSON.parse(r.message.selected_certificate_json);
const certName = r.message.selected_certificate;
ETaxes.dialogs.updateLoading(
null,
__('Selecting certificate'),
certName
);
ETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
} catch (e) {
console.error('Error parsing certificate:', e);
ETaxes.dialogs.hide();
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
}
} else {
ETaxes.dialogs.hide();
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
}
}
});
} else {
ETaxes.dialogs.setError(
__('Error'),
certR.message ? certR.message.message : __('Failed to get certificates')
);
}
}
});
},
// Выбор сертификата
_selectCertificate: function(asanLoginName, certData, certName, callback) {
frappe.call({
method: 'invoice_az.auth.select_certificate',
args: {
'asan_login_name': asanLoginName,
'certificate_data': certData,
'certificate_name': certName
},
callback: function(r) {
if (r.message && r.message.success) {
ETaxes.dialogs.updateLoading(
null,
__('Selecting taxpayer'),
__('Using certificate: ') + certName
);
frappe.call({
method: 'invoice_az.auth.select_taxpayer',
args: { 'asan_login_name': asanLoginName },
callback: function(r) {
if (r.message && r.message.success) {
ETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации
ETaxes.dialogs.setSuccess(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback
);
} else {
ETaxes.dialogs.setError(
__('Error'),
r.message ? r.message.message : __('Failed to select taxpayer')
);
}
}
});
} else {
ETaxes.dialogs.setError(
__('Error'),
r.message ? r.message.message : __('Failed to select certificate')
);
}
}
});
},
// Показать селектор сертификатов
_showCertificateSelector: function(certificates, asanLoginName, callback) {
if (!certificates || !certificates.length) {
frappe.msgprint(__('No certificates available'));
return;
}
let certHtml = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
certHtml += '<thead><tr><th>Type</th><th>Name</th><th>ID</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>';
certificates.forEach(function(cert, index) {
let name = '';
let id = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
name = cert.individualInfo.name || '';
id = cert.individualInfo.fin || '';
} else if (cert.legalInfo) {
name = cert.legalInfo.name || '';
id = cert.legalInfo.tin || cert.legalInfo.voen || '';
}
certHtml += '<tr>' +
'<td>' + (cert.taxpayerType || '') + '</td>' +
'<td>' + name + '</td>' +
'<td>' + id + '</td>' +
'<td>' + (cert.position || '') + '</td>' +
'<td>' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + '</td>' +
'<td><button class="btn btn-xs btn-primary select-cert" data-index="' + index + '">Select</button></td>' +
'</tr>';
});
certHtml += '</tbody></table></div>';
const dialog = new frappe.ui.Dialog({
title: __('Select Certificate'),
fields: [{
fieldtype: 'HTML',
fieldname: 'certificates',
options: certHtml
}]
});
dialog.show();
dialog.$wrapper.find('.select-cert').on('click', function() {
const index = $(this).data('index');
const cert = certificates[index];
let certName = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
} else if (cert.legalInfo) {
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
} else {
certName = `Certificate ${index + 1}`;
}
dialog.hide();
ETaxes.dialogs.showLoading(
__('Selecting Certificate'),
__('Processing your selection...'),
certName
);
ETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
});
}
};
// ======= УПРАВЛЕНИЕ ОШИБКАМИ =======
ETaxes.errors = {
// Добавить ошибку в лог
add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
const errorEntry = {
invoice_id: invoiceData.id || 'Unknown',
serial_number: invoiceData.serialNumber || 'Unknown',
sender_name: invoiceData.sender ? invoiceData.sender.name : 'Unknown',
creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown',
error_type: errorType,
error_message: errorMessage,
timestamp: new Date().toISOString(),
...additionalData
};
ETaxes.loadingErrors.push(errorEntry);
console.error('E-Taxes Import Error:', errorEntry);
},
// Очистить лог ошибок
clear: function() {
frappe.confirm(
__('Are you sure you want to clear the error log?'),
function() {
ETaxes.loadingErrors = [];
frappe.show_alert({
message: __('Error log cleared'),
indicator: 'green'
}, 2);
}
);
},
// Показать детальный лог ошибок
showDetails: function() {
if (ETaxes.loadingErrors.length === 0) {
frappe.msgprint(__('No errors to display'));
return;
}
let errorsHtml = '<div class="error-log-container">';
ETaxes.loadingErrors.forEach(function(error, index) {
let documentDate = 'No date';
if (error.creation_date && error.creation_date !== 'Unknown') {
try {
documentDate = moment(error.creation_date).format('DD.MM.YYYY');
} catch (e) {
documentDate = 'Invalid date';
}
}
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
// Заголовок ошибки
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
'<strong>' + (error.serial_number || error.invoice_id) + '</strong></h5>';
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
errorsHtml += '</div>';
errorsHtml += '<small style="color: var(--text-muted);">' + error.sender_name + '</small>';
errorsHtml += '</div>';
// Сообщение об ошибке
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
errorsHtml += '</div>';
// Детали ошибки
if (error.unmatched_items && error.unmatched_items.length > 0) {
errorsHtml += '<div class="error-details">';
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped items:') + '</div>';
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
error.unmatched_items.forEach(function(item, idx) {
errorsHtml += '<li><strong>Row ' + (idx + 1) + ':</strong> ' + item.name;
if (item.code) {
errorsHtml += ' <em>(Code: ' + item.code + ')</em>';
}
errorsHtml += '</li>';
});
errorsHtml += '</ul></div>';
} else if (error.unmatched_parties && error.unmatched_parties.length > 0) {
errorsHtml += '<div class="error-details">';
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped parties:') + '</div>';
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
error.unmatched_parties.forEach(function(party, idx) {
errorsHtml += '<li><strong>Party ' + (idx + 1) + ':</strong> ' + party.name;
if (party.tin) {
errorsHtml += ' <em>(TIN: ' + party.tin + ')</em>';
}
if (party.type) {
errorsHtml += ' <span class="label label-info" style="font-size: 10px;">' + party.type + '</span>';
}
errorsHtml += '</li>';
});
errorsHtml += '</ul></div>';
} else if (error.unmatched_units && error.unmatched_units.length > 0) {
errorsHtml += '<div class="error-details">';
errorsHtml += '<div style="font-weight: 600; margin-bottom: 5px; color: var(--text-color);">' + __('Unmapped units:') + '</div>';
errorsHtml += '<ul style="margin: 0; padding-left: 20px; color: var(--text-muted);">';
error.unmatched_units.forEach(function(unit, idx) {
errorsHtml += '<li><strong>Unit ' + (idx + 1) + ':</strong> ' + unit.name + '</li>';
});
errorsHtml += '</ul></div>';
}
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
errorsHtml += '</div>';
errorsHtml += '</div>';
});
errorsHtml += '</div>';
// Статистика
const errorTypes = {};
ETaxes.loadingErrors.forEach(function(error) {
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
});
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
statsHtml += '<div>';
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + ETaxes.loadingErrors.length + '<br>';
statsHtml += '<span style="color: var(--text-muted);">';
Object.keys(errorTypes).forEach(function(type, index) {
if (index > 0) statsHtml += ' • ';
statsHtml += type + ': ' + errorTypes[type];
});
statsHtml += '</span>';
statsHtml += '</div>';
statsHtml += '<div>';
statsHtml += '<button class="btn btn-default btn-sm" id="export-csv-dialog-btn" style="margin-right: 5px;">' +
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
statsHtml += '<button class="btn btn-warning btn-sm" id="clear-log-dialog-btn">' +
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
statsHtml += '</div></div>';
const d = new frappe.ui.Dialog({
title: __('Error Details') + ' (' + ETaxes.loadingErrors.length + ')',
size: 'large',
fields: [
{
fieldname: 'stats_html',
fieldtype: 'HTML',
options: statsHtml
},
{
fieldname: 'errors_html',
fieldtype: 'HTML',
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
}
],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Добавляем обработчики событий
setTimeout(function() {
$('#export-csv-dialog-btn').off('click').on('click', function() {
ETaxes.errors.exportCSV();
});
$('#clear-log-dialog-btn').off('click').on('click', function() {
ETaxes.errors.clear();
d.hide();
});
}, 200);
},
// Экспорт в CSV
exportCSV: function() {
if (ETaxes.loadingErrors.length === 0) {
frappe.msgprint(__('No errors to export'));
return;
}
try {
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
ETaxes.loadingErrors.forEach(function(error) {
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
const cleanSender = error.sender_name.replace(/"/g, '""');
const row = [
error.invoice_id,
error.serial_number,
error.creation_date,
cleanSender,
error.error_type,
cleanMessage,
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
].map(function(field) {
return '"' + (field || '') + '"';
}).join(',');
csvContent += row + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "etaxes_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
frappe.show_alert({
message: __('Error log exported successfully'),
indicator: 'green'
}, 3);
} catch (e) {
console.error('Export error:', e);
frappe.show_alert({
message: __('Failed to export error log'),
indicator: 'red'
}, 3);
}
},
// Показать сводку результатов
showSummary: function(totalInvoices, processedCount, errorsCount) {
let title = '';
let indicator = '';
let message = '';
if (errorsCount === 0) {
title = __('Import Completed Successfully');
indicator = 'green';
message = __('All ') + totalInvoices + __(' invoices were imported successfully.');
frappe.msgprint({
title: title,
indicator: indicator,
message: message
});
} else {
title = errorsCount === totalInvoices ? __('Import Failed') : __('Import Completed with Errors');
indicator = processedCount > 0 ? 'orange' : 'red';
message = '<div style="margin-bottom: 15px;">';
if (processedCount > 0) {
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' invoices') + '<br>';
}
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' invoices');
message += '</div>';
message += '<div style="text-align: center;">' +
'<button class="btn btn-primary btn-sm" id="view-error-details-btn" style="margin-right: 10px;">' +
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
'<button class="btn btn-default btn-sm" id="export-error-log-btn">' +
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
'</div>';
const summaryDialog = frappe.msgprint({
title: title,
indicator: indicator,
message: message
});
// Добавляем обработчики событий
setTimeout(function() {
$('#view-error-details-btn').off('click').on('click', function() {
summaryDialog.hide();
ETaxes.errors.showDetails();
});
$('#export-error-log-btn').off('click').on('click', function() {
ETaxes.errors.exportCSV();
});
}, 200);
}
}
};
// ======= МОДУЛЬ ИМПОРТА ИНВОЙСОВ =======
ETaxes.import = {
// Показать диалог импорта из E-Taxes
showDialog: function() {
ETaxes.auth.checkAndProcess(function() {
const minDate = moment("2020-01-01", "YYYY-MM-DD");
const d = new frappe.ui.Dialog({
title: __('Import invoice from E-Taxes'),
fields: [
{
fieldname: 'date_range_section',
fieldtype: 'Section Break',
label: __('Period')
},
{
fieldname: 'creationDateFrom',
fieldtype: 'Date',
label: __('Date from'),
default: moment().subtract(1, 'month').format('YYYY-MM-DD')
},
{
fieldname: 'creationDateTo',
fieldtype: 'Date',
label: __('Date to'),
default: moment().format('YYYY-MM-DD')
},
{
fieldname: 'settings_section',
fieldtype: 'Section Break',
label: __('Settings')
},
{
fieldname: 'warehouse',
fieldtype: 'Link',
label: __('Default warehouse'),
options: 'Warehouse',
reqd: true,
get_query: function() {
return {
filters: {
'is_group': 0,
'disabled': 0
}
};
}
}
],
primary_action_label: __('Search'),
primary_action: function() {
const values = d.get_values();
// Validate date range
const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
const todayMoment = moment();
if (fromDateMoment.isBefore(minDate)) {
frappe.msgprint(__('From Date cannot be earlier than January 1, 2020'));
return;
}
if (toDateMoment.isAfter(todayMoment)) {
frappe.msgprint(__('To Date cannot be later than today'));
return;
}
if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
frappe.msgprint(__('The date range cannot exceed one year.'));
return;
}
if (!values.warehouse) {
frappe.msgprint({
title: __('Warning'),
indicator: 'orange',
message: __('Please select a default warehouse')
});
return;
}
const fromDate = moment(values.creationDateFrom).format('DD-MM-YYYY 00:00');
const toDate = moment(values.creationDateTo).format('DD-MM-YYYY 23:59');
d.hide();
ETaxes.import.loadInvoices(fromDate, toDate, values.warehouse);
}
});
d.show();
});
},
// Загрузить инвойсы с фильтрацией дубликатов
loadInvoices: function(fromDate, toDate, warehouse, accumulatedInvoices = [], offset = 0) {
ETaxes.cancelLoading = false;
$(document).off('progress-cancel.etaxes_import');
if (offset === 0) {
frappe.show_alert({
message: __('Fetching E-Taxes invoices...'),
indicator: 'blue'
}, 3);
}
ETaxes.utils.getDefaultLogin(function(loginResponse) {
if (ETaxes.cancelLoading) return;
if (loginResponse && loginResponse.found) {
const mainToken = loginResponse.main_token;
if (!mainToken) {
frappe.msgprint({
title: __('Authentication Error'),
indicator: 'red',
message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
});
return;
}
const maxCount = 200;
frappe.call({
method: 'invoice_az.api.get_invoices',
args: {
'token': mainToken,
'filters': JSON.stringify({
"creationDateFrom": fromDate,
"creationDateTo": toDate,
"maxCount": maxCount,
"offset": offset
})
},
callback: function(r) {
if (ETaxes.cancelLoading) return;
if (r.message && !r.message.error) {
const currentInvoices = r.message.data || r.message.invoices || [];
const hasMore = r.message.hasMore || false;
const allInvoices = accumulatedInvoices.concat(currentInvoices);
if (hasMore && currentInvoices.length > 0) {
const newOffset = offset + currentInvoices.length;
ETaxes.import.loadInvoices(fromDate, toDate, warehouse, allInvoices, newOffset);
} else {
if (allInvoices.length > 0) {
frappe.show_alert({
message: __('Processing ') + allInvoices.length + __(' invoices...'),
indicator: 'blue'
}, 3);
ETaxes.import.filterDuplicates(allInvoices, mainToken, warehouse);
} else {
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
message: __('No invoices found for the specified period')
});
}
}
} else if (r.message && r.message.error === 'unauthorized') {
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
ETaxes.auth.startProcess(function() {
ETaxes.import.loadInvoices(fromDate, toDate, warehouse);
});
},
function() {
frappe.show_alert({
message: __('Authentication canceled. Operation cannot be completed.'),
indicator: 'red'
}, 5);
}
);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Error loading invoices')
});
}
},
error: function(xhr, status, error) {
console.error("Error fetching invoices:", error);
frappe.msgprint({
title: __('Network Error'),
indicator: 'red',
message: __('Error fetching data from server: ') + (error || 'Unknown error')
});
}
});
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('E-Taxes authentication settings not found')
});
}
});
},
// Фильтрация дубликатов
filterDuplicates: function(allInvoices, token, warehouse) {
frappe.call({
method: 'invoice_az.api.get_etaxes_purchases',
callback: function(etaxesR) {
try {
if (ETaxes.cancelLoading) return;
const importedInvoices = {};
if (etaxesR.message && etaxesR.message.success && etaxesR.message.purchases) {
etaxesR.message.purchases.forEach(function(purchase) {
if (purchase.etaxes_id) {
const normId = String(purchase.etaxes_id).trim();
importedInvoices[normId] = true;
}
});
}
const filteredInvoices = [];
for (let i = 0; i < allInvoices.length; i++) {
const invoice = allInvoices[i];
const invoiceId = String(invoice.id).trim();
if (!importedInvoices.hasOwnProperty(invoiceId)) {
filteredInvoices.push(invoice);
}
}
if (filteredInvoices.length > 0) {
ETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
} else {
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
message: __('No new invoices found for the specified period (all ' +
allInvoices.length + ' are already imported)')
});
}
} catch (e) {
console.error("Error processing purchase data:", e);
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('An error occurred while processing data: ') + e.message
});
}
},
error: function(err) {
console.error("Error fetching purchases:", err);
ETaxes.import.showInvoiceSelection(allInvoices, token, warehouse);
frappe.show_alert({
message: __('Failed to check for duplicates. Showing all invoices.'),
indicator: 'orange'
}, 5);
}
});
},
// Показать диалог выбора инвойсов
showInvoiceSelection: function(invoices, token, warehouse) {
let invoiceTable = '<div class="etaxes-purchase-scroll" style="max-height: calc(100vh - 360px); min-height: 220px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 6px;"><table class="table table-bordered etaxes-invoices-table" style="width: 100%; table-layout: fixed; margin-bottom: 0;">';
invoiceTable += '<thead style="position: sticky; top: 0; z-index: 1;"><tr>' +
'<th style="width: 6%; background: var(--control-bg, var(--bg-color));"><input type="checkbox" class="select-all-invoices"></th>' +
'<th style="width: 24%; background: var(--control-bg, var(--bg-color));">' + __('Number') + '</th>' +
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Date') + '</th>' +
'<th style="width: 38%; background: var(--control-bg, var(--bg-color));">' + __('Supplier') + '</th>' +
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('Amount') + '</th>' +
'</tr></thead><tbody>';
invoices.forEach(function(invoice) {
const serialNumber = invoice.serialNumber || '';
let creationDate = '';
if (invoice.createdAt) {
creationDate = moment(invoice.createdAt).format('DD.MM.YYYY');
} else if (invoice.creationDate) {
creationDate = moment(invoice.creationDate).format('DD.MM.YYYY');
}
const senderName = invoice.sender ? invoice.sender.name : (invoice.senderName || '');
const amount = invoice.totalAmount || invoice.amount || 0;
invoiceTable += '<tr>' +
'<td><input type="checkbox" class="select-invoice" data-id="' + invoice.id + '"></td>' +
'<td style="word-break: break-word;">' + serialNumber + '</td>' +
'<td>' + creationDate + '</td>' +
'<td style="word-break: break-word;">' + senderName + '</td>' +
'<td style="text-align: right;">' + ETaxes.utils.formatCurrency(amount) + '</td>' +
'</tr>';
});
invoiceTable += '</tbody></table></div>';
const infoMessage =
'<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:8px;">' +
'<span class="indicator-pill blue" style="font-size:13px;">' +
__('New invoices found: ') + '<strong>' + invoices.length + '</strong>' +
'</span>' +
'<span style="color: var(--text-muted);">' +
__('Selected warehouse: ') + '<strong>' + frappe.utils.escape_html(warehouse) + '</strong>' +
'</span>' +
'</div>';
const d = new frappe.ui.Dialog({
title: __('Select invoice'),
size: 'large',
fields: [
{
fieldname: 'info_html',
fieldtype: 'HTML',
options: infoMessage
},
{
fieldname: 'invoices_html',
fieldtype: 'HTML',
options: invoiceTable
}
],
primary_action_label: __('Load selected'),
primary_action: function() {
const selectedInvoiceIds = [];
d.$wrapper.find('.select-invoice:checked').each(function() {
selectedInvoiceIds.push($(this).data('id'));
});
if (selectedInvoiceIds.length === 0) {
frappe.msgprint({
title: __('Warning'),
indicator: 'orange',
message: __('No invoices selected')
});
return;
}
d.hide();
ETaxes.cancelLoading = false;
ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
d.hide();
}
});
d.$wrapper.find('.modal-dialog').css({
'max-width': '80%',
'width': '80%',
'margin': '30px auto'
});
d.$wrapper.find('.modal-body').css({
'padding': '15px'
});
d.show();
// Обработчик для чекбокса "выбрать все"
d.$wrapper.find('.select-all-invoices').on('change', function() {
const isChecked = $(this).prop('checked');
d.$wrapper.find('.select-invoice').prop('checked', isChecked);
});
},
// Надёжное закрытие прогресс-бара. frappe.hide_progress() в jey_theme/Bootstrap
// не всегда убирает модал (он зависает, и summary/диалог накладывается сверху).
// Принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
_closeProgress: function() {
if (frappe.cur_progress) {
try {
frappe.cur_progress.$wrapper.modal('hide');
frappe.cur_progress.$wrapper.remove();
} catch (e) {}
frappe.cur_progress = null;
}
if ($('.modal:visible').length === 0) {
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
},
// Загрузка выбранных инвойсов
loadSelectedInvoices: function(invoiceIds, token, warehouse, processedCount = 0, currentIndex = 0) {
if (processedCount === 0 && currentIndex === 0) {
ETaxes.loadingErrors = [];
window.totalInvoicesToProcess = invoiceIds.length;
}
$(document).off('progress-cancel.loading_invoices');
if (invoiceIds.length === 0) {
ETaxes.import._closeProgress();
const errorsCount = ETaxes.loadingErrors.length;
const totalInvoices = window.totalInvoicesToProcess || processedCount;
ETaxes.errors.showSummary(totalInvoices, processedCount, errorsCount);
window.totalInvoicesToProcess = null;
if (cur_list) {
cur_list.refresh();
}
return;
}
if (ETaxes.cancelLoading) {
invoiceIds = [];
ETaxes.import._closeProgress();
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
return;
}
const invoiceId = invoiceIds.shift();
const isLastInvoice = invoiceIds.length === 0;
const totalToProcess = window.totalInvoicesToProcess || (processedCount + invoiceIds.length + 1);
currentIndex++;
frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
__('Loading invoice ') + currentIndex + __(' of ') + totalToProcess, null, true);
$(document).on('progress-cancel.loading_invoices', function() {
ETaxes.cancelLoading = true;
frappe.show_alert({
message: __('Cancelling import... Finishing current invoice.'),
indicator: 'orange'
}, 3);
ETaxes.import._closeProgress();
});
frappe.call({
method: 'invoice_az.api.get_invoice_details',
args: {
'token': token,
'invoice_id': invoiceId
},
callback: function(r) {
if (r.message && !r.message.error) {
const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
const serialNumber = r.message.serialNumber || '';
const senderName = r.message.sender ? r.message.sender.name : '';
const total = r.message.totalAmount || r.message.amount || 0;
frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
__('Importing invoice: ') + serialNumber);
frappe.call({
method: 'invoice_az.api.import_invoice_with_mapping',
args: {
'invoice_data': r.message,
'purchase_order_name': null,
'schedule_date': scheduleDate,
'warehouse': warehouse
},
callback: function(importR) {
ETaxes.import._handleImportResult(importR, r.message, invoiceId, scheduleDate, senderName, total,
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
},
error: function(xhr, status, error) {
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
ETaxes.errors.add(r.message, 'Network Error', errorMessage, {
invoice_items: r.message.items || [],
xhr_status: xhr.status,
xhr_response: xhr.responseText
});
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
}
});
} else if (r.message && r.message.error === 'unauthorized') {
ETaxes.import._closeProgress();
$(document).off('progress-cancel.loading_invoices');
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
ETaxes.auth.startProcess(function() {
ETaxes.utils.getDefaultLogin(function(loginR) {
if (loginR && loginR.found && loginR.main_token) {
invoiceIds.unshift(invoiceId);
ETaxes.import.loadSelectedInvoices(invoiceIds, loginR.main_token, warehouse, processedCount, currentIndex - 1);
} else {
ETaxes.errors.add({id: invoiceId}, 'Authentication Error',
'Failed to get token after authentication');
const errorsCount = ETaxes.loadingErrors.length;
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
}
}, false);
});
},
function() {
ETaxes.errors.add({id: invoiceId}, 'Authentication Cancelled',
'User cancelled authentication process');
const errorsCount = ETaxes.loadingErrors.length;
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
}
);
} else {
const errorMessage = r.message ? r.message.message : 'Failed to load invoice details';
ETaxes.errors.add({id: invoiceId}, 'Invoice Data Error', errorMessage, {
server_response: r.message
});
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
}
},
error: function(xhr, status, error) {
const errorMessage = 'Network error loading invoice details: ' + (error || 'Unknown network error');
ETaxes.errors.add({id: invoiceId}, 'Network Error', errorMessage, {
xhr_status: xhr.status,
xhr_response: xhr.responseText
});
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
}
});
},
// Обработка результата импорта
_handleImportResult: function(importR, invoiceMessage, invoiceId, scheduleDate, senderName, total,
invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
if (importR.message && importR.message.success) {
frappe.show_alert({
message: __('Invoice successfully imported: ') + (invoiceMessage.serialNumber || ''),
indicator: 'green'
}, 3);
const purchaseOrderName = importR.message.purchase_order;
frappe.call({
method: 'invoice_az.api.create_etaxes_purchase',
args: {
'etaxes_id': invoiceId,
'date': scheduleDate,
'party': senderName,
'total': total
},
callback: function(etaxesR) {
if (etaxesR.message && etaxesR.message.success) {
frappe.call({
method: 'invoice_az.api.link_purchase_order_to_etaxes',
args: {
'purchase_order': purchaseOrderName,
'etaxes_purchase': etaxesR.message.name
},
callback: function(linkR) {
processedCount++;
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
}
});
} else {
ETaxes.errors.add(invoiceMessage, 'E-Taxes Purchase Creation Error',
etaxesR.message ? etaxesR.message.message : 'Failed to create E-Taxes Purchase record', {
server_response: etaxesR.message
});
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
}
}
});
} else if (importR.message && (importR.message.unmatched_items || importR.message.unmatched_parties || importR.message.unmatched_units)) {
let errorType = '';
const additionalData = { invoice_items: invoiceMessage.items || [] };
if (importR.message.unmatched_items) {
errorType = 'Unmapped Items';
additionalData.unmatched_items = importR.message.unmatched_items;
} else if (importR.message.unmatched_parties) {
errorType = 'Unmapped Parties';
additionalData.unmatched_parties = importR.message.unmatched_parties;
} else if (importR.message.unmatched_units) {
errorType = 'Unmapped Units';
additionalData.unmatched_units = importR.message.unmatched_units;
}
additionalData.server_response = importR.message;
ETaxes.errors.add(invoiceMessage, errorType, importR.message.message, additionalData);
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
} else {
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
const additionalData = {
invoice_items: invoiceMessage.items || [],
server_response: importR.message
};
if (importR.exc) {
additionalData.stack_trace = importR.exc;
}
ETaxes.errors.add(invoiceMessage, 'Import Error', errorMessage, additionalData);
ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
}
},
// Продолжить или завершить обработку
_continueOrFinish: function(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
if (isLastInvoice) {
ETaxes.import._closeProgress();
$(document).off('progress-cancel.loading_invoices');
const totalToProcess = window.totalInvoicesToProcess || processedCount;
const errorsCount = ETaxes.loadingErrors.length;
ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
if (cur_list) {
cur_list.refresh();
}
return;
}
setTimeout(function() {
if (invoiceIds.length === 0) {
ETaxes.import._closeProgress();
}
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
}, ETaxes.PROGRESS_UPDATE_DELAY);
},
// Socket.IO bulk import
loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
ETaxes.loadingErrors = [];
const total = invoiceIds.length;
// Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
// frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
// уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
let importFinished = false;
// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
const closeProgress = function() {
if (frappe.cur_progress) {
try {
frappe.cur_progress.$wrapper.modal('hide');
frappe.cur_progress.$wrapper.remove();
} catch (e) {}
frappe.cur_progress = null;
}
if ($('.modal:visible').length === 0) {
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
};
// Progress + cancel are shown by the global Background Tasks widget
// (bottom-right). No blocking modal here; we keep only the complete
// subscription to show the final import summary.
frappe.realtime.off('purchase_import_complete');
frappe.realtime.on('purchase_import_complete', function(data) {
importFinished = true;
frappe.realtime.off('purchase_import_progress');
frappe.realtime.off('purchase_import_complete');
closeProgress();
(data.errors || []).forEach(function(err) {
ETaxes.loadingErrors.push({
invoice_id: err.invoice_id || 'Unknown',
error_type: err.unmatched_items ? 'Unmapped Items'
: err.unmatched_parties ? 'Unmapped Parties'
: err.unmatched_units ? 'Unmapped Units'
: 'Import Error',
error_message: err.error || 'Unknown error',
unmatched_items: err.unmatched_items,
unmatched_parties: err.unmatched_parties,
unmatched_units: err.unmatched_units
});
});
const errorsCount = ETaxes.loadingErrors.length;
ETaxes.errors.showSummary(data.total, data.imported, errorsCount);
if (cur_list) {
cur_list.refresh();
}
});
frappe.call({
method: 'invoice_az.api.import_bulk_purchase_invoices',
args: {
invoice_ids: JSON.stringify(invoiceIds),
token: token,
warehouse: warehouse
},
callback: function(r) {
if (!r.message || !r.message.enqueued) {
frappe.realtime.off('purchase_import_progress');
frappe.realtime.off('purchase_import_complete');
closeProgress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Failed to start import job')
});
}
},
error: function() {
frappe.realtime.off('purchase_import_progress');
frappe.realtime.off('purchase_import_complete');
closeProgress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
}
};
// ======= ОСНОВНЫЕ ОБРАБОТЧИКИ ФОРМ И СПИСКОВ =======
// Purchase Order Form
frappe.ui.form.on('Purchase Order', {
refresh: function(frm) {
if (frm.doc.is_taxes_doc) {
frm.set_read_only(true);
frm.page.set_indicator(__('Tax Document'), 'blue');
}
},
onload: function(frm) {
frm.set_df_property('is_taxes_doc', 'read_only', 1);
}
});
// Purchase Order List
frappe.listview_settings['Purchase Order'] = {
add_fields: ['supplier', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
get_indicator: function(doc) {
if (doc.status === 'Closed') {
return [__('Closed'), 'green', 'status,=,Closed'];
} else if (doc.status === 'On Hold') {
return [__('On Hold'), 'orange', 'status,=,On Hold'];
} else if (doc.status === 'Delivered') {
return [__('Delivered'), 'green', 'status,=,Delivered'];
} else if (doc.docstatus == 1) {
return [__('To Receive and Bill'), 'orange', 'docstatus,=,1'];
} else if (doc.docstatus == 0) {
return [__('Draft'), 'red', 'docstatus,=,0'];
} else if (doc.docstatus == 2) {
return [__('Cancelled'), 'grey', 'docstatus,=,2'];
}
},
onload: function(listview) {
listview.page.add_inner_button(__('Import from E-Taxes'), function() {
ETaxes.import.showDialog();
}, __('E-Taxes'));
listview.page.add_inner_button(__('Open Settings'), function() {
frappe.set_route('Form', 'E-Taxes Settings');
}, __('E-Taxes'));
listview.columns.push({
type: 'Check',
df: {
label: __('Tax Doc'),
fieldname: 'is_taxes_doc'
},
width: 120
});
listview.refresh();
},
formatters: {
is_taxes_doc: function(value) {
return value ?
`<span class="indicator-pill green"></span>` :
`<span class="indicator-pill gray"></span>`;
}
}
};
// Purchase Invoice Form
frappe.ui.form.on('Purchase Invoice', {
refresh: function(frm) {
if (frm.doc.is_taxes_doc) {
frm.set_read_only(true);
frm.page.set_indicator(__('Tax Document'), 'blue');
}
},
onload: function(frm) {
frm.set_df_property('is_taxes_doc', 'read_only', 1);
}
});
// Purchase Invoice List
frappe.listview_settings['Purchase Invoice'] = {
onload: function(listview) {
listview.columns.push({
type: 'Check',
df: {
label: __('Tax Doc'),
fieldname: 'is_taxes_doc'
},
width: 120
});
listview.refresh();
},
formatters: {
is_taxes_doc: function(value) {
return value ?
`<span class="indicator-pill green"></span>` :
`<span class="indicator-pill gray"></span>`;
}
}
};