998 lines
45 KiB
Python
998 lines
45 KiB
Python
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,
|
||
) |