fix(etaxes): reliable progress-bar close + İzahat column & classification code
Realtime import progress modal could hang when a late 'progress' event
arrived after 'complete' (hide_progress nulls cur_progress, show_progress
then recreates an unmanaged modal). Guard every handler with an
importFinished flag and force-close via modal('hide')+remove()+backdrop
cleanup across company/employee/journal_entry/purchase_order/sales_*.
Also: show VAT operation İzahat (explanation) column in the selection
table and on the Journal Entry list; auto-create missing "Classification
code" records from taxCodeInfo so the Link field always resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b6b746793
commit
e67a2d2ec1
|
|
@ -864,6 +864,25 @@ function run_data_loading(frm, loaders) {
|
|||
// Handlers are closed over per-run so we can detach cleanly at end
|
||||
let progress_handler = null;
|
||||
let complete_handler = null;
|
||||
// Защита от гонки 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');
|
||||
}
|
||||
};
|
||||
|
||||
const detach = function() {
|
||||
if (progress_handler) frappe.realtime.off(PROGRESS_EVENT, progress_handler);
|
||||
|
|
@ -873,6 +892,7 @@ function run_data_loading(frm, loaders) {
|
|||
};
|
||||
|
||||
progress_handler = function(data) {
|
||||
if (importFinished) return;
|
||||
if (!data || !data.total) return;
|
||||
const pct = Math.round((data.current / data.total) * 100);
|
||||
const desc = data.message || '';
|
||||
|
|
@ -880,8 +900,9 @@ function run_data_loading(frm, loaders) {
|
|||
};
|
||||
|
||||
complete_handler = function(data) {
|
||||
importFinished = true;
|
||||
detach();
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
|
||||
if (data && data.error === 'unauthorized') {
|
||||
frappe.confirm(
|
||||
|
|
@ -925,7 +946,7 @@ function run_data_loading(frm, loaders) {
|
|||
const msg = r.message || {};
|
||||
if (!msg.enqueued) {
|
||||
detach();
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Failed to Start'),
|
||||
message: msg.message || __('Could not enqueue the loading job.'),
|
||||
|
|
@ -935,7 +956,7 @@ function run_data_loading(frm, loaders) {
|
|||
},
|
||||
error: function() {
|
||||
detach();
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -703,6 +703,11 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
|||
}
|
||||
};
|
||||
|
||||
// Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
|
||||
// frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
|
||||
// уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
|
||||
let importFinished = false;
|
||||
|
||||
let dialog = frappe.show_progress(
|
||||
__('Importing from ƏMAS'), 0, total,
|
||||
total
|
||||
|
|
@ -713,6 +718,7 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
|||
|
||||
frappe.realtime.off('amas_import_progress');
|
||||
frappe.realtime.on('amas_import_progress', function(data) {
|
||||
if (importFinished) return;
|
||||
const is_save = data.phase === 'save';
|
||||
const verb = is_save ? __('Saving: ') : __('Fetching: ');
|
||||
const detail = data.employee_name
|
||||
|
|
@ -726,9 +732,10 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
|||
|
||||
frappe.realtime.off('amas_import_complete');
|
||||
frappe.realtime.on('amas_import_complete', function(data) {
|
||||
importFinished = true;
|
||||
frappe.realtime.off('amas_import_progress');
|
||||
frappe.realtime.off('amas_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeAmasProgress();
|
||||
cleanup_cancel_dialog();
|
||||
|
||||
if (data.cancelled) {
|
||||
|
|
@ -767,6 +774,22 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
|||
});
|
||||
}
|
||||
|
||||
// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
|
||||
// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
|
||||
function closeAmasProgress() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
function create_employees(listview, asan_login_name, selected_employees, company, create_designation) {
|
||||
const total = selected_employees.length;
|
||||
|
||||
|
|
@ -784,7 +807,7 @@ function create_employees(listview, asan_login_name, selected_employees, company
|
|||
if (r.message && r.message.already_running) {
|
||||
frappe.realtime.off('amas_import_progress');
|
||||
frappe.realtime.off('amas_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeAmasProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Import Already Running'),
|
||||
indicator: 'orange',
|
||||
|
|
@ -795,7 +818,7 @@ function create_employees(listview, asan_login_name, selected_employees, company
|
|||
if (!r.message || !r.message.enqueued) {
|
||||
frappe.realtime.off('amas_import_progress');
|
||||
frappe.realtime.off('amas_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeAmasProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -806,7 +829,7 @@ function create_employees(listview, asan_login_name, selected_employees, company
|
|||
error: function() {
|
||||
frappe.realtime.off('amas_import_progress');
|
||||
frappe.realtime.off('amas_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeAmasProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
|
|||
|
|
@ -805,6 +805,8 @@ VATETaxes.errors = {
|
|||
let message = '';
|
||||
|
||||
if (errorsCount === 0) {
|
||||
// Прогресс-бар к этому моменту уже надёжно закрыт через _closeProgress,
|
||||
// поэтому показываем нормальное окно-итог (а не тост).
|
||||
title = __('Import Completed Successfully');
|
||||
indicator = 'green';
|
||||
message = __('All ') + totalOperations + __(' VAT operations were imported successfully.');
|
||||
|
|
@ -835,11 +837,14 @@ VATETaxes.errors = {
|
|||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
||||
'</div>';
|
||||
|
||||
const summaryDialog = frappe.msgprint({
|
||||
// Отдельный frappe.ui.Dialog (НЕ общий frappe.msg_dialog), чтобы остаточный
|
||||
// прогресс-бар из show_progress не протекал в это окно.
|
||||
const summaryDialog = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
indicator: indicator,
|
||||
message: message
|
||||
fields: [{ fieldtype: 'HTML', fieldname: 'summary_html', options: message }]
|
||||
});
|
||||
summaryDialog.show();
|
||||
|
||||
// Добавляем обработчики событий
|
||||
setTimeout(function() {
|
||||
|
|
@ -942,10 +947,11 @@ VATETaxes.import = {
|
|||
$(document).off('progress-cancel.vat_import');
|
||||
|
||||
if (offset === 0) {
|
||||
frappe.show_alert({
|
||||
message: __('Fetching VAT operations from E-Taxes...'),
|
||||
indicator: 'blue'
|
||||
}, 3);
|
||||
VATETaxes.dialogs.showLoading(
|
||||
__('Loading E-Taxes Documents'),
|
||||
__('Fetching VAT operations from E-Taxes...'),
|
||||
__('Please wait')
|
||||
);
|
||||
}
|
||||
|
||||
VATETaxes.utils.getDefaultLogin(function(loginResponse) {
|
||||
|
|
@ -955,6 +961,7 @@ VATETaxes.import = {
|
|||
const mainToken = loginResponse.main_token;
|
||||
|
||||
if (!mainToken) {
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Authentication Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -984,16 +991,23 @@ VATETaxes.import = {
|
|||
|
||||
if (hasMore && currentOperations.length > 0) {
|
||||
const newOffset = offset + currentOperations.length;
|
||||
VATETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Fetching VAT operations from E-Taxes...'),
|
||||
__('Loaded ') + allOperations.length + __(' so far...')
|
||||
);
|
||||
VATETaxes.import.loadOperations(fromDate, toDate, company, allOperations, newOffset, createAsDraft, autoCreateCustomers);
|
||||
} else {
|
||||
if (allOperations.length > 0) {
|
||||
frappe.show_alert({
|
||||
message: __('Processing ') + allOperations.length + __(' VAT operations...'),
|
||||
indicator: 'blue'
|
||||
}, 3);
|
||||
VATETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Processing ') + allOperations.length + __(' VAT operations...'),
|
||||
__('Checking for already imported documents')
|
||||
);
|
||||
|
||||
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft, autoCreateCustomers);
|
||||
} else {
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Information'),
|
||||
indicator: 'blue',
|
||||
|
|
@ -1002,6 +1016,7 @@ VATETaxes.import = {
|
|||
}
|
||||
}
|
||||
} else if (r.message && r.message.error === 'unauthorized') {
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.confirm(
|
||||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||||
function() {
|
||||
|
|
@ -1017,6 +1032,7 @@ VATETaxes.import = {
|
|||
}
|
||||
);
|
||||
} else {
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1026,6 +1042,7 @@ VATETaxes.import = {
|
|||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error fetching VAT operations:", error);
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Network Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1034,6 +1051,7 @@ VATETaxes.import = {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1081,6 +1099,7 @@ VATETaxes.import = {
|
|||
if (filteredOperations.length > 0) {
|
||||
VATETaxes.import.showOperationSelection(filteredOperations, token, company, createAsDraft, autoCreateCustomers);
|
||||
} else {
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Information'),
|
||||
indicator: 'blue',
|
||||
|
|
@ -1090,6 +1109,7 @@ VATETaxes.import = {
|
|||
}
|
||||
} catch (e) {
|
||||
console.error("Error processing VAT operations data:", e);
|
||||
VATETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1110,16 +1130,18 @@ VATETaxes.import = {
|
|||
|
||||
// Показать диалог выбора операций
|
||||
showOperationSelection: function(operations, token, company, createAsDraft = 0) {
|
||||
VATETaxes.dialogs.hide();
|
||||
let operationTable = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered vat-operations-table" style="width: 100%; table-layout: fixed;">';
|
||||
operationTable += '<thead><tr>' +
|
||||
'<th style="width: 3%;"><input type="checkbox" class="select-all-operations"></th>' +
|
||||
'<th style="width: 9%;">' + __('Date') + '</th>' +
|
||||
'<th style="width: 10%;">' + __('TIN') + '</th>' +
|
||||
'<th style="width: 18%;">' + __('Name') + '</th>' +
|
||||
'<th style="width: 18%;">' + __('Operation Type') + '</th>' +
|
||||
'<th style="width: 11%;">' + __('Classification Code') + '</th>' +
|
||||
'<th style="width: 13%;">' + __('Income') + '</th>' +
|
||||
'<th style="width: 13%;">' + __('Expense') + '</th>' +
|
||||
'<th style="width: 8%;">' + __('Date') + '</th>' +
|
||||
'<th style="width: 9%;">' + __('TIN') + '</th>' +
|
||||
'<th style="width: 15%;">' + __('Name') + '</th>' +
|
||||
'<th style="width: 15%;">' + __('Operation Type') + '</th>' +
|
||||
'<th style="width: 9%;">' + __('Classification Code') + '</th>' +
|
||||
'<th style="width: 17%;">' + __('Explanation') + '</th>' +
|
||||
'<th style="width: 12%;">' + __('Income') + '</th>' +
|
||||
'<th style="width: 12%;">' + __('Expense') + '</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
|
||||
operations.forEach(function(operation) {
|
||||
|
|
@ -1137,6 +1159,9 @@ VATETaxes.import = {
|
|||
classificationCode = operation.taxCodeInfo.code;
|
||||
}
|
||||
|
||||
// İzahat (свободный текст) — экранируем, чтобы спецсимволы не сломали таблицу
|
||||
const explanation = frappe.utils.escape_html(operation.explanation || '');
|
||||
|
||||
operationTable += '<tr>' +
|
||||
'<td><input type="checkbox" class="select-operation" data-id="' + operation.id + '"></td>' +
|
||||
'<td>' + operationDate + '</td>' +
|
||||
|
|
@ -1144,6 +1169,7 @@ VATETaxes.import = {
|
|||
'<td style="word-break: break-word;">' + name + '</td>' +
|
||||
'<td style="word-break: break-word;">' + operationType + '</td>' +
|
||||
'<td style="text-align: center;">' + classificationCode + '</td>' +
|
||||
'<td style="word-break: break-word;">' + explanation + '</td>' +
|
||||
'<td style="text-align: right;">' + VATETaxes.utils.formatCurrency(income) + '</td>' +
|
||||
'<td style="text-align: right;">' + VATETaxes.utils.formatCurrency(expense) + '</td>' +
|
||||
'</tr>';
|
||||
|
|
@ -1400,11 +1426,37 @@ VATETaxes.import = {
|
|||
}, VATETaxes.PROGRESS_UPDATE_DELAY);
|
||||
},
|
||||
|
||||
// Надёжное закрытие прогресс-бара. frappe.hide_progress()/dialog.hide() в этом
|
||||
// окружении (jey_theme/Bootstrap) НЕ всегда убирает модал — он зависает. Поэтому
|
||||
// принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
|
||||
_closeProgress: function() {
|
||||
if (frappe.cur_progress) {
|
||||
try {
|
||||
frappe.cur_progress.$wrapper.modal('hide');
|
||||
frappe.cur_progress.$wrapper.remove();
|
||||
} catch (e) {}
|
||||
frappe.cur_progress = null;
|
||||
}
|
||||
// backdrop/scroll-lock убираем только если не осталось видимых модалов
|
||||
// (иначе сломаем окно summary, которое показывается следом)
|
||||
if ($('.modal:visible').length === 0) {
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
},
|
||||
|
||||
// Socket.IO bulk import
|
||||
loadSelectedOperationsSocketIO: function(operations, token, company, createAsDraft, autoCreateCustomers) {
|
||||
VATETaxes.loadingErrors = [];
|
||||
const total = operations.length;
|
||||
|
||||
// Защита от гонки realtime: 'vat_import_complete' вызывает hide_progress()
|
||||
// (он обнуляет frappe.cur_progress). Если 'vat_import_progress' приходит ПОСЛЕ
|
||||
// complete, frappe.show_progress создаёт НОВЫЙ диалог, который уже никто не
|
||||
// закрывает — он зависает поверх окна результата. Флаг делает обработчик
|
||||
// прогресса no-op после завершения.
|
||||
let importFinished = false;
|
||||
|
||||
frappe.show_progress(
|
||||
__('Importing VAT Operations'), 0, total,
|
||||
__('Starting import of {0} operations...', [total])
|
||||
|
|
@ -1412,6 +1464,7 @@ VATETaxes.import = {
|
|||
|
||||
frappe.realtime.off('vat_import_progress');
|
||||
frappe.realtime.on('vat_import_progress', function(data) {
|
||||
if (importFinished) return;
|
||||
frappe.show_progress(
|
||||
__('Importing VAT Operations'), data.current, data.total,
|
||||
__('Processing operation {0} of {1}', [data.current, data.total])
|
||||
|
|
@ -1420,9 +1473,10 @@ VATETaxes.import = {
|
|||
|
||||
frappe.realtime.off('vat_import_complete');
|
||||
frappe.realtime.on('vat_import_complete', function(data) {
|
||||
importFinished = true;
|
||||
frappe.realtime.off('vat_import_progress');
|
||||
frappe.realtime.off('vat_import_complete');
|
||||
frappe.hide_progress();
|
||||
VATETaxes.import._closeProgress();
|
||||
|
||||
(data.errors || []).forEach(function(err) {
|
||||
VATETaxes.loadingErrors.push({
|
||||
|
|
@ -1452,25 +1506,25 @@ VATETaxes.import = {
|
|||
},
|
||||
callback: function(r) {
|
||||
if (!r.message || !r.message.enqueued) {
|
||||
importFinished = true;
|
||||
frappe.realtime.off('vat_import_progress');
|
||||
frappe.realtime.off('vat_import_complete');
|
||||
frappe.hide_progress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Failed to start import job')
|
||||
});
|
||||
VATETaxes.import._closeProgress();
|
||||
frappe.show_alert({
|
||||
message: __('Failed to start import job'),
|
||||
indicator: 'red'
|
||||
}, 7);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
importFinished = true;
|
||||
frappe.realtime.off('vat_import_progress');
|
||||
frappe.realtime.off('vat_import_complete');
|
||||
frappe.hide_progress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Network error starting import')
|
||||
});
|
||||
VATETaxes.import._closeProgress();
|
||||
frappe.show_alert({
|
||||
message: __('Network error starting import'),
|
||||
indicator: 'red'
|
||||
}, 7);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1480,7 +1534,7 @@ VATETaxes.import = {
|
|||
|
||||
// Journal Entry List
|
||||
frappe.listview_settings['Journal Entry'] = {
|
||||
add_fields: ['posting_date', 'voucher_type', 'total_debit', 'total_credit', 'docstatus', 'expense_income_type', 'etaxes_document_type', 'loaded_from_etaxes'],
|
||||
add_fields: ['posting_date', 'voucher_type', 'total_debit', 'total_credit', 'docstatus', 'expense_income_type', 'etaxes_document_type', 'loaded_from_etaxes', 'etaxes_explanation'],
|
||||
|
||||
hide_name_column: false,
|
||||
|
||||
|
|
|
|||
|
|
@ -1435,6 +1435,25 @@ ETaxes.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');
|
||||
}
|
||||
};
|
||||
|
||||
frappe.show_progress(
|
||||
__('Importing Invoices'), 0, total,
|
||||
|
|
@ -1443,6 +1462,7 @@ ETaxes.import = {
|
|||
|
||||
frappe.realtime.off('purchase_import_progress');
|
||||
frappe.realtime.on('purchase_import_progress', function(data) {
|
||||
if (importFinished) return;
|
||||
frappe.show_progress(
|
||||
__('Importing Invoices'), data.current, data.total,
|
||||
__('Importing invoice {0} of {1}', [data.current, data.total])
|
||||
|
|
@ -1451,9 +1471,10 @@ ETaxes.import = {
|
|||
|
||||
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');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
|
||||
(data.errors || []).forEach(function(err) {
|
||||
ETaxes.loadingErrors.push({
|
||||
|
|
@ -1488,7 +1509,7 @@ ETaxes.import = {
|
|||
if (!r.message || !r.message.enqueued) {
|
||||
frappe.realtime.off('purchase_import_progress');
|
||||
frappe.realtime.off('purchase_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1499,7 +1520,7 @@ ETaxes.import = {
|
|||
error: function() {
|
||||
frappe.realtime.off('purchase_import_progress');
|
||||
frappe.realtime.off('purchase_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
|
|||
|
|
@ -1454,6 +1454,25 @@ ETaxes.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');
|
||||
}
|
||||
};
|
||||
|
||||
frappe.show_progress(
|
||||
__('Importing Invoices'), 0, total,
|
||||
|
|
@ -1462,6 +1481,7 @@ ETaxes.import = {
|
|||
|
||||
frappe.realtime.off('purchase_import_progress');
|
||||
frappe.realtime.on('purchase_import_progress', function(data) {
|
||||
if (importFinished) return;
|
||||
frappe.show_progress(
|
||||
__('Importing Invoices'), data.current, data.total,
|
||||
__('Importing invoice {0} of {1}', [data.current, data.total])
|
||||
|
|
@ -1470,9 +1490,10 @@ ETaxes.import = {
|
|||
|
||||
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');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
|
||||
(data.errors || []).forEach(function(err) {
|
||||
ETaxes.loadingErrors.push({
|
||||
|
|
@ -1507,7 +1528,7 @@ ETaxes.import = {
|
|||
if (!r.message || !r.message.enqueued) {
|
||||
frappe.realtime.off('purchase_import_progress');
|
||||
frappe.realtime.off('purchase_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1518,7 +1539,7 @@ ETaxes.import = {
|
|||
error: function() {
|
||||
frappe.realtime.off('purchase_import_progress');
|
||||
frappe.realtime.off('purchase_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -2182,47 +2203,50 @@ function show_etaxes_details(frm) {
|
|||
* 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
|
||||
});
|
||||
// Make sure both signals reach the formatter, even though the column is bound
|
||||
// to only one of them.
|
||||
add_fields: ['is_taxes_doc', 'etaxes_send_status'],
|
||||
|
||||
// Add E-Taxes status column
|
||||
onload: function(listview) {
|
||||
// Single universal E-Taxes column. It reflects everything related to the
|
||||
// tax portal for this document: whether it was loaded from E-Taxes,
|
||||
// whether a draft was created there, or whether it was sent and signed.
|
||||
listview.columns.push({
|
||||
type: 'Select',
|
||||
df: {
|
||||
label: __('E-Taxes Status'),
|
||||
label: __('E-Taxes'),
|
||||
fieldname: 'etaxes_send_status'
|
||||
},
|
||||
width: 150
|
||||
width: 170
|
||||
});
|
||||
|
||||
// Force refresh list with new columns
|
||||
// Force refresh list with the new column
|
||||
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>`;
|
||||
// Universal E-Taxes status. The formatter receives the full row as its
|
||||
// third argument (value, df, doc), so we combine the send status with the
|
||||
// "loaded from E-Taxes" flag into a single indicator.
|
||||
etaxes_send_status: function(value, df, doc) {
|
||||
const send_status = doc.etaxes_send_status;
|
||||
|
||||
// Outgoing flow (document sent to the portal) takes priority — it is
|
||||
// the most specific state.
|
||||
if (send_status === 'Sent and Signed') {
|
||||
return `<span class="indicator-pill green">${__('Sent to E-Taxes')}</span>`;
|
||||
}
|
||||
return value || '';
|
||||
if (send_status === 'Created, not signed') {
|
||||
return `<span class="indicator-pill orange">${__('Draft on E-Taxes')}</span>`;
|
||||
}
|
||||
|
||||
// Incoming flow (document imported from the portal).
|
||||
if (doc.is_taxes_doc) {
|
||||
return `<span class="indicator-pill blue">${__('Loaded from E-Taxes')}</span>`;
|
||||
}
|
||||
|
||||
// Not related to the tax portal — keep the cell clean.
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -920,19 +920,21 @@ SalesETaxes.import = {
|
|||
$(document).off('progress-cancel.etaxes_sales_import');
|
||||
|
||||
if (offset === 0) {
|
||||
frappe.show_alert({
|
||||
message: __('Fetching E-Taxes sales invoices...'),
|
||||
indicator: 'blue'
|
||||
}, 3);
|
||||
SalesETaxes.dialogs.showLoading(
|
||||
__('Loading E-Taxes Documents'),
|
||||
__('Fetching E-Taxes sales invoices...'),
|
||||
__('Please wait')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
SalesETaxes.utils.getDefaultLogin(function(loginResponse) {
|
||||
if (SalesETaxes.cancelLoading) return;
|
||||
|
||||
|
||||
if (loginResponse && loginResponse.found) {
|
||||
const mainToken = loginResponse.main_token;
|
||||
|
||||
|
||||
if (!mainToken) {
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Authentication Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -964,16 +966,23 @@ SalesETaxes.import = {
|
|||
|
||||
if (hasMore && currentInvoices.length > 0) {
|
||||
const newOffset = offset + currentInvoices.length;
|
||||
SalesETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Fetching E-Taxes sales invoices...'),
|
||||
__('Loaded ') + allInvoices.length + __(' so far...')
|
||||
);
|
||||
SalesETaxes.import.loadInvoices(fromDate, toDate, warehouse, allInvoices, newOffset);
|
||||
} else {
|
||||
if (allInvoices.length > 0) {
|
||||
frappe.show_alert({
|
||||
message: __('Processing ') + allInvoices.length + __(' invoices...'),
|
||||
indicator: 'blue'
|
||||
}, 3);
|
||||
|
||||
SalesETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Processing ') + allInvoices.length + __(' invoices...'),
|
||||
__('Checking for already imported documents')
|
||||
);
|
||||
|
||||
SalesETaxes.import.filterDuplicates(allInvoices, mainToken, warehouse);
|
||||
} else {
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Information'),
|
||||
indicator: 'blue',
|
||||
|
|
@ -982,6 +991,7 @@ SalesETaxes.import = {
|
|||
}
|
||||
}
|
||||
} else if (r.message && r.message.error === 'unauthorized') {
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.confirm(
|
||||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||||
function() {
|
||||
|
|
@ -997,6 +1007,7 @@ SalesETaxes.import = {
|
|||
}
|
||||
);
|
||||
} else {
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1006,6 +1017,7 @@ SalesETaxes.import = {
|
|||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error fetching sales invoices:", error);
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Network Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1014,6 +1026,7 @@ SalesETaxes.import = {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1056,15 +1069,17 @@ SalesETaxes.import = {
|
|||
if (filteredInvoices.length > 0) {
|
||||
SalesETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
|
||||
} else {
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Information'),
|
||||
indicator: 'blue',
|
||||
message: __('No new sales invoices found for the specified period (all ' +
|
||||
message: __('No new sales invoices found for the specified period (all ' +
|
||||
allInvoices.length + ' are already imported)')
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error processing sales data:", e);
|
||||
SalesETaxes.dialogs.hide();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1085,6 +1100,7 @@ SalesETaxes.import = {
|
|||
|
||||
// Показать диалог выбора инвойсов
|
||||
showInvoiceSelection: function(invoices, token, warehouse) {
|
||||
SalesETaxes.dialogs.hide();
|
||||
let invoiceTable = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered etaxes-sales-invoices-table" style="width: 100%; table-layout: fixed;">';
|
||||
invoiceTable += '<thead><tr>' +
|
||||
'<th style="width: 6%;"><input type="checkbox" class="select-all-sales-invoices"></th>' +
|
||||
|
|
@ -1484,6 +1500,25 @@ SalesETaxes.import = {
|
|||
loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
|
||||
SalesETaxes.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');
|
||||
}
|
||||
};
|
||||
|
||||
frappe.show_progress(
|
||||
__('Importing Sales Invoices'), 0, total,
|
||||
|
|
@ -1493,6 +1528,7 @@ SalesETaxes.import = {
|
|||
// Listen for per-invoice progress
|
||||
frappe.realtime.off('sales_import_progress');
|
||||
frappe.realtime.on('sales_import_progress', function(data) {
|
||||
if (importFinished) return;
|
||||
frappe.show_progress(
|
||||
__('Importing Sales Invoices'), data.current, data.total,
|
||||
__('Importing invoice {0} of {1}', [data.current, data.total])
|
||||
|
|
@ -1502,9 +1538,10 @@ SalesETaxes.import = {
|
|||
// Listen for completion
|
||||
frappe.realtime.off('sales_import_complete');
|
||||
frappe.realtime.on('sales_import_complete', function(data) {
|
||||
importFinished = true;
|
||||
frappe.realtime.off('sales_import_progress');
|
||||
frappe.realtime.off('sales_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
|
||||
// Collect errors for the error panel
|
||||
(data.errors || []).forEach(function(err) {
|
||||
|
|
@ -1541,7 +1578,7 @@ SalesETaxes.import = {
|
|||
if (!r.message || !r.message.enqueued) {
|
||||
frappe.realtime.off('sales_import_progress');
|
||||
frappe.realtime.off('sales_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
@ -1552,7 +1589,7 @@ SalesETaxes.import = {
|
|||
error: function() {
|
||||
frappe.realtime.off('sales_import_progress');
|
||||
frappe.realtime.off('sales_import_complete');
|
||||
frappe.hide_progress();
|
||||
closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-10 20:52:37.650113",
|
||||
"modified": "2026-06-12 13:30:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "Classification code",
|
||||
|
|
|
|||
|
|
@ -7335,3 +7335,13 @@ msgstr "ƏMAS sessiyası uğurla yeniləndi"
|
|||
#: invoice_az/invoice_az/doctype/testapi/testapi.json
|
||||
msgid "Телефон"
|
||||
msgstr "Telefon"
|
||||
|
||||
#. AI-generated
|
||||
#: invoice_az/client/sales_invoice.js:2240
|
||||
msgid "Draft on E-Taxes"
|
||||
msgstr "E-Taxes-də qaralama"
|
||||
|
||||
#. AI-generated
|
||||
#: invoice_az/client/sales_invoice.js:2245
|
||||
msgid "Loaded from E-Taxes"
|
||||
msgstr "E-Taxes-dən yükləndi"
|
||||
|
|
|
|||
|
|
@ -7334,3 +7334,13 @@ msgstr "Сессия ƏMAS успешно обновлена"
|
|||
#: invoice_az/invoice_az/doctype/testapi/testapi.json
|
||||
msgid "Телефон"
|
||||
msgstr "Телефон"
|
||||
|
||||
#. AI-generated
|
||||
#: invoice_az/client/sales_invoice.js:2240
|
||||
msgid "Draft on E-Taxes"
|
||||
msgstr "Черновик в E-Taxes"
|
||||
|
||||
#. AI-generated
|
||||
#: invoice_az/client/sales_invoice.js:2245
|
||||
msgid "Loaded from E-Taxes"
|
||||
msgstr "Загружено из E-Taxes"
|
||||
|
|
|
|||
|
|
@ -608,6 +608,44 @@ def get_accounts_for_operation_type(operation_type, company, expense_income_type
|
|||
return (None, None)
|
||||
|
||||
|
||||
def ensure_classification_code(tax_code_info):
|
||||
"""Вернуть name записи "Classification code" для taxCodeInfo, создав её
|
||||
из payload (code + description), если её ещё нет в справочнике.
|
||||
|
||||
Возвращает код (str) или None, если taxCodeInfo пуст/без code. Никогда не
|
||||
бросает исключений — отсутствующий təsnifat kodu не должен ломать создание
|
||||
Journal Entry (поле — Link, поэтому ссылка должна резолвиться).
|
||||
"""
|
||||
if not tax_code_info or not isinstance(tax_code_info, dict):
|
||||
return None
|
||||
|
||||
code = tax_code_info.get('code')
|
||||
if not code:
|
||||
return None
|
||||
|
||||
code = str(code).strip()
|
||||
if not code:
|
||||
return None
|
||||
|
||||
try:
|
||||
if frappe.db.exists('Classification code', code):
|
||||
return code
|
||||
|
||||
description = (tax_code_info.get('description') or '').strip()
|
||||
doc = frappe.new_doc('Classification code')
|
||||
doc.name = code
|
||||
doc.classification_name = description
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
frappe.db.commit()
|
||||
frappe.logger().info(f"[VAT] Created Classification code '{code}' from taxCodeInfo")
|
||||
return code
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"Could not ensure Classification code '{code}': {e}\n{frappe.get_traceback()}",
|
||||
"VAT Classification Code Error")
|
||||
return None
|
||||
|
||||
|
||||
def create_journal_entry_from_vat_operation(operation_data, company, create_as_draft=0, auto_create_customers=0):
|
||||
"""
|
||||
Создание Journal Entry из VAT операции
|
||||
|
|
@ -659,6 +697,9 @@ def create_journal_entry_from_vat_operation(operation_data, company, create_as_d
|
|||
if tax_code_info and isinstance(tax_code_info, dict):
|
||||
tax_code = tax_code_info.get('code')
|
||||
|
||||
# Təsnifat kodu: ссылка на "Classification code" (заполняем ТОЛЬКО если код есть)
|
||||
classification_code_name = ensure_classification_code(tax_code_info)
|
||||
|
||||
# Ищем счета по маппингу с учетом tax_code (если есть)
|
||||
debit_account, credit_account = get_accounts_for_operation_type(
|
||||
operation_type, company, expense_income_type, tax_code
|
||||
|
|
@ -737,6 +778,11 @@ def create_journal_entry_from_vat_operation(operation_data, company, create_as_d
|
|||
je.customer_name_etaxes = operation_data.get('name', '')
|
||||
je.etaxes_document_type = 'ədv' # Always ədv for VAT operations
|
||||
je.loaded_from_etaxes = 1 # Flag that this was loaded from e-taxes
|
||||
je.etaxes_explanation = operation_data.get('explanation', '') # İzahat из VAT операции
|
||||
|
||||
# Təsnifat kodu из taxCodeInfo — только для документов, где он есть
|
||||
if classification_code_name:
|
||||
je.etaxes_classification_code = classification_code_name
|
||||
|
||||
# Строка 1: Дебет
|
||||
debit_entry = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue