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:
parent
ad4a6ccfd2
commit
15c626b007
|
|
@ -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(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? ` <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);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue