diff --git a/invoice_az/amas_api.py b/invoice_az/amas_api.py index 84c78b8..c554bbd 100644 --- a/invoice_az/amas_api.py +++ b/invoice_az/amas_api.py @@ -2610,3 +2610,82 @@ def on_delete_employee(doc, method): ) for rec in amas_records: 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, + ) diff --git a/invoice_az/api.py b/invoice_az/api.py index 682fccf..c3af384 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -4307,3 +4307,109 @@ def refresh_token_from_asan_login(): 'success': False, '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, + ) diff --git a/invoice_az/client/employee.js b/invoice_az/client/employee.js index 37cc260..51ce6b7 100644 --- a/invoice_az/client/employee.js +++ b/invoice_az/client/employee.js @@ -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) { - // Initialize counters - window.amas_employees_to_process = selected_employees.length; - window.amas_cancel_loading = false; + const total = selected_employees.length; - // Start processing - process_employees_one_by_one( - listview, - asan_login_name, - selected_employees, - company, - create_designation, - 0, // created_count - 0, // updated_count - 0, // error_count - [], // errors - 0 // current_index + frappe.show_progress( + __('Loading from ƏMAS'), 0, total, + __('Starting import of {0} employees...', [total]) ); + + 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) diff --git a/invoice_az/client/journal_entry.js b/invoice_az/client/journal_entry.js index 1808515..6a41c68 100644 --- a/invoice_az/client/journal_entry.js +++ b/invoice_az/client/journal_entry.js @@ -1232,7 +1232,7 @@ VATETaxes.import = { d.hide(); 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: function() { @@ -1398,6 +1398,81 @@ VATETaxes.import = { VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers); }, 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') + }); + } + }); } }; diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js index b06e167..7be19b4 100644 --- a/invoice_az/client/purchase_order.js +++ b/invoice_az/client/purchase_order.js @@ -1161,7 +1161,7 @@ ETaxes.import = { d.hide(); ETaxes.cancelLoading = false; - ETaxes.import.loadSelectedInvoices(selectedInvoiceIds, token, warehouse); + ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse); }, secondary_action_label: __('Cancel'), secondary_action: function() { @@ -1426,9 +1426,87 @@ ETaxes.import = { if (invoiceIds.length === 0) { frappe.hide_progress(); } - + 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; + + 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') + }); + } + }); } }; diff --git a/invoice_az/client/sales_invoice.js b/invoice_az/client/sales_invoice.js index 26ad049..2a97a73 100644 --- a/invoice_az/client/sales_invoice.js +++ b/invoice_az/client/sales_invoice.js @@ -1180,7 +1180,7 @@ ETaxes.import = { d.hide(); ETaxes.cancelLoading = false; - ETaxes.import.loadSelectedInvoices(selectedInvoiceIds, token, warehouse); + ETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse); }, secondary_action_label: __('Cancel'), secondary_action: function() { @@ -1445,9 +1445,87 @@ ETaxes.import = { if (invoiceIds.length === 0) { frappe.hide_progress(); } - + 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; + + 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') + }); + } + }); } }; diff --git a/invoice_az/client/sales_order.js b/invoice_az/client/sales_order.js index 4538790..3981ed6 100644 --- a/invoice_az/client/sales_order.js +++ b/invoice_az/client/sales_order.js @@ -1161,7 +1161,7 @@ SalesETaxes.import = { d.hide(); SalesETaxes.cancelLoading = false; - SalesETaxes.import.loadSelectedInvoices(selectedInvoiceIds, token, warehouse); + SalesETaxes.import.loadSelectedInvoicesSocketIO(selectedInvoiceIds, token, warehouse); }, secondary_action_label: __('Cancel'), secondary_action: function() { @@ -1475,9 +1475,91 @@ SalesETaxes.import = { if (invoiceIds.length === 0) { frappe.hide_progress(); } - + 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; + + 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') + }); + } + }); } }; diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.js b/invoice_az/invoice_az/doctype/asan_login/asan_login.js index afc1541..4ca194a 100644 --- a/invoice_az/invoice_az/doctype/asan_login/asan_login.js +++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.js @@ -87,13 +87,6 @@ frappe.ui.form.on('Asan Login', { // Обновляем форму после успешного получения сертификатов frm.reload_doc(); - // Показываем сообщение об успешном получении сертификатов - frappe.msgprint({ - title: __('Success'), - indicator: 'green', - message: __('Certificates retrieved successfully') - }); - // Отображаем список сертификатов в диалоговом окне if (r.message.certificates) { showCertificatesDialog(r.message.certificates, frm); diff --git a/invoice_az/sales_api.py b/invoice_az/sales_api.py index 343909b..fec448a 100644 --- a/invoice_az/sales_api.py +++ b/invoice_az/sales_api.py @@ -2,7 +2,6 @@ import frappe import json import requests from datetime import datetime -import time import re @frappe.whitelist() @@ -876,5 +875,105 @@ def on_delete_sales_order(doc, method): frappe.db.commit() frappe.logger().info(f"Successfully deleted E-Taxes Sales {doc.taxes_doc}") except Exception as e: - frappe.log_error(f"Error deleting E-Taxes Sales {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}", - "Sales Order Delete Error") \ No newline at end of file + frappe.log_error(f"Error deleting E-Taxes Sales {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}", + "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, + ) \ No newline at end of file diff --git a/invoice_az/vat_api.py b/invoice_az/vat_api.py index e0c45e1..0796978 100644 --- a/invoice_az/vat_api.py +++ b/invoice_az/vat_api.py @@ -1067,3 +1067,92 @@ def load_vat_operations_from_etaxes(date_from, date_to, max_count=50, offset=0): 'success': False, '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, + )