feat: switch all imports to Socket.IO background jobs for better performance

Replace sequential AJAX calls with frappe.enqueue + publish_realtime for:
- Sales Order import (sales_api.py + sales_order.js)
- Purchase Order import (api.py + purchase_order.js)
- Sales Invoice import (api.py + sales_invoice.js)
- VAT Operations import (vat_api.py + journal_entry.js)
- AMAS Employee import (amas_api.py + employee.js)

Also remove "Certificates retrieved successfully" msgprint from asan_login.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-04-10 13:47:08 +00:00
parent cdb6739308
commit 64961e4108
10 changed files with 758 additions and 33 deletions

View File

@ -2610,3 +2610,82 @@ def on_delete_employee(doc, method):
) )
for rec in amas_records: for rec in amas_records:
frappe.delete_doc("Amas Employees", rec.name, ignore_permissions=True) frappe.delete_doc("Amas Employees", rec.name, ignore_permissions=True)
@frappe.whitelist()
def import_bulk_employees(asan_login_name, employees_data, company, create_designation=0):
"""Enqueue bulk employee import as a background job with realtime progress via Socket.IO."""
if isinstance(employees_data, str):
employees_data = json.loads(employees_data)
try:
create_designation = int(create_designation)
except (ValueError, TypeError):
create_designation = 0
user = frappe.session.user
frappe.enqueue(
_process_bulk_employees_import,
asan_login_name=asan_login_name,
employees=employees_data,
company=company,
create_designation=create_designation,
user=user,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(employees_data)}
def _process_bulk_employees_import(asan_login_name, employees, company, create_designation, user):
"""Background job: import each employee with realtime progress."""
frappe.set_user(user)
total = len(employees)
created_count = 0
updated_count = 0
errors = []
for idx, emp_data in enumerate(employees):
emp_name = emp_data.get("full_name") or emp_data.get("identification_number") or "Employee"
try:
result = create_single_employee_from_amas(
asan_login_name=asan_login_name,
emp_data=emp_data,
company=company,
create_designation=create_designation,
)
if result and result.get("success"):
if result.get("action") == "created":
created_count += 1
elif result.get("action") == "updated":
updated_count += 1
else:
error_msg = result.get("message", "Unknown error") if result else "Empty result"
errors.append({"employee": emp_name, "error": error_msg})
except Exception as e:
errors.append({"employee": emp_name, "error": str(e)})
frappe.db.commit()
frappe.publish_realtime(
"amas_import_progress",
{"current": idx + 1, "total": total, "employee_name": emp_name},
user=user,
)
frappe.publish_realtime(
"amas_import_complete",
{
"total": total,
"created": created_count,
"updated": updated_count,
"errors": errors,
},
user=user,
)

View File

@ -4307,3 +4307,109 @@ def refresh_token_from_asan_login():
'success': False, 'success': False,
'message': str(e) 'message': str(e)
} }
@frappe.whitelist()
def import_bulk_purchase_invoices(invoice_ids, token, warehouse=None):
"""Enqueue bulk purchase import as a background job with realtime progress via Socket.IO."""
if isinstance(invoice_ids, str):
invoice_ids = json.loads(invoice_ids)
user = frappe.session.user
frappe.enqueue(
_process_bulk_purchase_import,
invoice_ids=invoice_ids,
token=token,
warehouse=warehouse,
user=user,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(invoice_ids)}
def _process_bulk_purchase_import(invoice_ids, token, warehouse, user):
"""Background job: fetch details and import each purchase invoice with realtime progress."""
frappe.set_user(user)
record_etaxes_activity()
total = len(invoice_ids)
imported_count = 0
errors = []
for idx, invoice_id in enumerate(invoice_ids):
try:
details = get_invoice_details(token, invoice_id)
except Exception:
details = None
if not details or details.get("error"):
error_msg = details.get("message", "Failed to fetch invoice details") if details else "Empty response"
if details and details.get("error") == "unauthorized":
asan_login = get_default_asan_login()
if asan_login.get("found") and asan_login.get("main_token"):
token = asan_login["main_token"]
details = get_invoice_details(token, invoice_id)
if not details or details.get("error"):
error_msg = details.get("message", "Failed after token refresh") if details else "Empty response"
errors.append({"invoice_id": invoice_id, "error": error_msg})
frappe.db.commit()
continue
else:
errors.append({"invoice_id": invoice_id, "error": "Authentication expired"})
frappe.db.commit()
continue
else:
errors.append({"invoice_id": invoice_id, "error": error_msg})
frappe.db.commit()
continue
schedule_date = None
created_at = details.get("creationDate") or details.get("date")
if created_at:
try:
schedule_date = datetime.datetime.strptime(str(created_at)[:10], "%Y-%m-%d").strftime("%Y-%m-%d")
except Exception:
pass
result = import_invoice_with_mapping(
invoice_data=details,
purchase_order_name=None,
schedule_date=schedule_date,
warehouse=warehouse,
)
if result and result.get("success"):
try:
sender_name = details.get("sender", {}).get("name", "") if details.get("sender") else ""
total_amount = details.get("totalAmount", 0) or details.get("amount", 0)
etaxes_result = create_etaxes_purchase(invoice_id, schedule_date, sender_name, total_amount)
if etaxes_result and etaxes_result.get("success"):
link_purchase_order_to_etaxes(result["purchase_order"], etaxes_result["name"])
except Exception:
pass
imported_count += 1
else:
error_msg = result.get("message", "Unknown import error") if result else "Empty result"
error_entry = {"invoice_id": invoice_id, "error": error_msg}
if result:
for key in ("unmatched_items", "unmatched_parties", "unmatched_units"):
if result.get(key):
error_entry[key] = result[key]
errors.append(error_entry)
frappe.db.commit()
frappe.publish_realtime(
"purchase_import_progress",
{"current": idx + 1, "total": total},
user=user,
)
frappe.publish_realtime(
"purchase_import_complete",
{"total": total, "imported": imported_count, "errors": errors},
user=user,
)

View File

@ -586,25 +586,71 @@ function show_employee_selection(listview, asan_login_name, organization_name, e
}); });
} }
// Step 5: Create employees via backend - ONE BY ONE like E-Taxes // Step 5: Create employees via backend - Socket.IO bulk import
function create_employees(listview, asan_login_name, selected_employees, company, create_designation) { function create_employees(listview, asan_login_name, selected_employees, company, create_designation) {
// Initialize counters const total = selected_employees.length;
window.amas_employees_to_process = selected_employees.length;
window.amas_cancel_loading = false;
// Start processing frappe.show_progress(
process_employees_one_by_one( __('Loading from ƏMAS'), 0, total,
listview, __('Starting import of {0} employees...', [total])
asan_login_name,
selected_employees,
company,
create_designation,
0, // created_count
0, // updated_count
0, // error_count
[], // errors
0 // current_index
); );
frappe.realtime.off('amas_import_progress');
frappe.realtime.on('amas_import_progress', function(data) {
let detail = __('Processing employee {0} of {1}', [data.current, data.total]);
if (data.employee_name) {
detail = __('Processing: ') + data.employee_name;
}
frappe.show_progress(__('Loading from ƏMAS'), data.current, data.total, detail);
});
frappe.realtime.off('amas_import_complete');
frappe.realtime.on('amas_import_complete', function(data) {
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
frappe.hide_progress();
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();
});
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.enqueued) {
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
frappe.hide_progress();
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');
frappe.hide_progress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
} }
// Process employees one by one (like E-Taxes loadSelectedInvoices) // Process employees one by one (like E-Taxes loadSelectedInvoices)

View File

@ -1232,7 +1232,7 @@ VATETaxes.import = {
d.hide(); d.hide();
VATETaxes.cancelLoading = false; VATETaxes.cancelLoading = false;
VATETaxes.import.loadSelectedOperations(selectedOperations, token, company, 0, 0, createAsDraftValue, autoCreateCustomers); VATETaxes.import.loadSelectedOperationsSocketIO(selectedOperations, token, company, createAsDraftValue, autoCreateCustomers);
}, },
secondary_action_label: __('Cancel'), secondary_action_label: __('Cancel'),
secondary_action: function() { secondary_action: function() {
@ -1398,6 +1398,81 @@ VATETaxes.import = {
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers); VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers);
}, VATETaxes.PROGRESS_UPDATE_DELAY); }, VATETaxes.PROGRESS_UPDATE_DELAY);
},
// Socket.IO bulk import
loadSelectedOperationsSocketIO: function(operations, token, company, createAsDraft, autoCreateCustomers) {
VATETaxes.loadingErrors = [];
const total = operations.length;
frappe.show_progress(
__('Importing VAT Operations'), 0, total,
__('Starting import of {0} operations...', [total])
);
frappe.realtime.off('vat_import_progress');
frappe.realtime.on('vat_import_progress', function(data) {
frappe.show_progress(
__('Importing VAT Operations'), data.current, data.total,
__('Processing operation {0} of {1}', [data.current, data.total])
);
});
frappe.realtime.off('vat_import_complete');
frappe.realtime.on('vat_import_complete', function(data) {
frappe.realtime.off('vat_import_progress');
frappe.realtime.off('vat_import_complete');
frappe.hide_progress();
(data.errors || []).forEach(function(err) {
VATETaxes.loadingErrors.push({
operation_id: err.operation_id || 'Unknown',
error_type: err.error_type || 'Import Error',
error_message: err.error || 'Unknown error',
tin: err.tin,
customer_name: err.customer_name
});
});
const errorsCount = VATETaxes.loadingErrors.length;
VATETaxes.errors.showSummary(data.total, data.imported, errorsCount, data.create_as_draft);
if (cur_list) {
cur_list.refresh();
}
});
frappe.call({
method: 'invoice_az.vat_api.import_bulk_vat_operations',
args: {
operations_data: JSON.stringify(operations),
company: company,
create_as_draft: createAsDraft,
auto_create_customers: autoCreateCustomers
},
callback: function(r) {
if (!r.message || !r.message.enqueued) {
frappe.realtime.off('vat_import_progress');
frappe.realtime.off('vat_import_complete');
frappe.hide_progress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Failed to start import job')
});
}
},
error: function() {
frappe.realtime.off('vat_import_progress');
frappe.realtime.off('vat_import_complete');
frappe.hide_progress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
} }
}; };

View File

@ -1161,7 +1161,7 @@ ETaxes.import = {
d.hide(); d.hide();
ETaxes.cancelLoading = false; ETaxes.cancelLoading = false;
ETaxes.import.loadSelectedInvoices(selectedInvoiceIds, token, warehouse); ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
}, },
secondary_action_label: __('Cancel'), secondary_action_label: __('Cancel'),
secondary_action: function() { secondary_action: function() {
@ -1429,6 +1429,84 @@ ETaxes.import = {
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex); ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
}, ETaxes.PROGRESS_UPDATE_DELAY); }, ETaxes.PROGRESS_UPDATE_DELAY);
},
// Socket.IO bulk import
loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
ETaxes.loadingErrors = [];
const total = invoiceIds.length;
frappe.show_progress(
__('Importing Invoices'), 0, total,
__('Starting import of {0} invoices...', [total])
);
frappe.realtime.off('purchase_import_progress');
frappe.realtime.on('purchase_import_progress', function(data) {
frappe.show_progress(
__('Importing Invoices'), data.current, data.total,
__('Importing invoice {0} of {1}', [data.current, data.total])
);
});
frappe.realtime.off('purchase_import_complete');
frappe.realtime.on('purchase_import_complete', function(data) {
frappe.realtime.off('purchase_import_progress');
frappe.realtime.off('purchase_import_complete');
frappe.hide_progress();
(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');
frappe.hide_progress();
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');
frappe.hide_progress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
} }
}; };

View File

@ -1180,7 +1180,7 @@ ETaxes.import = {
d.hide(); d.hide();
ETaxes.cancelLoading = false; ETaxes.cancelLoading = false;
ETaxes.import.loadSelectedInvoices(selectedInvoiceIds, token, warehouse); ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
}, },
secondary_action_label: __('Cancel'), secondary_action_label: __('Cancel'),
secondary_action: function() { secondary_action: function() {
@ -1448,6 +1448,84 @@ ETaxes.import = {
ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex); ETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
}, ETaxes.PROGRESS_UPDATE_DELAY); }, ETaxes.PROGRESS_UPDATE_DELAY);
},
// Socket.IO bulk import
loadSelectedInvoicesSocketIO: function(invoiceIds, token, warehouse) {
ETaxes.loadingErrors = [];
const total = invoiceIds.length;
frappe.show_progress(
__('Importing Invoices'), 0, total,
__('Starting import of {0} invoices...', [total])
);
frappe.realtime.off('purchase_import_progress');
frappe.realtime.on('purchase_import_progress', function(data) {
frappe.show_progress(
__('Importing Invoices'), data.current, data.total,
__('Importing invoice {0} of {1}', [data.current, data.total])
);
});
frappe.realtime.off('purchase_import_complete');
frappe.realtime.on('purchase_import_complete', function(data) {
frappe.realtime.off('purchase_import_progress');
frappe.realtime.off('purchase_import_complete');
frappe.hide_progress();
(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');
frappe.hide_progress();
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');
frappe.hide_progress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
} }
}; };

View File

@ -1161,7 +1161,7 @@ SalesETaxes.import = {
d.hide(); d.hide();
SalesETaxes.cancelLoading = false; SalesETaxes.cancelLoading = false;
SalesETaxes.import.loadSelectedInvoices(selectedInvoiceIds, token, warehouse); SalesETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse);
}, },
secondary_action_label: __('Cancel'), secondary_action_label: __('Cancel'),
secondary_action: function() { secondary_action: function() {
@ -1478,6 +1478,88 @@ SalesETaxes.import = {
SalesETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex); SalesETaxes.import.loadSelectedInvoices(invoiceIds, token, warehouse, processedCount, currentIndex);
}, SalesETaxes.PROGRESS_UPDATE_DELAY); }, 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;
frappe.show_progress(
__('Importing Sales Invoices'), 0, total,
__('Starting import of {0} invoices...', [total])
);
// Listen for per-invoice progress
frappe.realtime.off('sales_import_progress');
frappe.realtime.on('sales_import_progress', function(data) {
frappe.show_progress(
__('Importing Sales Invoices'), data.current, data.total,
__('Importing invoice {0} of {1}', [data.current, data.total])
);
});
// Listen for completion
frappe.realtime.off('sales_import_complete');
frappe.realtime.on('sales_import_complete', function(data) {
frappe.realtime.off('sales_import_progress');
frappe.realtime.off('sales_import_complete');
frappe.hide_progress();
// 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');
frappe.hide_progress();
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');
frappe.hide_progress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
} }
}; };

View File

@ -87,13 +87,6 @@ frappe.ui.form.on('Asan Login', {
// Обновляем форму после успешного получения сертификатов // Обновляем форму после успешного получения сертификатов
frm.reload_doc(); frm.reload_doc();
// Показываем сообщение об успешном получении сертификатов
frappe.msgprint({
title: __('Success'),
indicator: 'green',
message: __('Certificates retrieved successfully')
});
// Отображаем список сертификатов в диалоговом окне // Отображаем список сертификатов в диалоговом окне
if (r.message.certificates) { if (r.message.certificates) {
showCertificatesDialog(r.message.certificates, frm); showCertificatesDialog(r.message.certificates, frm);

View File

@ -2,7 +2,6 @@ import frappe
import json import json
import requests import requests
from datetime import datetime from datetime import datetime
import time
import re import re
@frappe.whitelist() @frappe.whitelist()
@ -878,3 +877,103 @@ def on_delete_sales_order(doc, method):
except Exception as e: except Exception as e:
frappe.log_error(f"Error deleting E-Taxes Sales {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}", frappe.log_error(f"Error deleting E-Taxes Sales {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
"Sales Order Delete Error") "Sales Order Delete Error")
@frappe.whitelist()
def import_bulk_sales_invoices(invoice_ids, token, warehouse=None):
"""Enqueue bulk sales import as a background job with realtime progress via Socket.IO."""
if isinstance(invoice_ids, str):
invoice_ids = json.loads(invoice_ids)
user = frappe.session.user
frappe.enqueue(
_process_bulk_sales_import,
invoice_ids=invoice_ids,
token=token,
warehouse=warehouse,
user=user,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(invoice_ids)}
def _process_bulk_sales_import(invoice_ids, token, warehouse, user):
"""Background job: fetch details and import each sales invoice with realtime progress."""
frappe.set_user(user)
record_etaxes_activity()
total = len(invoice_ids)
imported_count = 0
errors = []
for idx, invoice_id in enumerate(invoice_ids):
try:
details = get_sales_invoice_details(token, invoice_id)
except Exception:
details = None
if not details or details.get("error"):
error_msg = details.get("message", "Failed to fetch invoice details") if details else "Empty response"
if details and details.get("error") == "unauthorized":
from invoice_az.auth import get_default_asan_login
asan_login = get_default_asan_login()
if asan_login.get("found") and asan_login.get("main_token"):
token = asan_login["main_token"]
details = get_sales_invoice_details(token, invoice_id)
if not details or details.get("error"):
error_msg = details.get("message", "Failed after token refresh") if details else "Empty response"
errors.append({"invoice_id": invoice_id, "error": error_msg})
frappe.db.commit()
continue
else:
errors.append({"invoice_id": invoice_id, "error": "Authentication expired"})
frappe.db.commit()
continue
else:
errors.append({"invoice_id": invoice_id, "error": error_msg})
frappe.db.commit()
continue
schedule_date = None
created_at = details.get("creationDate") or details.get("date")
if created_at:
try:
from datetime import datetime as dt
schedule_date = dt.strptime(str(created_at)[:10], "%Y-%m-%d").strftime("%Y-%m-%d")
except Exception:
pass
result = import_sales_invoice_with_mapping(
invoice_data=details,
sales_order_name=None,
schedule_date=schedule_date,
warehouse=warehouse,
)
if result and result.get("success"):
imported_count += 1
else:
error_msg = result.get("message", "Unknown import error") if result else "Empty result"
error_entry = {"invoice_id": invoice_id, "error": error_msg}
if result:
for key in ("unmatched_items", "unmatched_parties", "unmatched_units"):
if result.get(key):
error_entry[key] = result[key]
errors.append(error_entry)
frappe.db.commit()
frappe.publish_realtime(
"sales_import_progress",
{"current": idx + 1, "total": total},
user=user,
)
frappe.publish_realtime(
"sales_import_complete",
{"total": total, "imported": imported_count, "errors": errors},
user=user,
)

View File

@ -1067,3 +1067,92 @@ def load_vat_operations_from_etaxes(date_from, date_to, max_count=50, offset=0):
'success': False, 'success': False,
'message': f'Load failed: {str(e)}' 'message': f'Load failed: {str(e)}'
} }
@frappe.whitelist()
def import_bulk_vat_operations(operations_data, company=None, create_as_draft=0, auto_create_customers=0):
"""Enqueue bulk VAT operations import as a background job with realtime progress via Socket.IO."""
if isinstance(operations_data, str):
operations_data = json.loads(operations_data)
try:
create_as_draft = int(create_as_draft)
except (ValueError, TypeError):
create_as_draft = 0
try:
auto_create_customers = int(auto_create_customers)
except (ValueError, TypeError):
auto_create_customers = 0
user = frappe.session.user
frappe.enqueue(
_process_bulk_vat_import,
operations=operations_data,
company=company,
create_as_draft=create_as_draft,
auto_create_customers=auto_create_customers,
user=user,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(operations_data)}
def _process_bulk_vat_import(operations, company, create_as_draft, auto_create_customers, user):
"""Background job: import each VAT operation with realtime progress."""
frappe.set_user(user)
record_etaxes_activity()
total = len(operations)
imported_count = 0
errors = []
for idx, operation in enumerate(operations):
try:
result = import_vat_operations(
operations_data=json.dumps([operation]),
company=company,
create_as_draft=create_as_draft,
auto_create_customers=auto_create_customers,
)
if result and result.get("success"):
if result.get("imported", 0) > 0:
imported_count += 1
elif result.get("failed", 0) > 0 and result.get("errors"):
err = result["errors"][0]
errors.append({
"operation_id": operation.get("id", "Unknown"),
"error": err.get("message", "Unknown error"),
"error_type": err.get("error_type", "Import Error"),
"tin": err.get("tin"),
"customer_name": err.get("customer_name"),
})
else:
error_msg = result.get("message", "Unknown error") if result else "Empty result"
errors.append({"operation_id": operation.get("id", "Unknown"), "error": error_msg})
except Exception as e:
errors.append({"operation_id": operation.get("id", "Unknown"), "error": str(e)})
frappe.db.commit()
frappe.publish_realtime(
"vat_import_progress",
{"current": idx + 1, "total": total},
user=user,
)
frappe.publish_realtime(
"vat_import_complete",
{
"total": total,
"imported": imported_count,
"errors": errors,
"create_as_draft": create_as_draft,
},
user=user,
)