diff --git a/invoice_az/client/e_taxes_items_list.js b/invoice_az/client/e_taxes_items_list.js index 35a3822..99826d7 100644 --- a/invoice_az/client/e_taxes_items_list.js +++ b/invoice_az/client/e_taxes_items_list.js @@ -694,8 +694,64 @@ function show_items_filter_dialog() { let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00'); let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59'); - // Запускаем загрузку с прогресс-баром - load_items_with_pagination(fromDate, toDate, 0, []); + // Загрузка теперь идёт фоновым джобом: прогресс/отмена показываются + // глобальным виджетом «Background Tasks» (снизу справа), страница не + // блокируется и загрузка переживает reload. (Старый клиентский цикл + // load_items_with_pagination / process_invoices_for_items больше не + // вызывается.) + const ITEMS_COMPLETE = 'items_loading_complete'; + let items_handler = null; + const detach_items = function() { + if (items_handler) frappe.realtime.off(ITEMS_COMPLETE, items_handler); + items_handler = null; + }; + items_handler = function(data) { + detach_items(); + data = data || {}; + if (data.error === 'unauthorized') { + frappe.confirm( + __('Your E-Taxes session has expired. Would you like to authenticate now?'), + function() { check_and_process_token_items(function() { show_items_filter_dialog(); }); } + ); + return; + } + if (data.cancelled) { + frappe.show_alert({ message: __('Items loading cancelled.'), indicator: 'orange' }, 6); + if (typeof cur_list !== 'undefined' && cur_list) cur_list.refresh(); + return; + } + if (data.error) { + frappe.msgprint({ title: __('Error'), message: data.message || __('Loading failed.'), indicator: 'red' }); + return; + } + if (data.empty) { + frappe.show_alert({ message: __('No invoices found for the selected period.'), indicator: 'blue' }, 6); + return; + } + frappe.show_alert({ + message: __('Created {0} items. Use "Add unmapped" to map them.', [data.created || 0]), + indicator: 'green' + }, 10); + if (typeof cur_list !== 'undefined' && cur_list) cur_list.refresh(); + }; + frappe.realtime.on(ITEMS_COMPLETE, items_handler); + + frappe.call({ + method: 'invoice_az.api.start_items_loading_bg', + args: { date_from: fromDate, date_to: toDate }, + callback: function(r) { + const m = r.message || {}; + if (!m.enqueued) { + detach_items(); + frappe.msgprint({ + title: __('Failed to Start'), + message: m.message || __('Could not start loading.'), + indicator: 'red' + }); + } + }, + error: function() { detach_items(); } + }); } }); diff --git a/invoice_az/client/e_taxes_unit_list.js b/invoice_az/client/e_taxes_unit_list.js index 0f64e4f..6cfe378 100644 --- a/invoice_az/client/e_taxes_unit_list.js +++ b/invoice_az/client/e_taxes_unit_list.js @@ -654,8 +654,63 @@ function show_units_filter_dialog() { let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00'); let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59'); - // Запускаем загрузку с прогресс-баром - load_units_with_pagination(fromDate, toDate, 0, []); + // Загрузка теперь идёт фоновым джобом: прогресс/отмена показываются + // глобальным виджетом «Background Tasks» (снизу справа). Старый клиентский + // цикл load_units_with_pagination / process_invoices_for_units больше не + // вызывается. + const UNITS_COMPLETE = 'units_loading_complete'; + let units_handler = null; + const detach_units = function() { + if (units_handler) frappe.realtime.off(UNITS_COMPLETE, units_handler); + units_handler = null; + }; + units_handler = function(data) { + detach_units(); + data = data || {}; + if (data.error === 'unauthorized') { + frappe.confirm( + __('Your E-Taxes session has expired. Would you like to authenticate now?'), + function() { check_and_process_token_units(function() { show_units_filter_dialog(); }); } + ); + return; + } + if (data.cancelled) { + frappe.show_alert({ message: __('Units loading cancelled.'), indicator: 'orange' }, 6); + if (typeof cur_list !== 'undefined' && cur_list) cur_list.refresh(); + return; + } + if (data.error) { + frappe.msgprint({ title: __('Error'), message: data.message || __('Loading failed.'), indicator: 'red' }); + return; + } + if (data.empty) { + frappe.show_alert({ message: __('No invoices found for the selected period.'), indicator: 'blue' }, 6); + return; + } + frappe.show_alert({ + message: __('Created {0} units. Use "Add unmapped" to map them.', [data.created || 0]), + indicator: 'green' + }, 10); + if (typeof cur_list !== 'undefined' && cur_list) cur_list.refresh(); + }; + frappe.realtime.on(UNITS_COMPLETE, units_handler); + + frappe.call({ + method: 'invoice_az.api.start_units_loading_bg', + args: { date_from: fromDate, date_to: toDate }, + callback: function(r) { + const m = r.message || {}; + if (!m.enqueued) { + detach_units(); + frappe.msgprint({ + title: __('Failed to Start'), + message: m.message || __('Could not start loading.'), + indicator: 'red' + }); + } + }, + error: function() { detach_units(); } + }); } }); diff --git a/invoice_az/client/journal_entry.js b/invoice_az/client/journal_entry.js index ab94c45..043e953 100644 --- a/invoice_az/client/journal_entry.js +++ b/invoice_az/client/journal_entry.js @@ -1473,20 +1473,9 @@ VATETaxes.import = { // прогресса no-op после завершения. let importFinished = false; - frappe.show_progress( - __('Importing VAT Operations'), 0, total, - __('Starting import of {0} operations...', [total]) - ); - - 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]) - ); - }); - + // Progress + cancel are shown by the global Background Tasks widget + // (bottom-right). No blocking modal here; we keep only the complete + // subscription to show the final import summary. frappe.realtime.off('vat_import_complete'); frappe.realtime.on('vat_import_complete', function(data) { importFinished = true; diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js index c332e39..81742f6 100644 --- a/invoice_az/client/purchase_order.js +++ b/invoice_az/client/purchase_order.js @@ -1481,20 +1481,9 @@ ETaxes.import = { } }; - frappe.show_progress( - __('Importing Invoices'), 0, total, - __('Starting import of {0} invoices...', [total]) - ); - - 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]) - ); - }); - + // Progress + cancel are shown by the global Background Tasks widget + // (bottom-right). No blocking modal here; we keep only the complete + // subscription to show the final import summary. frappe.realtime.off('purchase_import_complete'); frappe.realtime.on('purchase_import_complete', function(data) { importFinished = true; diff --git a/invoice_az/client/sales_invoice.js b/invoice_az/client/sales_invoice.js index 3a457a4..3c78df8 100644 --- a/invoice_az/client/sales_invoice.js +++ b/invoice_az/client/sales_invoice.js @@ -1500,20 +1500,9 @@ ETaxes.import = { } }; - frappe.show_progress( - __('Importing Invoices'), 0, total, - __('Starting import of {0} invoices...', [total]) - ); - - 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]) - ); - }); - + // Progress + cancel are shown by the global Background Tasks widget + // (bottom-right). No blocking modal here; we keep only the complete + // subscription to show the final import summary. frappe.realtime.off('purchase_import_complete'); frappe.realtime.on('purchase_import_complete', function(data) { importFinished = true; diff --git a/invoice_az/client/sales_order.js b/invoice_az/client/sales_order.js index bc9960b..5573172 100644 --- a/invoice_az/client/sales_order.js +++ b/invoice_az/client/sales_order.js @@ -1550,22 +1550,9 @@ SalesETaxes.import = { } }; - frappe.show_progress( - __('Importing Sales Invoices'), 0, total, - __('Starting import of {0} invoices...', [total]) - ); - - // 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]) - ); - }); - - // Listen for completion + // Progress + cancel are shown by the global Background Tasks widget + // (bottom-right). No blocking modal here; we keep only the complete + // subscription to show the final import summary. frappe.realtime.off('sales_import_complete'); frappe.realtime.on('sales_import_complete', function(data) { importFinished = true; diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js index 07ee073..df7528d 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js @@ -796,6 +796,9 @@ frappe.ui.form.on('E-Taxes Settings', { } }, __('Units')); + + // Setup wizard — compact "next step" bar at the top of the form. + etaxes_render_settings_wizard(frm); } }); // ================================================================= @@ -978,9 +981,124 @@ function show_load_dialog_with_summary(frm, summary) { d.show(); } +// Reference data loading теперь выполняется ПОЛНОСТЬЮ на сервере (фоновый джоб). +// Раньше весь цикл (fetch -> parties -> details) крутился в браузере на рекурсии, +// поэтому reload его убивал, а модальный прогресс-бар блокировал страницу. Теперь +// прогресс/отмена показываются глобальным виджетом «Background Tasks» (снизу справа), +// а сама загрузка переживает перезагрузку страницы. +// +// Старая клиентская оркестрация (start_staged_invoice_loading, load_invoice_batch, +// check_loading_completion, process_parties_from_list_chunked, +// process_invoices_for_reference_data, ...) больше не вызывается и оставлена как +// мёртвый код, чтобы не задеть смежные ссылки в этом большом файле. +const REFERENCE_LOADING_COMPLETE_EVENT = 'reference_data_loading_complete'; + function start_reference_data_loading(frm, values) { - // ИЗМЕНЕНО: Теперь используем поэтапную загрузку вместо одного большого запроса - start_staged_invoice_loading(frm, values); + let complete_handler = null; + const detach = function() { + if (complete_handler) frappe.realtime.off(REFERENCE_LOADING_COMPLETE_EVENT, complete_handler); + complete_handler = null; + }; + + complete_handler = function(data) { + detach(); + data = data || {}; + + if (data.error === 'unauthorized') { + frappe.confirm( + __('Your E-Taxes session has expired. Would you like to authenticate now?'), + function() { + if (typeof ETaxes !== 'undefined' && ETaxes.auth) { + ETaxes.auth.checkAndProcess(function() { + start_reference_data_loading(frm, values); + }); + } else { + frappe.msgprint(__('Please authenticate via Asan Login, then try again.')); + } + } + ); + return; + } + + if (data.cancelled) { + frappe.show_alert({ message: __('Reference data loading cancelled.'), indicator: 'orange' }, 6); + frm.reload_doc(); + return; + } + + if (data.error) { + frappe.msgprint({ title: __('Error'), message: data.message || __('Loading failed.'), indicator: 'red' }); + return; + } + + const s = data.summary || {}; + const parts = []; + if (s.items_created) parts.push(__('{0} items', [s.items_created])); + if (s.units_created) parts.push(__('{0} units', [s.units_created])); + if (s.customers_created) parts.push(__('{0} customers', [s.customers_created])); + if (s.suppliers_created) parts.push(__('{0} suppliers', [s.suppliers_created])); + + if (data.empty) { + frappe.show_alert({ message: __('No invoices found for the selected period.'), indicator: 'blue' }, 6); + } else { + frappe.show_alert({ + message: __('Created: ') + (parts.join(', ') || __('nothing new')) + + '. ' + __('Use "Add unmapped" buttons to add them to mappings.'), + indicator: 'green' + }, 10); + } + frm.reload_doc(); + }; + + frappe.realtime.on(REFERENCE_LOADING_COMPLETE_EVENT, complete_handler); + + frappe.call({ + method: 'invoice_az.api.start_reference_data_loading_bg', + args: { + date_from: values.date_from, + date_to: values.date_to, + load_items: values.load_items ? 1 : 0, + load_customers: values.load_customers ? 1 : 0, + load_suppliers: values.load_suppliers ? 1 : 0, + load_units: values.load_units ? 1 : 0 + }, + callback: function(r) { + const msg = r.message || {}; + if (!msg.enqueued) { + detach(); + frappe.msgprint({ + title: __('Failed to Start'), + message: msg.message || __('Could not start the loading job.'), + indicator: 'red' + }); + } + // No success toast — the bottom-right widget itself signals that the + // load started, and a toast would overlap it. + }, + error: function() { + detach(); + } + }); +} + +// frappe.hide_progress() под jey_theme не закрывает модалку надёжно — окно залипает +// на экране в последнем состоянии. Закрываем прогресс-бар жёстко (см. memory +// progress-bar-pattern: dialog.hide() оставляет модалку, нужен $wrapper.remove()). +function close_data_loading_progress() { + if (frappe.cur_progress) { + try { + frappe.cur_progress.$wrapper.modal('hide'); + frappe.cur_progress.$wrapper.remove(); + } catch (e) { + console.error('Error closing progress bar:', e); + } + frappe.cur_progress = null; + } + // Чистим подложку только если не открыто других модалок. + if ($('.modal:visible').length === 0) { + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open').css('padding-right', ''); + } } function process_invoices_for_reference_data(invoices, token, accumulated_data, current_index = 0, frm, load_flags = null) { @@ -1000,17 +1118,17 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data, // Проверка отмены if (window.cancelReferenceLoading) { - frappe.hide_progress(); + close_data_loading_progress(); frappe.show_alert({ message: __('Reference data loading cancelled'), indicator: 'red' }, 3); return; } - + // Если все инвойсы обработаны if (current_index >= invoices.length) { - frappe.hide_progress(); + close_data_loading_progress(); const msg = []; if (accumulated_data.items_created > 0) { @@ -1085,7 +1203,7 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data, }, 100); } else { // Не удалось обновить токен - останавливаем обработку - frappe.hide_progress(); + close_data_loading_progress(); frappe.msgprint(__('Authentication expired. Please re-authenticate and try again.')); } } @@ -2293,6 +2411,19 @@ function load_invoice_batch(loading_state, source, frm) { // Завершили загрузку из этого источника if (source === 'inbox') { loading_state.inbox_completed = true; + + // outbox (sales) нужен только ради customers (его sender) или товаров/единиц + // из деталей. Если запрошены ТОЛЬКО поставщики — outbox не даст ничего нового, + // поэтому пропускаем его список целиком. + const outbox_needed = !!(loading_state.load_customers || + loading_state.load_items || loading_state.load_units); + + if (!outbox_needed) { + loading_state.outbox_completed = true; + check_loading_completion(loading_state, frm); + return; + } + // Начинаем загрузку outbox loading_state.current_offset = 0; loading_state.last_update_count = 0; // Сбрасываем счетчик для outbox @@ -2319,39 +2450,384 @@ function load_invoice_batch(loading_state, source, frm) { // НОВАЯ ФУНКЦИЯ: Проверка завершения загрузки function check_loading_completion(loading_state, frm) { - if (loading_state.inbox_completed && loading_state.outbox_completed) { - hide_loading_dialog_settings(); + if (!(loading_state.inbox_completed && loading_state.outbox_completed)) { + return; + } - if (loading_state.all_invoices.length === 0) { - frappe.msgprint(__('No invoices found for the selected period.')); - return; + hide_loading_dialog_settings(); + + if (loading_state.all_invoices.length === 0) { + frappe.msgprint(__('No invoices found for the selected period.')); + return; + } + + frappe.show_alert({ + message: __('Found {0} invoices. Starting processing...', [loading_state.all_invoices.length]), + indicator: 'green' + }, 3); + + // ОПТИМИЗАЦИЯ: есть 2 источника данных. + // - Контрагенты (customers/suppliers) целиком лежат в СПИСКЕ инвойсов + // (sender/receiver -> name + tin), их можно создать без захода внутрь документа. + // - Товары и единицы измерения есть только в ДЕТАЛЯХ инвойса, поэтому ради них + // приходится открывать каждый документ (get_invoice_details). + // Если выбраны только контрагенты — построчный обход документов не нужен вовсе. + const needs_detail = !!(loading_state.load_items || loading_state.load_units); + const needs_parties = !!(loading_state.load_customers || loading_state.load_suppliers); + + const accumulated = { + items_created: 0, + items_skipped: 0, + units_created: 0, + units_skipped: 0, + customers_created: 0, + customers_skipped: 0, + suppliers_created: 0, + suppliers_skipped: 0, + total_invoices: loading_state.all_invoices.length + }; + + const run_detail_stage = function() { + if (needs_detail) { + // Контрагенты уже обработаны из списка выше -> отключаем их здесь, + // чтобы не делать двойную работу. Заходим в документы только за товарами/единицами. + setTimeout(() => { + process_invoices_for_reference_data( + loading_state.all_invoices, loading_state.token, accumulated, 0, frm, + { + load_items: loading_state.load_items, + load_units: loading_state.load_units, + load_customers: 0, + load_suppliers: 0 + } + ); + }, 400); + } else { + // Запрошены только контрагенты — заходить внутрь документов не нужно. + show_parties_only_result(accumulated, frm); } - - // ИЗМЕНЕНО: Показываем результат загрузки - frappe.show_alert({ - message: __('Found {0} invoices. Starting processing...', [loading_state.all_invoices.length]), - indicator: 'green' - }, 3); // Показываем на 3 секунды - - // Небольшая задержка и начинаем обработку (прогресс-бар появляется быстро, - // спиннер "Loading Data" к этому моменту уже снят). - setTimeout(() => { - process_invoices_for_reference_data(loading_state.all_invoices, loading_state.token, { - items_created: 0, - items_skipped: 0, - units_created: 0, - units_skipped: 0, - customers_created: 0, - customers_skipped: 0, - suppliers_created: 0, - suppliers_skipped: 0, - total_invoices: loading_state.all_invoices.length - }, 0, frm, { - load_items: loading_state.load_items, - load_units: loading_state.load_units, - load_customers: loading_state.load_customers, - load_suppliers: loading_state.load_suppliers - }); - }, 400); + }; + + if (needs_parties) { + process_parties_from_list_chunked(loading_state, accumulated, frm, run_detail_stage); + } else { + run_detail_stage(); } } + +// НОВАЯ ФУНКЦИЯ: создание контрагентов прямо из списка инвойсов (без деталей документа). +// Список может быть большим, поэтому отправляем порциями и показываем прогресс. +function process_parties_from_list_chunked(loading_state, accumulated, frm, onComplete) { + const invoices = loading_state.all_invoices; + const CHUNK = 500; + let index = 0; + + window.cancelReferenceLoading = false; + + frappe.show_progress(__('Processing Counterparties'), 0, invoices.length, + __('Reading counterparties from {0} invoices...', [invoices.length]), null, true); + + // Двигаем бар ПОСЛЕ обработки каждой порции (по числу реально обработанных + // инвойсов), иначе при одной порции значение остаётся 0 и бар «висит» на 0%. + function advanceProgress() { + const done = Math.min(index, invoices.length); + frappe.show_progress(__('Processing Counterparties'), done, invoices.length, + __('Processed {0} of {1} invoices...', [done, invoices.length])); + } + + function processChunk() { + if (window.cancelReferenceLoading) { + close_data_loading_progress(); + frappe.show_alert({ message: __('Loading cancelled'), indicator: 'red' }, 3); + return; + } + + if (index >= invoices.length) { + close_data_loading_progress(); + if (onComplete) onComplete(); + return; + } + + const chunk = invoices.slice(index, index + CHUNK); + + frappe.call({ + method: 'invoice_az.api.process_invoice_parties_from_list', + args: { + invoice_list_data: JSON.stringify(chunk), + token: loading_state.token, + load_customers: loading_state.load_customers, + load_suppliers: loading_state.load_suppliers + }, + callback: function(r) { + if (r.message && r.message.success) { + accumulated.customers_created += r.message.customers_created || 0; + accumulated.suppliers_created += r.message.suppliers_created || 0; + } + index += CHUNK; + advanceProgress(); + setTimeout(processChunk, 50); + }, + error: function() { + // Пропускаем проблемную порцию и продолжаем + index += CHUNK; + advanceProgress(); + setTimeout(processChunk, 100); + } + }); + } + + processChunk(); +} + +// НОВАЯ ФУНКЦИЯ: итоговое сообщение, когда загружались только контрагенты +function show_parties_only_result(accumulated, frm) { + const msg = []; + if (accumulated.customers_created > 0) { + msg.push(`${accumulated.customers_created} customers`); + } + if (accumulated.suppliers_created > 0) { + msg.push(`${accumulated.suppliers_created} suppliers`); + } + + const text = msg.length > 0 + ? __('Created: ') + msg.join(', ') + '. Use "Add unmapped" buttons to add them to mappings.' + : __('No new counterparties found.'); + + frappe.show_alert({ message: text, indicator: 'green' }, 10); + frm.reload_doc(); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// SETUP WIZARD — compact bar at the top of the E-Taxes Settings form showing the +// CURRENT step. Clicking it smoothly expands the full list of steps (current one +// highlighted). Each step row jumps to where that step is performed; steps can be +// skipped. Mirrors the Bank Integration Profile workflow guide for familiarity. +// ═══════════════════════════════════════════════════════════════════════════════ + +function _etaxes_inject_wizard_styles() { + if (document.getElementById('etaxes-wizard-form-styles')) return; + const css = ` + .etaxes-wizard-form { margin: 0 0 12px; border: 1px solid var(--border-color, #e2e6e9); + border-radius: 8px; background: var(--card-bg, #fff); overflow: hidden; } + .etaxes-wizard-bar { display: flex; align-items: center; gap: 9px; padding: 9px 12px; + cursor: pointer; user-select: none; } + .etaxes-wizard-bar:hover { background: var(--bg-light-gray, #f4f5f6); } + .etaxes-wizard-dot { width: 9px; height: 9px; border-radius: 50%; + background: var(--blue-500, #2490ef); flex: 0 0 auto; } + .etaxes-wizard-cur { flex: 1 1 auto; } + .etaxes-wizard-cur b { margin-right: 4px; } + .etaxes-wizard-toggle { font-size: 12px; color: var(--text-muted); white-space: nowrap; + display: inline-flex; align-items: center; gap: 4px; } + .etaxes-wizard-chev { display: inline-block; + transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1); } + .etaxes-wizard-form.open .etaxes-wizard-chev { transform: rotate(180deg); } + .etaxes-wizard-list { max-height: 0; overflow: hidden; + padding: 0 6px; border-top: 0 solid var(--border-color, #eef0f2); + transition: max-height 0.45s ease-in-out, padding 0.45s ease-in-out, + border-width 0.45s ease-in-out; } + .etaxes-wizard-form.open .etaxes-wizard-list { max-height: 700px; padding: 6px; + border-top-width: 1px; } + .etaxes-wizard-step { display: flex; align-items: flex-start; gap: 10px; + padding: 8px; border-radius: 6px; } + .etaxes-wizard-step-main { display: flex; align-items: flex-start; gap: 10px; + flex: 1 1 auto; cursor: pointer; min-width: 0; } + .etaxes-wizard-step:hover { background: var(--bg-light-gray, #f4f5f6); } + .etaxes-wizard-step.cur { background: var(--highlight-color, #f0f4ff); } + .etaxes-wizard-step.done { opacity: 0.65; } + .etaxes-wizard-step.skipped { opacity: 0.5; } + .etaxes-wizard-badge { flex: 0 0 22px; width: 22px; height: 22px; border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + font-weight: 600; font-size: 12px; margin-top: 1px; + background: var(--control-bg, #f4f5f6); } + .etaxes-wizard-step.done .etaxes-wizard-badge { background: var(--green-100, #d4f3e0); + color: var(--green-600, #1f9d55); } + .etaxes-wizard-step.cur .etaxes-wizard-badge { background: var(--blue-500, #2490ef); + color: #fff; } + .etaxes-wizard-step-title { font-weight: 600; } + .etaxes-wizard-step-hint { font-size: 12px; color: var(--text-muted); margin-top: 2px; } + .etaxes-wizard-tag { font-size: 11px; font-weight: 600; margin-left: 6px; } + .etaxes-wizard-tag.cur { color: var(--blue-600, #1373cc); } + .etaxes-wizard-tag.skip { color: var(--text-muted); } + .etaxes-wizard-skip { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); + cursor: pointer; white-space: nowrap; padding: 2px 4px; align-self: center; } + .etaxes-wizard-skip:hover { color: var(--text-color, #1f272e); text-decoration: underline; }`; + const style = document.createElement('style'); + style.id = 'etaxes-wizard-form-styles'; + style.textContent = css; + document.head.appendChild(style); +} + +// Skip state is per-user (kept in localStorage — a skipped step is "my view", not a +// property of the settings shared across users). +function _etaxes_skip_key() { return 'etaxes_wizard_skipped'; } +function _etaxes_get_skipped() { + try { return JSON.parse(localStorage.getItem(_etaxes_skip_key()) || '[]'); } + catch (e) { return []; } +} +function _etaxes_set_skipped(arr) { + try { localStorage.setItem(_etaxes_skip_key(), JSON.stringify(arr)); } catch (e) { /* ignore */ } +} + +// Activate the tab a field lives on, then smooth-scroll it into view. +function _etaxes_focus_field(frm, fieldname) { + try { + frm.scroll_to_field(fieldname); + } catch (e) { + const field = frm.get_field(fieldname); + if (field && field.$wrapper && field.$wrapper[0]) { + field.$wrapper[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } +} + +function etaxes_render_settings_wizard(frm, keepOpen) { + frappe.call({ + method: 'invoice_az.api.get_settings_workflow_state', + callback(r) { + const st = (r.message && r.message.success) ? r.message : {}; + const skipped = _etaxes_get_skipped(); + + const hasDefaults = !!(frm.doc.default_item_group && frm.doc.default_uom && + frm.doc.default_supplier_group && frm.doc.default_customer_group); + const hasReference = (st.reference_total || 0) > 0; + const itemsMapped = (st.items_new || 0) === 0; + const customersMapped = (st.customers_new || 0) === 0; + const suppliersMapped = (st.suppliers_new || 0) === 0; + const unitsMapped = (st.units_new || 0) === 0; + const hasVat = (frm.doc.vat_account_mappings || []).length > 0; + + const steps = [ + { + key: 'auth', off: true, + title: __('Authenticate with ASAN Login'), + hint: __('Open Asan Login and sign in via ASAN Imza so the app can reach e-taxes.'), + action: () => frappe.set_route('List', 'Asan Login'), + }, + { + key: 'defaults', done: hasDefaults, + title: __('Configure default settings'), + hint: __('On the Settings tab, set the default Item Group, UOM, Supplier and Customer groups.'), + action: () => _etaxes_focus_field(frm, 'default_item_group'), + }, + { + key: 'load', done: hasReference, + title: __('Load reference data'), + hint: __('Use "Load All Data" to pull items, customers, suppliers and units from e-taxes.'), + action: () => show_load_all_data_dialog(frm), + }, + { + key: 'map_items', done: itemsMapped, + title: __('Map items'), + hint: __('Match or create items, then link them in the Item Mappings table.'), + action: () => _etaxes_focus_field(frm, 'item_mappings'), + }, + { + key: 'map_customers', done: customersMapped, + title: __('Map customers'), + hint: __('Match or create customers (Customers menu above), then link them.'), + action: () => _etaxes_focus_field(frm, 'customer_mappings'), + }, + { + key: 'map_suppliers', done: suppliersMapped, + title: __('Map suppliers'), + hint: __('Match or create suppliers (Suppliers menu above), then link them.'), + action: () => _etaxes_focus_field(frm, 'supplier_mappings'), + }, + { + key: 'map_units', done: unitsMapped, + title: __('Map units of measure'), + hint: __('Match or create units, then link them in the Unit of Measure Mappings table.'), + action: () => _etaxes_focus_field(frm, 'unit_mappings'), + }, + { + key: 'vat', done: hasVat, optional: true, + title: __('Configure VAT account mappings'), + hint: __('Optional — only needed to import VAT operations as Journal Entries. Invoices work without it.'), + action: () => _etaxes_focus_field(frm, 'vat_account_mappings'), + }, + ]; + steps.forEach((s) => { s.skipped = !s.off && !s.optional && skipped.indexOf(s.key) !== -1; }); + + // Optional / navigation-only steps never become the forced "current" step, + // so an unconfigured optional step doesn't block the wizard or mislead. + let currentIdx = steps.findIndex((s) => !s.off && !s.optional && !s.done && !s.skipped); + if (currentIdx === -1) { + // Everything required is done/skipped → point at the last real step. + for (let i = steps.length - 1; i >= 0; i--) { + if (!steps[i].off && !steps[i].optional) { currentIdx = i; break; } + } + if (currentIdx === -1) currentIdx = steps.length - 1; + } + const cur = steps[currentIdx]; + + const esc = frappe.utils.escape_html; + const rows = steps.map((s, i) => { + const isCur = i === currentIdx; + let badge; + let cls; + let tag; + const optTag = s.optional + ? ` ${__('Optional')}` : ''; + if (s.skipped) { + badge = '–'; cls = 'skipped'; + tag = ` ${__('Skipped')}`; + } else if (s.done) { + badge = '✓'; cls = 'done'; tag = optTag; + } else if (isCur) { + badge = '➜'; cls = 'cur'; + tag = ` ${__('Current step')}`; + } else { + badge = (i + 1); cls = 'todo'; tag = optTag; + } + + const skipCtl = (s.off || s.optional) ? '' : + `${ + s.skipped ? __('Unskip') : __('Skip')}`; + + return ` +
+
+ ${badge} +
+
${esc(s.title)}${tag}
+
${esc(s.hint)}
+
+
+ ${skipCtl} +
`; + }).join(''); + + const html = ` +
+
+ + ${__('E-Taxes Setup')} · ${esc(cur.title)} + ${__('Steps')} +
+
${rows}
+
`; + + _etaxes_inject_wizard_styles(); + + // Mount at the top of the form body; re-render replaces any prior instance. + const $host = frm.$wrapper.find('.form-layout').first(); + frm.$wrapper.find('.etaxes-wizard-form').remove(); + const $w = $(html); + $host.prepend($w); + + $w.find('.etaxes-wizard-bar').on('click', () => $w.toggleClass('open')); + $w.find('.etaxes-wizard-step-main').on('click', function () { + steps[$(this).data('etaxes-step')].action(); + }); + $w.find('.etaxes-wizard-skip').on('click', function (e) { + e.stopPropagation(); + const key = $(this).data('etaxes-skip'); + const set = _etaxes_get_skipped(); + const idx = set.indexOf(key); + if (idx === -1) set.push(key); else set.splice(idx, 1); + _etaxes_set_skipped(set); + etaxes_render_settings_wizard(frm, /* keepOpen */ true); + }); + }, + }); +}