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 += '' + '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ''; operations.forEach(function(operation) { @@ -1137,6 +1159,9 @@ VATETaxes.import = { classificationCode = operation.taxCodeInfo.code; } + // İzahat (свободный текст) — экранируем, чтобы спецсимволы не сломали таблицу + const explanation = frappe.utils.escape_html(operation.explanation || ''); + operationTable += '' + '' + '' + @@ -1144,6 +1169,7 @@ VATETaxes.import = { '' + '' + '' + + '' + '' + '' + ''; @@ -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 = '
' + __('Date') + '' + __('TIN') + '' + __('Name') + '' + __('Operation Type') + '' + __('Classification Code') + '' + __('Income') + '' + __('Expense') + '' + __('Date') + '' + __('TIN') + '' + __('Name') + '' + __('Operation Type') + '' + __('Classification Code') + '' + __('Explanation') + '' + __('Income') + '' + __('Expense') + '
' + operationDate + '' + name + '' + operationType + '' + classificationCode + '' + explanation + '' + VATETaxes.utils.formatCurrency(income) + '' + VATETaxes.utils.formatCurrency(expense) + '
'; 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 = {