From 6df36695db166fa863bb15775ba10c37240318a0 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Mon, 8 Sep 2025 21:52:04 +0400 Subject: [PATCH] fixed invoices date --- invoice_az/api.py | 581 ++++++++++++++---- invoice_az/auth.py | 28 +- .../e_taxes_settings/e_taxes_settings.js | 337 ++++++++-- invoice_az/sales_api.py | 211 ++++++- 4 files changed, 907 insertions(+), 250 deletions(-) diff --git a/invoice_az/api.py b/invoice_az/api.py index a2b8870..368cdcc 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -55,6 +55,7 @@ def get_invoices(token, filters=None): if key in payload and value: # Update only existing keys and non-empty values payload[key] = value except Exception as e: + frappe.log_error(f"[GET_INVOICES] Error parsing filters: {str(e)}", "Filter Parse Error") return headers = { @@ -68,6 +69,7 @@ def get_invoices(token, filters=None): response = requests.post(url, data=json.dumps(payload), headers=headers) if response.status_code == 500: + frappe.log_error(f"[GET_INVOICES] Server error 500", "E-Taxes Server Error") return { "error": "server_error", "message": "Service temporarily unavailable. Please try again in a few minutes.", @@ -76,23 +78,47 @@ def get_invoices(token, filters=None): # Check response status to handle 401 error if response.status_code == 401: - return { - "error": "unauthorized", - "message": "Authentication required. Please login again." - } + frappe.log_error(f"[GET_INVOICES] Unauthorized 401 - token expired", "E-Taxes Auth Error") + + # Получаем свежий токен из Asan Login + asan_login_settings = get_default_asan_login() + if asan_login_settings.get('found'): + fresh_token = asan_login_settings['main_token'] + headers["x-authorization"] = f"Bearer {fresh_token}" + + frappe.log_error(f"[GET_INVOICES] Retrying with fresh token", "E-Taxes Auth Retry") + + # Повторяем запрос с новым токеном + response = requests.post(url, data=json.dumps(payload), headers=headers) + + if response.status_code == 401: + frappe.log_error(f"[GET_INVOICES] Still unauthorized after token refresh", "E-Taxes Auth Failed") + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + else: + frappe.log_error(f"[GET_INVOICES] No Asan Login settings found for token refresh", "E-Taxes Auth Error") + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: + frappe.log_error(f"[GET_INVOICES] HTTP error: {e.response.status_code} - {str(e)}", "E-Taxes HTTP Error") # Check if the error is 401 Unauthorized if e.response.status_code == 401: + frappe.log_error(f"[GET_INVOICES] HTTPError 401 Unauthorized", "E-Taxes Auth Error") return { "error": "unauthorized", "message": "Authentication required. Please login again." } elif e.response.status_code == 500: + frappe.log_error(f"[GET_INVOICES] HTTPError 500 Server Error", "E-Taxes Server Error") return { "error": "server_error", "message": "Service temporarily unavailable. Please try again in a few minutes.", @@ -102,6 +128,7 @@ def get_invoices(token, filters=None): frappe.log_error(f"HTTP error in get_invoices: {str(e)}", "API Error") return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: + frappe.log_error(f"[GET_INVOICES] Unexpected error: {str(e)}\n{frappe.get_traceback()}", "E-Taxes Error") frappe.log_error(f"Error getting invoices: {str(e)}", "API Error") return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @@ -124,6 +151,7 @@ def get_invoice_details(token, invoice_id): response = requests.get(url, headers=headers) if response.status_code == 500: + frappe.log_error(f"[GET_INVOICE_DETAILS] Server error 500 for invoice {invoice_id}", "E-Taxes Server Error") return { "error": "server_error", "message": "Service temporarily unavailable. Please try again in a few minutes.", @@ -132,23 +160,47 @@ def get_invoice_details(token, invoice_id): # Check for 401 error if response.status_code == 401: - return { - "error": "unauthorized", - "message": "Authentication required. Please login again." - } + frappe.log_error(f"[GET_INVOICE_DETAILS] Unauthorized 401 for invoice {invoice_id}", "E-Taxes Auth Error") + + # Получаем свежий токен из Asan Login + asan_login_settings = get_default_asan_login() + if asan_login_settings.get('found'): + fresh_token = asan_login_settings['main_token'] + headers["x-authorization"] = f"Bearer {fresh_token}" + + frappe.log_error(f"[GET_INVOICE_DETAILS] Retrying with fresh token for invoice {invoice_id}", "E-Taxes Auth Retry") + + # Повторяем запрос с новым токеном + response = requests.get(url, headers=headers) + + if response.status_code == 401: + frappe.log_error(f"[GET_INVOICE_DETAILS] Still unauthorized after token refresh for invoice {invoice_id}", "E-Taxes Auth Failed") + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + else: + frappe.log_error(f"[GET_INVOICE_DETAILS] No Asan Login settings found for token refresh", "E-Taxes Auth Error") + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: + frappe.log_error(f"[GET_INVOICE_DETAILS] HTTP error for invoice {invoice_id}: {e.response.status_code} - {str(e)}", "E-Taxes HTTP Error") # Check if the error is 401 Unauthorized if e.response.status_code == 401: + frappe.log_error(f"[GET_INVOICE_DETAILS] HTTPError 401 Unauthorized for invoice {invoice_id}", "E-Taxes Auth Error") return { "error": "unauthorized", "message": "Authentication required. Please login again." } elif e.response.status_code == 500: + frappe.log_error(f"[GET_INVOICE_DETAILS] HTTPError 500 Server Error for invoice {invoice_id}", "E-Taxes Server Error") return { "error": "server_error", "message": "Service temporarily unavailable. Please try again in a few minutes.", @@ -158,9 +210,10 @@ def get_invoice_details(token, invoice_id): frappe.log_error(f"HTTP error in get_invoice_details: {str(e)}", "API Error") return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: + frappe.log_error(f"[GET_INVOICE_DETAILS] Unexpected error for invoice {invoice_id}: {str(e)}\n{frappe.get_traceback()}", "E-Taxes Error") frappe.log_error(f"Error getting invoice details: {str(e)}", "API Error") return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} - + @frappe.whitelist() def get_uom_for_unit(unit_name): """Converts unit name from invoice to system UOM with support for mappings""" @@ -814,18 +867,50 @@ def create_purchase_invoice_from_order(purchase_order_name): # Импортируем стандартную функцию из модуля Purchase Order from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice + # Получаем Purchase Order ДО создания invoice + po_doc = frappe.get_doc('Purchase Order', purchase_order_name) + + # ИСПРАВЛЕНО: Правильная обработка дат + if po_doc.transaction_date: + posting_date = po_doc.transaction_date + # Конвертируем в datetime.date если это строка + if isinstance(posting_date, str): + posting_date = frappe.utils.getdate(posting_date) + else: + posting_date = frappe.utils.getdate(frappe.utils.nowdate()) + + frappe.log_error(f"[DATE DEBUG] PO transaction_date: {po_doc.transaction_date}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] PI posting_date to set: {posting_date}", "Date Debug") + # Создаем Purchase Invoice из Purchase Order pi_doc = make_purchase_invoice(purchase_order_name) - # Устанавливаем даты - pi_doc.posting_date = frappe.utils.nowdate() - pi_doc.due_date = frappe.utils.add_days(frappe.utils.nowdate(), 30) # 30 дней для оплаты + # ДОБАВЛЕНО: Логируем дату сразу после создания + frappe.log_error(f"[DATE DEBUG] PI posting_date after make_purchase_invoice: {pi_doc.posting_date}", "Date Debug") + + # ИСПРАВЛЕНО: Устанавливаем дату и флаг posting_time ДО других операций + pi_doc.posting_date = posting_date + pi_doc.set_posting_time = 1 # Важно! Разрешаем установку даты в прошлом + + # ИСПРАВЛЕНО: Убеждаемся что due_date всегда больше posting_date + due_date = frappe.utils.add_days(posting_date, 30) + + # Дополнительная проверка: если due_date в прошлом, используем текущую дату + 30 дней + today = frappe.utils.getdate(frappe.utils.nowdate()) + if due_date < today: + due_date = frappe.utils.add_days(today, 30) + frappe.log_error(f"[DATE DEBUG] Due date was in past, adjusted to: {due_date}", "Date Debug") + + pi_doc.due_date = due_date + + frappe.log_error(f"[DATE DEBUG] PI posting_date before insert: {pi_doc.posting_date}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] PI set_posting_time: {pi_doc.set_posting_time}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] PI due_date before insert: {pi_doc.due_date}", "Date Debug") # ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Purchase Invoice pi_doc.is_taxes_doc = 1 # ДОБАВЛЕНО: Копируем налоги из Purchase Order - po_doc = frappe.get_doc('Purchase Order', purchase_order_name) if po_doc.taxes: pi_doc.taxes = [] for tax_row in po_doc.taxes: @@ -841,6 +926,25 @@ def create_purchase_invoice_from_order(purchase_order_name): # Сохраняем документ pi_doc.insert(ignore_permissions=True) + # ДОБАВЛЕНО: Логируем дату после insert + frappe.log_error(f"[DATE DEBUG] PI posting_date after insert: {pi_doc.posting_date}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] PI due_date after insert: {pi_doc.due_date}", "Date Debug") + + # ДОБАВЛЕНО: Если дата все еще неправильная, принудительно устанавливаем + if pi_doc.posting_date != posting_date: + frappe.log_error(f"[DATE DEBUG] Date mismatch detected! Expected: {posting_date}, Got: {pi_doc.posting_date}", "Date Debug") + + # Принудительно устанавливаем дату через DB + frappe.db.set_value('Purchase Invoice', pi_doc.name, { + 'posting_date': posting_date, + 'set_posting_time': 1 + }) + frappe.db.commit() + + # Перезагружаем документ + pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name) + frappe.log_error(f"[DATE DEBUG] PI posting_date after DB update: {pi_doc.posting_date}", "Date Debug") + frappe.log_error(f"Purchase Invoice {pi_doc.name} created from Purchase Order {purchase_order_name}", "Purchase Invoice Creation") # Возвращаем имя созданного Purchase Invoice @@ -849,7 +953,7 @@ def create_purchase_invoice_from_order(purchase_order_name): except Exception as e: frappe.log_error(f"Error creating Purchase Invoice from PO {purchase_order_name}: {str(e)}\n{frappe.get_traceback()}", "Purchase Invoice Creation Error") return None - + @frappe.whitelist() def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None): """Import invoice taking into account NEW supplier mappings""" @@ -861,6 +965,11 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule if isinstance(invoice_data, str): invoice_data = json.loads(invoice_data) + # ДОБАВЛЕНО: Логируем входящие данные инвойса для отладки дат + frappe.log_error(f"[DATE DEBUG] Invoice data keys: {list(invoice_data.keys())}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] createdAt field: {invoice_data.get('createdAt')}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] createdAt type: {type(invoice_data.get('createdAt'))}", "Date Debug") + # Get active settings settings = get_active_settings() if not settings: @@ -909,6 +1018,57 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'message': 'Default warehouse not found. Please specify warehouse in E-Taxes settings.' } + # ИСПРАВЛЕНО: Определяем дату ДО создания Purchase Order + invoice_date = None + created_at_raw = invoice_data.get("createdAt") + + frappe.log_error(f"[DATE DEBUG] Raw createdAt value: '{created_at_raw}'", "Date Debug") + + if created_at_raw: + try: + # Попробуем разные форматы даты + if isinstance(created_at_raw, str): + # Если это строка, попробуем разные форматы + frappe.log_error(f"[DATE DEBUG] Trying to parse string date: '{created_at_raw}'", "Date Debug") + + # Попробуем стандартный парсинг ERPNext + invoice_date = frappe.utils.getdate(created_at_raw) + frappe.log_error(f"[DATE DEBUG] Successfully parsed date: {invoice_date}", "Date Debug") + + elif hasattr(created_at_raw, 'date'): + # Если это datetime объект + invoice_date = created_at_raw.date() + frappe.log_error(f"[DATE DEBUG] Extracted date from datetime: {invoice_date}", "Date Debug") + else: + # Попробуем привести к строке и парсить + date_str = str(created_at_raw) + frappe.log_error(f"[DATE DEBUG] Converting to string and parsing: '{date_str}'", "Date Debug") + invoice_date = frappe.utils.getdate(date_str) + frappe.log_error(f"[DATE DEBUG] Successfully parsed converted date: {invoice_date}", "Date Debug") + + except Exception as e: + frappe.log_error(f"[DATE DEBUG] Error parsing createdAt '{created_at_raw}': {str(e)}", "Date Debug") + invoice_date = frappe.utils.today() + frappe.log_error(f"[DATE DEBUG] Using fallback date: {invoice_date}", "Date Debug") + else: + invoice_date = frappe.utils.today() + frappe.log_error(f"[DATE DEBUG] No createdAt found, using today: {invoice_date}", "Date Debug") + + # ИСПРАВЛЕНО: Определяем date_to_use ДО создания Purchase Order + # ПРИОРИТЕТ: 1) дата из E-taxes, 2) schedule_date, 3) сегодня + date_to_use = None + if invoice_date: + date_to_use = invoice_date + frappe.log_error(f"[DATE DEBUG] Using invoice_date from E-taxes for date_to_use: {date_to_use}", "Date Debug") + elif schedule_date: + date_to_use = frappe.utils.getdate(schedule_date) + frappe.log_error(f"[DATE DEBUG] No E-taxes date, using provided schedule_date: {date_to_use}", "Date Debug") + else: + date_to_use = frappe.utils.today() + frappe.log_error(f"[DATE DEBUG] No dates available, using today for date_to_use: {date_to_use}", "Date Debug") + + frappe.log_error(f"[DATE DEBUG] Final date_to_use: {date_to_use}", "Date Debug") + # Get or create Purchase Order if purchase_order_name: po = frappe.get_doc('Purchase Order', purchase_order_name) @@ -935,15 +1095,13 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'message': f'No mapping found for supplier: {sender.get("name", "")}' } - # Set dates - po.transaction_date = frappe.utils.today() + frappe.log_error(f"[DATE DEBUG] Final invoice_date for PO: {invoice_date}", "Date Debug") - if schedule_date: - po.schedule_date = schedule_date - elif invoice_data.get("creationDate"): - po.schedule_date = frappe.utils.getdate(invoice_data.get("creationDate")) - else: - po.schedule_date = frappe.utils.today() + po.transaction_date = invoice_date + po.schedule_date = date_to_use + + frappe.log_error(f"[DATE DEBUG] PO transaction_date set to: {po.transaction_date}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] PO schedule_date set to: {po.schedule_date}", "Date Debug") po.company = frappe.defaults.get_user_default('Company') @@ -955,14 +1113,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule if purchase_order_name and invoice_data.get("items"): po.items = [] - date_to_use = None - if schedule_date: - date_to_use = schedule_date - elif invoice_data.get("creationDate"): - date_to_use = frappe.utils.getdate(invoice_data.get("creationDate")) - else: - date_to_use = frappe.utils.today() - # Variable for tracking added items added_items_count = 0 unmatched_items = [] @@ -1050,9 +1200,11 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule po_item.rate = item.get("pricePerUnit", 0) po_item.amount = item.get("cost", 0) po_item.uom = mapped_uom - po_item.schedule_date = date_to_use + po_item.schedule_date = date_to_use # ИСПРАВЛЕНО: Используем правильную дату po_item.warehouse = default_warehouse + frappe.log_error(f"[DATE DEBUG] Item {po_item.item_code} schedule_date set to: {po_item.schedule_date}", "Date Debug") + po.append("items", po_item) added_items_count += 1 @@ -1114,19 +1266,32 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule except Exception as e: frappe.log_error(f"Error adding ƏDV tax: {str(e)}", "Purchase Order Tax Error") + # ДОБАВЛЕНО: Логируем финальные даты перед сохранением + frappe.log_error(f"[DATE DEBUG] Final PO transaction_date: {po.transaction_date}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] Final PO schedule_date: {po.schedule_date}", "Date Debug") + # Save document po.save() - # Additional check that all schedule_date and warehouse are set + # ИСПРАВЛЕНО: Убедимся что все schedule_date установлены правильно for item in po.items: if not item.schedule_date: item.schedule_date = date_to_use + frappe.log_error(f"[DATE DEBUG] Fixed missing schedule_date for item {item.item_code}: {item.schedule_date}", "Date Debug") if not item.warehouse: item.warehouse = default_warehouse # Save again to ensure changes are applied po.save() + # ДОБАВЛЕНО: Логируем даты после сохранения + frappe.log_error(f"[DATE DEBUG] After save PO transaction_date: {po.transaction_date}", "Date Debug") + frappe.log_error(f"[DATE DEBUG] After save PO schedule_date: {po.schedule_date}", "Date Debug") + + # Логируем schedule_date каждого item после сохранения + for item in po.items: + frappe.log_error(f"[DATE DEBUG] After save item {item.item_code} schedule_date: {item.schedule_date}", "Date Debug") + # ИЗМЕНЕНИЕ: Создаем E-Taxes Purchase и устанавливаем связь ДО submit'а invoice_id = invoice_data.get('id', '') serial_number = invoice_data.get('serialNumber', '') @@ -1188,7 +1353,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'success': False, 'message': "An unknown error occurred, please try again in a few minutes." } - + @frappe.whitelist() def get_etaxes_purchases(): """Gets list of all E-Taxes Purchase records""" @@ -3576,107 +3741,7 @@ def get_reference_data_lists(data_type, limit=100, offset=0): except Exception as e: frappe.log_error(f"Error getting reference data list for {data_type}: {str(e)}\n{frappe.get_traceback()}", "Reference Data List Error") return {'success': False, 'message': str(e)} -@frappe.whitelist() -def load_reference_data_from_invoices(date_from, date_to, max_count=200, offset=0): - """Загружает только список инвойсов, без обработки""" - record_etaxes_activity() - - try: - # Получаем настройки токена - asan_login_settings = get_default_asan_login() - if not asan_login_settings.get('found'): - return { - 'success': False, - 'message': 'No Asan Login settings found' - } - - token = asan_login_settings['main_token'] - - # Преобразуем параметры - try: - max_count = int(max_count) - except (ValueError, TypeError): - max_count = 200 - - try: - offset = int(offset) - except (ValueError, TypeError): - offset = 0 - - # Настройки фильтров - filters = { - "actionOwner": None, - "amountFrom": None, - "amountTo": None, - "creationDateFrom": date_from, - "creationDateTo": date_to, - "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", - "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", - "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], - "maxCount": max_count, - "offset": offset, - "productCode": None, - "productName": None, - "receiverName": None, - "receiverTin": None, - "senderName": None, - "senderTin": None, - "serialNumber": None, - "sortAsc": True, - "sortBy": "creationDate", - "statuses": ["approved", "approvedBySystem"], - "types": ["current", "corrected"] - } - - # Получаем список инвойсов из inbox - response_inbox = get_invoices(token, json.dumps(filters)) - - if 'error' in response_inbox: - return { - 'success': False, - 'error': response_inbox.get('error'), - 'message': response_inbox.get('message', 'Failed to retrieve invoices') - } - - invoices_inbox = response_inbox.get('data', []) or response_inbox.get('invoices', []) - - for invoice in invoices_inbox: - invoice['_source'] = 'inbox' - - all_invoices = invoices_inbox - - # Пытаемся получить инвойсы из outbox - try: - from .sales_api import get_sales_invoices - response_outbox = get_sales_invoices(token, json.dumps(filters)) - - if 'error' not in response_outbox: - invoices_outbox = response_outbox.get('data', []) or response_outbox.get('invoices', []) - - for invoice in invoices_outbox: - invoice['_source'] = 'outbox' - - all_invoices.extend(invoices_outbox) - - except (ImportError, Exception) as e: - frappe.log_error(f"Could not load outbox invoices: {str(e)}", "Load Reference Data Warning") - - return { - 'success': True, - 'invoices': all_invoices, - 'token': token, - 'total': len(all_invoices), - 'inbox_count': len(invoices_inbox), - 'outbox_count': len(all_invoices) - len(invoices_inbox) - } - - except Exception as e: - frappe.log_error(f"Error in load_reference_data_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Reference Data Error") - return { - 'success': False, - 'message': "An unknown error occurred, please try again in a few minutes." - } - + @frappe.whitelist() def process_single_invoice_for_reference_data(token, invoice_id, source_type='purchase'): """Обрабатывает один инвойс и создает все 4 типа доктайпов""" @@ -3686,6 +3751,32 @@ def process_single_invoice_for_reference_data(token, invoice_id, source_type='pu # Получаем детали инвойса invoice_details = get_invoice_details(token, invoice_id) + # Проверяем на ошибку unauthorized и пробуем обновить токен + if isinstance(invoice_details, dict) and invoice_details.get('error') == 'unauthorized': + # Пробуем получить новый токен из Asan Login + token_result = refresh_token_from_asan_login() + if token_result.get('success'): + new_token = token_result.get('token') + # Повторяем запрос с новым токеном + invoice_details = get_invoice_details(new_token, invoice_id) + + # Если все еще ошибка - возвращаем с обновленным токеном для клиента + if isinstance(invoice_details, dict) and 'error' in invoice_details: + return { + 'success': False, + 'error': invoice_details.get('error'), + 'message': invoice_details.get('message', 'Failed to get invoice details'), + 'new_token': new_token # Передаем новый токен клиенту + } + else: + # Не удалось обновить токен + return { + 'success': False, + 'error': 'unauthorized', + 'message': 'Authentication required. Please login again.' + } + + # Если другая ошибка (не unauthorized) if isinstance(invoice_details, dict) and 'error' in invoice_details: return { 'success': False, @@ -3704,10 +3795,10 @@ def process_single_invoice_for_reference_data(token, invoice_id, source_type='pu 'success': False, 'message': "An unknown error occurred processing this invoice" } - + @frappe.whitelist() def create_reference_data_from_single_invoice(invoice_details, source_type): - """Создает все 4 типа доктайпов из одного инвойса""" + """Создает все 4 типа доктайпов из одного инвойса - оптимизированная версия""" try: stats = { 'items_created': 0, @@ -3927,4 +4018,222 @@ def create_reference_data_from_single_invoice(invoice_details, source_type): 'success': False, 'message': str(e) } + +# ===== НОВЫЕ PYTHON ФУНКЦИИ ===== +@frappe.whitelist() +def load_single_invoice_batch(date_from, date_to, offset=0, max_count=200): + """Загружает одну порцию инвойсов из inbox""" + record_etaxes_activity() + + try: + # Получаем настройки токена + asan_login_settings = get_default_asan_login() + if not asan_login_settings.get('found'): + return { + 'success': False, + 'message': 'No Asan Login settings found' + } + + token = asan_login_settings['main_token'] + + # Преобразуем параметры + try: + max_count = int(max_count) + if max_count != 200: # Принудительно устанавливаем 200 + max_count = 200 + except (ValueError, TypeError): + max_count = 200 + + try: + offset = int(offset) + except (ValueError, TypeError): + offset = 0 + + # Настройки фильтров + filters = { + "actionOwner": None, + "amountFrom": None, + "amountTo": None, + "creationDateFrom": date_from, + "creationDateTo": date_to, + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "maxCount": max_count, + "offset": offset, + "productCode": None, + "productName": None, + "receiverName": None, + "receiverTin": None, + "senderName": None, + "senderTin": None, + "serialNumber": None, + "sortAsc": True, + "sortBy": "creationDate", + "statuses": ["approved", "approvedBySystem"], + "types": ["current", "corrected"] + } + + # Получаем порцию инвойсов из inbox + response = get_invoices(token, json.dumps(filters)) + + if 'error' in response: + return { + 'success': False, + 'error': response.get('error'), + 'message': response.get('message', 'Failed to retrieve invoices') + } + + invoices = response.get('data', []) or response.get('invoices', []) + hasMore = response.get('hasMore', False) + total = response.get('total', 0) + + # Дополнительная проверка hasMore + if not hasMore and total > 0: + hasMore = (offset + max_count) < total + + return { + 'success': True, + 'invoices': invoices, + 'hasMore': hasMore, + 'token': token, + 'offset': offset, + 'total': total, + 'loaded': len(invoices) + } + + except Exception as e: + frappe.log_error(f"Error in load_single_invoice_batch: {str(e)}\n{frappe.get_traceback()}", "Load Invoice Batch Error") + return { + 'success': False, + 'message': "An unknown error occurred, please try again in a few minutes." + } + +@frappe.whitelist() +def load_single_sales_invoice_batch(date_from, date_to, offset=0, max_count=200): + """Загружает одну порцию инвойсов из outbox (sales)""" + record_etaxes_activity() + + try: + # Получаем настройки токена + asan_login_settings = get_default_asan_login() + if not asan_login_settings.get('found'): + return { + 'success': False, + 'message': 'No Asan Login settings found' + } + + token = asan_login_settings['main_token'] + + # Преобразуем параметры + try: + max_count = int(max_count) + if max_count != 200: # Принудительно устанавливаем 200 + max_count = 200 + except (ValueError, TypeError): + max_count = 200 + + try: + offset = int(offset) + except (ValueError, TypeError): + offset = 0 + + # Настройки фильтров + filters = { + "actionOwner": None, + "amountFrom": None, + "amountTo": None, + "creationDateFrom": date_from, + "creationDateTo": date_to, + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "maxCount": max_count, + "offset": offset, + "productCode": None, + "productName": None, + "receiverName": None, + "receiverTin": None, + "senderName": None, + "senderTin": None, + "serialNumber": None, + "sortAsc": True, + "sortBy": "creationDate", + "statuses": ["approved", "approvedBySystem"], + "types": ["current", "corrected"] + } + + # Получаем порцию инвойсов из outbox + try: + from .sales_api import get_sales_invoices + response = get_sales_invoices(token, json.dumps(filters)) + + if 'error' in response: + return { + 'success': False, + 'error': response.get('error'), + 'message': response.get('message', 'Failed to retrieve sales invoices') + } + + invoices = response.get('data', []) or response.get('invoices', []) + hasMore = response.get('hasMore', False) + total = response.get('total', 0) + + # Дополнительная проверка hasMore + if not hasMore and total > 0: + hasMore = (offset + max_count) < total + + return { + 'success': True, + 'invoices': invoices, + 'hasMore': hasMore, + 'token': token, + 'offset': offset, + 'total': total, + 'loaded': len(invoices) + } + + except ImportError: + # Sales API недоступен + return { + 'success': True, + 'invoices': [], + 'hasMore': False, + 'token': token, + 'offset': offset, + 'total': 0, + 'loaded': 0 + } + + except Exception as e: + frappe.log_error(f"Error in load_single_sales_invoice_batch: {str(e)}\n{frappe.get_traceback()}", "Load Sales Invoice Batch Error") + return { + 'success': False, + 'message': "An unknown error occurred, please try again in a few minutes." + } + +@frappe.whitelist() +def refresh_token_from_asan_login(): + """Обновляет токен из Asan Login""" + try: + asan_login = frappe.get_all('Asan Login', + filters={'is_default': 1}, + fields=['name', 'main_token'], + limit=1) + + if asan_login and asan_login[0].main_token: + return { + 'success': True, + 'token': asan_login[0].main_token + } + else: + return { + 'success': False, + 'message': 'No valid token found in Asan Login' + } + except Exception as e: + return { + 'success': False, + 'message': str(e) + } \ No newline at end of file diff --git a/invoice_az/auth.py b/invoice_az/auth.py index 95caffd..f6ce518 100644 --- a/invoice_az/auth.py +++ b/invoice_az/auth.py @@ -21,11 +21,11 @@ DEFAULT_HEADERS = { "Pragma": "no-cache", "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan" } -ACTIVITY_TIMEOUT_MINUTES = 10 # Настраиваемый параметр времени неактивности +ACTIVITY_TIMEOUT_MINUTES = 10000000000000000 # Настраиваемый параметр времени неактивности @frappe.whitelist() def record_etaxes_activity(asan_login_name=None): - """Записывает время последней активности с e-taxes с задержкой 2 секунды""" + """Записывает время последней активности с e-taxes напрямую""" try: # Если имя не указано, получаем дефолтный профиль if not asan_login_name: @@ -41,29 +41,23 @@ def record_etaxes_activity(asan_login_name=None): asan_login_name = default_settings[0].name - # Запускаем отложенную задачу для обновления поля last_activity_time - frappe.enqueue( - 'invoice_az.auth.update_activity_time', - asan_login_name=asan_login_name, - queue='short', - timeout=300 - ) + # Обновляем поле last_activity_time напрямую через БД + frappe.db.set_value("Asan Login", asan_login_name, "last_activity_time", now_datetime(), update_modified=False) + frappe.db.commit() + except Exception as e: frappe.log_error(f"Error recording activity: {str(e)}", "Activity Recording Error") @frappe.whitelist() def update_activity_time(asan_login_name): - """Выполняет фактическое обновление поля last_activity_time с задержкой""" + """Выполняет фактическое обновление поля last_activity_time - упрощенная версия""" try: - # Добавляем задержку для избежания конфликтов - time.sleep(2) - # Обновляем поле last_activity_time напрямую через БД frappe.db.set_value("Asan Login", asan_login_name, "last_activity_time", now_datetime(), update_modified=False) frappe.db.commit() except Exception as e: frappe.log_error(f"Error updating activity time: {str(e)}", "Activity Update Error") - + def check_recent_activity(asan_login_name=None): """Проверяет, была ли активность пользователя за последние N минут""" try: @@ -98,7 +92,7 @@ def check_recent_activity(asan_login_name=None): return False # В случае ошибки считаем, что активности не было @frappe.whitelist() -def renew_token(asan_login_name=None, retry_count=0, force=False): +def renew_token(asan_login_name=None, retry_count=0): """Renews the main token with optimized performance and error handling""" try: # Получаем документ Asan Login @@ -117,7 +111,7 @@ def renew_token(asan_login_name=None, retry_count=0, force=False): asan_login_name = default_settings[0].name - # Проверяем наличие активности, если force=False + # Проверяем наличие активности if not check_recent_activity(asan_login_name): frappe.logger().info(f"Token renewal skipped due to inactivity for {asan_login_name}") return {"success": True, "message": "Token renewal skipped due to inactivity"} @@ -236,7 +230,7 @@ def renew_token(asan_login_name=None, retry_count=0, force=False): f"Token Renewal for {asan_login_name}" ) return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."} - + @frappe.whitelist() def setup_token_renewal(): """Настраивает задание планировщика для обновления токенов""" diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js index 3331f27..608f4f0 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js @@ -961,8 +961,14 @@ function show_load_dialog_with_summary(frm, summary) { if (values.load_suppliers) selected_types.push(__('Suppliers')); if (values.load_units) selected_types.push(__('Units')); + // УЛУЧШЕНО: Более подробное сообщение о процессе frappe.confirm( - __('This will load {0} from E-Taxes for the period {1} to {2}.

This operation may take several minutes depending on the number of invoices.

Continue?', + __('This will load {0} from E-Taxes for the period {1} to {2}.

' + + 'Process:
' + + '1. First, all invoices will be fetched (may take 1-2 minutes)
' + + '2. Then each invoice will be processed for reference data

' + + 'This operation may take several minutes depending on the number of invoices.

' + + 'Continue?', [selected_types.join(', '), values.date_from, values.date_to]), function() { d.hide(); @@ -976,50 +982,8 @@ function show_load_dialog_with_summary(frm, summary) { } function start_reference_data_loading(frm, values) { - // frappe.show_progress(__('Loading'), 0, 100, __('Loading invoices...')); - - frappe.call({ - method: 'invoice_az.api.load_reference_data_from_invoices', - args: { - date_from: moment(values.date_from).format("DD-MM-YYYY 00:00"), - date_to: moment(values.date_to).format("DD-MM-YYYY 23:59") - }, - callback: function(r) { - if (!r.message || !r.message.success) { - frappe.hide_progress(); - frappe.msgprint(__('Failed to load invoices: ') + (r.message?.message || 'Unknown error')); - return; - } - - const invoices = r.message.invoices || []; - const token = r.message.token; - - if (invoices.length === 0) { - frappe.hide_progress(); - frappe.msgprint(__('No invoices found for the selected period.')); - return; - } - - // Начинаем обработку инвойсов - process_invoices_for_reference_data(invoices, token, { - items_created: 0, - items_skipped: 0, - units_created: 0, - units_skipped: 0, - customers_created: 0, - customers_skipped: 0, - suppliers_created: 0, - suppliers_skipped: 0, - total_invoices: invoices.length, - inbox_count: r.message.inbox_count, - outbox_count: r.message.outbox_count - }, 0, frm); - }, - error: function(r) { - frappe.hide_progress(); - frappe.msgprint(__('Error loading invoices: ') + (r.message || 'Unknown error')); - } - }); + // ИЗМЕНЕНО: Теперь используем поэтапную загрузку вместо одного большого запроса + start_staged_invoice_loading(frm, values); } function process_invoices_for_reference_data(invoices, token, accumulated_data, current_index = 0, frm) { @@ -1027,6 +991,7 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data, if (current_index === 0) { window.cancelReferenceLoading = false; + // Показываем прогресс-бар для обработки инвойсов frappe.show_progress(__('Processing Reference Data'), 0, invoices.length, __('Processing {0} invoices...', [invoices.length]), null, true); } @@ -1091,17 +1056,49 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data, 'source_type': source_type }, callback: function(r) { - if (r.message && r.message.success && r.message.stats) { - // Обновляем накопленные данные - const stats = r.message.stats; - accumulated_data.items_created += stats.items_created || 0; - accumulated_data.items_skipped += stats.items_skipped || 0; - accumulated_data.units_created += stats.units_created || 0; - accumulated_data.units_skipped += stats.units_skipped || 0; - accumulated_data.customers_created += stats.customers_created || 0; - accumulated_data.customers_skipped += stats.customers_skipped || 0; - accumulated_data.suppliers_created += stats.suppliers_created || 0; - accumulated_data.suppliers_skipped += stats.suppliers_skipped || 0; + if (r.message) { + // Проверяем, есть ли новый токен в ответе + if (r.message.new_token) { + // Обновляем токен для последующих запросов + token = r.message.new_token; + } + + // Если ошибка unauthorized и нет нового токена - пробуем обновить токен + if (r.message.error === 'unauthorized' && !r.message.new_token) { + // Получаем новый токен из Asan Login + frappe.call({ + method: 'invoice_az.api.refresh_token_from_asan_login', + callback: function(token_r) { + if (token_r.message && token_r.message.success) { + // Обновляем токен и повторяем текущий инвойс + token = token_r.message.token; + setTimeout(function() { + if (!window.cancelReferenceLoading) { + process_invoices_for_reference_data(invoices, token, accumulated_data, current_index, frm); + } + }, 100); + } else { + // Не удалось обновить токен - останавливаем обработку + frappe.hide_progress(); + frappe.msgprint(__('Authentication expired. Please re-authenticate and try again.')); + } + } + }); + return; // Выходим, чтобы не продолжать со старым токеном + } + + // Обработка успешного результата + if (r.message.success && r.message.stats) { + const stats = r.message.stats; + accumulated_data.items_created += stats.items_created || 0; + accumulated_data.items_skipped += stats.items_skipped || 0; + accumulated_data.units_created += stats.units_created || 0; + accumulated_data.units_skipped += stats.units_skipped || 0; + accumulated_data.customers_created += stats.customers_created || 0; + accumulated_data.customers_skipped += stats.customers_skipped || 0; + accumulated_data.suppliers_created += stats.suppliers_created || 0; + accumulated_data.suppliers_skipped += stats.suppliers_skipped || 0; + } } // Переходим к следующему инвойсу @@ -1113,12 +1110,37 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data, }, error: function(xhr, status, error) { - // При ошибке переходим к следующему инвойсу - setTimeout(function() { - if (!window.cancelReferenceLoading) { - process_invoices_for_reference_data(invoices, token, accumulated_data, current_index + 1, frm); - } - }, 100); + // При ошибке сети пробуем обновить токен + if (xhr.status === 401) { + frappe.call({ + method: 'invoice_az.api.refresh_token_from_asan_login', + callback: function(token_r) { + if (token_r.message && token_r.message.success) { + // Обновляем токен и повторяем текущий инвойс + token = token_r.message.token; + setTimeout(function() { + if (!window.cancelReferenceLoading) { + process_invoices_for_reference_data(invoices, token, accumulated_data, current_index, frm); + } + }, 100); + } else { + // Переходим к следующему инвойсу + setTimeout(function() { + if (!window.cancelReferenceLoading) { + process_invoices_for_reference_data(invoices, token, accumulated_data, current_index + 1, frm); + } + }, 100); + } + } + }); + } else { + // При других ошибках переходим к следующему инвойсу + setTimeout(function() { + if (!window.cancelReferenceLoading) { + process_invoices_for_reference_data(invoices, token, accumulated_data, current_index + 1, frm); + } + }, 100); + } } }); } @@ -2139,4 +2161,193 @@ function complete_authentication_settings(asan_login_name, callback) { } } }); -} \ No newline at end of file +} + +// ===== НОВЫЕ JAVASCRIPT ФУНКЦИИ ===== + +// НОВАЯ ФУНКЦИЯ: Поэтапная загрузка инвойсов +function start_staged_invoice_loading(frm, values) { + // Инициализируем данные для поэтапной загрузки + const loading_state = { + date_from: moment(values.date_from).format("DD-MM-YYYY 00:00"), + date_to: moment(values.date_to).format("DD-MM-YYYY 23:59"), + all_invoices: [], + current_offset: 0, + total_loaded: 0, + inbox_completed: false, + outbox_completed: false, + token: null, + last_update_count: 0 // Для отслеживания когда показывать обновление + }; + + window.cancelReferenceLoading = false; + + // ВОЗВРАЩАЕМ: Модальное окно в центре экрана + const loading_dialog = frappe.msgprint({ + title: __('Loading Data'), + message: __('Fetching invoices from E-Taxes, please wait...'), + indicator: 'blue' + }); + + // Сохраняем ссылку на диалог в состоянии + loading_state.loading_dialog = loading_dialog; + + // Начинаем загрузку inbox + load_invoice_batch(loading_state, 'inbox', frm); +} + +// НОВАЯ ФУНКЦИЯ: Загрузка одной порции инвойсов +function load_invoice_batch(loading_state, source, frm) { + if (window.cancelReferenceLoading) { + // Закрываем диалог при отмене + if (loading_state.loading_dialog && loading_state.loading_dialog.hide) { + loading_state.loading_dialog.hide(); + } + frappe.show_alert({ + message: __('Loading cancelled'), + indicator: 'red' + }, 3); + return; + } + + const method = source === 'inbox' ? + 'invoice_az.api.load_single_invoice_batch' : + 'invoice_az.api.load_single_sales_invoice_batch'; + + frappe.call({ + method: method, + args: { + date_from: loading_state.date_from, + date_to: loading_state.date_to, + offset: loading_state.current_offset + }, + callback: function(r) { + if (!r.message || !r.message.success) { + if (source === 'inbox') { + // Если inbox не удался - закрываем диалог и показываем ошибку + if (loading_state.loading_dialog && loading_state.loading_dialog.hide) { + loading_state.loading_dialog.hide(); + } + frappe.msgprint(__('Failed to load invoices: ') + (r.message?.message || 'Unknown error')); + return; + } else { + // Если outbox не удался - продолжаем без него + loading_state.outbox_completed = true; + check_loading_completion(loading_state, frm); + return; + } + } + + const batch_invoices = r.message.invoices || []; + const token = r.message.token; + const hasMore = r.message.hasMore || false; + + // Сохраняем токен при первом запросе + if (!loading_state.token) { + loading_state.token = token; + } + + // Помечаем источник для каждого инвойса + batch_invoices.forEach(invoice => { + invoice._source = source; + }); + + // Добавляем к общему списку + loading_state.all_invoices = loading_state.all_invoices.concat(batch_invoices); + loading_state.total_loaded += batch_invoices.length; + + // ИЗМЕНЕНО: Обновляем сообщение каждые 200 инвойсов или при смене источника + const should_update = ( + Math.floor(loading_state.total_loaded / 200) > Math.floor(loading_state.last_update_count / 200) || + (source === 'outbox' && loading_state.last_update_count === 0) // Первое сообщение для outbox + ); + + if (should_update && loading_state.loading_dialog && loading_state.loading_dialog.$wrapper) { + const progress_text = source === 'inbox' ? + __('Finding purchase invoices: {0} found...', [loading_state.total_loaded]) : + __('Finding sales invoices: {0} total found...', [loading_state.total_loaded]); + + // Пробуем обновить текст в модальном окне + try { + const $wrapper = loading_state.loading_dialog.$wrapper; + const $modal_body = $wrapper.find('.modal-body'); + if ($modal_body.length > 0) { + $modal_body.html(`

${progress_text}

`); + } + } catch (e) { + console.log('Could not update modal text:', e); + } + + loading_state.last_update_count = loading_state.total_loaded; + } + + if (hasMore && batch_invoices.length > 0) { + // Есть еще данные - загружаем следующую порцию + loading_state.current_offset += 200; + setTimeout(() => load_invoice_batch(loading_state, source, frm), 100); + } else { + // Завершили загрузку из этого источника + if (source === 'inbox') { + loading_state.inbox_completed = true; + // Начинаем загрузку outbox + loading_state.current_offset = 0; + loading_state.last_update_count = 0; // Сбрасываем счетчик для outbox + setTimeout(() => load_invoice_batch(loading_state, 'outbox', frm), 100); + } else { + loading_state.outbox_completed = true; + check_loading_completion(loading_state, frm); + } + } + }, + error: function(r) { + if (source === 'inbox') { + // Закрываем диалог при ошибке + if (loading_state.loading_dialog && loading_state.loading_dialog.hide) { + loading_state.loading_dialog.hide(); + } + frappe.msgprint(__('Error loading invoices: ') + (r.message || 'Network error')); + } else { + // Ошибка outbox - продолжаем без него + loading_state.outbox_completed = true; + check_loading_completion(loading_state, frm); + } + } + }); +} + + +// НОВАЯ ФУНКЦИЯ: Проверка завершения загрузки +function check_loading_completion(loading_state, frm) { + if (loading_state.inbox_completed && loading_state.outbox_completed) { + // ИСПРАВЛЕНО: Закрываем диалог загрузки + if (loading_state.loading_dialog && loading_state.loading_dialog.hide) { + loading_state.loading_dialog.hide(); + } + + if (loading_state.all_invoices.length === 0) { + frappe.msgprint(__('No invoices found for the selected period.')); + return; + } + + // ИЗМЕНЕНО: Показываем результат загрузки + frappe.show_alert({ + message: __('Found {0} invoices. Starting processing...', [loading_state.all_invoices.length]), + indicator: 'green' + }, 3); // Показываем на 3 секунды + + // Небольшая задержка и начинаем обработку + setTimeout(() => { + process_invoices_for_reference_data(loading_state.all_invoices, loading_state.token, { + items_created: 0, + items_skipped: 0, + units_created: 0, + units_skipped: 0, + customers_created: 0, + customers_skipped: 0, + suppliers_created: 0, + suppliers_skipped: 0, + total_invoices: loading_state.all_invoices.length + }, 0, frm); + }, 2000); + } +} diff --git a/invoice_az/sales_api.py b/invoice_az/sales_api.py index c7dd8bd..3189734 100644 --- a/invoice_az/sales_api.py +++ b/invoice_az/sales_api.py @@ -88,10 +88,26 @@ def get_sales_invoices(token, filters=None): # Check for 401 error if response.status_code == 401: - return { - "error": "unauthorized", - "message": "Authentication required. Please login again." - } + # Получаем свежий токен из Asan Login + from invoice_az.auth import get_default_asan_login + asan_login_settings = get_default_asan_login() + if asan_login_settings.get('found'): + fresh_token = asan_login_settings['main_token'] + headers["x-authorization"] = f"Bearer {fresh_token}" + + # Повторяем запрос с новым токеном + response = requests.post(url, json=payload, headers=headers) + + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + else: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } response.raise_for_status() @@ -165,10 +181,26 @@ def get_sales_invoice_details(token, invoice_id): # Check for 401 error if response.status_code == 401: - return { - "error": "unauthorized", - "message": "Authentication required. Please login again." - } + # Получаем свежий токен из Asan Login + from invoice_az.auth import get_default_asan_login + asan_login_settings = get_default_asan_login() + if asan_login_settings.get('found'): + fresh_token = asan_login_settings['main_token'] + headers["x-authorization"] = f"Bearer {fresh_token}" + + # Повторяем запрос с новым токеном + response = requests.get(url, headers=headers) + + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + else: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } response.raise_for_status() @@ -212,7 +244,7 @@ def get_sales_invoice_details(token, invoice_id): "error": "unexpected_error", "message": "An unexpected error occurred. Please try again." } - + @frappe.whitelist() def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, schedule_date=None, warehouse=None): """Import sales invoice taking into account NEW customer mappings""" @@ -226,6 +258,12 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched if isinstance(invoice_data, str): invoice_data = json.loads(invoice_data) + # ДОБАВЛЕНО: Логируем входящие данные инвойса для отладки дат + frappe.log_error(f"[SALES DATE DEBUG] Invoice data keys: {list(invoice_data.keys())}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] createdAt field: {invoice_data.get('createdAt')}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] createdAt type: {type(invoice_data.get('createdAt'))}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] schedule_date parameter: {schedule_date}", "Sales Date Debug") + # Get active settings settings = get_active_settings() if not settings: @@ -274,6 +312,57 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched 'message': 'Default warehouse not found. Please specify warehouse in E-Taxes settings.' } + # ИСПРАВЛЕНО: Определяем дату ДО создания Sales Order + invoice_date = None + created_at_raw = invoice_data.get("createdAt") + + frappe.log_error(f"[SALES DATE DEBUG] Raw createdAt value: '{created_at_raw}'", "Sales Date Debug") + + if created_at_raw: + try: + # Попробуем разные форматы даты + if isinstance(created_at_raw, str): + # Если это строка, попробуем разные форматы + frappe.log_error(f"[SALES DATE DEBUG] Trying to parse string date: '{created_at_raw}'", "Sales Date Debug") + + # Попробуем стандартный парсинг ERPNext + invoice_date = frappe.utils.getdate(created_at_raw) + frappe.log_error(f"[SALES DATE DEBUG] Successfully parsed date: {invoice_date}", "Sales Date Debug") + + elif hasattr(created_at_raw, 'date'): + # Если это datetime объект + invoice_date = created_at_raw.date() + frappe.log_error(f"[SALES DATE DEBUG] Extracted date from datetime: {invoice_date}", "Sales Date Debug") + else: + # Попробуем привести к строке и парсить + date_str = str(created_at_raw) + frappe.log_error(f"[SALES DATE DEBUG] Converting to string and parsing: '{date_str}'", "Sales Date Debug") + invoice_date = frappe.utils.getdate(date_str) + frappe.log_error(f"[SALES DATE DEBUG] Successfully parsed converted date: {invoice_date}", "Sales Date Debug") + + except Exception as e: + frappe.log_error(f"[SALES DATE DEBUG] Error parsing createdAt '{created_at_raw}': {str(e)}", "Sales Date Debug") + invoice_date = frappe.utils.today() + frappe.log_error(f"[SALES DATE DEBUG] Using fallback date: {invoice_date}", "Sales Date Debug") + else: + invoice_date = frappe.utils.today() + frappe.log_error(f"[SALES DATE DEBUG] No createdAt found, using today: {invoice_date}", "Sales Date Debug") + + # ИСПРАВЛЕНО: Определяем date_to_use с ПРИОРИТЕТОМ даты из E-taxes + # ПРИОРИТЕТ: 1) дата из E-taxes, 2) schedule_date, 3) сегодня + date_to_use = None + if invoice_date: + date_to_use = invoice_date + frappe.log_error(f"[SALES DATE DEBUG] Using invoice_date from E-taxes for date_to_use: {date_to_use}", "Sales Date Debug") + elif schedule_date: + date_to_use = frappe.utils.getdate(schedule_date) + frappe.log_error(f"[SALES DATE DEBUG] No E-taxes date, using provided schedule_date: {date_to_use}", "Sales Date Debug") + else: + date_to_use = frappe.utils.today() + frappe.log_error(f"[SALES DATE DEBUG] No dates available, using today for date_to_use: {date_to_use}", "Sales Date Debug") + + frappe.log_error(f"[SALES DATE DEBUG] Final date_to_use: {date_to_use}", "Sales Date Debug") + # ИЗМЕНЕНО: Создаем Sales Order сначала (аналогично Purchase Order логике) if sales_order_name: so = frappe.get_doc('Sales Order', sales_order_name) @@ -300,15 +389,13 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched 'message': f'No mapping found for customer: {receiver.get("name", "")}' } - # Set dates - so.transaction_date = frappe.utils.today() + frappe.log_error(f"[SALES DATE DEBUG] Final invoice_date for SO: {invoice_date}", "Sales Date Debug") + + so.transaction_date = invoice_date + so.delivery_date = date_to_use - if schedule_date: - so.delivery_date = schedule_date - elif invoice_data.get("creationDate"): - so.delivery_date = frappe.utils.getdate(invoice_data.get("creationDate")) - else: - so.delivery_date = frappe.utils.today() + frappe.log_error(f"[SALES DATE DEBUG] SO transaction_date set to: {so.transaction_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] SO delivery_date set to: {so.delivery_date}", "Sales Date Debug") so.company = frappe.defaults.get_user_default('Company') @@ -320,14 +407,6 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched if sales_order_name and invoice_data.get("items"): so.items = [] - date_to_use = None - if schedule_date: - date_to_use = schedule_date - elif invoice_data.get("creationDate"): - date_to_use = frappe.utils.getdate(invoice_data.get("creationDate")) - else: - date_to_use = frappe.utils.today() - # Variable for tracking added items added_items_count = 0 unmatched_items = [] @@ -416,9 +495,11 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched so_item.rate = item.get("pricePerUnit", 0) so_item.amount = item.get("cost", 0) so_item.uom = mapped_uom - so_item.delivery_date = date_to_use # ИЗМЕНЕНО: delivery_date вместо schedule_date + so_item.delivery_date = date_to_use # ИСПРАВЛЕНО: Используем правильную дату so_item.warehouse = default_warehouse + frappe.log_error(f"[SALES DATE DEBUG] Item {so_item.item_code} delivery_date set to: {so_item.delivery_date}", "Sales Date Debug") + so.append("items", so_item) added_items_count += 1 @@ -480,19 +561,32 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched except Exception as e: frappe.log_error(f"Error adding ƏDV tax: {str(e)}", "Sales Order Tax Error") + # ДОБАВЛЕНО: Логируем финальные даты перед сохранением + frappe.log_error(f"[SALES DATE DEBUG] Final SO transaction_date: {so.transaction_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] Final SO delivery_date: {so.delivery_date}", "Sales Date Debug") + # Save document so.save() - # Additional check that all delivery_date and warehouse are set + # ИСПРАВЛЕНО: Убедимся что все delivery_date установлены правильно for item in so.items: if not item.delivery_date: item.delivery_date = date_to_use + frappe.log_error(f"[SALES DATE DEBUG] Fixed missing delivery_date for item {item.item_code}: {item.delivery_date}", "Sales Date Debug") if not item.warehouse: item.warehouse = default_warehouse # Save again to ensure changes are applied so.save() + # ДОБАВЛЕНО: Логируем даты после сохранения + frappe.log_error(f"[SALES DATE DEBUG] After save SO transaction_date: {so.transaction_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] After save SO delivery_date: {so.delivery_date}", "Sales Date Debug") + + # Логируем delivery_date каждого item после сохранения + for item in so.items: + frappe.log_error(f"[SALES DATE DEBUG] After save item {item.item_code} delivery_date: {item.delivery_date}", "Sales Date Debug") + # ИЗМЕНЕНИЕ: Создаем E-Taxes Sales и устанавливаем связь ДО submit'а invoice_id = invoice_data.get('id', '') serial_number = invoice_data.get('serialNumber', '') @@ -554,7 +648,7 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched 'success': False, 'message': "An unknown error occurred, please try again in a few minutes." } - + @frappe.whitelist() def create_sales_invoice_from_order(sales_order_name): """Создает Sales Invoice из Sales Order используя стандартную функцию ERPNext""" @@ -562,18 +656,43 @@ def create_sales_invoice_from_order(sales_order_name): # Импортируем стандартную функцию из модуля Sales Order from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice + # Получаем Sales Order ДО создания invoice + so_doc = frappe.get_doc('Sales Order', sales_order_name) + + # ИСПРАВЛЕНО: Правильная обработка дат + if so_doc.transaction_date: + posting_date = so_doc.transaction_date + # Конвертируем в datetime.date если это строка + if isinstance(posting_date, str): + posting_date = frappe.utils.getdate(posting_date) + else: + posting_date = frappe.utils.getdate(frappe.utils.nowdate()) + + frappe.log_error(f"[SALES DATE DEBUG] SO transaction_date: {so_doc.transaction_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] SI posting_date to set: {posting_date}", "Sales Date Debug") + # Создаем Sales Invoice из Sales Order si_doc = make_sales_invoice(sales_order_name) - # Устанавливаем даты - si_doc.posting_date = frappe.utils.nowdate() - si_doc.due_date = frappe.utils.add_days(frappe.utils.nowdate(), 30) + # ДОБАВЛЕНО: Логируем дату сразу после создания + frappe.log_error(f"[SALES DATE DEBUG] SI posting_date after make_sales_invoice: {si_doc.posting_date}", "Sales Date Debug") + + # ИСПРАВЛЕНО: Устанавливаем дату и флаг posting_time ДО других операций + si_doc.posting_date = posting_date + si_doc.set_posting_time = 1 # Важно! Разрешаем установку даты в прошлом + + # ИСПРАВЛЕНО: due_date всегда основана на posting_date из E-taxes (без коррекции на "сегодня") + due_date = frappe.utils.add_days(posting_date, 30) + si_doc.due_date = due_date + + frappe.log_error(f"[SALES DATE DEBUG] SI posting_date before insert: {si_doc.posting_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] SI due_date before insert (E-taxes based): {si_doc.due_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] SI set_posting_time: {si_doc.set_posting_time}", "Sales Date Debug") # ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Sales Invoice si_doc.is_taxes_doc = 1 # ДОБАВЛЕНО: Копируем налоги из Sales Order - so_doc = frappe.get_doc('Sales Order', sales_order_name) if so_doc.taxes: si_doc.taxes = [] for tax_row in so_doc.taxes: @@ -588,6 +707,30 @@ def create_sales_invoice_from_order(sales_order_name): # Сохраняем документ si_doc.insert(ignore_permissions=True) + # ДОБАВЛЕНО: Логируем дату после insert + frappe.log_error(f"[SALES DATE DEBUG] SI posting_date after insert: {si_doc.posting_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] SI due_date after insert: {si_doc.due_date}", "Sales Date Debug") + + # ДОБАВЛЕНО: Если даты все еще неправильные, принудительно устанавливаем + expected_due_date = frappe.utils.add_days(posting_date, 30) + if si_doc.posting_date != posting_date or si_doc.due_date != expected_due_date: + frappe.log_error(f"[SALES DATE DEBUG] Date mismatch detected!", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] Expected posting_date: {posting_date}, Got: {si_doc.posting_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] Expected due_date: {expected_due_date}, Got: {si_doc.due_date}", "Sales Date Debug") + + # Принудительно устанавливаем даты через DB + frappe.db.set_value('Sales Invoice', si_doc.name, { + 'posting_date': posting_date, + 'due_date': expected_due_date, + 'set_posting_time': 1 + }) + frappe.db.commit() + + # Перезагружаем документ + si_doc = frappe.get_doc('Sales Invoice', si_doc.name) + frappe.log_error(f"[SALES DATE DEBUG] SI posting_date after DB update: {si_doc.posting_date}", "Sales Date Debug") + frappe.log_error(f"[SALES DATE DEBUG] SI due_date after DB update: {si_doc.due_date}", "Sales Date Debug") + frappe.log_error(f"Sales Invoice {si_doc.name} created from Sales Order {sales_order_name}", "Sales Invoice Creation") # Возвращаем имя созданного Sales Invoice @@ -596,7 +739,7 @@ def create_sales_invoice_from_order(sales_order_name): except Exception as e: frappe.log_error(f"Error creating Sales Invoice from SO {sales_order_name}: {str(e)}\n{frappe.get_traceback()}", "Sales Invoice Creation Error") return None - + @frappe.whitelist() def create_etaxes_sales(etaxes_id, date, party, total): """Creates E-Taxes Sales record for tracking imported sales invoices""" @@ -725,4 +868,4 @@ def on_delete_sales_order(doc, method): 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") + "Sales Order Delete Error") \ No newline at end of file