diff --git a/invoice_az/client/employee.js b/invoice_az/client/employee.js
index b7a5e32..f1b6b46 100644
--- a/invoice_az/client/employee.js
+++ b/invoice_az/client/employee.js
@@ -272,1310 +272,3 @@ function populate_amas_integration_tab(data) {
$('#amas_integration_data').html(html);
}
-
-// Bootstrap 4.6 stacked-modal fix (twbs/bootstrap#4182): when any modal hides,
-// BS4 unconditionally strips `modal-open` from
, which orphans the
-// backdrop of any modal still open underneath (e.g. progress + nested confirm).
-// Re-apply the class if at least one modal is still visible. Idempotent: the
-// flag prevents duplicate listeners if this file is re-loaded.
-if (!window._amas_stacked_modal_fix_installed) {
- window._amas_stacked_modal_fix_installed = true;
- $(document).on('hidden.bs.modal', '.modal', function () {
- if ($('.modal.show').length) {
- $('body').addClass('modal-open');
- }
- });
-}
-
-// List view — add "Load from ƏMAS" button
-if (frappe.listview_settings['Employee']) {
- const orig_onload = frappe.listview_settings['Employee'].onload;
- frappe.listview_settings['Employee'].onload = function(listview) {
- if (orig_onload) orig_onload(listview);
- add_amas_load_button(listview);
- reattach_amas_import_if_running(listview);
- };
-} else {
- frappe.listview_settings['Employee'] = {
- onload: function(listview) {
- add_amas_load_button(listview);
- reattach_amas_import_if_running(listview);
- }
- };
-}
-
-function add_amas_load_button(listview) {
- listview.page.add_inner_button(__('Load from ƏMAS'), function() {
- start_amas_employee_import(listview);
- });
-}
-
-// On listview mount, check if a worker is currently running an import for any
-// of the user's connected Asan Logins. If yes, re-register the realtime
-// listeners so the user can see progress + Cancel on the bar even after a
-// page reload.
-function reattach_amas_import_if_running(listview) {
- frappe.call({
- method: 'invoice_az.amas_api.get_connected_asan_logins',
- callback: function(r) {
- if (!r.message || !r.message.success) return;
- const asan_logins = r.message.asan_logins || [];
- if (!asan_logins.length) return;
-
- // Probe each login until we find one with an active job.
- const tryNext = function(idx) {
- if (idx >= asan_logins.length) return;
- const name = asan_logins[idx].name;
- frappe.call({
- method: 'invoice_az.amas_api.get_amas_import_status',
- args: { asan_login_name: name },
- callback: function(r2) {
- if (r2.message && r2.message.running) {
- attach_amas_import_listeners(listview, name, 0);
- } else {
- tryNext(idx + 1);
- }
- }
- });
- };
- tryNext(0);
- }
- });
-}
-
-// Step 1: Get default connected Asan Login → fetch employees from ƏMAS API
-function start_amas_employee_import(listview) {
- frappe.call({
- method: 'invoice_az.amas_api.get_connected_asan_logins',
- freeze: true,
- freeze_message: __('Checking ƏMAS connection...'),
- callback: function(r) {
- if (!r.message || !r.message.success) {
- frappe.msgprint({
- title: __('Error'),
- indicator: 'red',
- message: r.message ? r.message.message : __('Failed to check ƏMAS connections')
- });
- return;
- }
-
- const asan_logins = r.message.asan_logins || [];
-
- if (asan_logins.length === 0) {
- // No connected ƏMAS - start connection process automatically
- start_amas_connection_for_import(listview);
- return;
- }
-
- // Use the first (default) connected login
- fetch_employees_from_amas(listview, asan_logins[0].name, asan_logins[0].organization_name);
- }
- });
-}
-
-// Start ƏMAS connection process automatically for import
-function start_amas_connection_for_import(listview) {
- // Get first available Asan Login
- frappe.call({
- method: 'frappe.client.get_list',
- args: {
- doctype: 'Asan Login',
- fields: ['name', 'phone', 'user_id'],
- limit_page_length: 1
- },
- callback: function(r) {
- if (!r.message || r.message.length === 0) {
- frappe.msgprint({
- title: __('No Asan Login Found'),
- indicator: 'red',
- message: __('Please create an Asan Login first.')
- });
- return;
- }
-
- const asan_login = r.message[0];
-
- // Show confirmation dialog (same design as e-taxes)
- const d = new frappe.ui.Dialog({
- title: __('ƏMAS Connection Required'),
- fields: [
- {
- fieldname: 'message',
- fieldtype: 'HTML',
- options: `
- ' + __('Cancel the running ƏMAS import? Already-saved employees will stay; the rest will be skipped.') + '
'
- );
- }
- cancel_confirm_dialog.show();
- // Progress dialog forces z-index 2000; lift the confirm above it.
- cancel_confirm_dialog.$wrapper.css('z-index', 2100);
- });
- };
-
- // Hand the reusable confirm dialog over to the import-complete handler so
- // it can fully remove the leftover element from DOM when the import ends.
- const cleanup_cancel_dialog = function() {
- if (cancel_confirm_dialog) {
- cancel_confirm_dialog.hide();
- cancel_confirm_dialog.$wrapper.remove();
- cancel_confirm_dialog = null;
- }
- };
-
- // Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
- // frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
- // уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
- let importFinished = false;
- // True once we've observed the import actually running (via a realtime tick
- // or a status poll). Guards the poll-based completion detector so it can't
- // fire before the worker has even started (the running flag is set
- // server-side a beat after import_bulk_employees is called).
- let sawRunning = (total > 0);
- // Highest overall percent (0-100) rendered so far. Realtime and polling both
- // feed the bar; this keeps it monotonic so a stale poll can't rewind a
- // fresher realtime tick (or vice-versa).
- let lastPercent = 0;
-
- let dialog = frappe.show_progress(
- __('Importing from ƏMAS'), 0, 100,
- total
- ? __('Starting import of {0} employees...', [total])
- : __('Preparing import...')
- );
- ensure_cancel_button(dialog);
-
- const phaseLabel = function(phase) {
- if (phase === 'save') return __('Saving employees');
- if (phase === 'fetch') return __('Fetching details');
- if (phase === 'connecting') return __('Connecting to ƏMAS...');
- if (phase === 'queued') return __('Waiting to start...');
- return __('Preparing import...');
- };
-
- // The bar tracks ONE overall percent across both import phases (fetch =
- // 0-50, save = 50-100), computed server-side. Fall back to deriving it the
- // same way if an older worker sends no `percent`.
- const overallPercent = function(percent, current, total, phase) {
- if (typeof percent === 'number' && percent >= 0) return percent;
- if (!(total > 0)) return 0;
- const frac = Math.max(0, Math.min(1, current / total));
- if (phase === 'save') return 50 + frac * 50;
- if (phase === 'fetch') return frac * 50;
- return 0;
- };
-
- // Single entry point for moving the bar, fed by both the realtime handler
- // and the status poll. Never regresses.
- const renderProgress = function(percent, current, total, phase, employee_name) {
- if (importFinished) return;
- const pct = overallPercent(percent, current, total, phase);
- if (pct > lastPercent) lastPercent = pct;
-
- const label = phaseLabel(phase);
- let detail;
- if (phase === 'queued' || phase === 'connecting' || !(total > 0)) {
- detail = label;
- } else if (employee_name) {
- detail = label + ': ' + employee_name + ' (' + current + '/' + total + ')';
- } else {
- detail = label + ': ' + __('{0} of {1}', [current, total]);
- }
- const d = frappe.show_progress(
- __('Importing from ƏMAS'), Math.round(lastPercent), 100, detail
- );
- ensure_cancel_button(d);
- };
-
- frappe.realtime.off('amas_import_progress');
- frappe.realtime.on('amas_import_progress', function(data) {
- if (importFinished) return;
- sawRunning = true;
- renderProgress(data.percent, data.current, data.total, data.phase, data.employee_name);
- });
-
- // Polling fallback. Realtime events are transient — anything published
- // before this page subscribed (after the wizard redirects to /app, or a
- // reload) is lost, leaving the bar frozen. Polling the server-persisted
- // progress guarantees the bar moves, and lets us detect completion even
- // when the 'amas_import_complete' event itself was missed.
- const finishFromPoll = function() {
- if (importFinished) return;
- importFinished = true;
- frappe.realtime.off('amas_import_progress');
- frappe.realtime.off('amas_import_complete');
- closeAmasProgress(); // also clears amasPollTimer
- cleanup_cancel_dialog();
- listview.refresh();
- frappe.show_alert({ message: __('ƏMAS import finished.'), indicator: 'green' }, 5);
- };
-
- const pollStatus = function() {
- if (importFinished) return;
- frappe.call({
- method: 'invoice_az.amas_api.get_amas_import_status',
- args: { asan_login_name: asan_login_name },
- callback: function(r) {
- if (importFinished) return;
- const m = r.message || {};
- if (m.running) {
- sawRunning = true;
- renderProgress(m.percent, m.current, m.total, m.phase, m.employee_name);
- } else if (sawRunning) {
- // We watched it run and now it's gone — the realtime
- // 'complete' was missed. Wrap up gracefully.
- finishFromPoll();
- }
- }
- });
- };
-
- if (amasPollTimer) clearInterval(amasPollTimer);
- amasPollTimer = setInterval(pollStatus, 1000);
- // Reconnect path (no total hint): the caller already confirmed an import is
- // running, so seed the bar from real numbers immediately instead of waiting
- // a full poll interval on the bare "preparing" text.
- if (!total) pollStatus();
-
- frappe.realtime.off('amas_import_complete');
- frappe.realtime.on('amas_import_complete', function(data) {
- if (importFinished) return; // poll-based finish already wrapped up
- importFinished = true;
- frappe.realtime.off('amas_import_progress');
- frappe.realtime.off('amas_import_complete');
- closeAmasProgress();
- cleanup_cancel_dialog();
-
- if (data.cancelled) {
- frappe.msgprint({
- title: __('Import Cancelled'),
- indicator: 'orange',
- message: __(
- 'Cancelled. Created: {0}, updated: {1}.',
- [data.created || 0, data.updated || 0]
- )
- });
- listview.refresh();
- return;
- }
-
- if (data.aborted) {
- const reason = (data.errors && data.errors[0] && data.errors[0].error)
- || __('Import was aborted.');
- frappe.msgprint({
- title: __('Import Aborted'),
- indicator: 'red',
- message: reason
- });
- listview.refresh();
- return;
- }
-
- const error_messages = (data.errors || []).map(function(err) {
- return (err.employee || 'Unknown') + ': ' + (err.error || 'Unknown error');
- });
-
- const total_processed = data.created + data.updated + (data.errors || []).length;
- show_import_summary(total_processed, data.created, data.updated, (data.errors || []).length, error_messages);
-
- listview.refresh();
- });
-}
-
-// Module-level handle for the status-poll interval (fallback that drives the
-// progress bar when transient realtime events are missed). Only one ƏMAS import
-// is ever tracked at a time, so a single timer is enough. closeAmasProgress()
-// is the universal teardown path, so clearing it there covers every exit.
-let amasPollTimer = null;
-
-// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
-// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
-function closeAmasProgress() {
- if (amasPollTimer) {
- clearInterval(amasPollTimer);
- amasPollTimer = null;
- }
- if (frappe.cur_progress) {
- try {
- frappe.cur_progress.$wrapper.modal('hide');
- frappe.cur_progress.$wrapper.remove();
- } catch (e) {}
- frappe.cur_progress = null;
- }
- if ($('.modal:visible').length === 0) {
- $('.modal-backdrop').remove();
- $('body').removeClass('modal-open');
- }
-}
-
-function create_employees(listview, asan_login_name, selected_employees, company, create_designation) {
- const total = selected_employees.length;
-
- attach_amas_import_listeners(listview, asan_login_name, total);
-
- frappe.call({
- method: 'invoice_az.amas_api.import_bulk_employees',
- args: {
- asan_login_name: asan_login_name,
- employees_data: JSON.stringify(selected_employees),
- company: company,
- create_designation: create_designation ? 1 : 0
- },
- callback: function(r) {
- if (r.message && r.message.already_running) {
- frappe.realtime.off('amas_import_progress');
- frappe.realtime.off('amas_import_complete');
- closeAmasProgress();
- frappe.msgprint({
- title: __('Import Already Running'),
- indicator: 'orange',
- message: r.message.message
- });
- return;
- }
- if (!r.message || !r.message.enqueued) {
- frappe.realtime.off('amas_import_progress');
- frappe.realtime.off('amas_import_complete');
- closeAmasProgress();
- frappe.msgprint({
- title: __('Error'),
- indicator: 'red',
- message: __('Failed to start import job')
- });
- }
- },
- error: function() {
- frappe.realtime.off('amas_import_progress');
- frappe.realtime.off('amas_import_complete');
- closeAmasProgress();
- frappe.msgprint({
- title: __('Error'),
- indicator: 'red',
- message: __('Network error starting import')
- });
- }
- });
-}
-
-// Process employees one by one (like E-Taxes loadSelectedInvoices)
-function process_employees_one_by_one(listview, asan_login_name, employees_queue, company, create_designation,
- created_count, updated_count, error_count, errors, current_index) {
- // Check if finished
- if (employees_queue.length === 0) {
- closeAmasProgress();
- $(document).off('progress-cancel.loading_amas_employees');
-
- const total = created_count + updated_count + error_count;
- show_import_summary(total, created_count, updated_count, error_count, errors);
-
- window.amas_employees_to_process = null;
- listview.refresh();
- return;
- }
-
- // Check if cancelled
- if (window.amas_cancel_loading) {
- employees_queue = [];
- closeAmasProgress();
- $(document).off('progress-cancel.loading_amas_employees');
-
- const total = created_count + updated_count + error_count;
- frappe.show_alert({
- message: __('Import cancelled. Processed ') + total + __(' employees.'),
- indicator: 'orange'
- }, 5);
-
- listview.refresh();
- return;
- }
-
- // Take first employee from queue
- const emp_data = employees_queue.shift();
- const is_last = employees_queue.length === 0;
- const total_to_process = window.amas_employees_to_process || (created_count + updated_count + error_count + employees_queue.length + 1);
-
- current_index++;
-
- // Show progress
- frappe.show_progress(
- __('Loading from ƏMAS'),
- current_index - 1,
- total_to_process,
- __('Processing employee ') + current_index + __(' of ') + total_to_process,
- null,
- true
- );
-
- // Setup cancel handler
- $(document).off('progress-cancel.loading_amas_employees');
- $(document).on('progress-cancel.loading_amas_employees', function() {
- window.amas_cancel_loading = true;
- frappe.show_alert({
- message: __('Cancelling import... Finishing current employee.'),
- indicator: 'orange'
- }, 3);
- closeAmasProgress();
- });
-
- // Get employee name for progress message
- const emp_name = emp_data.full_name || emp_data.identification_number || 'Employee';
-
- // Update progress with employee name
- frappe.show_progress(
- __('Loading from ƏMAS'),
- current_index - 1,
- total_to_process,
- __('Processing: ') + emp_name
- );
-
- // Create/update single employee
- frappe.call({
- method: 'invoice_az.amas_api.create_single_employee_from_amas',
- args: {
- asan_login_name: asan_login_name,
- emp_data: emp_data,
- company: company,
- create_designation: create_designation ? 1 : 0
- },
- callback: function(r) {
- if (r.message && r.message.success) {
- // Success - increment counters
- if (r.message.action === 'created') {
- created_count++;
- frappe.show_alert({
- message: __('Created: ') + r.message.full_name,
- indicator: 'green'
- }, 2);
- } else if (r.message.action === 'updated') {
- updated_count++;
- frappe.show_alert({
- message: __('Updated: ') + r.message.full_name,
- indicator: 'blue'
- }, 2);
- }
- } else {
- // Error
- error_count++;
- const error_msg = r.message ? r.message.message : 'Unknown error';
- errors.push(error_msg);
-
- frappe.show_alert({
- message: __('Error: ') + (r.message ? r.message.full_name : emp_name),
- indicator: 'red'
- }, 3);
- }
-
- // Continue with next employee after short delay (like E-Taxes)
- setTimeout(function() {
- process_employees_one_by_one(
- listview,
- asan_login_name,
- employees_queue,
- company,
- create_designation,
- created_count,
- updated_count,
- error_count,
- errors,
- current_index
- );
- }, 50); // 50ms delay like E-Taxes PROGRESS_UPDATE_DELAY
- },
- error: function(xhr, status, error) {
- // Network error
- error_count++;
- const error_msg = emp_name + ': Network error - ' + (error || 'Unknown error');
- errors.push(error_msg);
-
- frappe.show_alert({
- message: __('Network error: ') + emp_name,
- indicator: 'red'
- }, 3);
-
- // Continue with next employee
- setTimeout(function() {
- process_employees_one_by_one(
- listview,
- asan_login_name,
- employees_queue,
- company,
- create_designation,
- created_count,
- updated_count,
- error_count,
- errors,
- current_index
- );
- }, 50);
- }
- });
-}
-
-// Show final summary (like E-Taxes)
-function show_import_summary(total, created_count, updated_count, error_count, errors) {
- if (error_count === 0) {
- let msg = __('All ') + total + __(' employees were processed successfully.');
- if (created_count > 0) {
- msg += '';
- if (created_count > 0) {
- msg += __('Successfully created: ') + '' + created_count + ' ';
- }
- if (updated_count > 0) {
- msg += __('Updated: ') + '' + updated_count + ' ';
- }
- msg += __('Failed: ') + '' + error_count + ' ';
- msg += '
';
-
- if (errors && errors.length > 0) {
- msg += 'MyGovID authentication successful. Now connecting to ƏMAS...
- `);
- $('#mygovid_code').hide();
-
- // Connect to ƏMAS
- frappe.call({
- method: 'invoice_az.amas_api.connect_amas',
- args: {
- 'asan_login_name': asan_login_name
- },
- callback: function(r2) {
- if (r2.message && r2.message.success) {
- const accounts = r2.message.accounts || [];
-
- if (accounts.length > 0) {
- // Check if we should show selection dialog or auto-select
- if (show_org_selection) {
- // Show organization selection dialog (for re-authentication)
- status_dialog.hide();
- show_amas_accounts_dialog(asan_login_name, accounts, retry_callback);
- } else {
- // Auto-select first organization (for initial connection)
- const first_account = accounts[0];
- const oid = first_account.accountOid || first_account.oid || '';
- const name = first_account.accountName || first_account.name || 'Unknown';
- const number = first_account.accountNumber || first_account.voen || first_account.number || '';
- const role = first_account.roleName || first_account.role || 'chairman';
-
- $('#mygovid_status').html(`
- ƏMAS connected. Selecting ${frappe.utils.escape_html(name)}...
- `);
-
- // Auto-select first account
- frappe.call({
- method: 'invoice_az.amas_api.change_amas_account',
- args: {
- 'asan_login_name': asan_login_name,
- 'account_oid': oid,
- 'account_name': name,
- 'account_number': number,
- 'role_name': role
- },
- callback: function(r3) {
- if (r3.message && r3.message.success) {
- $('#mygovid_status').html(`
- Successfully connected to ${frappe.utils.escape_html(name)}.
- `);
- $('.mygovid-spinner').html(`
- Successfully connected to ƏMAS.
- `);
- $('.mygovid-spinner').html(`
- The request timed out. Please try again.
- `);
- $('.mygovid-spinner').css('display', 'none');
- $('#mygovid_code').hide();
-
- status_dialog.set_primary_action(__('Close'), function() {
- status_dialog.hide();
- });
- }
- });
-
- // Get verification code
- setTimeout(function() {
- frappe.call({
- method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code',
- args: { 'asan_login_name': asan_login_name },
- callback: function(r) {
- if (r.message && r.message.success) {
- $('#mygovid_code_text').text(r.message.code);
- $('#mygovid_code').show();
- }
- }
- });
- }, 500);
-}
-
-// Show ƏMAS accounts selection dialog
-function show_amas_accounts_dialog(asan_login_name, accounts, retry_callback) {
- if (!accounts || accounts.length === 0) {
- frappe.msgprint(__('No organizations found in ƏMAS'));
- return;
- }
-
- // Build accounts table
- let table_html = '' + __('Cancel the running ƏMAS import? Already-saved employees will stay; the rest will be skipped.') + '
'
+ );
+ }
+ cancel_confirm_dialog.show();
+ // Progress dialog forces z-index 2000; lift the confirm above it.
+ cancel_confirm_dialog.$wrapper.css('z-index', 2100);
+ });
+ };
+
+ // Hand the reusable confirm dialog over to the import-complete handler so
+ // it can fully remove the leftover element from DOM when the import ends.
+ const cleanup_cancel_dialog = function() {
+ if (cancel_confirm_dialog) {
+ cancel_confirm_dialog.hide();
+ cancel_confirm_dialog.$wrapper.remove();
+ cancel_confirm_dialog = null;
+ }
+ };
+
+ // Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
+ // frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
+ // уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
+ let importFinished = false;
+ // True once we've observed the import actually running (via a realtime tick
+ // or a status poll). Guards the poll-based completion detector so it can't
+ // fire before the worker has even started (the running flag is set
+ // server-side a beat after import_bulk_employees is called).
+ let sawRunning = (total > 0);
+ // Highest overall percent (0-100) rendered so far. Realtime and polling both
+ // feed the bar; this keeps it monotonic so a stale poll can't rewind a
+ // fresher realtime tick (or vice-versa).
+ let lastPercent = 0;
+
+ let dialog = frappe.show_progress(
+ __('Importing from ƏMAS'), 0, 100,
+ total
+ ? __('Starting import of {0} employees...', [total])
+ : __('Preparing import...')
+ );
+ ensure_cancel_button(dialog);
+
+ const phaseLabel = function(phase) {
+ if (phase === 'save') return __('Saving employees');
+ if (phase === 'fetch') return __('Fetching details');
+ if (phase === 'connecting') return __('Connecting to ƏMAS...');
+ if (phase === 'queued') return __('Waiting to start...');
+ return __('Preparing import...');
+ };
+
+ // The bar tracks ONE overall percent across both import phases (fetch =
+ // 0-50, save = 50-100), computed server-side. Fall back to deriving it the
+ // same way if an older worker sends no `percent`.
+ const overallPercent = function(percent, current, total, phase) {
+ if (typeof percent === 'number' && percent >= 0) return percent;
+ if (!(total > 0)) return 0;
+ const frac = Math.max(0, Math.min(1, current / total));
+ if (phase === 'save') return 50 + frac * 50;
+ if (phase === 'fetch') return frac * 50;
+ return 0;
+ };
+
+ // Single entry point for moving the bar, fed by both the realtime handler
+ // and the status poll. Never regresses.
+ const renderProgress = function(percent, current, total, phase, employee_name) {
+ if (importFinished) return;
+ const pct = overallPercent(percent, current, total, phase);
+ if (pct > lastPercent) lastPercent = pct;
+
+ const label = phaseLabel(phase);
+ let detail;
+ if (phase === 'queued' || phase === 'connecting' || !(total > 0)) {
+ detail = label;
+ } else if (employee_name) {
+ detail = label + ': ' + employee_name + ' (' + current + '/' + total + ')';
+ } else {
+ detail = label + ': ' + __('{0} of {1}', [current, total]);
+ }
+ const d = frappe.show_progress(
+ __('Importing from ƏMAS'), Math.round(lastPercent), 100, detail
+ );
+ ensure_cancel_button(d);
+ };
+
+ frappe.realtime.off('amas_import_progress');
+ frappe.realtime.on('amas_import_progress', function(data) {
+ if (importFinished) return;
+ sawRunning = true;
+ renderProgress(data.percent, data.current, data.total, data.phase, data.employee_name);
+ });
+
+ // Polling fallback. Realtime events are transient — anything published
+ // before this page subscribed (after the wizard redirects to /app, or a
+ // reload) is lost, leaving the bar frozen. Polling the server-persisted
+ // progress guarantees the bar moves, and lets us detect completion even
+ // when the 'amas_import_complete' event itself was missed.
+ const finishFromPoll = function() {
+ if (importFinished) return;
+ importFinished = true;
+ frappe.realtime.off('amas_import_progress');
+ frappe.realtime.off('amas_import_complete');
+ closeAmasProgress(); // also clears amasPollTimer
+ cleanup_cancel_dialog();
+ listview.refresh();
+ frappe.show_alert({ message: __('ƏMAS import finished.'), indicator: 'green' }, 5);
+ };
+
+ const pollStatus = function() {
+ if (importFinished) return;
+ frappe.call({
+ method: 'invoice_az.amas_api.get_amas_import_status',
+ args: { asan_login_name: asan_login_name },
+ callback: function(r) {
+ if (importFinished) return;
+ const m = r.message || {};
+ if (m.running) {
+ sawRunning = true;
+ renderProgress(m.percent, m.current, m.total, m.phase, m.employee_name);
+ } else if (sawRunning) {
+ // We watched it run and now it's gone — the realtime
+ // 'complete' was missed. Wrap up gracefully.
+ finishFromPoll();
+ }
+ }
+ });
+ };
+
+ if (amasPollTimer) clearInterval(amasPollTimer);
+ amasPollTimer = setInterval(pollStatus, 1000);
+ // Reconnect path (no total hint): the caller already confirmed an import is
+ // running, so seed the bar from real numbers immediately instead of waiting
+ // a full poll interval on the bare "preparing" text.
+ if (!total) pollStatus();
+
+ frappe.realtime.off('amas_import_complete');
+ frappe.realtime.on('amas_import_complete', function(data) {
+ if (importFinished) return; // poll-based finish already wrapped up
+ importFinished = true;
+ frappe.realtime.off('amas_import_progress');
+ frappe.realtime.off('amas_import_complete');
+ closeAmasProgress();
+ cleanup_cancel_dialog();
+
+ if (data.cancelled) {
+ frappe.msgprint({
+ title: __('Import Cancelled'),
+ indicator: 'orange',
+ message: __(
+ 'Cancelled. Created: {0}, updated: {1}.',
+ [data.created || 0, data.updated || 0]
+ )
+ });
+ listview.refresh();
+ return;
+ }
+
+ if (data.aborted) {
+ const reason = (data.errors && data.errors[0] && data.errors[0].error)
+ || __('Import was aborted.');
+ frappe.msgprint({
+ title: __('Import Aborted'),
+ indicator: 'red',
+ message: reason
+ });
+ listview.refresh();
+ return;
+ }
+
+ const error_messages = (data.errors || []).map(function(err) {
+ return (err.employee || 'Unknown') + ': ' + (err.error || 'Unknown error');
+ });
+
+ const total_processed = data.created + data.updated + (data.errors || []).length;
+ show_import_summary(total_processed, data.created, data.updated, (data.errors || []).length, error_messages);
+
+ listview.refresh();
+ });
+}
+
+// Module-level handle for the status-poll interval (fallback that drives the
+// progress bar when transient realtime events are missed). Only one ƏMAS import
+// is ever tracked at a time, so a single timer is enough. closeAmasProgress()
+// is the universal teardown path, so clearing it there covers every exit.
+let amasPollTimer = null;
+
+// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
+// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
+function closeAmasProgress() {
+ if (amasPollTimer) {
+ clearInterval(amasPollTimer);
+ amasPollTimer = null;
+ }
+ if (frappe.cur_progress) {
+ try {
+ frappe.cur_progress.$wrapper.modal('hide');
+ frappe.cur_progress.$wrapper.remove();
+ } catch (e) {}
+ frappe.cur_progress = null;
+ }
+ if ($('.modal:visible').length === 0) {
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+}
+
+function create_employees(listview, asan_login_name, selected_employees, company, create_designation) {
+ const total = selected_employees.length;
+
+ attach_amas_import_listeners(listview, asan_login_name, total);
+
+ frappe.call({
+ method: 'invoice_az.amas_api.import_bulk_employees',
+ args: {
+ asan_login_name: asan_login_name,
+ employees_data: JSON.stringify(selected_employees),
+ company: company,
+ create_designation: create_designation ? 1 : 0
+ },
+ callback: function(r) {
+ if (r.message && r.message.already_running) {
+ frappe.realtime.off('amas_import_progress');
+ frappe.realtime.off('amas_import_complete');
+ closeAmasProgress();
+ frappe.msgprint({
+ title: __('Import Already Running'),
+ indicator: 'orange',
+ message: r.message.message
+ });
+ return;
+ }
+ if (!r.message || !r.message.enqueued) {
+ frappe.realtime.off('amas_import_progress');
+ frappe.realtime.off('amas_import_complete');
+ closeAmasProgress();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Failed to start import job')
+ });
+ }
+ },
+ error: function() {
+ frappe.realtime.off('amas_import_progress');
+ frappe.realtime.off('amas_import_complete');
+ closeAmasProgress();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Network error starting import')
+ });
+ }
+ });
+}
+
+// Process employees one by one (like E-Taxes loadSelectedInvoices)
+function process_employees_one_by_one(listview, asan_login_name, employees_queue, company, create_designation,
+ created_count, updated_count, error_count, errors, current_index) {
+ // Check if finished
+ if (employees_queue.length === 0) {
+ closeAmasProgress();
+ $(document).off('progress-cancel.loading_amas_employees');
+
+ const total = created_count + updated_count + error_count;
+ show_import_summary(total, created_count, updated_count, error_count, errors);
+
+ window.amas_employees_to_process = null;
+ listview.refresh();
+ return;
+ }
+
+ // Check if cancelled
+ if (window.amas_cancel_loading) {
+ employees_queue = [];
+ closeAmasProgress();
+ $(document).off('progress-cancel.loading_amas_employees');
+
+ const total = created_count + updated_count + error_count;
+ frappe.show_alert({
+ message: __('Import cancelled. Processed ') + total + __(' employees.'),
+ indicator: 'orange'
+ }, 5);
+
+ listview.refresh();
+ return;
+ }
+
+ // Take first employee from queue
+ const emp_data = employees_queue.shift();
+ const is_last = employees_queue.length === 0;
+ const total_to_process = window.amas_employees_to_process || (created_count + updated_count + error_count + employees_queue.length + 1);
+
+ current_index++;
+
+ // Show progress
+ frappe.show_progress(
+ __('Loading from ƏMAS'),
+ current_index - 1,
+ total_to_process,
+ __('Processing employee ') + current_index + __(' of ') + total_to_process,
+ null,
+ true
+ );
+
+ // Setup cancel handler
+ $(document).off('progress-cancel.loading_amas_employees');
+ $(document).on('progress-cancel.loading_amas_employees', function() {
+ window.amas_cancel_loading = true;
+ frappe.show_alert({
+ message: __('Cancelling import... Finishing current employee.'),
+ indicator: 'orange'
+ }, 3);
+ closeAmasProgress();
+ });
+
+ // Get employee name for progress message
+ const emp_name = emp_data.full_name || emp_data.identification_number || 'Employee';
+
+ // Update progress with employee name
+ frappe.show_progress(
+ __('Loading from ƏMAS'),
+ current_index - 1,
+ total_to_process,
+ __('Processing: ') + emp_name
+ );
+
+ // Create/update single employee
+ frappe.call({
+ method: 'invoice_az.amas_api.create_single_employee_from_amas',
+ args: {
+ asan_login_name: asan_login_name,
+ emp_data: emp_data,
+ company: company,
+ create_designation: create_designation ? 1 : 0
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ // Success - increment counters
+ if (r.message.action === 'created') {
+ created_count++;
+ frappe.show_alert({
+ message: __('Created: ') + r.message.full_name,
+ indicator: 'green'
+ }, 2);
+ } else if (r.message.action === 'updated') {
+ updated_count++;
+ frappe.show_alert({
+ message: __('Updated: ') + r.message.full_name,
+ indicator: 'blue'
+ }, 2);
+ }
+ } else {
+ // Error
+ error_count++;
+ const error_msg = r.message ? r.message.message : 'Unknown error';
+ errors.push(error_msg);
+
+ frappe.show_alert({
+ message: __('Error: ') + (r.message ? r.message.full_name : emp_name),
+ indicator: 'red'
+ }, 3);
+ }
+
+ // Continue with next employee after short delay (like E-Taxes)
+ setTimeout(function() {
+ process_employees_one_by_one(
+ listview,
+ asan_login_name,
+ employees_queue,
+ company,
+ create_designation,
+ created_count,
+ updated_count,
+ error_count,
+ errors,
+ current_index
+ );
+ }, 50); // 50ms delay like E-Taxes PROGRESS_UPDATE_DELAY
+ },
+ error: function(xhr, status, error) {
+ // Network error
+ error_count++;
+ const error_msg = emp_name + ': Network error - ' + (error || 'Unknown error');
+ errors.push(error_msg);
+
+ frappe.show_alert({
+ message: __('Network error: ') + emp_name,
+ indicator: 'red'
+ }, 3);
+
+ // Continue with next employee
+ setTimeout(function() {
+ process_employees_one_by_one(
+ listview,
+ asan_login_name,
+ employees_queue,
+ company,
+ create_designation,
+ created_count,
+ updated_count,
+ error_count,
+ errors,
+ current_index
+ );
+ }, 50);
+ }
+ });
+}
+
+// Show final summary (like E-Taxes)
+function show_import_summary(total, created_count, updated_count, error_count, errors) {
+ if (error_count === 0) {
+ let msg = __('All ') + total + __(' employees were processed successfully.');
+ if (created_count > 0) {
+ msg += '';
+ if (created_count > 0) {
+ msg += __('Successfully created: ') + '' + created_count + ' ';
+ }
+ if (updated_count > 0) {
+ msg += __('Updated: ') + '' + updated_count + ' ';
+ }
+ msg += __('Failed: ') + '' + error_count + ' ';
+ msg += '
';
+
+ if (errors && errors.length > 0) {
+ msg += 'MyGovID authentication successful. Now connecting to ƏMAS...
+ `);
+ $('#mygovid_code').hide();
+
+ // Connect to ƏMAS
+ frappe.call({
+ method: 'invoice_az.amas_api.connect_amas',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r2) {
+ if (r2.message && r2.message.success) {
+ const accounts = r2.message.accounts || [];
+
+ if (accounts.length > 0) {
+ // Check if we should show selection dialog or auto-select
+ if (show_org_selection) {
+ // Show organization selection dialog (for re-authentication)
+ status_dialog.hide();
+ show_amas_accounts_dialog(asan_login_name, accounts, retry_callback);
+ } else {
+ // Auto-select first organization (for initial connection)
+ const first_account = accounts[0];
+ const oid = first_account.accountOid || first_account.oid || '';
+ const name = first_account.accountName || first_account.name || 'Unknown';
+ const number = first_account.accountNumber || first_account.voen || first_account.number || '';
+ const role = first_account.roleName || first_account.role || 'chairman';
+
+ $('#mygovid_status').html(`
+ ƏMAS connected. Selecting ${frappe.utils.escape_html(name)}...
+ `);
+
+ // Auto-select first account
+ frappe.call({
+ method: 'invoice_az.amas_api.change_amas_account',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'account_oid': oid,
+ 'account_name': name,
+ 'account_number': number,
+ 'role_name': role
+ },
+ callback: function(r3) {
+ if (r3.message && r3.message.success) {
+ $('#mygovid_status').html(`
+ Successfully connected to ${frappe.utils.escape_html(name)}.
+ `);
+ $('.mygovid-spinner').html(`
+ Successfully connected to ƏMAS.
+ `);
+ $('.mygovid-spinner').html(`
+ The request timed out. Please try again.
+ `);
+ $('.mygovid-spinner').css('display', 'none');
+ $('#mygovid_code').hide();
+
+ status_dialog.set_primary_action(__('Close'), function() {
+ status_dialog.hide();
+ });
+ }
+ });
+
+ // Get verification code
+ setTimeout(function() {
+ frappe.call({
+ method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code',
+ args: { 'asan_login_name': asan_login_name },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ $('#mygovid_code_text').text(r.message.code);
+ $('#mygovid_code').show();
+ }
+ }
+ });
+ }, 500);
+}
+
+// Show ƏMAS accounts selection dialog
+function show_amas_accounts_dialog(asan_login_name, accounts, retry_callback) {
+ if (!accounts || accounts.length === 0) {
+ frappe.msgprint(__('No organizations found in ƏMAS'));
+ return;
+ }
+
+ // Build accounts table
+ let table_html = '';
+ statsHtml += '
';
+ statsHtml += '
';
+ statsHtml += '' + __('Total Errors:') + ' ' + ETaxes.loadingErrors.length + ' ';
+ statsHtml += '';
+ Object.keys(errorTypes).forEach(function(type, index) {
+ if (index > 0) statsHtml += ' • ';
+ statsHtml += type + ': ' + errorTypes[type];
+ });
+ statsHtml += ' ';
+ statsHtml += '
';
+ statsHtml += '
';
+ statsHtml += '' +
+ ' ' + __('Export CSV') + ' ';
+ statsHtml += '' +
+ ' ' + __('Clear') + ' ';
+ statsHtml += '
';
+
+ const d = new frappe.ui.Dialog({
+ title: __('Error Details') + ' (' + ETaxes.loadingErrors.length + ')',
+ size: 'large',
+ fields: [
+ {
+ fieldname: 'stats_html',
+ fieldtype: 'HTML',
+ options: statsHtml
+ },
+ {
+ fieldname: 'errors_html',
+ fieldtype: 'HTML',
+ options: '
' + errorsHtml + '
'
+ }
+ ],
+ primary_action_label: __('Close'),
+ primary_action: function() {
+ d.hide();
+ }
+ });
+
+ d.show();
+
+ // Добавляем обработчики событий
+ setTimeout(function() {
+ $('#export-csv-dialog-btn').off('click').on('click', function() {
+ ETaxes.errors.exportCSV();
+ });
+
+ $('#clear-log-dialog-btn').off('click').on('click', function() {
+ ETaxes.errors.clear();
+ d.hide();
+ });
+ }, 200);
+ },
+
+ // Экспорт в CSV
+ exportCSV: function() {
+ if (ETaxes.loadingErrors.length === 0) {
+ frappe.msgprint(__('No errors to export'));
+ return;
+ }
+
+ try {
+ let csvContent = "data:text/csv;charset=utf-8,";
+ csvContent += "Invoice ID,Serial Number,Date,Supplier,Error Type,Error Message,Timestamp\n";
+
+ ETaxes.loadingErrors.forEach(function(error) {
+ const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
+ const cleanSender = error.sender_name.replace(/"/g, '""');
+
+ const row = [
+ error.invoice_id,
+ error.serial_number,
+ error.creation_date,
+ cleanSender,
+ error.error_type,
+ cleanMessage,
+ moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
+ ].map(function(field) {
+ return '"' + (field || '') + '"';
+ }).join(',');
+
+ csvContent += row + "\n";
+ });
+
+ const encodedUri = encodeURI(csvContent);
+ const link = document.createElement("a");
+ link.setAttribute("href", encodedUri);
+ link.setAttribute("download", "etaxes_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ frappe.show_alert({
+ message: __('Error log exported successfully'),
+ indicator: 'green'
+ }, 3);
+ } catch (e) {
+ console.error('Export error:', e);
+ frappe.show_alert({
+ message: __('Failed to export error log'),
+ indicator: 'red'
+ }, 3);
+ }
+ },
+
+ // Показать сводку результатов
+ showSummary: function(totalInvoices, processedCount, errorsCount) {
+ let title = '';
+ let indicator = '';
+ let message = '';
+
+ if (errorsCount === 0) {
+ title = __('Import Completed Successfully');
+ indicator = 'green';
+ message = __('All ') + totalInvoices + __(' invoices were imported successfully.');
+
+ frappe.msgprint({
+ title: title,
+ indicator: indicator,
+ message: message
+ });
+ } else {
+ title = errorsCount === totalInvoices ? __('Import Failed') : __('Import Completed with Errors');
+ indicator = processedCount > 0 ? 'orange' : 'red';
+
+ message = '
';
+ if (processedCount > 0) {
+ message += __('Successfully imported: ') + '' + processedCount + ' ' + __(' invoices') + ' ';
+ }
+ message += __('Failed: ') + '' + errorsCount + ' ' + __(' invoices');
+ message += '
';
+
+ message += '
' +
+ '' +
+ ' ' + __('View Error Details') + ' ' +
+ '' +
+ ' ' + __('Export CSV') + ' ' +
+ '
';
+
+ const summaryDialog = frappe.msgprint({
+ title: title,
+ indicator: indicator,
+ message: message
+ });
+
+ // Добавляем обработчики событий
+ setTimeout(function() {
+ $('#view-error-details-btn').off('click').on('click', function() {
+ summaryDialog.hide();
+ ETaxes.errors.showDetails();
+ });
+
+ $('#export-error-log-btn').off('click').on('click', function() {
+ ETaxes.errors.exportCSV();
+ });
+ }, 200);
+ }
+ }
+};
+
+// ======= МОДУЛЬ ИМПОРТА ИНВОЙСОВ =======
+ETaxes.import = {
+ // Показать диалог импорта из E-Taxes
+ showDialog: function() {
+ ETaxes.auth.checkAndProcess(function() {
+ const minDate = moment("2020-01-01", "YYYY-MM-DD");
+
+ const d = new frappe.ui.Dialog({
+ title: __('Import invoice from E-Taxes'),
+ fields: [
+ {
+ fieldname: 'date_range_section',
+ fieldtype: 'Section Break',
+ label: __('Period')
+ },
+ {
+ fieldname: 'creationDateFrom',
+ fieldtype: 'Date',
+ label: __('Date from'),
+ default: moment().subtract(1, 'month').format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'creationDateTo',
+ fieldtype: 'Date',
+ label: __('Date to'),
+ default: moment().format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'settings_section',
+ fieldtype: 'Section Break',
+ label: __('Settings')
+ },
+ {
+ fieldname: 'warehouse',
+ fieldtype: 'Link',
+ label: __('Default warehouse'),
+ options: 'Warehouse',
+ reqd: true,
+ get_query: function() {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'disabled': 0
+ }
+ };
+ }
+ }
+ ],
+ primary_action_label: __('Search'),
+ primary_action: function() {
+ const values = d.get_values();
+
+ // Validate date range
+ const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
+ const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
+ const todayMoment = moment();
+
+ if (fromDateMoment.isBefore(minDate)) {
+ frappe.msgprint(__('From Date cannot be earlier than January 1, 2020'));
+ return;
+ }
+
+ if (toDateMoment.isAfter(todayMoment)) {
+ frappe.msgprint(__('To Date cannot be later than today'));
+ return;
+ }
+
+ if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
+ frappe.msgprint(__('The date range cannot exceed one year.'));
+ return;
+ }
+
+ if (!values.warehouse) {
+ frappe.msgprint({
+ title: __('Warning'),
+ indicator: 'orange',
+ message: __('Please select a default warehouse')
+ });
+ return;
+ }
+
+ const fromDate = moment(values.creationDateFrom).format('DD-MM-YYYY 00:00');
+ const toDate = moment(values.creationDateTo).format('DD-MM-YYYY 23:59');
+
+ d.hide();
+ ETaxes.import.loadInvoices(fromDate, toDate, values.warehouse);
+ }
+ });
+
+ d.show();
+ });
+ },
+
+ // Загрузить инвойсы с фильтрацией дубликатов
+ loadInvoices: function(fromDate, toDate, warehouse, accumulatedInvoices = [], offset = 0) {
+ ETaxes.cancelLoading = false;
+ $(document).off('progress-cancel.etaxes_import');
+
+ if (offset === 0) {
+ frappe.show_alert({
+ message: __('Fetching E-Taxes invoices...'),
+ indicator: 'blue'
+ }, 3);
+ }
+
+ ETaxes.utils.getDefaultLogin(function(loginResponse) {
+ if (ETaxes.cancelLoading) return;
+
+ if (loginResponse && loginResponse.found) {
+ const mainToken = loginResponse.main_token;
+
+ if (!mainToken) {
+ frappe.msgprint({
+ title: __('Authentication Error'),
+ indicator: 'red',
+ message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
+ });
+ return;
+ }
+
+ const maxCount = 200;
+
+ frappe.call({
+ method: 'invoice_az.api.get_invoices',
+ args: {
+ 'token': mainToken,
+ 'filters': JSON.stringify({
+ "creationDateFrom": fromDate,
+ "creationDateTo": toDate,
+ "maxCount": maxCount,
+ "offset": offset
+ })
+ },
+ callback: function(r) {
+ if (ETaxes.cancelLoading) return;
+
+ if (r.message && !r.message.error) {
+ const currentInvoices = r.message.data || r.message.invoices || [];
+ const hasMore = r.message.hasMore || false;
+ const allInvoices = accumulatedInvoices.concat(currentInvoices);
+
+ if (hasMore && currentInvoices.length > 0) {
+ const newOffset = offset + currentInvoices.length;
+ ETaxes.import.loadInvoices(fromDate, toDate, warehouse, allInvoices, newOffset);
+ } else {
+ if (allInvoices.length > 0) {
+ frappe.show_alert({
+ message: __('Processing ') + allInvoices.length + __(' invoices...'),
+ indicator: 'blue'
+ }, 3);
+
+ ETaxes.import.filterDuplicates(allInvoices, mainToken, warehouse);
+ } else {
+ frappe.msgprint({
+ title: __('Information'),
+ indicator: 'blue',
+ message: __('No invoices found for the specified period')
+ });
+ }
+ }
+ } else if (r.message && r.message.error === 'unauthorized') {
+ frappe.confirm(
+ __('Your E-Taxes session has expired. Would you like to authenticate now?'),
+ function() {
+ ETaxes.auth.startProcess(function() {
+ ETaxes.import.loadInvoices(fromDate, toDate, warehouse);
+ });
+ },
+ function() {
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ } else {
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: r.message ? r.message.message : __('Error loading invoices')
+ });
+ }
+ },
+ error: function(xhr, status, error) {
+ console.error("Error fetching invoices:", error);
+ frappe.msgprint({
+ title: __('Network Error'),
+ indicator: 'red',
+ message: __('Error fetching data from server: ') + (error || 'Unknown error')
+ });
+ }
+ });
+ } else {
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('E-Taxes authentication settings not found')
+ });
+ }
+ });
+ },
+
+ // Фильтрация дубликатов
+ filterDuplicates: function(allInvoices, token, warehouse) {
+ frappe.call({
+ method: 'invoice_az.api.get_etaxes_purchases',
+ callback: function(etaxesR) {
+ try {
+ if (ETaxes.cancelLoading) return;
+
+ const importedInvoices = {};
+
+ if (etaxesR.message && etaxesR.message.success && etaxesR.message.purchases) {
+ etaxesR.message.purchases.forEach(function(purchase) {
+ if (purchase.etaxes_id) {
+ const normId = String(purchase.etaxes_id).trim();
+ importedInvoices[normId] = true;
+ }
+ });
+ }
+
+ const filteredInvoices = [];
+
+ for (let i = 0; i < allInvoices.length; i++) {
+ const invoice = allInvoices[i];
+ const invoiceId = String(invoice.id).trim();
+
+ if (!importedInvoices.hasOwnProperty(invoiceId)) {
+ filteredInvoices.push(invoice);
+ }
+ }
+
+ if (filteredInvoices.length > 0) {
+ ETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
+ } else {
+ frappe.msgprint({
+ title: __('Information'),
+ indicator: 'blue',
+ message: __('No new invoices found for the specified period (all ' +
+ allInvoices.length + ' are already imported)')
+ });
+ }
+ } catch (e) {
+ console.error("Error processing purchase data:", e);
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('An error occurred while processing data: ') + e.message
+ });
+ }
+ },
+ error: function(err) {
+ console.error("Error fetching purchases:", err);
+ ETaxes.import.showInvoiceSelection(allInvoices, token, warehouse);
+ frappe.show_alert({
+ message: __('Failed to check for duplicates. Showing all invoices.'),
+ indicator: 'orange'
+ }, 5);
+ }
+ });
+ },
+
+ // Показать диалог выбора инвойсов
+ showInvoiceSelection: function(invoices, token, warehouse) {
+ let invoiceTable = '
';
+
+ const infoMessage =
+ '
' +
+ '' +
+ __('New invoices found: ') + '' + invoices.length + ' ' +
+ ' ' +
+ '' +
+ __('Selected warehouse: ') + '' + frappe.utils.escape_html(warehouse) + ' ' +
+ ' ' +
+ '
';
+
+ const d = new frappe.ui.Dialog({
+ title: __('Select invoice'),
+ size: 'large',
+ fields: [
+ {
+ fieldname: 'info_html',
+ fieldtype: 'HTML',
+ options: infoMessage
+ },
+ {
+ fieldname: 'invoices_html',
+ fieldtype: 'HTML',
+ options: invoiceTable
+ }
+ ],
+ primary_action_label: __('Load selected'),
+ primary_action: function() {
+ const selectedInvoiceIds = [];
+ d.$wrapper.find('.select-invoice:checked').each(function() {
+ selectedInvoiceIds.push($(this).data('id'));
+ });
+
+ if (selectedInvoiceIds.length === 0) {
+ frappe.msgprint({
+ title: __('Warning'),
+ indicator: 'orange',
+ message: __('No invoices selected')
+ });
+ return;
+ }
+
+ d.hide();
+ ETaxes.cancelLoading = false;
+ ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
+ },
+ secondary_action_label: __('Cancel'),
+ secondary_action: function() {
+ d.hide();
+ }
+ });
+
+ d.$wrapper.find('.modal-dialog').css({
+ 'max-width': '80%',
+ 'width': '80%',
+ 'margin': '30px auto'
+ });
+
+ d.$wrapper.find('.modal-body').css({
+ 'padding': '15px'
+ });
+
+ d.show();
+
+ // Обработчик для чекбокса "выбрать все"
+ d.$wrapper.find('.select-all-invoices').on('change', function() {
+ const isChecked = $(this).prop('checked');
+ d.$wrapper.find('.select-invoice').prop('checked', isChecked);
+ });
+ },
+
+ // Надёжное закрытие прогресс-бара. frappe.hide_progress() в jey_theme/Bootstrap
+ // не всегда убирает модал (он зависает, и summary/диалог накладывается сверху).
+ // Принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
+ _closeProgress: function() {
+ if (frappe.cur_progress) {
+ try {
+ frappe.cur_progress.$wrapper.modal('hide');
+ frappe.cur_progress.$wrapper.remove();
+ } catch (e) {}
+ frappe.cur_progress = null;
+ }
+ if ($('.modal:visible').length === 0) {
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+ },
+
+ // Загрузка выбранных инвойсов
+ loadSelectedInvoices: function(invoiceIds, token, warehouse, processedCount = 0, currentIndex = 0) {
+ if (processedCount === 0 && currentIndex === 0) {
+ ETaxes.loadingErrors = [];
+ window.totalInvoicesToProcess = invoiceIds.length;
+ }
+
+ $(document).off('progress-cancel.loading_invoices');
+
+ if (invoiceIds.length === 0) {
+ ETaxes.import._closeProgress();
+
+ const errorsCount = ETaxes.loadingErrors.length;
+ const totalInvoices = window.totalInvoicesToProcess || processedCount;
+
+ ETaxes.errors.showSummary(totalInvoices, processedCount, errorsCount);
+ window.totalInvoicesToProcess = null;
+
+ if (cur_list) {
+ cur_list.refresh();
+ }
+
+ return;
+ }
+
+ if (ETaxes.cancelLoading) {
+ invoiceIds = [];
+ ETaxes.import._closeProgress();
+ ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
+ return;
+ }
+
+ const invoiceId = invoiceIds.shift();
+ const isLastInvoice = invoiceIds.length === 0;
+ const totalToProcess = window.totalInvoicesToProcess || (processedCount + invoiceIds.length + 1);
+
+ currentIndex++;
+
+ frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
+ __('Loading invoice ') + currentIndex + __(' of ') + totalToProcess, null, true);
+
+ $(document).on('progress-cancel.loading_invoices', function() {
+ ETaxes.cancelLoading = true;
+ frappe.show_alert({
+ message: __('Cancelling import... Finishing current invoice.'),
+ indicator: 'orange'
+ }, 3);
+ ETaxes.import._closeProgress();
+ });
+
+ frappe.call({
+ method: 'invoice_az.api.get_invoice_details',
+ args: {
+ 'token': token,
+ 'invoice_id': invoiceId
+ },
+ callback: function(r) {
+ if (r.message && !r.message.error) {
+ const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
+ const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
+ const serialNumber = r.message.serialNumber || '';
+ const senderName = r.message.sender ? r.message.sender.name : '';
+ const total = r.message.totalAmount || r.message.amount || 0;
+
+ frappe.show_progress(__('Importing Invoices'), currentIndex - 1, totalToProcess,
+ __('Importing invoice: ') + serialNumber);
+
+ frappe.call({
+ method: 'invoice_az.api.import_invoice_with_mapping',
+ args: {
+ 'invoice_data': r.message,
+ 'purchase_order_name': null,
+ 'schedule_date': scheduleDate,
+ 'warehouse': warehouse
+ },
+ callback: function(importR) {
+ ETaxes.import._handleImportResult(importR, r.message, invoiceId, scheduleDate, senderName, total,
+ invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ },
+ error: function(xhr, status, error) {
+ const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
+ ETaxes.errors.add(r.message, 'Network Error', errorMessage, {
+ invoice_items: r.message.items || [],
+ xhr_status: xhr.status,
+ xhr_response: xhr.responseText
+ });
+
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ });
+ } else if (r.message && r.message.error === 'unauthorized') {
+ ETaxes.import._closeProgress();
+ $(document).off('progress-cancel.loading_invoices');
+
+ frappe.confirm(
+ __('Your E-Taxes session has expired. Would you like to authenticate now?'),
+ function() {
+ ETaxes.auth.startProcess(function() {
+ ETaxes.utils.getDefaultLogin(function(loginR) {
+ if (loginR && loginR.found && loginR.main_token) {
+ invoiceIds.unshift(invoiceId);
+ ETaxes.import.loadSelectedInvoices(invoiceIds, loginR.main_token, warehouse, processedCount, currentIndex - 1);
+ } else {
+ ETaxes.errors.add({id: invoiceId}, 'Authentication Error',
+ 'Failed to get token after authentication');
+
+ const errorsCount = ETaxes.loadingErrors.length;
+ ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
+ }
+ }, false);
+ });
+ },
+ function() {
+ ETaxes.errors.add({id: invoiceId}, 'Authentication Cancelled',
+ 'User cancelled authentication process');
+
+ const errorsCount = ETaxes.loadingErrors.length;
+ ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
+ }
+ );
+ } else {
+ const errorMessage = r.message ? r.message.message : 'Failed to load invoice details';
+ ETaxes.errors.add({id: invoiceId}, 'Invoice Data Error', errorMessage, {
+ server_response: r.message
+ });
+
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ },
+ error: function(xhr, status, error) {
+ const errorMessage = 'Network error loading invoice details: ' + (error || 'Unknown network error');
+ ETaxes.errors.add({id: invoiceId}, 'Network Error', errorMessage, {
+ xhr_status: xhr.status,
+ xhr_response: xhr.responseText
+ });
+
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ });
+ },
+
+ // Обработка результата импорта
+ _handleImportResult: function(importR, invoiceMessage, invoiceId, scheduleDate, senderName, total,
+ invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
+ if (importR.message && importR.message.success) {
+ frappe.show_alert({
+ message: __('Invoice successfully imported: ') + (invoiceMessage.serialNumber || ''),
+ indicator: 'green'
+ }, 3);
+
+ const purchaseOrderName = importR.message.purchase_order;
+
+ frappe.call({
+ method: 'invoice_az.api.create_etaxes_purchase',
+ args: {
+ 'etaxes_id': invoiceId,
+ 'date': scheduleDate,
+ 'party': senderName,
+ 'total': total
+ },
+ callback: function(etaxesR) {
+ if (etaxesR.message && etaxesR.message.success) {
+ frappe.call({
+ method: 'invoice_az.api.link_purchase_order_to_etaxes',
+ args: {
+ 'purchase_order': purchaseOrderName,
+ 'etaxes_purchase': etaxesR.message.name
+ },
+ callback: function(linkR) {
+ processedCount++;
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ });
+ } else {
+ ETaxes.errors.add(invoiceMessage, 'E-Taxes Purchase Creation Error',
+ etaxesR.message ? etaxesR.message.message : 'Failed to create E-Taxes Purchase record', {
+ server_response: etaxesR.message
+ });
+
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ }
+ });
+ } else if (importR.message && (importR.message.unmatched_items || importR.message.unmatched_parties || importR.message.unmatched_units)) {
+ let errorType = '';
+ const additionalData = { invoice_items: invoiceMessage.items || [] };
+
+ if (importR.message.unmatched_items) {
+ errorType = 'Unmapped Items';
+ additionalData.unmatched_items = importR.message.unmatched_items;
+ } else if (importR.message.unmatched_parties) {
+ errorType = 'Unmapped Parties';
+ additionalData.unmatched_parties = importR.message.unmatched_parties;
+ } else if (importR.message.unmatched_units) {
+ errorType = 'Unmapped Units';
+ additionalData.unmatched_units = importR.message.unmatched_units;
+ }
+
+ additionalData.server_response = importR.message;
+ ETaxes.errors.add(invoiceMessage, errorType, importR.message.message, additionalData);
+
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ } else {
+ const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
+ const additionalData = {
+ invoice_items: invoiceMessage.items || [],
+ server_response: importR.message
+ };
+
+ if (importR.exc) {
+ additionalData.stack_trace = importR.exc;
+ }
+
+ ETaxes.errors.add(invoiceMessage, 'Import Error', errorMessage, additionalData);
+ ETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ },
+
+ // Продолжить или завершить обработку
+ _continueOrFinish: function(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
+ if (isLastInvoice) {
+ ETaxes.import._closeProgress();
+ $(document).off('progress-cancel.loading_invoices');
+
+ const totalToProcess = window.totalInvoicesToProcess || processedCount;
+ const errorsCount = ETaxes.loadingErrors.length;
+ ETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
+
+ if (cur_list) {
+ cur_list.refresh();
+ }
+ return;
+ }
+
+ setTimeout(function() {
+ if (invoiceIds.length === 0) {
+ ETaxes.import._closeProgress();
+ }
+
+ ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
+ }, ETaxes.PROGRESS_UPDATE_DELAY);
+ },
+
+ // Socket.IO bulk import
+ loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
+ ETaxes.loadingErrors = [];
+ const total = invoiceIds.length;
+ // Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
+ // frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
+ // уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
+ let importFinished = false;
+ // Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
+ // (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
+ const closeProgress = function() {
+ if (frappe.cur_progress) {
+ try {
+ frappe.cur_progress.$wrapper.modal('hide');
+ frappe.cur_progress.$wrapper.remove();
+ } catch (e) {}
+ frappe.cur_progress = null;
+ }
+ if ($('.modal:visible').length === 0) {
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+ };
+
+ // 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;
+ frappe.realtime.off('purchase_import_progress');
+ frappe.realtime.off('purchase_import_complete');
+ closeProgress();
+
+ (data.errors || []).forEach(function(err) {
+ ETaxes.loadingErrors.push({
+ invoice_id: err.invoice_id || 'Unknown',
+ error_type: err.unmatched_items ? 'Unmapped Items'
+ : err.unmatched_parties ? 'Unmapped Parties'
+ : err.unmatched_units ? 'Unmapped Units'
+ : 'Import Error',
+ error_message: err.error || 'Unknown error',
+ unmatched_items: err.unmatched_items,
+ unmatched_parties: err.unmatched_parties,
+ unmatched_units: err.unmatched_units
+ });
+ });
+
+ const errorsCount = ETaxes.loadingErrors.length;
+ ETaxes.errors.showSummary(data.total, data.imported, errorsCount);
+
+ if (cur_list) {
+ cur_list.refresh();
+ }
+ });
+
+ frappe.call({
+ method: 'invoice_az.api.import_bulk_purchase_invoices',
+ args: {
+ invoice_ids: JSON.stringify(invoiceIds),
+ token: token,
+ warehouse: warehouse
+ },
+ callback: function(r) {
+ if (!r.message || !r.message.enqueued) {
+ frappe.realtime.off('purchase_import_progress');
+ frappe.realtime.off('purchase_import_complete');
+ closeProgress();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Failed to start import job')
+ });
+ }
+ },
+ error: function() {
+ frappe.realtime.off('purchase_import_progress');
+ frappe.realtime.off('purchase_import_complete');
+ closeProgress();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Network error starting import')
+ });
+ }
+ });
+ }
+};
+
+// Purchase Order List
+frappe.listview_settings['Purchase Order'] = {
+ add_fields: ['supplier', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
+
+ get_indicator: function(doc) {
+ if (doc.status === 'Closed') {
+ return [__('Closed'), 'green', 'status,=,Closed'];
+ } else if (doc.status === 'On Hold') {
+ return [__('On Hold'), 'orange', 'status,=,On Hold'];
+ } else if (doc.status === 'Delivered') {
+ return [__('Delivered'), 'green', 'status,=,Delivered'];
+ } else if (doc.docstatus == 1) {
+ return [__('To Receive and Bill'), 'orange', 'docstatus,=,1'];
+ } else if (doc.docstatus == 0) {
+ return [__('Draft'), 'red', 'docstatus,=,0'];
+ } else if (doc.docstatus == 2) {
+ return [__('Cancelled'), 'grey', 'docstatus,=,2'];
+ }
+ },
+
+ onload: function(listview) {
+ listview.page.add_inner_button(__('Import from E-Taxes'), function() {
+ ETaxes.import.showDialog();
+ }, __('E-Taxes'));
+
+ listview.page.add_inner_button(__('Open Settings'), function() {
+ frappe.set_route('Form', 'E-Taxes Settings');
+ }, __('E-Taxes'));
+
+ listview.columns.push({
+ type: 'Check',
+ df: {
+ label: __('Tax Doc'),
+ fieldname: 'is_taxes_doc'
+ },
+ width: 120
+ });
+
+ listview.refresh();
+ },
+
+ formatters: {
+ is_taxes_doc: function(value) {
+ return value ?
+ `
` :
+ `
`;
+ }
+ }
+};
+
+// Purchase Invoice List
+frappe.listview_settings['Purchase Invoice'] = {
+ onload: function(listview) {
+ listview.columns.push({
+ type: 'Check',
+ df: {
+ label: __('Tax Doc'),
+ fieldname: 'is_taxes_doc'
+ },
+ width: 120
+ });
+
+ listview.refresh();
+ },
+
+ formatters: {
+ is_taxes_doc: function(value) {
+ return value ?
+ `
` :
+ `
`;
+ }
+ }
+};
\ No newline at end of file
diff --git a/invoice_az/client/sales_invoice.js b/invoice_az/client/sales_invoice.js
index ee9fbf6..8ec6a4c 100644
--- a/invoice_az/client/sales_invoice.js
+++ b/invoice_az/client/sales_invoice.js
@@ -2218,55 +2218,3 @@ function show_etaxes_details(frm) {
message: details_html
});
}
-
-/**
- * List view settings
- */
-frappe.listview_settings['Sales Invoice'] = {
- // Make sure both signals reach the formatter, even though the column is bound
- // to only one of them.
- add_fields: ['is_taxes_doc', 'etaxes_send_status'],
-
- onload: function(listview) {
- // Single universal E-Taxes column. It reflects everything related to the
- // tax portal for this document: whether it was loaded from E-Taxes,
- // whether a draft was created there, or whether it was sent and signed.
- listview.columns.push({
- type: 'Select',
- df: {
- label: __('E-Taxes'),
- fieldname: 'etaxes_send_status'
- },
- width: 170
- });
-
- // Force refresh list with the new column
- listview.refresh();
- },
-
- formatters: {
- // Universal E-Taxes status. The formatter receives the full row as its
- // third argument (value, df, doc), so we combine the send status with the
- // "loaded from E-Taxes" flag into a single indicator.
- etaxes_send_status: function(value, df, doc) {
- const send_status = doc.etaxes_send_status;
-
- // Outgoing flow (document sent to the portal) takes priority — it is
- // the most specific state.
- if (send_status === 'Sent and Signed') {
- return `
${__('Sent to E-Taxes')} `;
- }
- if (send_status === 'Created, not signed') {
- return `
${__('Draft on E-Taxes')} `;
- }
-
- // Incoming flow (document imported from the portal).
- if (doc.is_taxes_doc) {
- return `
${__('Loaded from E-Taxes')} `;
- }
-
- // Not related to the tax portal — keep the cell clean.
- return '';
- }
- }
-};
diff --git a/invoice_az/client/sales_invoice_list.js b/invoice_az/client/sales_invoice_list.js
new file mode 100644
index 0000000..76ba32b
--- /dev/null
+++ b/invoice_az/client/sales_invoice_list.js
@@ -0,0 +1,50 @@
+// List view settings for Sales Invoice. Loaded only via doctype_list_js. Do NOT add frappe.ui.form.on(...) here.
+
+frappe.listview_settings['Sales Invoice'] = {
+ // Make sure both signals reach the formatter, even though the column is bound
+ // to only one of them.
+ add_fields: ['is_taxes_doc', 'etaxes_send_status'],
+
+ onload: function(listview) {
+ // Single universal E-Taxes column. It reflects everything related to the
+ // tax portal for this document: whether it was loaded from E-Taxes,
+ // whether a draft was created there, or whether it was sent and signed.
+ listview.columns.push({
+ type: 'Select',
+ df: {
+ label: __('E-Taxes'),
+ fieldname: 'etaxes_send_status'
+ },
+ width: 170
+ });
+
+ // Force refresh list with the new column
+ listview.refresh();
+ },
+
+ formatters: {
+ // Universal E-Taxes status. The formatter receives the full row as its
+ // third argument (value, df, doc), so we combine the send status with the
+ // "loaded from E-Taxes" flag into a single indicator.
+ etaxes_send_status: function(value, df, doc) {
+ const send_status = doc.etaxes_send_status;
+
+ // Outgoing flow (document sent to the portal) takes priority — it is
+ // the most specific state.
+ if (send_status === 'Sent and Signed') {
+ return `
${__('Sent to E-Taxes')} `;
+ }
+ if (send_status === 'Created, not signed') {
+ return `
${__('Draft on E-Taxes')} `;
+ }
+
+ // Incoming flow (document imported from the portal).
+ if (doc.is_taxes_doc) {
+ return `
${__('Loaded from E-Taxes')} `;
+ }
+
+ // Not related to the tax portal — keep the cell clean.
+ return '';
+ }
+ }
+};
diff --git a/invoice_az/client/sales_order.js b/invoice_az/client/sales_order.js
index a430b3a..bdbe4c7 100644
--- a/invoice_az/client/sales_order.js
+++ b/invoice_az/client/sales_order.js
@@ -1638,56 +1638,6 @@ frappe.ui.form.on('Sales Order', {
}
});
-frappe.listview_settings['Sales Order'] = {
- add_fields: ['customer', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
-
- get_indicator: function(doc) {
- if (doc.status === 'Closed') {
- return [__('Closed'), 'green', 'status,=,Closed'];
- } else if (doc.status === 'On Hold') {
- return [__('On Hold'), 'orange', 'status,=,On Hold'];
- } else if (doc.status === 'Delivered') {
- return [__('Delivered'), 'green', 'status,=,Delivered'];
- } else if (doc.docstatus == 1) {
- return [__('To Deliver and Bill'), 'orange', 'docstatus,=,1'];
- } else if (doc.docstatus == 0) {
- return [__('Draft'), 'red', 'docstatus,=,0'];
- } else if (doc.docstatus == 2) {
- return [__('Cancelled'), 'grey', 'docstatus,=,2'];
- }
- },
-
- onload: function(listview) {
- // ДОБАВЛЕНО: кнопка импорта из E-Taxes для Sales Order
- listview.page.add_inner_button(__('Import from E-Taxes'), function() {
- SalesETaxes.import.showDialog();
- }, __('E-Taxes'));
-
- listview.page.add_inner_button(__('Open Settings'), function() {
- frappe.set_route('Form', 'E-Taxes Settings');
- }, __('E-Taxes'));
-
- listview.columns.push({
- type: 'Check',
- df: {
- label: __('Tax Doc'),
- fieldname: 'is_taxes_doc'
- },
- width: 120
- });
-
- listview.refresh();
- },
-
- formatters: {
- is_taxes_doc: function(value) {
- return value ?
- `
` :
- `
`;
- }
- }
-};
-
// Sales Invoice Form
frappe.ui.form.on('Sales Invoice', {
refresh: function(frm) {
@@ -1700,32 +1650,4 @@ frappe.ui.form.on('Sales Invoice', {
onload: function(frm) {
frm.set_df_property('is_taxes_doc', 'read_only', 1);
}
-});
-
-// Sales Invoice List - УБРАНО: кнопка импорта (теперь только в Sales Order)
-frappe.listview_settings['Sales Invoice'] = {
- add_fields: ['customer', 'posting_date', 'status', 'docstatus', 'is_taxes_doc'],
-
- onload: function(listview) {
- // УБРАНО: кнопка импорта - теперь импорт только через Sales Order
-
- listview.columns.push({
- type: 'Check',
- df: {
- label: __('Tax Doc'),
- fieldname: 'is_taxes_doc'
- },
- width: 120
- });
-
- listview.refresh();
- },
-
- formatters: {
- is_taxes_doc: function(value) {
- return value ?
- `
` :
- `
`;
- }
- }
-};
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/invoice_az/client/sales_order_list.js b/invoice_az/client/sales_order_list.js
new file mode 100644
index 0000000..6343222
--- /dev/null
+++ b/invoice_az/client/sales_order_list.js
@@ -0,0 +1,1703 @@
+// List view settings for Sales Order and Sales Invoice. Loaded only via doctype_list_js. Do NOT add frappe.ui.form.on(...) here.
+
+// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
+const SalesETaxes = {
+ // Константы
+ PROGRESS_UPDATE_DELAY: 50,
+ AUTH_POLL_INTERVAL: 6000,
+ AUTH_MAX_ATTEMPTS: 20,
+
+ // Глобальные переменные
+ loadingDialog: null,
+ cancelLoading: false,
+ loadingErrors: [],
+
+ // Кэш
+ cache: {
+ defaultLogin: null,
+ cacheTime: null,
+ cacheDuration: 300000 // 5 минут
+ }
+};
+
+// ======= БАЗОВЫЕ УТИЛИТЫ =======
+SalesETaxes.utils = {
+ // Кэшированное получение настроек входа
+ getDefaultLogin: function(callback, useCache = true) {
+ const now = Date.now();
+
+ if (useCache && SalesETaxes.cache.defaultLogin && SalesETaxes.cache.cacheTime &&
+ (now - SalesETaxes.cache.cacheTime) < SalesETaxes.cache.cacheDuration) {
+ callback(SalesETaxes.cache.defaultLogin);
+ return;
+ }
+
+ frappe.call({
+ method: 'invoice_az.auth.get_default_asan_login',
+ callback: function(r) {
+ if (r.message) {
+ SalesETaxes.cache.defaultLogin = r.message;
+ SalesETaxes.cache.cacheTime = now;
+ }
+ callback(r.message);
+ },
+ error: function() {
+ callback({found: false, error: 'Network error'});
+ }
+ });
+ },
+
+ // Проверка валидности токена
+ checkTokenValidity: function(callback) {
+ frappe.call({
+ method: 'invoice_az.auth.check_token_validity',
+ callback: function(r) {
+ callback(r.message && r.message.valid);
+ },
+ error: function() {
+ callback(false);
+ }
+ });
+ },
+
+ // Форматирование валюты
+ formatCurrency: function(amount) {
+ return new Intl.NumberFormat('az-AZ', {
+ style: 'currency',
+ currency: 'AZN',
+ minimumFractionDigits: 2
+ }).format(amount || 0);
+ },
+
+ // Очистка кэша
+ clearCache: function() {
+ SalesETaxes.cache.defaultLogin = null;
+ SalesETaxes.cache.cacheTime = null;
+ }
+};
+
+// ======= ДИАЛОГИ ЗАГРУЗКИ =======
+SalesETaxes.dialogs = {
+ // Показать диалог загрузки
+ showLoading: function(title, message, submessage) {
+ if (SalesETaxes.loadingDialog) {
+ this.updateLoading(title, message, submessage);
+ return;
+ }
+
+ SalesETaxes.loadingDialog = new frappe.ui.Dialog({
+ title: title,
+ fields: [{
+ fieldname: 'loading_html',
+ fieldtype: 'HTML',
+ options: this._getLoadingHTML(title, message, submessage)
+ }],
+ primary_action_label: __('Cancel'),
+ primary_action: function() {
+ // Флаг ставим ПЕРЕД закрытием: hide() его больше не сбрасывает,
+ // поэтому in-flight callback'и пагинации увидят отмену и выйдут.
+ SalesETaxes.cancelLoading = true;
+ SalesETaxes.dialogs.hide();
+ }
+ });
+
+ SalesETaxes.cancelLoading = false;
+ SalesETaxes.loadingDialog.show();
+ SalesETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
+ },
+
+ // Обновить сообщение в диалоге
+ updateLoading: function(title, message, submessage) {
+ if (!SalesETaxes.loadingDialog) return;
+
+ if (title) $('#loading_title').text(title);
+ if (message) $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+ },
+
+ // Показать код верификации
+ showVerificationCode: function(code) {
+ if (SalesETaxes.loadingDialog && code) {
+ $('#verification_code').text(code);
+ $('#verification_code_container').show();
+ }
+ },
+
+ // Установить статус успеха
+ setSuccess: function(message, submessage, callback, delay = 2000) {
+ if (!SalesETaxes.loadingDialog) {
+ if (callback) callback();
+ return;
+ }
+
+ SalesETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
+
+
+
+ `);
+
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+
+ $('#verification_code_container').hide();
+ SalesETaxes.loadingDialog.set_primary_action(null);
+ SalesETaxes.loadingDialog.set_secondary_action(null);
+
+ setTimeout(() => {
+ this.hide();
+ if (callback) callback();
+ }, delay);
+ },
+
+ // Установить статус ошибки
+ setError: function(message, submessage, showCloseButton = true) {
+ if (!SalesETaxes.loadingDialog) return;
+
+ SalesETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
+
+
+
+ `);
+
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+
+ $('#verification_code_container').hide();
+
+ if (showCloseButton) {
+ SalesETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
+ }
+ },
+
+ // Скрыть диалог. Мягкий loadingDialog.hide() в jey_theme/Bootstrap НЕ всегда
+ // убирает модал (он зависает и следующий диалог накладывается сверху). Поэтому
+ // принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
+ // cancelLoading здесь НЕ сбрасываем: иначе отмена потеряется для in-flight
+ // callback'ов. Флаг сбрасывается в начале операции (loadInvoices/showLoading).
+ hide: function() {
+ if (SalesETaxes.loadingDialog) {
+ try {
+ SalesETaxes.loadingDialog.$wrapper.modal('hide');
+ SalesETaxes.loadingDialog.$wrapper.remove();
+ } catch (e) {
+ console.error("Error hiding loading dialog:", e);
+ }
+ SalesETaxes.loadingDialog = null;
+ }
+ // backdrop/scroll-lock убираем только если не осталось видимых модалов
+ // (иначе сломаем диалог, который показывается следом, напр. выбор даты).
+ if ($('.modal:visible').length === 0) {
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+ },
+
+ // Получить HTML для диалога загрузки
+ _getLoadingHTML: function(title, message, submessage) {
+ return `
+
+
+
+
+
${title || __('Please wait...')}
+
${message || __('The operation may take some time')}
+
${submessage || ''}
+
+
+
+ ----
+
+
Verification Code
+
+
+ `;
+ }
+};
+
+// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
+SalesETaxes.auth = {
+ // Проверить токен и обработать аутентификацию при необходимости
+ checkAndProcess: function(callback) {
+ SalesETaxes.utils.checkTokenValidity(function(isValid) {
+ if (isValid) {
+ callback();
+ } else {
+ frappe.confirm(
+ __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
+ function() {
+ SalesETaxes.auth.startProcess(callback);
+ },
+ function() {
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ }
+ });
+ },
+
+ // Начать процесс аутентификации
+ startProcess: function(callback) {
+ SalesETaxes.dialogs.showLoading(
+ __('Starting Authentication'),
+ __('Getting Asan Login settings...'),
+ __('Please wait')
+ );
+
+ SalesETaxes.utils.getDefaultLogin(function(loginData) {
+ if (loginData && loginData.found) {
+ const asanLoginName = loginData.name;
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Sending authentication request...'),
+ __('This will send a request to your Asan Imza mobile app')
+ );
+
+ frappe.call({
+ method: 'invoice_az.auth.handle_authentication',
+ args: { 'asan_login_name': asanLoginName },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ const bearerToken = r.message.bearer_token;
+
+ if (r.message.verification_code) {
+ SalesETaxes.dialogs.showVerificationCode(r.message.verification_code);
+ }
+
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request')
+ );
+
+ SalesETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
+ if (success) {
+ SalesETaxes.auth.complete(asanLoginName, callback);
+ } else {
+ SalesETaxes.dialogs.setError(
+ __('Authentication Failed'),
+ __('Could not authenticate with Asan Imza')
+ );
+ }
+ });
+ } else {
+ SalesETaxes.dialogs.setError(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('Authentication request failed')
+ );
+ }
+ }
+ });
+ } else {
+ SalesETaxes.dialogs.setError(
+ __('No Asan Login Settings'),
+ __('Please configure Asan Login settings first')
+ );
+ }
+ }, false); // Не использовать кэш для аутентификации
+ },
+
+ // Опрос статуса аутентификации
+ pollStatus: function(asanLoginName, bearerToken, callback) {
+ let attempts = 0;
+ let stopPolling = false;
+
+ function pollStatusStep() {
+ if (attempts >= SalesETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
+ if (attempts >= SalesETaxes.AUTH_MAX_ATTEMPTS) {
+ SalesETaxes.dialogs.setError(
+ __('Authentication Timeout'),
+ __('The authentication request has timed out. Please try again.')
+ );
+ callback(false);
+ }
+ return;
+ }
+
+ attempts++;
+
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request') +
+ ' (' + attempts + '/' + SalesETaxes.AUTH_MAX_ATTEMPTS + ')'
+ );
+
+ frappe.call({
+ method: 'invoice_az.auth.poll_auth_status',
+ args: {
+ 'asan_login_name': asanLoginName,
+ 'bearer_token': bearerToken
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ if (r.message.authenticated) {
+ stopPolling = true;
+ SalesETaxes.dialogs.setSuccess(
+ __('Authentication Successful'),
+ __('You are now authenticated with Asan Imza'),
+ function() {
+ callback(true);
+ }
+ );
+ } else {
+ setTimeout(pollStatusStep, SalesETaxes.AUTH_POLL_INTERVAL);
+ }
+ } else {
+ stopPolling = true;
+ SalesETaxes.dialogs.setError(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('An unknown error occurred')
+ );
+ callback(false);
+ }
+ },
+ error: function(err) {
+ console.error('Error during status check:', err);
+ setTimeout(pollStatusStep, SalesETaxes.AUTH_POLL_INTERVAL);
+ }
+ });
+ }
+
+ pollStatusStep();
+ },
+
+ // Завершение аутентификации
+ complete: function(asanLoginName, callback) {
+ SalesETaxes.dialogs.showLoading(
+ __('Completing Authentication'),
+ __('Getting certificates...'),
+ __('Please wait')
+ );
+
+ frappe.call({
+ method: 'invoice_az.auth.get_auth_certificates',
+ args: { 'asan_login_name': asanLoginName },
+ callback: function(certR) {
+ if (certR.message && certR.message.success) {
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Certificates retrieved'),
+ __('Selecting certificate and taxpayer')
+ );
+
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Asan Login',
+ name: asanLoginName
+ },
+ callback: function(r) {
+ if (r.message && r.message.selected_certificate_json) {
+ try {
+ const certData = JSON.parse(r.message.selected_certificate_json);
+ const certName = r.message.selected_certificate;
+
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Selecting certificate'),
+ certName
+ );
+
+ SalesETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
+ } catch (e) {
+ console.error('Error parsing certificate:', e);
+ SalesETaxes.dialogs.hide();
+ SalesETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
+ }
+ } else {
+ SalesETaxes.dialogs.hide();
+ SalesETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
+ }
+ }
+ });
+ } else {
+ SalesETaxes.dialogs.setError(
+ __('Error'),
+ certR.message ? certR.message.message : __('Failed to get certificates')
+ );
+ }
+ }
+ });
+ },
+
+ // Выбор сертификата
+ _selectCertificate: function(asanLoginName, certData, certName, callback) {
+ frappe.call({
+ method: 'invoice_az.auth.select_certificate',
+ args: {
+ 'asan_login_name': asanLoginName,
+ 'certificate_data': certData,
+ 'certificate_name': certName
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Selecting taxpayer'),
+ __('Using certificate: ') + certName
+ );
+
+ frappe.call({
+ method: 'invoice_az.auth.select_taxpayer',
+ args: { 'asan_login_name': asanLoginName },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ SalesETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации
+ SalesETaxes.dialogs.setSuccess(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback
+ );
+ } else {
+ SalesETaxes.dialogs.setError(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select taxpayer')
+ );
+ }
+ }
+ });
+ } else {
+ SalesETaxes.dialogs.setError(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select certificate')
+ );
+ }
+ }
+ });
+ },
+
+ // Показать селектор сертификатов
+ _showCertificateSelector: function(certificates, asanLoginName, callback) {
+ if (!certificates || !certificates.length) {
+ frappe.msgprint(__('No certificates available'));
+ return;
+ }
+
+ let certHtml = '
';
+ certHtml += 'Type Name ID Position Status ';
+
+ certificates.forEach(function(cert, index) {
+ let name = '';
+ let id = '';
+
+ if (cert.taxpayerType === 'individual' && cert.individualInfo) {
+ name = cert.individualInfo.name || '';
+ id = cert.individualInfo.fin || '';
+ } else if (cert.legalInfo) {
+ name = cert.legalInfo.name || '';
+ id = cert.legalInfo.tin || cert.legalInfo.voen || '';
+ }
+
+ certHtml += '' +
+ '' + (cert.taxpayerType || '') + ' ' +
+ '' + name + ' ' +
+ '' + id + ' ' +
+ '' + (cert.position || '') + ' ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + ' ' +
+ 'Select ' +
+ ' ';
+ });
+
+ certHtml += '
';
+
+ const dialog = new frappe.ui.Dialog({
+ title: __('Select Certificate'),
+ fields: [{
+ fieldtype: 'HTML',
+ fieldname: 'certificates',
+ options: certHtml
+ }]
+ });
+
+ dialog.show();
+
+ dialog.$wrapper.find('.select-cert').on('click', function() {
+ const index = $(this).data('index');
+ const cert = certificates[index];
+
+ let certName = '';
+ if (cert.taxpayerType === 'individual' && cert.individualInfo) {
+ certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
+ } else if (cert.legalInfo) {
+ certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
+ } else {
+ certName = `Certificate ${index + 1}`;
+ }
+
+ dialog.hide();
+
+ SalesETaxes.dialogs.showLoading(
+ __('Selecting Certificate'),
+ __('Processing your selection...'),
+ certName
+ );
+
+ SalesETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
+ });
+ }
+};
+
+// ======= УПРАВЛЕНИЕ ОШИБКАМИ =======
+SalesETaxes.errors = {
+ // Добавить ошибку в лог
+ add: function(invoiceData, errorType, errorMessage, additionalData = {}) {
+ const errorEntry = {
+ invoice_id: invoiceData.id || 'Unknown',
+ serial_number: invoiceData.serialNumber || 'Unknown',
+ receiver_name: invoiceData.receiver ? invoiceData.receiver.name : 'Unknown',
+ creation_date: invoiceData.creationDate || invoiceData.createdAt || invoiceData.date || 'Unknown',
+ error_type: errorType,
+ error_message: errorMessage,
+ timestamp: new Date().toISOString(),
+ ...additionalData
+ };
+
+ SalesETaxes.loadingErrors.push(errorEntry);
+ console.error('E-Taxes Sales Import Error:', errorEntry);
+ },
+
+ // Очистить лог ошибок
+ clear: function() {
+ frappe.confirm(
+ __('Are you sure you want to clear the error log?'),
+ function() {
+ SalesETaxes.loadingErrors = [];
+ frappe.show_alert({
+ message: __('Error log cleared'),
+ indicator: 'green'
+ }, 2);
+ }
+ );
+ },
+
+ // Показать детальный лог ошибок
+ showDetails: function() {
+ if (SalesETaxes.loadingErrors.length === 0) {
+ frappe.msgprint(__('No errors to display'));
+ return;
+ }
+
+ let errorsHtml = '
';
+
+ SalesETaxes.loadingErrors.forEach(function(error, index) {
+ let documentDate = 'No date';
+ if (error.creation_date && error.creation_date !== 'Unknown') {
+ try {
+ documentDate = moment(error.creation_date).format('DD.MM.YYYY');
+ } catch (e) {
+ documentDate = 'Invalid date';
+ }
+ }
+
+ errorsHtml += '
';
+
+ // Заголовок ошибки
+ errorsHtml += '';
+
+ // Сообщение об ошибке
+ errorsHtml += '
';
+ errorsHtml += '' + error.error_message + ' ';
+ errorsHtml += '
';
+
+ // Детали ошибки
+ if (error.unmatched_items && error.unmatched_items.length > 0) {
+ errorsHtml += '
';
+ errorsHtml += '
' + __('Unmapped items:') + '
';
+ errorsHtml += '
';
+ error.unmatched_items.forEach(function(item, idx) {
+ errorsHtml += 'Row ' + (idx + 1) + ': ' + item.name;
+ if (item.code) {
+ errorsHtml += ' (Code: ' + item.code + ') ';
+ }
+ errorsHtml += ' ';
+ });
+ errorsHtml += ' ';
+ } else if (error.unmatched_parties && error.unmatched_parties.length > 0) {
+ errorsHtml += '
';
+ errorsHtml += '
' + __('Unmapped parties:') + '
';
+ errorsHtml += '
';
+ error.unmatched_parties.forEach(function(party, idx) {
+ errorsHtml += 'Party ' + (idx + 1) + ': ' + party.name;
+ if (party.tin) {
+ errorsHtml += ' (TIN: ' + party.tin + ') ';
+ }
+ if (party.type) {
+ errorsHtml += ' ' + party.type + ' ';
+ }
+ errorsHtml += ' ';
+ });
+ errorsHtml += ' ';
+ } else if (error.unmatched_units && error.unmatched_units.length > 0) {
+ errorsHtml += '
';
+ errorsHtml += '
' + __('Unmapped units:') + '
';
+ errorsHtml += '
';
+ error.unmatched_units.forEach(function(unit, idx) {
+ errorsHtml += 'Unit ' + (idx + 1) + ': ' + unit.name + ' ';
+ });
+ errorsHtml += ' ';
+ }
+
+ errorsHtml += '
';
+ errorsHtml += '' + documentDate + ' ';
+ errorsHtml += '
';
+
+ errorsHtml += '
';
+ });
+
+ errorsHtml += '
';
+
+ // Статистика
+ const errorTypes = {};
+ SalesETaxes.loadingErrors.forEach(function(error) {
+ errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
+ });
+
+ let statsHtml = '
';
+ statsHtml += '
';
+ statsHtml += '
';
+ statsHtml += '' + __('Total Errors:') + ' ' + SalesETaxes.loadingErrors.length + ' ';
+ statsHtml += '';
+ Object.keys(errorTypes).forEach(function(type, index) {
+ if (index > 0) statsHtml += ' • ';
+ statsHtml += type + ': ' + errorTypes[type];
+ });
+ statsHtml += ' ';
+ statsHtml += '
';
+ statsHtml += '
';
+ statsHtml += '' +
+ ' ' + __('Export CSV') + ' ';
+ statsHtml += '' +
+ ' ' + __('Clear') + ' ';
+ statsHtml += '
';
+
+ const d = new frappe.ui.Dialog({
+ title: __('Error Details') + ' (' + SalesETaxes.loadingErrors.length + ')',
+ size: 'large',
+ fields: [
+ {
+ fieldname: 'stats_html',
+ fieldtype: 'HTML',
+ options: statsHtml
+ },
+ {
+ fieldname: 'errors_html',
+ fieldtype: 'HTML',
+ options: '
' + errorsHtml + '
'
+ }
+ ],
+ primary_action_label: __('Close'),
+ primary_action: function() {
+ d.hide();
+ }
+ });
+
+ d.show();
+
+ // Добавляем обработчики событий
+ setTimeout(function() {
+ $('#export-csv-dialog-btn').off('click').on('click', function() {
+ SalesETaxes.errors.exportCSV();
+ });
+
+ $('#clear-log-dialog-btn').off('click').on('click', function() {
+ SalesETaxes.errors.clear();
+ d.hide();
+ });
+ }, 200);
+ },
+
+ // Экспорт в CSV
+ exportCSV: function() {
+ if (SalesETaxes.loadingErrors.length === 0) {
+ frappe.msgprint(__('No errors to export'));
+ return;
+ }
+
+ try {
+ let csvContent = "data:text/csv;charset=utf-8,";
+ csvContent += "Invoice ID,Serial Number,Date,Customer,Error Type,Error Message,Timestamp\n";
+
+ SalesETaxes.loadingErrors.forEach(function(error) {
+ const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
+ const cleanReceiver = error.receiver_name.replace(/"/g, '""');
+
+ const row = [
+ error.invoice_id,
+ error.serial_number,
+ error.creation_date,
+ cleanReceiver,
+ error.error_type,
+ cleanMessage,
+ moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
+ ].map(function(field) {
+ return '"' + (field || '') + '"';
+ }).join(',');
+
+ csvContent += row + "\n";
+ });
+
+ const encodedUri = encodeURI(csvContent);
+ const link = document.createElement("a");
+ link.setAttribute("href", encodedUri);
+ link.setAttribute("download", "etaxes_sales_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ frappe.show_alert({
+ message: __('Error log exported successfully'),
+ indicator: 'green'
+ }, 3);
+ } catch (e) {
+ console.error('Export error:', e);
+ frappe.show_alert({
+ message: __('Failed to export error log'),
+ indicator: 'red'
+ }, 3);
+ }
+ },
+
+ // Показать сводку результатов
+ showSummary: function(totalInvoices, processedCount, errorsCount) {
+ let title = '';
+ let indicator = '';
+ let message = '';
+
+ if (errorsCount === 0) {
+ title = __('Import Completed Successfully');
+ indicator = 'green';
+ message = __('All ') + totalInvoices + __(' sales invoices were imported successfully.');
+
+ frappe.msgprint({
+ title: title,
+ indicator: indicator,
+ message: message
+ });
+ } else {
+ title = errorsCount === totalInvoices ? __('Import Failed') : __('Import Completed with Errors');
+ indicator = processedCount > 0 ? 'orange' : 'red';
+
+ message = '
';
+ if (processedCount > 0) {
+ message += __('Successfully imported: ') + '' + processedCount + ' ' + __(' sales invoices') + ' ';
+ }
+ message += __('Failed: ') + '' + errorsCount + ' ' + __(' sales invoices');
+ message += '
';
+
+ message += '
' +
+ '' +
+ ' ' + __('View Error Details') + ' ' +
+ '' +
+ ' ' + __('Export CSV') + ' ' +
+ '
';
+
+ const summaryDialog = frappe.msgprint({
+ title: title,
+ indicator: indicator,
+ message: message
+ });
+
+ // Добавляем обработчики событий
+ setTimeout(function() {
+ $('#view-error-details-btn').off('click').on('click', function() {
+ summaryDialog.hide();
+ SalesETaxes.errors.showDetails();
+ });
+
+ $('#export-error-log-btn').off('click').on('click', function() {
+ SalesETaxes.errors.exportCSV();
+ });
+ }, 200);
+ }
+ }
+};
+
+// ======= МОДУЛЬ ИМПОРТА ИНВОЙСОВ =======
+SalesETaxes.import = {
+ // Показать диалог импорта из E-Taxes
+ showDialog: function() {
+ SalesETaxes.auth.checkAndProcess(function() {
+ const minDate = moment("2020-01-01", "YYYY-MM-DD");
+
+ const d = new frappe.ui.Dialog({
+ title: __('Import sales invoice from E-Taxes'),
+ fields: [
+ {
+ fieldname: 'date_range_section',
+ fieldtype: 'Section Break',
+ label: __('Period')
+ },
+ {
+ fieldname: 'creationDateFrom',
+ fieldtype: 'Date',
+ label: __('Date from'),
+ default: moment().subtract(1, 'month').format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'creationDateTo',
+ fieldtype: 'Date',
+ label: __('Date to'),
+ default: moment().format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'settings_section',
+ fieldtype: 'Section Break',
+ label: __('Settings')
+ },
+ {
+ fieldname: 'warehouse',
+ fieldtype: 'Link',
+ label: __('Delivery warehouse'),
+ options: 'Warehouse',
+ reqd: true,
+ get_query: function() {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'disabled': 0
+ }
+ };
+ }
+ }
+ ],
+ primary_action_label: __('Search'),
+ primary_action: function() {
+ const values = d.get_values();
+
+ // Validate date range
+ const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
+ const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
+ const todayMoment = moment();
+
+ if (fromDateMoment.isBefore(minDate)) {
+ frappe.msgprint(__('From Date cannot be earlier than January 1, 2020'));
+ return;
+ }
+
+ if (toDateMoment.isAfter(todayMoment)) {
+ frappe.msgprint(__('To Date cannot be later than today'));
+ return;
+ }
+
+ if (toDateMoment.diff(fromDateMoment, 'days') > 366) {
+ frappe.msgprint(__('The date range cannot exceed one year.'));
+ return;
+ }
+
+ if (!values.warehouse) {
+ frappe.msgprint({
+ title: __('Warning'),
+ indicator: 'orange',
+ message: __('Please select a delivery warehouse')
+ });
+ return;
+ }
+
+ const fromDate = moment(values.creationDateFrom).format('DD-MM-YYYY 00:00');
+ const toDate = moment(values.creationDateTo).format('DD-MM-YYYY 23:59');
+
+ d.hide();
+ SalesETaxes.import.loadInvoices(fromDate, toDate, values.warehouse);
+ }
+ });
+
+ d.show();
+ });
+ },
+
+ // Загрузить инвойсы с фильтрацией дубликатов
+ loadInvoices: function(fromDate, toDate, warehouse, accumulatedInvoices = [], offset = 0) {
+ SalesETaxes.cancelLoading = false;
+ $(document).off('progress-cancel.etaxes_sales_import');
+
+ if (offset === 0) {
+ SalesETaxes.dialogs.showLoading(
+ __('Loading E-Taxes Documents'),
+ __('Fetching E-Taxes sales invoices...'),
+ __('Please wait')
+ );
+ }
+
+ SalesETaxes.utils.getDefaultLogin(function(loginResponse) {
+ if (SalesETaxes.cancelLoading) return;
+
+ if (loginResponse && loginResponse.found) {
+ const mainToken = loginResponse.main_token;
+
+ if (!mainToken) {
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Authentication Error'),
+ indicator: 'red',
+ message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.')
+ });
+ return;
+ }
+
+ const maxCount = 200;
+
+ frappe.call({
+ method: 'invoice_az.sales_api.get_sales_invoices',
+ args: {
+ 'token': mainToken,
+ 'filters': JSON.stringify({
+ "creationDateFrom": fromDate,
+ "creationDateTo": toDate,
+ "maxCount": maxCount,
+ "offset": offset
+ })
+ },
+ callback: function(r) {
+ if (SalesETaxes.cancelLoading) return;
+
+ if (r.message && !r.message.error) {
+ const currentInvoices = r.message.data || r.message.invoices || [];
+ const hasMore = r.message.hasMore || false;
+ const allInvoices = accumulatedInvoices.concat(currentInvoices);
+
+ if (hasMore && currentInvoices.length > 0) {
+ const newOffset = offset + currentInvoices.length;
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Fetching E-Taxes sales invoices...'),
+ __('Loaded ') + allInvoices.length + __(' so far...')
+ );
+ SalesETaxes.import.loadInvoices(fromDate, toDate, warehouse, allInvoices, newOffset);
+ } else {
+ if (allInvoices.length > 0) {
+ SalesETaxes.dialogs.updateLoading(
+ null,
+ __('Processing ') + allInvoices.length + __(' invoices...'),
+ __('Checking for already imported documents')
+ );
+
+ SalesETaxes.import.filterDuplicates(allInvoices, mainToken, warehouse);
+ } else {
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Information'),
+ indicator: 'blue',
+ message: __('No sales invoices found for the specified period')
+ });
+ }
+ }
+ } else if (r.message && r.message.error === 'unauthorized') {
+ SalesETaxes.dialogs.hide();
+ frappe.confirm(
+ __('Your E-Taxes session has expired. Would you like to authenticate now?'),
+ function() {
+ SalesETaxes.auth.startProcess(function() {
+ SalesETaxes.import.loadInvoices(fromDate, toDate, warehouse);
+ });
+ },
+ function() {
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ } else {
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: r.message ? r.message.message : __('Error loading sales invoices')
+ });
+ }
+ },
+ error: function(xhr, status, error) {
+ console.error("Error fetching sales invoices:", error);
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Network Error'),
+ indicator: 'red',
+ message: __('Error fetching data from server: ') + (error || 'Unknown error')
+ });
+ }
+ });
+ } else {
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('E-Taxes authentication settings not found')
+ });
+ }
+ });
+ },
+
+ // Фильтрация дубликатов
+ filterDuplicates: function(allInvoices, token, warehouse) {
+ frappe.call({
+ method: 'invoice_az.sales_api.get_etaxes_sales',
+ callback: function(etaxesR) {
+ try {
+ if (SalesETaxes.cancelLoading) return;
+
+ const importedInvoices = {};
+
+ if (etaxesR.message && etaxesR.message.success && etaxesR.message.sales) {
+ etaxesR.message.sales.forEach(function(sale) {
+ if (sale.etaxes_id) {
+ const normId = String(sale.etaxes_id).trim();
+ importedInvoices[normId] = true;
+ }
+ });
+ }
+
+ const filteredInvoices = [];
+
+ for (let i = 0; i < allInvoices.length; i++) {
+ const invoice = allInvoices[i];
+ const invoiceId = String(invoice.id).trim();
+
+ if (!importedInvoices.hasOwnProperty(invoiceId)) {
+ filteredInvoices.push(invoice);
+ }
+ }
+
+ if (filteredInvoices.length > 0) {
+ SalesETaxes.import.showInvoiceSelection(filteredInvoices, token, warehouse);
+ } else {
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Information'),
+ indicator: 'blue',
+ message: __('No new sales invoices found for the specified period (all ' +
+ allInvoices.length + ' are already imported)')
+ });
+ }
+ } catch (e) {
+ console.error("Error processing sales data:", e);
+ SalesETaxes.dialogs.hide();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('An error occurred while processing data: ') + e.message
+ });
+ }
+ },
+ error: function(err) {
+ console.error("Error fetching sales:", err);
+ SalesETaxes.import.showInvoiceSelection(allInvoices, token, warehouse);
+ frappe.show_alert({
+ message: __('Failed to check for duplicates. Showing all sales invoices.'),
+ indicator: 'orange'
+ }, 5);
+ }
+ });
+ },
+
+ // Показать диалог выбора инвойсов
+ showInvoiceSelection: function(invoices, token, warehouse) {
+ SalesETaxes.dialogs.hide();
+ let invoiceTable = '
';
+
+ // Компактная шапка: количество инвойсов + склад в одной строке.
+ // Заменяет два отдельных alert'а (верхний со счётчиком и нижний со складом,
+ // который раньше уезжал под таблицу и требовал скролла).
+ const headerHtml =
+ '
' +
+ '' +
+ __('Sales invoices found: ') + '' + invoices.length + ' ' +
+ ' ' +
+ '' +
+ __('Selected delivery warehouse: ') + '' + frappe.utils.escape_html(warehouse) + ' ' +
+ ' ' +
+ '
';
+
+ const d = new frappe.ui.Dialog({
+ title: __('Select sales invoice'),
+ size: 'large',
+ fields: [
+ {
+ fieldname: 'header_html',
+ fieldtype: 'HTML',
+ options: headerHtml
+ },
+ {
+ fieldname: 'invoices_html',
+ fieldtype: 'HTML',
+ options: invoiceTable
+ }
+ ],
+ primary_action_label: __('Load selected'),
+ primary_action: function() {
+ const selectedInvoiceIds = [];
+ d.$wrapper.find('.select-sales-invoice:checked').each(function() {
+ selectedInvoiceIds.push($(this).data('id'));
+ });
+
+ if (selectedInvoiceIds.length === 0) {
+ frappe.msgprint({
+ title: __('Warning'),
+ indicator: 'orange',
+ message: __('No sales invoices selected')
+ });
+ return;
+ }
+
+ d.hide();
+ SalesETaxes.cancelLoading = false;
+ SalesETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
+ },
+ secondary_action_label: __('Cancel'),
+ secondary_action: function() {
+ d.hide();
+ }
+ });
+
+ d.$wrapper.find('.modal-dialog').css({
+ 'max-width': '80%',
+ 'width': '80%',
+ 'margin': '30px auto'
+ });
+
+ d.$wrapper.find('.modal-body').css({
+ 'padding': '15px'
+ });
+
+ d.show();
+
+ // Обработчик для чекбокса "выбрать все"
+ d.$wrapper.find('.select-all-sales-invoices').on('change', function() {
+ const isChecked = $(this).prop('checked');
+ d.$wrapper.find('.select-sales-invoice').prop('checked', isChecked);
+ });
+ },
+
+ // Загрузка выбранных инвойсов
+ // Надёжное закрытие прогресс-бара. frappe.hide_progress() в jey_theme/Bootstrap
+ // не всегда убирает модал (он зависает, и summary/диалог накладывается сверху).
+ // Принудительно: modal('hide') + remove() + чистка осиротевшего backdrop.
+ _closeProgress: function() {
+ if (frappe.cur_progress) {
+ try {
+ frappe.cur_progress.$wrapper.modal('hide');
+ frappe.cur_progress.$wrapper.remove();
+ } catch (e) {}
+ frappe.cur_progress = null;
+ }
+ if ($('.modal:visible').length === 0) {
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+ },
+
+ loadSelectedInvoices: function(invoiceIds, token, warehouse, processedCount = 0, currentIndex = 0) {
+ if (processedCount === 0 && currentIndex === 0) {
+ SalesETaxes.loadingErrors = [];
+ window.totalSalesInvoicesToProcess = invoiceIds.length;
+ }
+
+ $(document).off('progress-cancel.loading_sales_invoices');
+
+ if (invoiceIds.length === 0) {
+ SalesETaxes.import._closeProgress();
+
+ const errorsCount = SalesETaxes.loadingErrors.length;
+ const totalInvoices = window.totalSalesInvoicesToProcess || processedCount;
+
+ SalesETaxes.errors.showSummary(totalInvoices, processedCount, errorsCount);
+ window.totalSalesInvoicesToProcess = null;
+
+ if (cur_list) {
+ cur_list.refresh();
+ }
+
+ return;
+ }
+
+ if (SalesETaxes.cancelLoading) {
+ invoiceIds = [];
+ SalesETaxes.import._closeProgress();
+ SalesETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
+ return;
+ }
+
+ const invoiceId = invoiceIds.shift();
+ const isLastInvoice = invoiceIds.length === 0;
+ const totalToProcess = window.totalSalesInvoicesToProcess || (processedCount + invoiceIds.length + 1);
+
+ currentIndex++;
+
+ frappe.show_progress(__('Importing Sales Invoices'), currentIndex - 1, totalToProcess,
+ __('Loading sales invoice ') + currentIndex + __(' of ') + totalToProcess, null, true);
+
+ $(document).on('progress-cancel.loading_sales_invoices', function() {
+ SalesETaxes.cancelLoading = true;
+ frappe.show_alert({
+ message: __('Cancelling import... Finishing current sales invoice.'),
+ indicator: 'orange'
+ }, 3);
+ SalesETaxes.import._closeProgress();
+ });
+
+ frappe.call({
+ method: 'invoice_az.sales_api.get_sales_invoice_details',
+ args: {
+ 'token': token,
+ 'invoice_id': invoiceId
+ },
+ callback: function(r) {
+ if (r.message && !r.message.error) {
+ const creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
+ const scheduleDate = moment(creationDate).format('YYYY-MM-DD');
+ const serialNumber = r.message.serialNumber || '';
+ const receiverName = r.message.receiver ? r.message.receiver.name : '';
+ const total = r.message.totalAmount || r.message.amount || 0;
+
+ frappe.show_progress(__('Importing Sales Invoices'), currentIndex - 1, totalToProcess,
+ __('Importing sales invoice: ') + serialNumber);
+
+ frappe.call({
+ method: 'invoice_az.sales_api.import_sales_invoice_with_mapping',
+ args: {
+ 'invoice_data': r.message,
+ 'sales_order_name': null,
+ 'schedule_date': scheduleDate,
+ 'warehouse': warehouse // ДОБАВЛЕНО: передача warehouse
+ },
+ callback: function(importR) {
+ SalesETaxes.import._handleImportResult(importR, r.message, invoiceId, scheduleDate, receiverName, total,
+ invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ },
+ error: function(xhr, status, error) {
+ const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
+ SalesETaxes.errors.add(r.message, 'Network Error', errorMessage, {
+ invoice_items: r.message.items || [],
+ xhr_status: xhr.status,
+ xhr_response: xhr.responseText
+ });
+
+ SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ });
+ } else if (r.message && r.message.error === 'unauthorized') {
+ SalesETaxes.import._closeProgress();
+ $(document).off('progress-cancel.loading_sales_invoices');
+
+ frappe.confirm(
+ __('Your E-Taxes session has expired. Would you like to authenticate now?'),
+ function() {
+ SalesETaxes.auth.startProcess(function() {
+ SalesETaxes.utils.getDefaultLogin(function(loginR) {
+ if (loginR && loginR.found && loginR.main_token) {
+ invoiceIds.unshift(invoiceId);
+ SalesETaxes.import.loadSelectedInvoices(invoiceIds, loginR.main_token, warehouse, processedCount, currentIndex - 1);
+ } else {
+ SalesETaxes.errors.add({id: invoiceId}, 'Authentication Error',
+ 'Failed to get token after authentication');
+
+ const errorsCount = SalesETaxes.loadingErrors.length;
+ SalesETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
+ }
+ }, false);
+ });
+ },
+ function() {
+ SalesETaxes.errors.add({id: invoiceId}, 'Authentication Cancelled',
+ 'User cancelled authentication process');
+
+ const errorsCount = SalesETaxes.loadingErrors.length;
+ SalesETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
+ }
+ );
+ } else {
+ const errorMessage = r.message ? r.message.message : 'Failed to load sales invoice details';
+ SalesETaxes.errors.add({id: invoiceId}, 'Sales Invoice Data Error', errorMessage, {
+ server_response: r.message
+ });
+
+ SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ },
+ error: function(xhr, status, error) {
+ const errorMessage = 'Network error loading sales invoice details: ' + (error || 'Unknown network error');
+ SalesETaxes.errors.add({id: invoiceId}, 'Network Error', errorMessage, {
+ xhr_status: xhr.status,
+ xhr_response: xhr.responseText
+ });
+
+ SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ });
+ },
+
+ // Обработка результата импорта - ОБНОВЛЕНО для Sales Order + Sales Invoice
+ _handleImportResult: function(importR, invoiceMessage, invoiceId, scheduleDate, receiverName, total,
+ invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
+ if (importR.message && importR.message.success) {
+ frappe.show_alert({
+ message: __('Sales invoice successfully imported: ') + (invoiceMessage.serialNumber || ''),
+ indicator: 'green'
+ }, 3);
+
+ // ИЗМЕНЕНО: теперь получаем sales_order и sales_invoice из ответа
+ const salesOrderName = importR.message.sales_order;
+ const salesInvoiceName = importR.message.sales_invoice;
+
+ // ИЗМЕНЕНО: создание E-Taxes Sales и связывание с Sales Order происходит на сервере
+ // Поэтому просто увеличиваем счетчик и продолжаем
+ processedCount++;
+ SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+
+ } else if (importR.message && (importR.message.unmatched_items || importR.message.unmatched_parties || importR.message.unmatched_units)) {
+ let errorType = '';
+ const additionalData = { invoice_items: invoiceMessage.items || [] };
+
+ if (importR.message.unmatched_items) {
+ errorType = 'Unmapped Items';
+ additionalData.unmatched_items = importR.message.unmatched_items.map(function(item, index) {
+ let itemName = '';
+ let itemCode = '';
+
+ if (typeof item === 'string') {
+ itemName = item;
+ } else if (item && typeof item === 'object') {
+ itemName = item.name || item.productName || item.item_name || '';
+ itemCode = item.code || item.itemId || item.item_code || '';
+ } else {
+ if (invoiceMessage.items && invoiceMessage.items[index]) {
+ const originalItem = invoiceMessage.items[index];
+ itemName = originalItem.productName || originalItem.name || '';
+ itemCode = originalItem.itemId || originalItem.code || '';
+ }
+ }
+
+ if (!itemName) {
+ itemName = 'Item ' + (index + 1) + ' (name not available)';
+ }
+
+ return {
+ name: itemName,
+ code: itemCode
+ };
+ });
+ } else if (importR.message.unmatched_parties) {
+ errorType = 'Unmapped Parties';
+ additionalData.unmatched_parties = importR.message.unmatched_parties.map(function(party, index) {
+ let partyName = '';
+ let partyTin = '';
+ let partyType = '';
+
+ if (typeof party === 'string') {
+ partyName = party;
+ } else if (party && typeof party === 'object') {
+ partyName = party.name || party.party_name || '';
+ partyTin = party.tin || party.tax_id || '';
+ partyType = party.type || party.party_type || '';
+ } else {
+ if (invoiceMessage.receiver) {
+ partyName = invoiceMessage.receiver.name || '';
+ partyTin = invoiceMessage.receiver.tin || '';
+ partyType = 'Receiver';
+ }
+ }
+
+ if (!partyName) {
+ partyName = 'Party ' + (index + 1) + ' (name not available)';
+ }
+
+ return {
+ name: partyName,
+ tin: partyTin,
+ type: partyType
+ };
+ });
+ } else if (importR.message.unmatched_units) {
+ errorType = 'Unmapped Units';
+ additionalData.unmatched_units = importR.message.unmatched_units.map(function(unit, index) {
+ let unitName = '';
+
+ if (typeof unit === 'string') {
+ unitName = unit;
+ } else if (unit && typeof unit === 'object') {
+ unitName = unit.name || unit.unit_name || '';
+ } else {
+ if (invoiceMessage.items && invoiceMessage.items[index] && invoiceMessage.items[index].unit) {
+ unitName = invoiceMessage.items[index].unit;
+ }
+ }
+
+ if (!unitName) {
+ unitName = 'Unit ' + (index + 1) + ' (name not available)';
+ }
+
+ return {
+ name: unitName
+ };
+ });
+ }
+
+ additionalData.server_response = importR.message;
+ SalesETaxes.errors.add(invoiceMessage, errorType, importR.message.message, additionalData);
+
+ SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ } else {
+ const errorMessage = importR.message ? importR.message.message : 'Unknown sales import error';
+ const additionalData = {
+ invoice_items: invoiceMessage.items || [],
+ server_response: importR.message
+ };
+
+ if (importR.exc) {
+ additionalData.stack_trace = importR.exc;
+ }
+
+ SalesETaxes.errors.add(invoiceMessage, 'Sales Import Error', errorMessage, additionalData);
+ SalesETaxes.import._continueOrFinish(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice);
+ }
+ },
+
+ // Продолжить или завершить обработку
+ _continueOrFinish: function(invoiceIds, token, warehouse, processedCount, currentIndex, isLastInvoice) {
+ if (isLastInvoice) {
+ SalesETaxes.import._closeProgress();
+ $(document).off('progress-cancel.loading_sales_invoices');
+
+ const totalToProcess = window.totalSalesInvoicesToProcess || processedCount;
+ const errorsCount = SalesETaxes.loadingErrors.length;
+ SalesETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount);
+
+ if (cur_list) {
+ cur_list.refresh();
+ }
+ return;
+ }
+
+ setTimeout(function() {
+ if (invoiceIds.length === 0) {
+ SalesETaxes.import._closeProgress();
+ }
+
+ SalesETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
+ }, SalesETaxes.PROGRESS_UPDATE_DELAY);
+ },
+
+ // Socket.IO bulk import — all work happens on the server in a background job
+ loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
+ SalesETaxes.loadingErrors = [];
+ const total = invoiceIds.length;
+ // Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
+ // frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
+ // уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
+ let importFinished = false;
+ // Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
+ // (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
+ const closeProgress = function() {
+ if (frappe.cur_progress) {
+ try {
+ frappe.cur_progress.$wrapper.modal('hide');
+ frappe.cur_progress.$wrapper.remove();
+ } catch (e) {}
+ frappe.cur_progress = null;
+ }
+ if ($('.modal:visible').length === 0) {
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+ };
+
+ // 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;
+ frappe.realtime.off('sales_import_progress');
+ frappe.realtime.off('sales_import_complete');
+ closeProgress();
+
+ // Collect errors for the error panel
+ (data.errors || []).forEach(function(err) {
+ SalesETaxes.loadingErrors.push({
+ invoice_id: err.invoice_id || 'Unknown',
+ error_type: err.unmatched_items ? 'Unmapped Items'
+ : err.unmatched_parties ? 'Unmapped Parties'
+ : err.unmatched_units ? 'Unmapped Units'
+ : 'Import Error',
+ error_message: err.error || 'Unknown error',
+ unmatched_items: err.unmatched_items,
+ unmatched_parties: err.unmatched_parties,
+ unmatched_units: err.unmatched_units
+ });
+ });
+
+ const errorsCount = SalesETaxes.loadingErrors.length;
+ SalesETaxes.errors.showSummary(data.total, data.imported, errorsCount);
+
+ if (cur_list) {
+ cur_list.refresh();
+ }
+ });
+
+ // Enqueue the background job
+ frappe.call({
+ method: 'invoice_az.sales_api.import_bulk_sales_invoices',
+ args: {
+ invoice_ids: JSON.stringify(invoiceIds),
+ token: token,
+ warehouse: warehouse
+ },
+ callback: function(r) {
+ if (!r.message || !r.message.enqueued) {
+ frappe.realtime.off('sales_import_progress');
+ frappe.realtime.off('sales_import_complete');
+ closeProgress();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Failed to start import job')
+ });
+ }
+ },
+ error: function() {
+ frappe.realtime.off('sales_import_progress');
+ frappe.realtime.off('sales_import_complete');
+ closeProgress();
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Network error starting import')
+ });
+ }
+ });
+ }
+};
+
+frappe.listview_settings['Sales Order'] = {
+ add_fields: ['customer', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
+
+ get_indicator: function(doc) {
+ if (doc.status === 'Closed') {
+ return [__('Closed'), 'green', 'status,=,Closed'];
+ } else if (doc.status === 'On Hold') {
+ return [__('On Hold'), 'orange', 'status,=,On Hold'];
+ } else if (doc.status === 'Delivered') {
+ return [__('Delivered'), 'green', 'status,=,Delivered'];
+ } else if (doc.docstatus == 1) {
+ return [__('To Deliver and Bill'), 'orange', 'docstatus,=,1'];
+ } else if (doc.docstatus == 0) {
+ return [__('Draft'), 'red', 'docstatus,=,0'];
+ } else if (doc.docstatus == 2) {
+ return [__('Cancelled'), 'grey', 'docstatus,=,2'];
+ }
+ },
+
+ onload: function(listview) {
+ // ДОБАВЛЕНО: кнопка импорта из E-Taxes для Sales Order
+ listview.page.add_inner_button(__('Import from E-Taxes'), function() {
+ SalesETaxes.import.showDialog();
+ }, __('E-Taxes'));
+
+ listview.page.add_inner_button(__('Open Settings'), function() {
+ frappe.set_route('Form', 'E-Taxes Settings');
+ }, __('E-Taxes'));
+
+ listview.columns.push({
+ type: 'Check',
+ df: {
+ label: __('Tax Doc'),
+ fieldname: 'is_taxes_doc'
+ },
+ width: 120
+ });
+
+ listview.refresh();
+ },
+
+ formatters: {
+ is_taxes_doc: function(value) {
+ return value ?
+ `
` :
+ `
`;
+ }
+ }
+};
+
+// Sales Invoice List - УБРАНО: кнопка импорта (теперь только в Sales Order)
+frappe.listview_settings['Sales Invoice'] = {
+ add_fields: ['customer', 'posting_date', 'status', 'docstatus', 'is_taxes_doc'],
+
+ onload: function(listview) {
+ // УБРАНО: кнопка импорта - теперь импорт только через Sales Order
+
+ listview.columns.push({
+ type: 'Check',
+ df: {
+ label: __('Tax Doc'),
+ fieldname: 'is_taxes_doc'
+ },
+ width: 120
+ });
+
+ listview.refresh();
+ },
+
+ formatters: {
+ is_taxes_doc: function(value) {
+ return value ?
+ `
` :
+ `
`;
+ }
+ }
+};
\ No newline at end of file
diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py
index f12f03b..0aa44b1 100644
--- a/invoice_az/hooks.py
+++ b/invoice_az/hooks.py
@@ -10,7 +10,6 @@ doctype_js = {
"Purchase Invoice": "client/purchase_invoice.js",
"Sales Order": "client/sales_order.js",
"Sales Invoice": "client/sales_invoice.js",
- "Journal Entry": "client/journal_entry.js",
"Supplier": "client/supplier.js",
"Amas Employees": "client/amas_employees.js",
"Employee": "client/employee.js",
@@ -22,12 +21,12 @@ doctype_list_js = {
"E-Taxes Suppliers": "client/e_taxes_suppliers_list.js",
"E-Taxes Customers": "client/e_taxes_customers_list.js",
"E-Taxes Unit": "client/e_taxes_unit_list.js",
- "Purchase Order": "client/purchase_order.js",
- "Purchase Invoice": "client/purchase_invoice.js",
- "Sales Order": "client/sales_order.js",
- "Sales Invoice": "client/sales_invoice.js",
+ "Purchase Order": "client/purchase_order_list.js",
+ "Purchase Invoice": "client/purchase_invoice_list.js",
+ "Sales Order": "client/sales_order_list.js",
+ "Sales Invoice": "client/sales_invoice_list.js",
"Journal Entry": "client/journal_entry.js",
- "Employee": "client/employee.js"
+ "Employee": "client/employee_list.js"
}
# Хуки для добавления обработчиков событий Purchase Order