fixed invoices date

This commit is contained in:
Ali 2025-09-08 21:52:04 +04:00
parent f0c19854fe
commit 6df36695db
4 changed files with 907 additions and 250 deletions

View File

@ -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,6 +78,27 @@ def get_invoices(token, filters=None):
# Check response status to handle 401 error
if response.status_code == 401:
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."
@ -85,14 +108,17 @@ def get_invoices(token, filters=None):
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,6 +160,27 @@ def get_invoice_details(token, invoice_id):
# Check for 401 error
if response.status_code == 401:
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."
@ -141,14 +190,17 @@ def get_invoice_details(token, invoice_id):
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,6 +210,7 @@ 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."}
@ -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
@ -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', '')
@ -3576,106 +3741,6 @@ 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'):
@ -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,
@ -3707,7 +3798,7 @@ def process_single_invoice_for_reference_data(token, invoice_id, source_type='pu
@frappe.whitelist()
def create_reference_data_from_single_invoice(invoice_details, source_type):
"""Создает все 4 типа доктайпов из одного инвойса"""
"""Создает все 4 типа доктайпов из одного инвойса - оптимизированная версия"""
try:
stats = {
'items_created': 0,
@ -3928,3 +4019,221 @@ def create_reference_data_from_single_invoice(invoice_details, source_type):
'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)
}

View File

@ -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,23 +41,17 @@ 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()
@ -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"}

View File

@ -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}.<br><br><strong>This operation may take several minutes depending on the number of invoices.</strong><br><br>Continue?',
__('This will load {0} from E-Taxes for the period {1} to {2}.<br><br>' +
'<strong>Process:</strong><br>' +
'1. First, all invoices will be fetched (may take 1-2 minutes)<br>' +
'2. Then each invoice will be processed for reference data<br><br>' +
'<strong>This operation may take several minutes depending on the number of invoices.</strong><br><br>' +
'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,8 +1056,39 @@ 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) {
// Обновляем накопленные данные
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;
@ -1103,6 +1099,7 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data,
accumulated_data.suppliers_created += stats.suppliers_created || 0;
accumulated_data.suppliers_skipped += stats.suppliers_skipped || 0;
}
}
// Переходим к следующему инвойсу
setTimeout(function() {
@ -1113,13 +1110,38 @@ function process_invoices_for_reference_data(invoices, token, accumulated_data,
},
error: function(xhr, status, error) {
// При ошибке переходим к следующему инвойсу
// При ошибке сети пробуем обновить токен
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);
}
}
});
}
@ -2140,3 +2162,192 @@ function complete_authentication_settings(asan_login_name, callback) {
}
});
}
// ===== НОВЫЕ 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(`<p>${progress_text}</p>`);
}
} 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);
}
}

View File

@ -88,6 +88,22 @@ def get_sales_invoices(token, filters=None):
# Check for 401 error
if response.status_code == 401:
# Получаем свежий токен из 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."
@ -165,6 +181,22 @@ def get_sales_invoice_details(token, invoice_id):
# Check for 401 error
if response.status_code == 401:
# Получаем свежий токен из 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."
@ -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")
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()
so.transaction_date = invoice_date
so.delivery_date = date_to_use
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', '')
@ -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