refactor(client): delegate bulk-load progress/cancel to the global widget

Drop the per-page progress/cancel UI from the e-taxes list views, journal
entry, purchase/sales order and sales invoice loaders, and the settings
reference loader; progress and cancellation are now shown by the global
Background Tasks widget via realtime events.
This commit is contained in:
Ali 2026-06-16 16:02:14 +00:00
parent ad4a6ccfd2
commit 15c626b007
7 changed files with 641 additions and 100 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +1118,7 @@ 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'
@ -1010,7 +1128,7 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data,
// Если все инвойсы обработаны
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,7 +2450,10 @@ function load_invoice_batch(loading_state, source, frm) {
// НОВАЯ ФУНКЦИЯ: Проверка завершения загрузки
function check_loading_completion(loading_state, frm) {
if (loading_state.inbox_completed && loading_state.outbox_completed) {
if (!(loading_state.inbox_completed && loading_state.outbox_completed)) {
return;
}
hide_loading_dialog_settings();
if (loading_state.all_invoices.length === 0) {
@ -2327,16 +2461,21 @@ function check_loading_completion(loading_state, frm) {
return;
}
// ИЗМЕНЕНО: Показываем результат загрузки
frappe.show_alert({
message: __('Found {0} invoices. Starting processing...', [loading_state.all_invoices.length]),
indicator: 'green'
}, 3); // Показываем на 3 секунды
}, 3);
// Небольшая задержка и начинаем обработку (прогресс-бар появляется быстро,
// спиннер "Loading Data" к этому моменту уже снят).
setTimeout(() => {
process_invoices_for_reference_data(loading_state.all_invoices, loading_state.token, {
// ОПТИМИЗАЦИЯ: есть 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,
@ -2346,12 +2485,349 @@ function check_loading_completion(loading_state, frm) {
suppliers_created: 0,
suppliers_skipped: 0,
total_invoices: loading_state.all_invoices.length
}, 0, frm, {
};
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);
}
};
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);
}
});
}, 400);
}
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
? ` <span class="etaxes-wizard-tag skip">${__('Optional')}</span>` : '';
if (s.skipped) {
badge = ''; cls = 'skipped';
tag = ` <span class="etaxes-wizard-tag skip">${__('Skipped')}</span>`;
} else if (s.done) {
badge = '✓'; cls = 'done'; tag = optTag;
} else if (isCur) {
badge = '➜'; cls = 'cur';
tag = ` <span class="etaxes-wizard-tag cur">${__('Current step')}</span>`;
} else {
badge = (i + 1); cls = 'todo'; tag = optTag;
}
const skipCtl = (s.off || s.optional) ? '' :
`<span class="etaxes-wizard-skip" data-etaxes-skip="${s.key}">${
s.skipped ? __('Unskip') : __('Skip')}</span>`;
return `
<div class="etaxes-wizard-step ${cls}">
<div class="etaxes-wizard-step-main" data-etaxes-step="${i}">
<span class="etaxes-wizard-badge">${badge}</span>
<div class="etaxes-wizard-step-body">
<div class="etaxes-wizard-step-title">${esc(s.title)}${tag}</div>
<div class="etaxes-wizard-step-hint">${esc(s.hint)}</div>
</div>
</div>
${skipCtl}
</div>`;
}).join('');
const html = `
<div class="etaxes-wizard-form${keepOpen ? ' open' : ''}">
<div class="etaxes-wizard-bar">
<span class="etaxes-wizard-dot"></span>
<span class="etaxes-wizard-cur"><b>${__('E-Taxes Setup')}</b> · ${esc(cur.title)}</span>
<span class="etaxes-wizard-toggle">${__('Steps')}<span class="etaxes-wizard-chev"></span></span>
</div>
<div class="etaxes-wizard-list">${rows}</div>
</div>`;
_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);
});
},
});
}