invoice_az/invoice_az/sales_api.py

998 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import frappe
import json
import requests
from frappe.utils import now_datetime
from invoice_az import background_tasks
from datetime import datetime
import re
@frappe.whitelist()
def get_active_settings():
"""Getting active E-Taxes settings"""
return frappe.get_single('E-Taxes Settings')
@frappe.whitelist()
def record_etaxes_activity(asan_login_name=None):
"""Записывает время последней активности с e-taxes напрямую"""
try:
# Если имя не указано, получаем дефолтный профиль
if not asan_login_name:
default_settings = frappe.get_all(
"Asan Login",
filters={"is_default": 1},
fields=["name"],
limit=1
)
if not default_settings:
return
asan_login_name = default_settings[0].name
# Обновляем поле 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 get_sales_invoices(token, filters=None):
"""Getting list of sales invoices with filtering options"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.outbox"
# Base request parameters - изменено: только approved и approvedBySystem статусы
payload = {
"sortBy": "creationDate",
"sortAsc": True,
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
"types": ["current", "corrected"],
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
"exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"],
"serialNumber": None,
"senderTin": None,
"senderName": None,
"productName": None,
"productCode": None,
"receiverTin": None,
"receiverName": None,
"creationDateFrom": "01-01-2024 00:00",
"creationDateTo": "31-12-2024 23:59",
"amountFrom": None,
"amountTo": None,
"offset": 0,
"maxCount": 200,
"actionOwner": None
}
# Apply custom filters if they are passed
if filters:
try:
filters_dict = json.loads(filters)
for key, value in filters_dict.items():
if key in payload and value: # Update only existing keys and non-empty values
payload[key] = value
except Exception as e:
return
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"x-authorization": f"Bearer {token}"
}
try:
response = requests.post(url, json=payload, headers=headers)
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# 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."
}
response.raise_for_status()
result = response.json()
return result
except requests.exceptions.HTTPError as e:
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_sales_invoices: {str(e)}", "API Error")
return {
"error": "http_error",
"message": f"HTTP error {e.response.status_code}: {str(e)}"
}
except requests.exceptions.ConnectionError as e:
frappe.log_error(f"Connection error in get_sales_invoices: {str(e)}", "API Error")
return {
"error": "connection_error",
"message": "Cannot connect to E-Taxes service. Please check your internet connection and try again."
}
except requests.exceptions.Timeout as e:
frappe.log_error(f"Timeout error in get_sales_invoices: {str(e)}", "API Error")
return {
"error": "timeout_error",
"message": "Request timed out. Please try again."
}
except Exception as e:
frappe.log_error(f"Unexpected error in get_sales_invoices: {str(e)}", "API Error")
return {
"error": "unexpected_error",
"message": "An unexpected error occurred. Please try again."
}
@frappe.whitelist()
def get_sales_invoice_details(token, invoice_id):
"""Getting detailed information about a sales invoice"""
# Записываем активность
record_etaxes_activity()
url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis"
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"x-authorization": f"Bearer {token}"
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# 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."
}
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_sales_invoice_details: {str(e)}", "API Error")
return {
"error": "http_error",
"message": f"HTTP error {e.response.status_code}: {str(e)}"
}
except requests.exceptions.ConnectionError as e:
frappe.log_error(f"Connection error in get_sales_invoice_details: {str(e)}", "API Error")
return {
"error": "connection_error",
"message": "Cannot connect to E-Taxes service. Please check your internet connection and try again."
}
except requests.exceptions.Timeout as e:
frappe.log_error(f"Timeout error in get_sales_invoice_details: {str(e)}", "API Error")
return {
"error": "timeout_error",
"message": "Request timed out. Please try again."
}
except Exception as e:
frappe.log_error(f"Unexpected error in get_sales_invoice_details: {str(e)}", "API Error")
return {
"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"""
# Записываем активность
record_etaxes_activity()
try:
import re
# Deserialize if necessary
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:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Create mapping dictionaries
item_mappings = {}
for mapping in settings.item_mappings:
if mapping.etaxes_item_name and mapping.erp_item:
item_mappings[mapping.etaxes_item_name] = mapping.erp_item
# ИЗМЕНЕНО: Используем customer_mappings вместо party_mappings
customer_mappings = {}
for mapping in settings.customer_mappings:
if mapping.etaxes_customer_name and mapping.erp_customer:
# ИСПРАВЛЕНИЕ: Обрезаем ключ до 140 символов
customer_name = mapping.etaxes_customer_name[:140] if len(mapping.etaxes_customer_name) > 140 else mapping.etaxes_customer_name
key = f"{customer_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}"
customer_mappings[key] = mapping.erp_customer
# Get default warehouse
default_warehouse = warehouse
if not default_warehouse:
if hasattr(settings, 'default_warehouse') and settings.default_warehouse:
default_warehouse = settings.default_warehouse
else:
company = frappe.defaults.get_user_default('Company')
if company:
default_warehouse = frappe.db.get_value('Company', company, 'default_warehouse')
if not default_warehouse:
warehouses = frappe.get_all('Warehouse',
filters={'is_group': 0, 'disabled': 0},
fields=['name'],
limit=1)
if warehouses:
default_warehouse = warehouses[0].name
if not default_warehouse:
return {
'success': False,
'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)
else:
so = frappe.new_doc('Sales Order')
# ИЗМЕНЕНО: Set customer используя customer_mappings с обрезкой
receiver = invoice_data.get('receiver', {})
receiver_name = receiver.get('name', '')
# ИСПРАВЛЕНИЕ: Обрезаем название клиента до 140 символов для поиска
receiver_name_truncated = receiver_name[:140] if len(receiver_name) > 140 else receiver_name
receiver_key = f"{receiver_name_truncated.lower()}|{receiver.get('tin', '').lower()}"
if receiver_key in customer_mappings and customer_mappings[receiver_key]:
so.customer = customer_mappings[receiver_key]
else:
return {
'success': False,
'unmatched_parties': [{
'name': receiver.get('name', ''),
'tin': receiver.get('tin', ''),
'type': 'Receiver'
}],
'message': f'No mapping found for customer: {receiver.get("name", "")}'
}
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
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')
# Add/update additional fields from invoice
so.title = f"Sales Order {invoice_data.get('serialNumber', '')}"
so.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
# Clear existing items if need to update them completely
if sales_order_name and invoice_data.get("items"):
so.items = []
# Variable for tracking added items
added_items_count = 0
unmatched_items = []
unmatched_units = []
# Add items from invoice
if invoice_data.get("items"):
for item in invoice_data.get("items", []):
# Подготавливаем данные товара с очисткой от невидимых символов
item_name = re.sub(r'\s+', ' ', item.get("productName", "")).strip()
item_code = item.get("itemId", "") or f"CODE-{invoice_data.get('serialNumber', '')}"
# ИСПРАВЛЕНИЕ: Обрезаем название товара до 140 символов для поиска
item_name_truncated = item_name[:140] if len(item_name) > 140 else item_name
# Ищем соответствие товара сначала по обрезанному названию
mapped_item = item_mappings.get(item_name_truncated)
# Если не найдено, ищем без учета регистра
if not mapped_item:
item_name_lower = item_name_truncated.lower()
for mapping_key, mapping_value in item_mappings.items():
if mapping_key.lower() == item_name_lower:
mapped_item = mapping_value
break
# Если все еще не найдено, ищем с нормализацией азербайджанских символов
if not mapped_item:
normalized_input = normalize_string(item_name_truncated, consider_azeri=True)
for mapping_key, mapping_value in item_mappings.items():
normalized_key = normalize_string(mapping_key, consider_azeri=True)
if normalized_key == normalized_input:
mapped_item = mapping_value
break
if not mapped_item:
unmatched_items.append({
'name': item_name,
'code': item_code
})
continue
# Для единиц измерения ищем в E-Taxes Unit
unit_name = item.get("unit", "")
mapped_uom = None
if unit_name:
try:
mapped_uom = frappe.db.get_value('E-Taxes Unit',
filters={'etaxes_unit_name': unit_name},
fieldname='mapped_unit')
except Exception:
pass
# Если соответствие единицы не найдено, используем UOM из товара или настроек
if not mapped_uom:
if unit_name:
unmatched_units.append({
'name': unit_name
})
try:
item_doc = frappe.get_doc('Item', mapped_item)
mapped_uom = item_doc.stock_uom
except:
mapped_uom = settings.default_uom if settings.default_uom else "Nos"
# ИЗМЕНЕНО: Add position to Sales Order (не Sales Invoice)
so_item = frappe.new_doc("Sales Order Item")
so_item.parent = so.name
so_item.parenttype = "Sales Order"
so_item.parentfield = "items"
so_item.item_code = mapped_item
# Получаем название и описание из базы данных Item
try:
item_doc = frappe.get_doc('Item', mapped_item)
so_item.item_name = item_doc.item_name
so_item.description = item_doc.description or item_doc.item_name
except:
so_item.item_name = item.get("productName", "")
so_item.description = item.get("productName", "")
so_item.qty = item.get("quantity", 0)
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 # ИСПРАВЛЕНО: Используем правильную дату
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
# Check if there are unmapped items
if unmatched_items:
return {
'success': False,
'unmatched_items': unmatched_items,
'message': f'No mapping found for {len(unmatched_items)} items'
}
# Check if there are unmapped units
if unmatched_units:
return {
'success': False,
'unmatched_units': unmatched_units,
'message': f'No mapping found for {len(unmatched_units)} units'
}
# Check that there is at least one element in table
if added_items_count == 0:
frappe.log_error(
f"No items were added to Sales Order. Invoice data: {invoice_data}",
"Import Sales Invoice Error"
)
return {
'success': False,
'message': 'Could not add any items to sales order'
}
# ДОБАВЛЕНО: Создание строки ƏDV в Sales Taxes and Charges ТОЛЬКО если есть vat в данных
try:
vat_amount = invoice_data.get('vat', 0)
if vat_amount and vat_amount > 0:
vat_account = frappe.db.get_value('Account',
filters={'account_number': '521.3'},
fieldname='name')
if vat_account:
net_total = sum(item.get('cost', 0) for item in invoice_data.get('items', []))
vat_rate = (vat_amount / net_total * 100) if net_total > 0 else 0
so.append("taxes", {
"account_head": vat_account,
"charge_type": "On Net Total",
"rate": vat_rate,
"tax_amount": vat_amount,
"description": "ƏDV",
"add_deduct_tax": "Add"
})
frappe.log_error(f"Added ƏDV tax: Amount={vat_amount}, Rate={vat_rate}%", "Sales Order Tax")
else:
frappe.log_error("ƏDV account (521.3) not found", "Sales Order Tax Warning")
else:
frappe.log_error("No VAT amount in E-Taxes data, skipping ƏDV tax creation", "Sales Order Tax Info")
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()
# ИСПРАВЛЕНО: Убедимся что все 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', '')
receiver_name = invoice_data.get('receiver', {}).get('name', '') if invoice_data.get('receiver') else ''
total = invoice_data.get('totalAmount', 0) or invoice_data.get('amount', 0)
# Создаем E-Taxes Sales
etaxes_sales_result = create_etaxes_sales(invoice_id, date_to_use, receiver_name, total)
if etaxes_sales_result and etaxes_sales_result.get('success'):
# Устанавливаем поля E-Taxes ДО submit'а
so.is_taxes_doc = 1
so.taxes_doc = etaxes_sales_result.get('name')
# Сохраняем изменения
so.save()
# ИЗМЕНЕНИЕ: Делаем Submit для Sales Order
try:
so.submit()
frappe.log_error(f"Sales Order {so.name} submitted successfully", "Import Sales Invoice Success")
except Exception as e:
frappe.log_error(f"Error submitting Sales Order {so.name}: {str(e)}", "Submit SO Error")
return {
'success': False,
'message': f'Failed to submit Sales Order: {str(e)}'
}
# ИЗМЕНЕНИЕ: Создаем Sales Invoice из Sales Order
try:
si_name = create_sales_invoice_from_order(so.name)
if si_name:
# Делаем Submit для Sales Invoice
si = frappe.get_doc("Sales Invoice", si_name)
si.submit()
frappe.log_error(f"Sales Invoice {si_name} created and submitted successfully", "Import Sales Invoice Success")
return {
'success': True,
'message': 'Sales invoice data imported successfully. Sales Order and Sales Invoice created.',
'sales_order': so.name,
'sales_invoice': si_name
}
else:
return {
'success': True,
'message': 'Sales invoice data imported successfully. Sales Order created, but Sales Invoice creation failed.',
'sales_order': so.name
}
except Exception as e:
frappe.log_error(f"Error creating or submitting Sales Invoice: {str(e)}", "Submit SI Error")
return {
'success': True,
'message': 'Sales invoice data imported successfully. Sales Order created, but Sales Invoice creation failed.',
'sales_order': so.name
}
except Exception as e:
frappe.log_error(f"Error in import_sales_invoice_with_mapping: {str(e)}\n{frappe.get_traceback()}", "Import Sales Invoice Error")
return {
'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"""
try:
# Импортируем стандартную функцию из модуля 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)
# ДОБАВЛЕНО: Логируем дату сразу после создания
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
if so_doc.taxes:
si_doc.taxes = []
for tax_row in so_doc.taxes:
si_doc.append("taxes", {
"account_head": tax_row.account_head,
"charge_type": tax_row.charge_type,
"rate": tax_row.rate,
"tax_amount": tax_row.tax_amount,
"description": tax_row.description
})
# Сохраняем документ
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
return si_doc.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"""
# Записываем активность
record_etaxes_activity()
try:
# Normalize ID for storage
etaxes_id = str(etaxes_id).strip() if etaxes_id else ""
# Debug logging
frappe.logger().info(f"Creating E-Taxes Sales: ID={etaxes_id}, date={date}, party={party}, total={total}")
# Check if record with such etaxes_id already exists
existing = frappe.db.get_value('E-Taxes Sales', {'etaxes_id': etaxes_id}, 'name')
if existing:
# Debug logging
frappe.logger().info(f"E-Taxes Sales already exists: {existing}")
return {
'success': True,
'message': 'Record already exists',
'name': existing
}
# Create new record
etaxes_sales = frappe.new_doc('E-Taxes Sales')
etaxes_sales.etaxes_id = etaxes_id
etaxes_sales.date = date
etaxes_sales.party = party
etaxes_sales.total = total
etaxes_sales.insert()
frappe.db.commit() # Explicit commit
# Debug logging
frappe.logger().info(f"Created new E-Taxes Sales: {etaxes_sales.name}")
return {
'success': True,
'name': etaxes_sales.name
}
except Exception as e:
frappe.log_error(f"Error in create_etaxes_sales: {str(e)}\n{frappe.get_traceback()}", "API Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_etaxes_sales():
"""Gets list of all E-Taxes Sales records"""
try:
sales = frappe.get_all('E-Taxes Sales',
fields=['name', 'etaxes_id', 'date', 'party', 'total'])
# Debug logging
frappe.logger().info(f"Retrieved {len(sales)} E-Taxes Sales records")
return {
'success': True,
'sales': sales
}
except Exception as e:
frappe.log_error(f"Error in get_etaxes_sales: {str(e)}", "API Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def normalize_string(text, consider_azeri=True):
"""Normalizing string for comparison"""
import re
if not text:
return ''
# ДОБАВЛЕНО: Сначала заменяем азербайджанские/турецкие символы ДО приведения к нижнему регистру
if consider_azeri:
azeri_replacements = {
# Заглавные буквы
'Ə': 'E',
'Ü': 'U',
'Ö': 'O',
'Ğ': 'G',
'İ': 'I',
'Ç': 'C',
'Ş': 'S',
# Строчные буквы
'ə': 'e',
'ü': 'u',
'ö': 'o',
'ğ': 'g',
'ı': 'i',
'ç': 'c',
'ş': 's'
}
for azeri_char, latin_char in azeri_replacements.items():
text = text.replace(azeri_char, latin_char)
# Convert to lowercase ПОСЛЕ замены символов
text = text.lower()
# Remove all special characters and numbers
text = re.sub(r'[^\w\s]', '', text)
text = re.sub(r'\d+', '', text)
# Remove extra spaces
text = ' '.join(text.split())
return text
@frappe.whitelist()
def on_delete_sales_order(doc, method):
"""Deletes related E-Taxes Sales record when Sales Order is deleted"""
if doc.is_taxes_doc and doc.taxes_doc:
try:
frappe.logger().info(f"Deleting E-Taxes Sales {doc.taxes_doc} due to Sales Order {doc.name} deletion")
# Check if E-Taxes Sales record exists
if frappe.db.exists("E-Taxes Sales", doc.taxes_doc):
frappe.delete_doc('E-Taxes Sales', doc.taxes_doc, ignore_permissions=True, force=True)
frappe.db.commit()
frappe.logger().info(f"Successfully deleted E-Taxes Sales {doc.taxes_doc}")
except Exception as e:
frappe.log_error(f"Error deleting E-Taxes Sales {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
"Sales Order Delete Error")
@frappe.whitelist()
def import_bulk_sales_invoices(invoice_ids, token, warehouse=None):
"""Enqueue bulk sales import as a background job with realtime progress via Socket.IO."""
if isinstance(invoice_ids, str):
invoice_ids = json.loads(invoice_ids)
user = frappe.session.user
job_id = background_tasks.new_job_id()
frappe.enqueue(
_process_bulk_sales_import,
invoice_ids=invoice_ids,
token=token,
warehouse=warehouse,
user=user,
bg_job_id=job_id,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(invoice_ids), "job_id": job_id}
def _process_bulk_sales_import(invoice_ids, token, warehouse, user, bg_job_id=None):
"""Background job: fetch details and import each sales invoice with realtime progress."""
job_id = bg_job_id
frappe.set_user(user)
record_etaxes_activity()
total = len(invoice_ids)
imported_count = 0
errors = []
background_tasks.register_task(job_id, user, "sales_import", "Importing sales invoices", total=total)
for idx, invoice_id in enumerate(invoice_ids):
if job_id and background_tasks.is_cancel_requested(job_id):
background_tasks.finish_task(job_id, user, "cancelled")
frappe.publish_realtime(
"sales_import_complete",
{"total": total, "imported": imported_count, "errors": errors, "cancelled": True},
user=user,
)
return
background_tasks.update_task(job_id, user, current=idx, total=total,
message=f"Importing invoice {idx + 1}/{total}")
try:
details = get_sales_invoice_details(token, invoice_id)
except Exception:
details = None
if not details or details.get("error"):
error_msg = details.get("message", "Failed to fetch invoice details") if details else "Empty response"
if details and details.get("error") == "unauthorized":
from invoice_az.auth import get_default_asan_login
asan_login = get_default_asan_login()
if asan_login.get("found") and asan_login.get("main_token"):
token = asan_login["main_token"]
details = get_sales_invoice_details(token, invoice_id)
if not details or details.get("error"):
error_msg = details.get("message", "Failed after token refresh") if details else "Empty response"
errors.append({"invoice_id": invoice_id, "error": error_msg})
frappe.db.commit()
continue
else:
errors.append({"invoice_id": invoice_id, "error": "Authentication expired"})
frappe.db.commit()
continue
else:
errors.append({"invoice_id": invoice_id, "error": error_msg})
frappe.db.commit()
continue
schedule_date = None
created_at = details.get("creationDate") or details.get("date")
if created_at:
try:
from datetime import datetime as dt
schedule_date = dt.strptime(str(created_at)[:10], "%Y-%m-%d").strftime("%Y-%m-%d")
except Exception:
pass
result = import_sales_invoice_with_mapping(
invoice_data=details,
sales_order_name=None,
schedule_date=schedule_date,
warehouse=warehouse,
)
if result and result.get("success"):
imported_count += 1
else:
error_msg = result.get("message", "Unknown import error") if result else "Empty result"
error_entry = {"invoice_id": invoice_id, "error": error_msg}
if result:
for key in ("unmatched_items", "unmatched_parties", "unmatched_units"):
if result.get(key):
error_entry[key] = result[key]
errors.append(error_entry)
frappe.db.commit()
frappe.publish_realtime(
"sales_import_progress",
{"current": idx + 1, "total": total},
user=user,
)
background_tasks.finish_task(job_id, user, "done")
frappe.publish_realtime(
"sales_import_complete",
{"total": total, "imported": imported_count, "errors": errors},
user=user,
)