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:
Ali 2026-06-12 16:19:16 +00:00
parent 9b6b746793
commit e67a2d2ec1
10 changed files with 335 additions and 89 deletions

View File

@ -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();
}
});
}

View File

@ -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',

View File

@ -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,

View File

@ -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',

View File

@ -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 '';
}
}
};

View File

@ -920,10 +920,11 @@ 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) {
@ -933,6 +934,7 @@ SalesETaxes.import = {
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,6 +1069,7 @@ SalesETaxes.import = {
if (filteredInvoices.length > 0) {
SalesETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
} else {
SalesETaxes.dialogs.hide();
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
@ -1065,6 +1079,7 @@ SalesETaxes.import = {
}
} 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',

View File

@ -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",

View File

@ -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"

View File

@ -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"

View File

@ -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 = {