diff --git a/invoice_az/client/company.js b/invoice_az/client/company.js
index 9ef5abf..8b7868a 100644
--- a/invoice_az/client/company.js
+++ b/invoice_az/client/company.js
@@ -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();
}
});
}
diff --git a/invoice_az/client/employee.js b/invoice_az/client/employee.js
index 86f010d..4f475c9 100644
--- a/invoice_az/client/employee.js
+++ b/invoice_az/client/employee.js
@@ -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',
diff --git a/invoice_az/client/journal_entry.js b/invoice_az/client/journal_entry.js
index 49a94a1..a9bf96e 100644
--- a/invoice_az/client/journal_entry.js
+++ b/invoice_az/client/journal_entry.js
@@ -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 = {
' ' + __('Export CSV') + '' +
'';
- 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 = '
';
operationTable += '' +
' | ' +
- '' + __('Date') + ' | ' +
- '' + __('TIN') + ' | ' +
- '' + __('Name') + ' | ' +
- '' + __('Operation Type') + ' | ' +
- '' + __('Classification Code') + ' | ' +
- '' + __('Income') + ' | ' +
- '' + __('Expense') + ' | ' +
+ '' + __('Date') + ' | ' +
+ '' + __('TIN') + ' | ' +
+ '' + __('Name') + ' | ' +
+ '' + __('Operation Type') + ' | ' +
+ '' + __('Classification Code') + ' | ' +
+ '' + __('Explanation') + ' | ' +
+ '' + __('Income') + ' | ' +
+ '' + __('Expense') + ' | ' +
'
';
operations.forEach(function(operation) {
@@ -1137,6 +1159,9 @@ VATETaxes.import = {
classificationCode = operation.taxCodeInfo.code;
}
+ // İzahat (свободный текст) — экранируем, чтобы спецсимволы не сломали таблицу
+ const explanation = frappe.utils.escape_html(operation.explanation || '');
+
operationTable += '' +
' | ' +
'' + operationDate + ' | ' +
@@ -1144,6 +1169,7 @@ VATETaxes.import = {
'' + name + ' | ' +
'' + operationType + ' | ' +
'' + classificationCode + ' | ' +
+ '' + explanation + ' | ' +
'' + VATETaxes.utils.formatCurrency(income) + ' | ' +
'' + VATETaxes.utils.formatCurrency(expense) + ' | ' +
'
';
@@ -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,
diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js
index 2313689..030ba07 100644
--- a/invoice_az/client/purchase_order.js
+++ b/invoice_az/client/purchase_order.js
@@ -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',
diff --git a/invoice_az/client/sales_invoice.js b/invoice_az/client/sales_invoice.js
index bdfad73..68f66aa 100644
--- a/invoice_az/client/sales_invoice.js
+++ b/invoice_az/client/sales_invoice.js
@@ -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 ?
- `` :
- ``;
- },
- etaxes_send_status: function(value) {
- if (value === 'Sent and Signed') {
- return `${__('Sent and Signed')}`;
- } else if (value === 'Created, not signed') {
- return `${__('Draft Created')}`;
- } else if (value === 'Not Sent') {
- return `${__('Not Sent')}`;
+ // 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 `${__('Sent to E-Taxes')}`;
}
- return value || '';
+ if (send_status === 'Created, not signed') {
+ return `${__('Draft on E-Taxes')}`;
+ }
+
+ // Incoming flow (document imported from the portal).
+ if (doc.is_taxes_doc) {
+ return `${__('Loaded from E-Taxes')}`;
+ }
+
+ // Not related to the tax portal — keep the cell clean.
+ return '';
}
}
};
diff --git a/invoice_az/client/sales_order.js b/invoice_az/client/sales_order.js
index 7f36627..c2b8794 100644
--- a/invoice_az/client/sales_order.js
+++ b/invoice_az/client/sales_order.js
@@ -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 = '';
invoiceTable += '' +
' | ' +
@@ -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',
diff --git a/invoice_az/invoice_az/doctype/classification_code/classification_code.json b/invoice_az/invoice_az/doctype/classification_code/classification_code.json
index e7a4e91..ccf1591 100644
--- a/invoice_az/invoice_az/doctype/classification_code/classification_code.json
+++ b/invoice_az/invoice_az/doctype/classification_code/classification_code.json
@@ -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",
diff --git a/invoice_az/locale/az.po b/invoice_az/locale/az.po
index 11a437a..7799657 100644
--- a/invoice_az/locale/az.po
+++ b/invoice_az/locale/az.po
@@ -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"
diff --git a/invoice_az/locale/ru.po b/invoice_az/locale/ru.po
index fa0c3d6..52a4895 100644
--- a/invoice_az/locale/ru.po
+++ b/invoice_az/locale/ru.po
@@ -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"
diff --git a/invoice_az/vat_api.py b/invoice_az/vat_api.py
index b5fd3af..255b62e 100644
--- a/invoice_az/vat_api.py
+++ b/invoice_az/vat_api.py
@@ -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 = {