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