invoice_az/invoice_az/api.py

4425 lines
196 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 requests
import json
from frappe.utils import nowdate, now, cint, now_datetime, get_datetime
from frappe.model.document import Document
import re
from difflib import SequenceMatcher
import datetime
import random
import time
# Импорты из модуля аутентификации
from invoice_az.auth import record_etaxes_activity, get_default_asan_login
from .utils import resolve_customer_group
from .sales_api import get_sales_invoices
@frappe.whitelist()
def get_invoices(token, filters=None):
"""Getting list of invoices with filtering options"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox"
# 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:
frappe.log_error(f"[GET_INVOICES] Error parsing filters: {str(e)}", "Filter Parse Error")
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, 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.",
"status_code": 500
}
# 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."
}
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
frappe.log_error(f"[GET_INVOICES] HTTP error: {e.response.status_code} - {str(e)}", "E-Taxes HTTP Error")
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
frappe.log_error(f"[GET_INVOICES] HTTPError 401 Unauthorized", "E-Taxes Auth Error")
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
frappe.log_error(f"[GET_INVOICES] HTTPError 500 Server Error", "E-Taxes Server Error")
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
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."}
@frappe.whitelist()
def get_invoice_details(token, invoice_id):
"""Getting detailed information about an 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:
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.",
"status_code": 500
}
# 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."
}
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
frappe.log_error(f"[GET_INVOICE_DETAILS] HTTP error for invoice {invoice_id}: {e.response.status_code} - {str(e)}", "E-Taxes HTTP Error")
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
frappe.log_error(f"[GET_INVOICE_DETAILS] HTTPError 401 Unauthorized for invoice {invoice_id}", "E-Taxes Auth Error")
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
frappe.log_error(f"[GET_INVOICE_DETAILS] HTTPError 500 Server Error for invoice {invoice_id}", "E-Taxes Server Error")
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_invoice_details: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"[GET_INVOICE_DETAILS] Unexpected error for invoice {invoice_id}: {str(e)}\n{frappe.get_traceback()}", "E-Taxes Error")
frappe.log_error(f"Error getting invoice details: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_uom_for_unit(unit_name):
"""Converts unit name from invoice to system UOM with support for mappings"""
if not unit_name:
# Получаем default_uom из настроек
try:
settings = get_active_settings()
if settings and hasattr(settings, 'default_uom') and settings.default_uom:
return settings.default_uom
except Exception:
pass
return "Nos" # Fallback if no settings
try:
# Ищем существующую запись E-Taxes Unit
etaxes_unit = frappe.db.get_value('E-Taxes Unit',
filters={'etaxes_unit_name': unit_name},
fieldname=['mapped_unit'])
# Если найдена запись и у неё есть mapped_unit - возвращаем его
if etaxes_unit and etaxes_unit:
return etaxes_unit
# Если записи нет или mapped_unit пустой - берем default_uom из настроек
settings = get_active_settings()
if settings and hasattr(settings, 'default_uom') and settings.default_uom:
return settings.default_uom
except Exception as e:
frappe.log_error(f"Error getting UOM mapping: {str(e)}", "UOM Mapping Error")
# Fallback to default
return "Nos"
@frappe.whitelist()
def load_items_from_invoices(date_from, date_to, max_count=200, offset=0):
"""Loading invoices list for a period - returns invoices for frontend processing"""
# Записываем активность
record_etaxes_activity()
try:
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
try:
offset = int(offset)
except (ValueError, TypeError):
offset = 0
# Format filters for the API request
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(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response_inbox:
return {
'success': False,
'error': response_inbox.get('error'),
'message': response_inbox['message'] if 'message' in response_inbox else '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(asan_login_settings['main_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'
# Объединяем инвойсы из inbox и outbox
all_invoices = invoices_inbox + invoices_outbox
# Определяем hasMore на основе обоих источников
hasMore_inbox = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
hasMore_outbox = response_outbox.get('hasMore', False) or response_outbox.get('total', 0) > offset + max_count
hasMore = hasMore_inbox or hasMore_outbox
else:
# Если не удалось загрузить из outbox, используем только inbox
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
except ImportError:
# Если модуль sales_api недоступен, используем только inbox
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
except Exception as e:
# При любой другой ошибке используем только inbox
frappe.log_error(f"Error loading from outbox: {str(e)}", "Load Items Outbox Error")
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
return {
'success': True,
'invoices': all_invoices,
'hasMore': hasMore,
'token': asan_login_settings['main_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_items_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Items Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_default_settings():
"""Getting active E-Taxes settings"""
settings = frappe.get_single('E-Taxes Settings')
return {'found': True, 'settings': settings.as_dict()}
@frappe.whitelist()
def normalize_azeri_text(text):
"""Normalization of Azərbaycan text for matching"""
if not text:
return text
# Replacements dictionary
replacements = {
'ə': 'e',
'Ə': 'E',
'ü': 'u',
'Ü': 'U',
'ö': 'o',
'Ö': 'O',
'ğ': 'g',
'Ğ': 'G',
'ı': 'i',
'I': 'I',
'ç': 'c',
'Ç': 'C',
'ş': 's',
'Ş': 'S'
}
# Also add variant ə->a and ş->sh
replacements_alt = replacements.copy()
replacements_alt.update({
'ə': 'a',
'Ə': 'A',
'ş': 'sh',
'Ş': 'SH'
})
# Create normalized versions
text_normalized = text
for azeri_char, latin_char in replacements.items():
text_normalized = text_normalized.replace(azeri_char, latin_char)
text_normalized_alt = text
for azeri_char, latin_char in replacements_alt.items():
text_normalized_alt = text_normalized_alt.replace(azeri_char, latin_char)
return text_normalized, text_normalized_alt
@frappe.whitelist()
def get_unmapped_items():
"""Getting unmapped items from E-Taxes"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped items from E-Taxes (single query)
etaxes_items = frappe.get_all('E-Taxes Item',
filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code', 'etaxes_unit',
'etaxes_price', 'is_service_item', 'is_from_purchase', 'is_from_sales'])
# Build existing mappings dict (single loop)
existing_mappings = {mapping.etaxes_item_name: mapping.erp_item for mapping in settings.item_mappings}
# Cache UOM mappings to avoid repeated DB queries
unit_mappings = {}
if etaxes_items:
unique_units = {item.etaxes_unit for item in etaxes_items if item.etaxes_unit}
if unique_units:
unit_results = frappe.get_all('E-Taxes Unit',
filters={'etaxes_unit_name': ['in', list(unique_units)]},
fields=['etaxes_unit_name', 'mapped_unit'])
unit_mappings = {r.etaxes_unit_name: r.mapped_unit for r in unit_results if r.mapped_unit}
# Cache item group existence checks
services_group_exists = frappe.db.exists('Item Group', 'Services')
services_az_group_exists = frappe.db.exists('Item Group', 'Услуги')
unmapped_items = []
# Process items (optimized loop)
for item in etaxes_items:
if item.name not in existing_mappings:
# Get UOM efficiently
uom_to_use = unit_mappings.get(item.etaxes_unit, settings.default_uom)
# Extract values once
is_service = item.get('is_service_item', 0)
is_from_purchase = item.get('is_from_purchase', 0)
is_from_sales = item.get('is_from_sales', 0)
# Calculate flags efficiently
is_stock_item = 0 if is_service else 1
# Purchase item logic
if is_from_purchase:
is_purchase_item = 1
elif is_service and not is_from_purchase and not is_from_sales:
is_purchase_item = 1
else:
is_purchase_item = 0
# Sales item logic
if is_from_sales:
is_sales_item = 1
elif is_service and not is_from_purchase and not is_from_sales:
is_sales_item = 1
else:
is_sales_item = 0
# Item group logic
item_group = settings.default_item_group
if is_service:
if services_group_exists:
item_group = 'Services'
elif services_az_group_exists:
item_group = 'Услуги'
# Build item data
item_data = {
'name': item.name,
'etaxes_item_name': item.etaxes_item_name,
'etaxes_item_code': item.etaxes_item_code,
'item_group': item_group,
'uom': uom_to_use,
'is_stock_item': is_stock_item,
'is_purchase_item': is_purchase_item,
'is_sales_item': is_sales_item,
'item_tax_template': settings.default_item_tax_template,
'is_service_item': is_service,
'is_from_purchase': is_from_purchase,
'is_from_sales': is_from_sales
}
# Add price if exists
if item.etaxes_price:
item_data['standard_rate'] = item.etaxes_price
unmapped_items.append(item_data)
return {
'success': True,
'items': unmapped_items
}
except Exception as e:
frappe.log_error(f"Error in get_unmapped_items: {str(e)}\n{frappe.get_traceback()}", "Get Unmapped Items Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_items():
"""Matching items by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all items with "New" status from E-Taxes
etaxes_items = frappe.get_all('E-Taxes Item',
filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.item_mappings:
existing_mappings[mapping.etaxes_item_name] = mapping.erp_item
# Get items with empty mapping (exist in table but erp_item is None or empty)
items_with_empty_mapping = []
for etaxes_id, erp_item in existing_mappings.items():
if not erp_item: # If erp_item is None or empty string
# Find corresponding E-Taxes item
for item in etaxes_items:
if item.name == etaxes_id:
items_with_empty_mapping.append(item)
break
# Filter only those that are not in mappings
unmapped_items = []
for item in etaxes_items:
if item.name not in existing_mappings:
unmapped_items.append(item)
# Combine two lists: items without mappings and items with empty mappings
items_to_process = unmapped_items + items_with_empty_mapping
# Get all items from system
system_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code'])
matched_count = 0
total_processed = len(items_to_process)
# Get fresh copy of settings document
doc = frappe.get_single('E-Taxes Settings')
# Process all items
for etaxes_item in items_to_process:
# Find similar item
best_match = None
best_score = 0
# Normalize E-Taxes item name
etaxes_name = normalize_string(etaxes_item.etaxes_item_name, consider_azeri)
for item in system_items:
# Normalize system item name
item_name = normalize_string(item.item_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, item_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = item
if best_match:
# IMPORTANT: check if there is already a record for this item in mappings
existing_row = None
for idx, mapping in enumerate(doc.item_mappings):
if mapping.etaxes_item_name == etaxes_item.name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_item value
doc.item_mappings[existing_row].erp_item = best_match.name
doc.item_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('item_mappings', {
'etaxes_item_name': etaxes_item.name,
'erp_item': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Item status only for items that have a match
item_doc = frappe.get_doc('E-Taxes Item', etaxes_item.name)
item_doc.status = 'Mapped'
item_doc.mapped_item = best_match.name
item_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} items'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_items():
"""Creating items for unmapped elements from E-Taxes settings table with EQM Code support"""
# Записываем активность
record_etaxes_activity()
try:
import re
# Get settings
settings_doc = frappe.get_single('E-Taxes Settings')
# Get unmapped items efficiently
unmapped_etaxes_items = []
for mapping_item in settings_doc.item_mappings:
if not mapping_item.erp_item and frappe.db.exists('E-Taxes Item', mapping_item.etaxes_item_name):
unmapped_etaxes_items.append(mapping_item.etaxes_item_name)
if not unmapped_etaxes_items:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped items in table to create"
}
# Batch load E-Taxes Items с EQM кодом
etaxes_items_data = frappe.get_all('E-Taxes Item',
filters={'name': ['in', unmapped_etaxes_items]},
fields=['name', 'etaxes_item_name', 'etaxes_item_code',
'etaxes_product_group_code', 'etaxes_product_group_name',
'is_service_item', 'is_from_purchase', 'is_from_sales',
'eqm_code']) # НОВОЕ: Добавляем eqm_code
# Build mapping settings dict
mapping_settings = {}
for mapping in settings_doc.item_mappings:
if any([mapping.item_group, mapping.uom, mapping.is_stock_item is not None,
mapping.is_purchase_item is not None, mapping.is_sales_item is not None,
mapping.item_tax_template]):
mapping_settings[mapping.etaxes_item_name] = {
'item_group': mapping.item_group,
'uom': mapping.uom,
'is_stock_item': mapping.is_stock_item,
'is_purchase_item': mapping.is_purchase_item,
'is_sales_item': mapping.is_sales_item,
'item_tax_template': mapping.item_tax_template
}
# Cache existence checks
services_group_exists = frappe.db.exists('Item Group', 'Services')
services_az_group_exists = frappe.db.exists('Item Group', 'Услуги')
default_group_exists = frappe.db.exists('Item Group', settings_doc.default_item_group)
default_uom_exists = frappe.db.exists('UOM', settings_doc.default_uom)
# Check for custom fields once
product_group_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_code'})
product_group_name_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_name'})
eqm_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'eqm_code'}) # НОВОЕ: Проверяем поле eqm_code
item_group_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'product_group_code'}) # НОВОЕ: Проверяем поле product_group_code
product_group_name_field_length = 140
if product_group_name_field_exists:
custom_field = frappe.get_doc('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_name'})
product_group_name_field_length = custom_field.length or 140
created_count = 0
error_count = 0
for etaxes_item_data in etaxes_items_data:
try:
# Skip if item already exists
if frappe.db.exists('Item', {'item_name': etaxes_item_data.etaxes_item_name}):
continue
# Create new item
item_doc = frappe.new_doc("Item")
# Generate unique item_code
item_code = etaxes_item_data.etaxes_item_code or etaxes_item_data.etaxes_item_name
item_code = re.sub(r'[^a-zA-Z0-9_.-]', '', item_code)
if not item_code or len(item_code) < 3:
item_code = "ITEM-" + frappe.generate_hash(length=8)
if frappe.db.exists('Item', {'item_code': item_code}):
item_code = item_code + "-" + frappe.generate_hash(length=5)
item_doc.item_code = item_code
item_doc.item_name = etaxes_item_data.etaxes_item_name
item_doc.description = etaxes_item_data.etaxes_item_name
# Get individual settings
individual_settings = mapping_settings.get(etaxes_item_data.name, {})
# Set item group
item_group = individual_settings.get('item_group') or settings_doc.default_item_group
if not default_group_exists and item_group == settings_doc.default_item_group:
item_group = "All Item Groups"
item_doc.item_group = item_group
# Set UOM
stock_uom = individual_settings.get('uom') or settings_doc.default_uom
if not default_uom_exists and stock_uom == settings_doc.default_uom:
stock_uom = "Nos"
item_doc.stock_uom = stock_uom
# Set item flags based on logic
is_service = etaxes_item_data.get('is_service_item', 0)
is_from_purchase = etaxes_item_data.get('is_from_purchase', 0)
is_from_sales = etaxes_item_data.get('is_from_sales', 0)
# is_stock_item
if individual_settings.get('is_stock_item') is not None:
item_doc.is_stock_item = individual_settings['is_stock_item']
else:
item_doc.is_stock_item = 0 if is_service else 1
# is_purchase_item
if individual_settings.get('is_purchase_item') is not None:
item_doc.is_purchase_item = individual_settings['is_purchase_item']
else:
if is_from_purchase or (is_service and not is_from_purchase and not is_from_sales):
item_doc.is_purchase_item = 1
else:
item_doc.is_purchase_item = 0
# is_sales_item
if individual_settings.get('is_sales_item') is not None:
item_doc.is_sales_item = individual_settings['is_sales_item']
else:
if is_from_sales or (is_service and not is_from_purchase and not is_from_sales):
item_doc.is_sales_item = 1
else:
item_doc.is_sales_item = 0
# Auto-set service item group
if is_service and not individual_settings.get('item_group'):
if services_group_exists:
item_doc.item_group = 'Services'
elif services_az_group_exists:
item_doc.item_group = 'Услуги'
# Add product group info to custom fields
if product_group_code_field_exists and etaxes_item_data.etaxes_product_group_code:
item_doc.etaxes_product_group_code = etaxes_item_data.etaxes_product_group_code
if product_group_name_field_exists and etaxes_item_data.etaxes_product_group_name:
product_group_name = etaxes_item_data.etaxes_product_group_name
if len(product_group_name) > product_group_name_field_length:
product_group_name = product_group_name[:product_group_name_field_length]
item_doc.etaxes_product_group_name = product_group_name
# НОВОЕ: Добавляем EQM код если поле существует и код есть
if eqm_code_field_exists and etaxes_item_data.eqm_code:
item_doc.eqm_code = etaxes_item_data.eqm_code
frappe.log_error(f"Added EQM Code {etaxes_item_data.eqm_code} to item {item_doc.item_name}", "EQM Code Transfer")
# НОВОЕ: Добавляем product_group_code (Item Group Code) если поле существует и код есть
if item_group_code_field_exists and etaxes_item_data.etaxes_product_group_code:
item_doc.product_group_code = etaxes_item_data.etaxes_product_group_code
# Add tax template
tax_template = individual_settings.get('item_tax_template') or settings_doc.default_item_tax_template
if tax_template and frappe.db.exists('Item Tax Template', tax_template):
tax_row = item_doc.append('taxes', {})
tax_row.item_tax_template = tax_template
# Save item
item_doc.insert()
# Update mapping in settings
for mapping in settings_doc.item_mappings:
if mapping.etaxes_item_name == etaxes_item_data.name:
mapping.erp_item = item_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Item status
frappe.db.set_value('E-Taxes Item', etaxes_item_data.name, {
'status': 'Mapped',
'mapped_item': item_doc.name
})
created_count += 1
except Exception as e:
frappe.log_error(f"Error creating item for {etaxes_item_data.etaxes_item_name}: {str(e)}", "Create Item Error")
error_count += 1
# Save settings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} items, errors: {error_count}"
}
except Exception as e:
frappe.log_error(f"Error in create_unmapped_items: {str(e)}\n{frappe.get_traceback()}", "Create Items Error")
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_purchase_invoice_from_order(purchase_order_name):
"""Создает Purchase Invoice из Purchase Order используя стандартную функцию ERPNext"""
try:
# Импортируем стандартную функцию из модуля 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)
# ДОБАВЛЕНО: Логируем дату сразу после создания
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
if po_doc.taxes:
pi_doc.taxes = []
for tax_row in po_doc.taxes:
pi_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,
"add_deduct_tax": tax_row.add_deduct_tax
})
# Сохраняем документ
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
return pi_doc.name
except Exception as e:
frappe.log_error(f"Error creating Purchase Invoice from PO {purchase_order_name}: {str(e)}\n{frappe.get_traceback()}", "Purchase Invoice Creation Error")
return None
@frappe.whitelist()
def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None):
"""Import invoice taking into account NEW supplier mappings"""
# Записываем активность
record_etaxes_activity()
try:
# Deserialize if necessary
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:
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
# ИЗМЕНЕНО: Используем supplier_mappings вместо party_mappings
supplier_mappings = {}
for mapping in settings.supplier_mappings:
if mapping.etaxes_supplier_name and mapping.erp_supplier:
# ИСПРАВЛЕНИЕ: Обрезаем ключ до 140 символов
supplier_name = mapping.etaxes_supplier_name[:140] if len(mapping.etaxes_supplier_name) > 140 else mapping.etaxes_supplier_name
key = f"{supplier_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}"
supplier_mappings[key] = mapping.erp_supplier
# 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.'
}
# ИСПРАВЛЕНО: Определяем дату ДО создания 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)
else:
po = frappe.new_doc('Purchase Order')
# ИЗМЕНЕНО: Set supplier используя supplier_mappings с обрезкой
sender = invoice_data.get('sender', {})
sender_name = sender.get('name', '')
# ИСПРАВЛЕНИЕ: Обрезаем название поставщика до 140 символов для поиска
sender_name_truncated = sender_name[:140] if len(sender_name) > 140 else sender_name
sender_key = f"{sender_name_truncated.lower()}|{sender.get('tin', '').lower()}"
if sender_key in supplier_mappings and supplier_mappings[sender_key]:
po.supplier = supplier_mappings[sender_key]
else:
return {
'success': False,
'unmatched_suppliers': [{
'name': sender.get('name', ''),
'tin': sender.get('tin', ''),
'type': 'Sender'
}],
'message': f'No mapping found for supplier: {sender.get("name", "")}'
}
frappe.log_error(f"[DATE DEBUG] Final invoice_date for PO: {invoice_date}", "Date Debug")
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')
# Add/update additional fields from invoice
po.title = f"Invoice {invoice_data.get('serialNumber', '')}"
po.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
# Clear existing items if need to update them completely
if purchase_order_name and invoice_data.get("items"):
po.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 = 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 PO
po_item = frappe.new_doc("Purchase Order Item")
po_item.parent = po.name
po_item.parenttype = "Purchase Order"
po_item.parentfield = "items"
po_item.item_code = mapped_item
try:
item_doc = frappe.get_doc('Item', mapped_item)
po_item.item_name = item_doc.item_name
po_item.description = item_doc.description or item_doc.item_name
except:
po_item.item_name = item.get("productName", "").strip()
po_item.description = item.get("productName", "").strip()
po_item.qty = item.get("quantity", 0)
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.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
# 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 PO. Invoice data: {invoice_data}",
"Import Invoice Error"
)
return {
'success': False,
'message': 'Could not add any items to order'
}
# ДОБАВЛЕНО: Создание строки ƏDV в Purchase 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
po.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}%", "Purchase Order Tax")
else:
frappe.log_error("ƏDV account (521.3) not found", "Purchase Order Tax Warning")
else:
frappe.log_error("No VAT amount in E-Taxes data, skipping ƏDV tax creation", "Purchase Order Tax Info")
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()
# ИСПРАВЛЕНО: Убедимся что все 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', '')
sender_name = invoice_data.get('sender', {}).get('name', '') if invoice_data.get('sender') else ''
total = invoice_data.get('totalAmount', 0) or invoice_data.get('amount', 0)
# Создаем E-Taxes Purchase
etaxes_purchase_result = create_etaxes_purchase(invoice_id, date_to_use, sender_name, total)
if etaxes_purchase_result and etaxes_purchase_result.get('success'):
# Устанавливаем поля E-Taxes ДО submit'а
po.is_taxes_doc = 1
po.taxes_doc = etaxes_purchase_result.get('name')
# Сохраняем изменения
po.save()
# ИЗМЕНЕНИЕ: Делаем Submit для Purchase Order
try:
po.submit()
frappe.log_error(f"Purchase Order {po.name} submitted successfully", "Import Invoice Success")
except Exception as e:
frappe.log_error(f"Error submitting Purchase Order {po.name}: {str(e)}", "Submit PO Error")
return {
'success': False,
'message': f'Failed to submit Purchase Order: {str(e)}'
}
# ИЗМЕНЕНИЕ: Создаем Purchase Invoice
try:
pi_name = create_purchase_invoice_from_order(po.name)
if pi_name:
# Делаем Submit для Purchase Invoice
pi = frappe.get_doc("Purchase Invoice", pi_name)
pi.submit()
frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success")
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.',
'purchase_order': po.name,
'purchase_invoice': pi_name
}
else:
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
'purchase_order': po.name
}
except Exception as e:
frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}", "Submit PI Error")
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
'purchase_order': po.name
}
except Exception as e:
frappe.log_error(f"Error in import_invoice_with_mapping: {str(e)}\n{frappe.get_traceback()}", "Import Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_etaxes_purchases():
"""Gets list of all E-Taxes Purchase records"""
try:
purchases = frappe.get_all('E-Taxes Purchase',
fields=['name', 'etaxes_id', 'date', 'party', 'total'])
# Debug logging
frappe.logger().info(f"Retrieved {len(purchases)} E-Taxes Purchase records")
return {
'success': True,
'purchases': purchases
}
except Exception as e:
frappe.log_error(f"Error in get_etaxes_purchases: {str(e)}", "API Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_etaxes_purchase(etaxes_id, date, party, total):
"""Creates E-Taxes Purchase record for tracking imported 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 Purchase: 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 Purchase', {'etaxes_id': etaxes_id}, 'name')
if existing:
# Debug logging
frappe.logger().info(f"E-Taxes Purchase already exists: {existing}")
return {
'success': True,
'message': 'Record already exists',
'name': existing
}
# Create new record
etaxes_purchase = frappe.new_doc('E-Taxes Purchase')
etaxes_purchase.etaxes_id = etaxes_id
etaxes_purchase.date = date
etaxes_purchase.party = party
etaxes_purchase.total = total
etaxes_purchase.insert()
frappe.db.commit() # Explicit commit
# Debug logging
frappe.logger().info(f"Created new E-Taxes Purchase: {etaxes_purchase.name}")
return {
'success': True,
'name': etaxes_purchase.name
}
except Exception as e:
frappe.log_error(f"Error in create_etaxes_purchase: {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 link_purchase_order_to_etaxes(purchase_order, etaxes_purchase):
"""Sets link between Purchase Order and E-Taxes Purchase"""
# Записываем активность
record_etaxes_activity()
try:
# Debug logging
frappe.logger().info(f"Linking Purchase Order {purchase_order} to E-Taxes Purchase {etaxes_purchase}")
# Check if both documents exist
po_exists = frappe.db.exists('Purchase Order', purchase_order)
etaxes_exists = frappe.db.exists('E-Taxes Purchase', etaxes_purchase)
if not po_exists or not etaxes_exists:
frappe.log_error(f"Link error: PO exists={po_exists}, E-Taxes exists={etaxes_exists}", "Link Error")
return {
'success': False,
'message': 'One or both documents do not exist'
}
# Get Purchase Order document
po_doc = frappe.get_doc('Purchase Order', purchase_order)
# Set link to E-Taxes Purchase
po_doc.is_taxes_doc = 1
po_doc.taxes_doc = etaxes_purchase
# Save changes
po_doc.save()
frappe.db.commit() # Explicit commit
# Debug logging
frappe.logger().info(f"Successfully linked PO and E-Taxes Purchase")
return {
'success': True
}
except Exception as e:
frappe.log_error(f"Error in link_purchase_order_to_etaxes: {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 on_delete_purchase_order(doc, method):
"""Deletes related E-Taxes Purchase record when Purchase Order is deleted"""
if doc.is_taxes_doc and doc.taxes_doc:
try:
frappe.logger().info(f"Deleting E-Taxes Purchase {doc.taxes_doc} due to Purchase Order {doc.name} deletion")
# Check if E-Taxes Purchase record exists
if frappe.db.exists("E-Taxes Purchase", doc.taxes_doc):
frappe.delete_doc('E-Taxes Purchase', doc.taxes_doc, ignore_permissions=True, force=True)
frappe.db.commit()
frappe.logger().info(f"Successfully deleted E-Taxes Purchase {doc.taxes_doc}")
except Exception as e:
frappe.log_error(f"Error deleting E-Taxes Purchase {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
"Purchase Order Delete Error")
@frappe.whitelist()
def get_unmapped_units():
"""Getting unmapped units from E-Taxes"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped units from E-Taxes
unmapped_units = []
# 1. Get all units with 'New' status
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'New'},
fields=['name', 'etaxes_unit_name', 'etaxes_unit_code'])
# 2. Check if they are already in settings
existing_mappings = {}
for mapping in settings.unit_mappings:
existing_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# 3. Add only those that are not in settings
for unit in etaxes_units:
if unit.name not in existing_mappings:
# ПРАВИЛЬНО: возвращаем объект с нужными полями для Link форматера
unit_data = {
'value': unit.name, # ID документа для Link поля
'label': unit.etaxes_unit_name, # Отображаемое имя
'etaxes_unit_name': unit.etaxes_unit_name, # Для link_formatter
'name': unit.name,
'etaxes_unit_code': unit.etaxes_unit_code
}
unmapped_units.append(unit_data)
return {
'success': True,
'units': unmapped_units
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_units():
"""Matching units by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all units with "New" status from E-Taxes
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'New'},
fields=['name', 'etaxes_unit_name', 'etaxes_unit_code'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.unit_mappings:
existing_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# Get units with empty mapping (exist in table but erp_unit is None or empty)
units_with_empty_mapping = []
for etaxes_id, erp_unit in existing_mappings.items():
if not erp_unit: # If erp_unit is None or empty string
# Find corresponding E-Taxes unit
for unit in etaxes_units:
if unit.name == etaxes_id:
units_with_empty_mapping.append(unit)
break
# Filter only those that are not in mappings
unmapped_units = []
for unit in etaxes_units:
if unit.name not in existing_mappings:
unmapped_units.append(unit)
# Combine two lists: units without mappings and units with empty mappings
units_to_process = unmapped_units + units_with_empty_mapping
# Get all UOMs from system
system_units = frappe.get_all('UOM', fields=['name', 'uom_name'])
matched_count = 0
total_processed = len(units_to_process)
# Get fresh copy of settings document
doc = frappe.get_single('E-Taxes Settings')
# Process all units
for etaxes_unit in units_to_process:
# Find similar unit
best_match = None
best_score = 0
# Normalize E-Taxes unit name
etaxes_name = normalize_string(etaxes_unit.etaxes_unit_name, consider_azeri)
for unit in system_units:
# Normalize system unit name
unit_name = normalize_string(unit.uom_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, unit_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = unit
if best_match:
# IMPORTANT: check if there is already a record for this unit in mappings
existing_row = None
for idx, mapping in enumerate(doc.unit_mappings):
if mapping.etaxes_unit_name == etaxes_unit.name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_unit value
doc.unit_mappings[existing_row].erp_unit = best_match.name
doc.unit_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('unit_mappings', {
'etaxes_unit_name': etaxes_unit.name,
'erp_unit': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Unit status only for units that have a match
unit_doc = frappe.get_doc('E-Taxes Unit', etaxes_unit.name)
unit_doc.status = 'Mapped'
unit_doc.mapped_unit = best_match.name
unit_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} units'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_units():
"""Creating units for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
import re
# Get settings
settings_doc = frappe.get_single('E-Taxes Settings')
# Get elements from table that have no mapping
unmapped_units = []
for mapping_unit in settings_doc.unit_mappings:
# Check that unit has no mapping or it's empty
if not mapping_unit.erp_unit:
# Get E-Taxes Unit document for this record
if frappe.db.exists('E-Taxes Unit', mapping_unit.etaxes_unit_name):
etaxes_unit = frappe.get_doc('E-Taxes Unit', mapping_unit.etaxes_unit_name)
unmapped_units.append(etaxes_unit)
if len(unmapped_units) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped units in table to create"
}
created_count = 0
error_count = 0
for etaxes_unit in unmapped_units:
try:
# Check if a UOM with this name already exists
uom_exists = frappe.db.exists('UOM', {'uom_name': etaxes_unit.etaxes_unit_name})
if uom_exists:
continue
# Create new UOM
uom_doc = frappe.new_doc("UOM")
# Standard fields for UOM
uom_doc.uom_name = etaxes_unit.etaxes_unit_name
uom_doc.enabled = 1
# Set a default conversion factor if needed
uom_doc.must_be_whole_number = 0
# Save UOM
uom_doc.insert()
# Update mapping in settings
for mapping in settings_doc.unit_mappings:
if mapping.etaxes_unit_name == etaxes_unit.name:
mapping.erp_unit = uom_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Unit status
etaxes_unit.status = 'Mapped'
etaxes_unit.mapped_unit = uom_doc.name
etaxes_unit.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} units, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def load_units_from_invoices(date_from, date_to, max_count=200, offset=0):
"""Loading invoices list for a period - returns invoices for frontend processing"""
# Записываем активность
record_etaxes_activity()
try:
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
try:
offset = int(offset)
except (ValueError, TypeError):
offset = 0
# Format filters for the API request
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(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response_inbox:
return {
'success': False,
'error': response_inbox.get('error'),
'message': response_inbox['message'] if 'message' in response_inbox else 'Failed to retrieve invoices'
}
invoices_inbox = response_inbox.get('data', []) or response_inbox.get('invoices', [])
# Помечаем источник для каждого инвойса из inbox
for invoice in invoices_inbox:
invoice['_source'] = 'inbox'
# Инициализируем all_invoices с inbox инвойсами
all_invoices = invoices_inbox[:] # Создаем копию списка
# Попытка загрузить из outbox (исходящие инвойсы/продажи)
try:
from .sales_api import get_sales_invoices
response_outbox = get_sales_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' not in response_outbox:
invoices_outbox = response_outbox.get('data', []) or response_outbox.get('invoices', [])
# Помечаем источник для каждого инвойса из outbox
for invoice in invoices_outbox:
invoice['_source'] = 'outbox'
# Добавляем outbox инвойсы к inbox инвойсам
all_invoices.extend(invoices_outbox)
# Определяем hasMore на основе обоих источников
hasMore_inbox = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
hasMore_outbox = response_outbox.get('hasMore', False) or response_outbox.get('total', 0) > offset + max_count
hasMore = hasMore_inbox or hasMore_outbox
else:
# Если не удалось загрузить из outbox, используем только inbox
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
except ImportError:
# Если модуль sales_api недоступен, используем только inbox
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
except Exception as e:
# При любой другой ошибке используем только inbox
frappe.log_error(f"Error loading from outbox: {str(e)}", "Load Units Outbox Error")
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
return {
'success': True,
'invoices': all_invoices,
'hasMore': hasMore,
'token': asan_login_settings['main_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_units_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Units Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def update_mapped_statuses(doc, method=None):
"""Update mapped statuses in E-Taxes DocTypes when settings are saved"""
try:
# 1. Handle E-Taxes Unit mappings
if hasattr(doc, 'unit_mappings'):
# Создаем словарь текущих соответствий
current_unit_mappings = {}
for mapping in doc.unit_mappings:
if mapping.etaxes_unit_name:
# Проверяем что erp_unit не None и не пустая строка
erp_unit = getattr(mapping, 'erp_unit', None)
current_unit_mappings[mapping.etaxes_unit_name] = erp_unit if erp_unit else None
# Обновляем статусы для всех единиц в mappings
for etaxes_unit_name, erp_unit in current_unit_mappings.items():
try:
if frappe.db.exists('E-Taxes Unit', etaxes_unit_name):
if erp_unit: # Если есть соответствие
frappe.db.set_value('E-Taxes Unit', etaxes_unit_name, {
'status': 'Mapped',
'mapped_unit': erp_unit
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Unit', etaxes_unit_name, {
'status': 'New',
'mapped_unit': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Unit {etaxes_unit_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для единиц, которые были удалены из mappings
try:
mapped_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'Mapped'},
fields=['name', 'mapped_unit'])
for unit in mapped_units:
if unit.name not in current_unit_mappings:
frappe.db.set_value('E-Taxes Unit', unit.name, {
'status': 'New',
'mapped_unit': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped units: {str(e)}", "Status Update Error")
# 2. Handle E-Taxes Item mappings
if hasattr(doc, 'item_mappings'):
# Создаем словарь текущих соответствий
current_item_mappings = {}
for mapping in doc.item_mappings:
if mapping.etaxes_item_name:
# Проверяем что erp_item не None и не пустая строка
erp_item = getattr(mapping, 'erp_item', None)
current_item_mappings[mapping.etaxes_item_name] = erp_item if erp_item else None
# Обновляем статусы для всех товаров в mappings
for etaxes_item_name, erp_item in current_item_mappings.items():
try:
if frappe.db.exists('E-Taxes Item', etaxes_item_name):
if erp_item: # Если есть соответствие
frappe.db.set_value('E-Taxes Item', etaxes_item_name, {
'status': 'Mapped',
'mapped_item': erp_item
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Item', etaxes_item_name, {
'status': 'New',
'mapped_item': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Item {etaxes_item_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для товаров, которые были удалены из mappings
try:
mapped_items = frappe.get_all('E-Taxes Item',
filters={'status': 'Mapped'},
fields=['name', 'mapped_item'])
for item in mapped_items:
if item.name not in current_item_mappings:
frappe.db.set_value('E-Taxes Item', item.name, {
'status': 'New',
'mapped_item': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped items: {str(e)}", "Status Update Error")
# 3. Handle E-Taxes Customers mappings
if hasattr(doc, 'customer_mappings'):
# Создаем словарь текущих соответствий
current_customer_mappings = {}
for mapping in doc.customer_mappings:
if mapping.etaxes_customer_name:
# Проверяем что erp_customer не None и не пустая строка
erp_customer = getattr(mapping, 'erp_customer', None)
current_customer_mappings[mapping.etaxes_customer_name] = erp_customer if erp_customer else None
# Обновляем статусы для всех customers в mappings
for etaxes_customer_name, erp_customer in current_customer_mappings.items():
try:
if frappe.db.exists('E-Taxes Customers', etaxes_customer_name):
if erp_customer: # Если есть соответствие
frappe.db.set_value('E-Taxes Customers', etaxes_customer_name, {
'status': 'Mapped',
'mapped_customer': erp_customer
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Customers', etaxes_customer_name, {
'status': 'New',
'mapped_customer': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Customers {etaxes_customer_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для customers, которые были удалены из mappings
try:
mapped_customers = frappe.get_all('E-Taxes Customers',
filters={'status': 'Mapped'},
fields=['name', 'mapped_customer'])
for customer in mapped_customers:
if customer.etaxes_party_name not in current_customer_mappings:
frappe.db.set_value('E-Taxes Customers', customer.name, {
'status': 'New',
'mapped_customer': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped customers: {str(e)}", "Status Update Error")
# 4. Handle E-Taxes Suppliers mappings
if hasattr(doc, 'supplier_mappings'):
# Создаем словарь текущих соответствий
current_supplier_mappings = {}
for mapping in doc.supplier_mappings:
if mapping.etaxes_supplier_name:
# Проверяем что erp_supplier не None и не пустая строка
erp_supplier = getattr(mapping, 'erp_supplier', None)
current_supplier_mappings[mapping.etaxes_supplier_name] = erp_supplier if erp_supplier else None
# Обновляем статусы для всех suppliers в mappings
for etaxes_supplier_name, erp_supplier in current_supplier_mappings.items():
try:
if frappe.db.exists('E-Taxes Suppliers', etaxes_supplier_name):
if erp_supplier: # Если есть соответствие
frappe.db.set_value('E-Taxes Suppliers', etaxes_supplier_name, {
'status': 'Mapped',
'mapped_supplier': erp_supplier
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Suppliers', etaxes_supplier_name, {
'status': 'New',
'mapped_supplier': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Suppliers {etaxes_supplier_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для suppliers, которые были удалены из mappings
try:
mapped_suppliers = frappe.get_all('E-Taxes Suppliers',
filters={'status': 'Mapped'},
fields=['name', 'mapped_supplier'])
for supplier in mapped_suppliers:
if supplier.etaxes_party_name not in current_supplier_mappings:
frappe.db.set_value('E-Taxes Suppliers', supplier.name, {
'status': 'New',
'mapped_supplier': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped suppliers: {str(e)}", "Status Update Error")
# Явно коммитим изменения
frappe.db.commit()
except Exception as e:
frappe.log_error(f"Error updating mapped statuses: {str(e)}\n{frappe.get_traceback()}",
"E-Taxes Settings Error")
@frappe.whitelist()
def refresh_item_mappings_display():
"""Операция-пустышка для обновления отображения item mappings"""
try:
# Получаем документ настроек
settings_doc = frappe.get_single('E-Taxes Settings')
# Подготавливаем данные для обновления отображения на клиенте
updated_mappings = []
for mapping in settings_doc.item_mappings:
if mapping.etaxes_item_name:
# Получаем display name для Link поля
display_name = frappe.db.get_value('E-Taxes Item', mapping.etaxes_item_name, 'etaxes_item_name')
updated_mappings.append({
'idx': mapping.idx,
'name': mapping.name,
'etaxes_item_name_id': mapping.etaxes_item_name,
'etaxes_item_name_display': display_name or mapping.etaxes_item_name
})
return {
'success': True,
'message': 'Display refreshed successfully',
'updated_mappings': updated_mappings
}
except Exception as e:
frappe.log_error(f"Error refreshing display: {str(e)}", "Display Refresh Error")
return {
'success': False,
'message': "An error occurred while refreshing display"
}
@frappe.whitelist()
def process_single_invoice_for_items(token, invoice_id, source_type='purchase'):
"""Processing a single invoice for item extraction with EQM Code assignment"""
# Записываем активность
record_etaxes_activity()
try:
created_count = 0
skipped_count = 0
failed_count = 0
updated_count = 0
# Получаем детали инвойса
invoice_details = get_invoice_details(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')
}
serial_number = invoice_details.get('serialNumber', '')
items = invoice_details.get('items', [])
# Определяем тип источника
is_from_purchase = 1 if source_type == 'purchase' else 0
is_from_sales = 1 if source_type == 'sales' else 0
# Словарь для предотвращения дубликатов в пределах одного инвойса
unique_items = {}
for item in items:
item_name = item.get('productName', '').strip()
item_code = item.get('itemId', '') or f"CODE-{serial_number}"
# Пропускаем пустые товары
if not item_name:
failed_count += 1
continue
# ИСПРАВЛЕНИЕ: Просто обрезаем до 140 символов
if len(item_name) > 140:
item_name = item_name[:140]
# Проверяем существование по обрезанному имени
existing_item = frappe.db.get_value('E-Taxes Item', item_name,
['name', 'is_from_purchase', 'is_from_sales'], as_dict=True)
if existing_item:
# Если товар уже существует, обновляем флаги источника при необходимости
need_update = False
update_data = {}
if is_from_purchase and not existing_item.is_from_purchase:
update_data['is_from_purchase'] = 1
need_update = True
if is_from_sales and not existing_item.is_from_sales:
update_data['is_from_sales'] = 1
need_update = True
if need_update:
try:
frappe.db.set_value('E-Taxes Item', item_name, update_data)
updated_count += 1
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Item {item_name}: {str(e)}", "Update Item Source Error")
skipped_count += 1
continue
# Используем обрезанное имя товара как ключ
if item_name in unique_items:
continue
# Обрабатываем информацию о группе товаров
product_group = item.get('productGroup', {})
product_group_code = product_group.get('code', '') if product_group else ''
product_group_type = product_group.get('type', '') if product_group else ''
# Получаем название группы товаров (предпочитаем азербайджанский язык)
product_group_name = ''
if product_group and product_group.get('name'):
names = product_group.get('name', {})
if isinstance(names, dict):
product_group_name = names.get('az', '') or names.get('en', '') or names.get('ru', '')
elif isinstance(names, str):
product_group_name = names
# НОВОЕ: Обрезаем название группы товаров до 1000 символов
if len(product_group_name) > 1000:
product_group_name = product_group_name[:1000]
frappe.log_error(f"Product group name truncated for item {item_name}: original length {len(product_group.get('name', {}).get('az', ''))}", "Product Group Name Truncation")
# НОВОЕ: Поиск EQM Code по коду группы товаров - возвращаем name для Link поля
eqm_code = None
if product_group_code:
try:
frappe.log_error(f"[EQM DEBUG] Searching for product_group_code: '{product_group_code}'", "EQM Code Search")
# Ищем запись и получаем и name, и eqm_name
eqm_record = frappe.db.sql("""
SELECT name, eqm_name
FROM `tabEQM Codes`
WHERE eqm_name LIKE %s
LIMIT 1
""", (f"{product_group_code} -%",), as_dict=True)
frappe.log_error(f"[EQM DEBUG] SQL search result: {eqm_record}", "EQM Code Search")
if eqm_record:
# ИСПРАВЛЕНИЕ: Сохраняем name (ID записи) для Link поля, а не eqm_name
eqm_code = eqm_record[0].get('name') # Изменено с eqm_name на name
frappe.log_error(f"[EQM DEBUG] Found EQM Code ID: '{eqm_code}' (description: '{eqm_record[0].get('eqm_name')}') for product group '{product_group_code}' in item {item_name}", "EQM Code Assignment")
else:
frappe.log_error(f"[EQM DEBUG] EQM Code not found for product group '{product_group_code}' in item {item_name}", "EQM Code Not Found")
except Exception as e:
frappe.log_error(f"[EQM DEBUG] Error searching EQM Code for '{product_group_code}': {str(e)}", "EQM Code Search Error")
# Логируем финальное значение
frappe.log_error(f"[EQM DEBUG] Final eqm_code ID to save: '{eqm_code}' for item {item_name}", "EQM Code Final")
# Логируем финальное значение
frappe.log_error(f"[EQM DEBUG] Final eqm_code value to save: '{eqm_code}' for item {item_name}", "EQM Code Final")
# Определяем, является ли товар услугой
is_service = 1 if product_group_type == 'service' else 0
# Добавляем в словарь уникальных товаров
unique_items[item_name] = {
'etaxes_item_name': item_name,
'etaxes_item_code': item_code,
'etaxes_unit': item.get('unit', ''),
'etaxes_price': item.get('pricePerUnit', 0),
'etaxes_product_group_code': product_group_code,
'etaxes_product_group_name': product_group_name,
'etaxes_product_group_type': product_group_type,
'is_service_item': is_service,
'source_invoice': serial_number,
'is_from_purchase': is_from_purchase,
'is_from_sales': is_from_sales,
'status': 'New',
'eqm_code': eqm_code # НОВОЕ: Добавляем EQM код
}
# Создаём записи для уникальных товаров
for item_name, item_data in unique_items.items():
try:
# Финальная проверка перед созданием
existing_check = frappe.db.exists('E-Taxes Item', item_name)
if existing_check:
skipped_count += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Item',
**item_data
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
created_count += 1
except frappe.DuplicateEntryError:
skipped_count += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Item {item_data['etaxes_item_name']}: {str(e)}", "Process Single Invoice Error")
failed_count += 1
return {
'success': True,
'created_count': created_count,
'updated_count': updated_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'invoice_id': invoice_id,
'serial_number': serial_number,
'source_type': source_type
}
except Exception as e:
frappe.log_error(f"Error in process_single_invoice_for_items: {str(e)}\n{frappe.get_traceback()}", "Process Single Invoice Error")
return {
'success': False,
'message': "An unknown error occurred processing this invoice"
}
@frappe.whitelist()
def process_single_invoice_for_units(token, invoice_id, source_type='purchase'):
"""Processing a single invoice for unit extraction with progress support"""
# Записываем активность
record_etaxes_activity()
try:
created_count = 0
skipped_count = 0
failed_count = 0
updated_count = 0
# Получаем детали инвойса
invoice_details = get_invoice_details(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')
}
serial_number = invoice_details.get('serialNumber', '')
items = invoice_details.get('items', [])
# Определяем тип источника
source_type_text = 'Purchase' if source_type == 'purchase' else 'Sales'
# Словарь для предотвращения дубликатов в пределах одного инвойса
unique_units = {}
for item in items:
unit_name = item.get('unit', '')
# Пропускаем пустые единицы
if not unit_name:
continue
# ИСПРАВЛЕНО: Проверяем существование по name документа (unit_name)
# так как autoname = "field:etaxes_unit_name"
existing_check = frappe.db.exists('E-Taxes Unit', unit_name)
if existing_check:
skipped_count += 1
continue
# Используем имя единицы как ключ для предотвращения дубликатов в одном инвойсе
if unit_name in unique_units:
continue
# Generate a consistent code for the unit
unit_code = unit_name
unique_units[unit_name] = {
'etaxes_unit_name': unit_name,
'etaxes_unit_code': unit_code,
'source_invoice': serial_number,
'source_type': source_type_text,
'status': 'New'
}
# Создаём записи для уникальных единиц
for unit_name, unit_data in unique_units.items():
try:
# Финальная проверка перед созданием (защита от race condition)
# ИСПРАВЛЕНО: Проверяем по name документа
existing_check = frappe.db.exists('E-Taxes Unit', unit_name)
if existing_check:
skipped_count += 1
continue
# ИСПРАВЛЕНО: Используем insert с ignore_if_duplicate=True
doc = frappe.get_doc({
'doctype': 'E-Taxes Unit',
**unit_data
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
created_count += 1
except frappe.DuplicateEntryError:
# Если всё-таки произошло дублирование, считаем как пропущенный
skipped_count += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Unit {unit_name}: {str(e)}", "Process Single Invoice Error")
failed_count += 1
return {
'success': True,
'created_count': created_count,
'updated_count': updated_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'invoice_id': invoice_id,
'serial_number': serial_number,
'unique_units': len(unique_units),
'source_type': source_type
}
except Exception as e:
frappe.log_error(f"Error in process_single_invoice_for_units: {str(e)}\n{frappe.get_traceback()}", "Process Single Invoice Error")
return {
'success': False,
'message': "An unknown error occurred processing this invoice"
}
@frappe.whitelist()
def process_invoice_parties_from_list(invoice_list_data, token):
"""Обрабатывает контрагентов из списка инвойсов с разделением на customers и suppliers"""
try:
if isinstance(invoice_list_data, str):
invoices = json.loads(invoice_list_data)
else:
invoices = invoice_list_data
total_customers_created = 0
total_suppliers_created = 0
total_skipped = 0
total_failed = 0
processed_invoices = 0
# Для отслеживания уникальных контрагентов в рамках батча
batch_unique_customers = {}
batch_unique_suppliers = {}
for invoice in invoices:
try:
# Извлекаем данные напрямую из списка инвойсов
serial_number = invoice.get('serialNumber', invoice.get('id', ''))
source = invoice.get('_source', 'inbox')
# Обработка отправителя
sender = invoice.get('sender', {})
if sender:
sender_name = (sender.get('name') or '').strip()
sender_tin = (sender.get('tin') or '').strip()
sender_address = (sender.get('address') or '').strip()
sender_name = ' '.join(sender_name.split())
if sender_name and sender_tin:
# Логика определения типа:
# Purchase invoices (inbox): Sender -> Supplier
# Sales invoices (outbox): Sender -> Customer
if source == 'inbox':
# Inbox: Sender = Supplier
if sender_name not in batch_unique_suppliers:
existing = frappe.db.exists('E-Taxes Suppliers', sender_name)
if not existing:
batch_unique_suppliers[sender_name] = {
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'etaxes_address': sender_address,
'etaxes_party_type': 'Sender',
'source_invoice': serial_number,
'status': 'New'
}
else:
# Outbox: Sender = Customer
if sender_name not in batch_unique_customers:
existing = frappe.db.exists('E-Taxes Customers', sender_name)
if not existing:
batch_unique_customers[sender_name] = {
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'etaxes_address': sender_address,
'etaxes_party_type': 'Sender',
'source_invoice': serial_number,
'status': 'New'
}
# Обработка получателя
receiver = invoice.get('receiver', {})
if receiver:
receiver_name = (receiver.get('name') or '').strip()
receiver_tin = (receiver.get('tin') or '').strip()
receiver_address = (receiver.get('address') or '').strip()
receiver_name = ' '.join(receiver_name.split())
if receiver_name and receiver_tin:
# Логика: Receiver всегда Customer (и для inbox, и для outbox)
if receiver_name not in batch_unique_customers:
existing = frappe.db.exists('E-Taxes Customers', receiver_name)
if not existing:
batch_unique_customers[receiver_name] = {
'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin,
'etaxes_address': receiver_address,
'etaxes_party_type': 'Receiver',
'source_invoice': serial_number,
'status': 'New'
}
processed_invoices += 1
except Exception:
total_failed += 1
processed_invoices += 1
# Создание записей для всех уникальных customers в батче
for customer_name, customer_data in batch_unique_customers.items():
try:
existing = frappe.db.exists('E-Taxes Customers', customer_name)
if existing:
total_skipped += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Customers',
**customer_data
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
total_customers_created += 1
except frappe.DuplicateEntryError:
total_skipped += 1
except Exception:
total_failed += 1
# Создание записей для всех уникальных suppliers в батче
for supplier_name, supplier_data in batch_unique_suppliers.items():
try:
existing = frappe.db.exists('E-Taxes Suppliers', supplier_name)
if existing:
total_skipped += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Suppliers',
**supplier_data
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
total_suppliers_created += 1
except frappe.DuplicateEntryError:
total_skipped += 1
except Exception:
total_failed += 1
return {
'success': True,
'customers_created': total_customers_created,
'suppliers_created': total_suppliers_created,
'skipped_count': total_skipped,
'failed_count': total_failed,
'processed_invoices': processed_invoices,
'total_invoices': len(invoices),
'unique_customers_found': len(batch_unique_customers),
'unique_suppliers_found': len(batch_unique_suppliers)
}
except Exception as e:
frappe.log_error(f"Error in process_invoice_parties_from_list: {str(e)}", "Process Parties Error")
return {
'success': False,
'message': "An unknown error occurred processing invoice list"
}
@frappe.whitelist()
def load_combined_data_from_etaxes(date_from, date_to, load_items=False, load_parties=False, max_count=200):
"""Загружает комбинированные данные из E-Taxes (товары, единицы, контрагенты) за один заход"""
# Записываем активность
record_etaxes_activity()
try:
# Преобразуем строковые параметры в boolean
load_items = load_items in [True, 'true', '1', 1]
load_parties = load_parties in [True, 'true', '1', 1]
frappe.log_error(f"Combined load started: items={load_items}, parties={load_parties}, period={date_from} to {date_to}", "Combined Load Debug")
if not load_items and not load_parties:
return {
'success': False,
'message': 'No data types selected for loading'
}
# Получаем настройки для токена
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']
frappe.log_error(f"Token obtained: {token[:20]}...", "Combined Load Debug")
# Инициализируем статистику
total_stats = {
'items_created': 0,
'items_skipped': 0,
'items_failed': 0,
'units_created': 0,
'units_skipped': 0,
'units_failed': 0,
'parties_created': 0,
'parties_skipped': 0,
'parties_failed': 0,
'invoices_processed': 0,
'invoices_failed': 0
}
# Загружаем список инвойсов (используем существующую функцию)
frappe.log_error(f"Loading invoices list...", "Combined Load Debug")
invoices_result = load_items_from_invoices(date_from, date_to, max_count, 0)
if not invoices_result.get('success'):
frappe.log_error(f"Failed to load invoices: {invoices_result}", "Combined Load Error")
return {
'success': False,
'message': invoices_result.get('message', 'Failed to load invoices'),
'error': invoices_result.get('error')
}
invoices = invoices_result.get('invoices', [])
frappe.log_error(f"Loaded {len(invoices)} invoices", "Combined Load Debug")
if not invoices:
return {
'success': True,
'message': 'No invoices found for the selected period',
'stats': total_stats
}
# Если выбрана загрузка контрагентов - обрабатываем весь список сразу
if load_parties:
frappe.log_error(f"Processing parties from {len(invoices)} invoices", "Combined Load Debug")
try:
parties_result = process_invoice_parties_from_list(invoices, token)
if parties_result.get('success'):
total_stats['parties_created'] = parties_result.get('created_count', 0)
total_stats['parties_skipped'] = parties_result.get('skipped_count', 0)
total_stats['parties_failed'] = parties_result.get('failed_count', 0)
frappe.log_error(f"Parties processed: {total_stats['parties_created']} created, {total_stats['parties_skipped']} skipped", "Combined Load Debug")
else:
total_stats['parties_failed'] = len(invoices)
frappe.log_error(f"Parties processing failed: {parties_result}", "Combined Load Error")
except Exception as e:
frappe.log_error(f"Error processing parties: {str(e)}\n{frappe.get_traceback()}", "Combined Load Error")
total_stats['parties_failed'] = len(invoices)
# Если выбрана загрузка товаров - обрабатываем каждый инвойс отдельно
if load_items:
frappe.log_error(f"Processing items from {len(invoices)} invoices", "Combined Load Debug")
processed_invoices = 0
for i, invoice in enumerate(invoices):
invoice_id = invoice.get('id')
if not invoice_id:
total_stats['invoices_failed'] += 1
continue
# Логируем каждые 10 инвойсов
if i % 10 == 0:
frappe.log_error(f"Processing invoice {i+1}/{len(invoices)}: {invoice_id}", "Combined Load Progress")
try:
# Обрабатываем товары из инвойса
items_result = process_single_invoice_for_items(token, invoice_id)
if items_result.get('success'):
total_stats['items_created'] += items_result.get('created_count', 0)
total_stats['items_skipped'] += items_result.get('skipped_count', 0)
total_stats['items_failed'] += items_result.get('failed_count', 0)
else:
# Проверяем на ошибку аутентификации
if items_result.get('error') == 'unauthorized':
frappe.log_error(f"Unauthorized error during items processing at invoice {i+1}", "Combined Load Error")
return {
'success': False,
'error': 'unauthorized',
'message': 'Authentication required. Please login again.',
'partial_stats': total_stats
}
total_stats['items_failed'] += 1
# Обрабатываем единицы измерения из того же инвойса (автоматически)
units_result = process_single_invoice_for_units(token, invoice_id)
if units_result.get('success'):
total_stats['units_created'] += units_result.get('created_count', 0)
total_stats['units_skipped'] += units_result.get('skipped_count', 0)
total_stats['units_failed'] += units_result.get('failed_count', 0)
else:
# Проверяем на ошибку аутентификации
if units_result.get('error') == 'unauthorized':
frappe.log_error(f"Unauthorized error during units processing at invoice {i+1}", "Combined Load Error")
return {
'success': False,
'error': 'unauthorized',
'message': 'Authentication required. Please login again.',
'partial_stats': total_stats
}
total_stats['units_failed'] += 1
processed_invoices += 1
total_stats['invoices_processed'] = processed_invoices
# Коммитим каждые 20 инвойсов для сохранения прогресса
if processed_invoices % 20 == 0:
frappe.db.commit()
frappe.log_error(f"Progress saved: {processed_invoices} invoices processed", "Combined Load Progress")
except Exception as e:
frappe.log_error(f"Error processing invoice {invoice_id}: {str(e)}", "Combined Load Error")
total_stats['invoices_failed'] += 1
# Финальный коммит
frappe.db.commit()
# Формируем сообщение с результатами
result_messages = []
if load_items:
result_messages.append(f"Items: {total_stats['items_created']} created, {total_stats['items_skipped']} skipped")
result_messages.append(f"Units: {total_stats['units_created']} created, {total_stats['units_skipped']} skipped")
if load_parties:
result_messages.append(f"Parties: {total_stats['parties_created']} created, {total_stats['parties_skipped']} skipped")
result_message = "; ".join(result_messages)
frappe.log_error(f"Combined load completed successfully: {result_message}", "Combined Load Debug")
return {
'success': True,
'message': f'Data loading completed. {result_message}',
'stats': total_stats
}
except Exception as e:
frappe.log_error(f"Error in load_combined_data_from_etaxes: {str(e)}\n{frappe.get_traceback()}", "Combined Load Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_unmapped_customers():
"""Getting unmapped customers from E-Taxes Customers"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped customers from E-Taxes Customers
unmapped_customers = []
# Get list of existing mappings
existing_mappings = {}
for mapping in settings.customer_mappings:
existing_mappings[mapping.etaxes_customer_name] = mapping.erp_customer
# Get unmapped customers from E-Taxes Customers
etaxes_customers = frappe.get_all('E-Taxes Customers',
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id',
'etaxes_party_type', 'etaxes_address', 'source_invoice'])
# Filter only those that are not in mappings
for customer in etaxes_customers:
if customer.etaxes_party_name not in existing_mappings:
# Create customer object with default settings
customer_data = {
'name': customer.name,
'etaxes_party_name': customer.etaxes_party_name,
'etaxes_tax_id': customer.etaxes_tax_id,
'etaxes_party_type': customer.etaxes_party_type,
# Common settings
'customer_group': settings.default_customer_group,
'territory': 'All Territories',
'payment_terms': settings.default_payment_terms
}
# Add address if it exists
if customer.etaxes_address:
customer_data['address'] = customer.etaxes_address
# Add source if it exists
if customer.source_invoice:
customer_data['source'] = customer.source_invoice
unmapped_customers.append(customer_data)
return {
'success': True,
'customers': unmapped_customers
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_unmapped_suppliers():
"""Getting unmapped suppliers from E-Taxes Suppliers"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped suppliers from E-Taxes Suppliers
unmapped_suppliers = []
# Get list of existing mappings
existing_mappings = {}
for mapping in settings.supplier_mappings:
existing_mappings[mapping.etaxes_supplier_name] = mapping.erp_supplier
# Get unmapped suppliers from E-Taxes Suppliers
etaxes_suppliers = frappe.get_all('E-Taxes Suppliers',
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id',
'etaxes_party_type', 'etaxes_address', 'source_invoice'])
# Filter only those that are not in mappings
for supplier in etaxes_suppliers:
if supplier.etaxes_party_name not in existing_mappings:
# Create supplier object with default settings
supplier_data = {
'name': supplier.name,
'etaxes_party_name': supplier.etaxes_party_name,
'etaxes_tax_id': supplier.etaxes_tax_id,
'etaxes_party_type': supplier.etaxes_party_type,
# Common settings
'supplier_group': settings.default_supplier_group,
'payment_terms': settings.default_payment_terms
}
# Add address if it exists
if supplier.etaxes_address:
supplier_data['address'] = supplier.etaxes_address
# Add source if it exists
if supplier.source_invoice:
supplier_data['source'] = supplier.source_invoice
unmapped_suppliers.append(supplier_data)
return {
'success': True,
'suppliers': unmapped_suppliers
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_customers():
"""Matching customers by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all customers with "New" status from E-Taxes Customers
etaxes_customers = frappe.get_all('E-Taxes Customers',
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.customer_mappings:
existing_mappings[mapping.etaxes_customer_name] = mapping.erp_customer
# Get customers with empty mapping (exist in table but erp_customer is None or empty)
customers_with_empty_mapping = []
for etaxes_id, erp_customer in existing_mappings.items():
if not erp_customer: # If erp_customer is None or empty string
# Find corresponding E-Taxes customer
for customer in etaxes_customers:
if customer.etaxes_party_name == etaxes_id:
customers_with_empty_mapping.append(customer)
break
# Filter only those that are not in mappings
unmapped_customers = []
for customer in etaxes_customers:
if customer.etaxes_party_name not in existing_mappings:
unmapped_customers.append(customer)
# Combine two lists: customers without mappings and customers with empty mappings
customers_to_process = unmapped_customers + customers_with_empty_mapping
# Get all customers from system
system_customers = frappe.get_all('Customer', fields=['name', 'customer_name'])
matched_count = 0
total_processed = len(customers_to_process)
# Get fresh copy of settings document
doc = frappe.get_single('E-Taxes Settings')
# Process all customers
for etaxes_customer in customers_to_process:
# Find similar customer
best_match = None
best_score = 0
# Normalize E-Taxes customer name
etaxes_name = normalize_string(etaxes_customer.etaxes_party_name, consider_azeri)
for customer in system_customers:
# Normalize system customer name
customer_name = normalize_string(customer.customer_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, customer_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = customer
if best_match:
# IMPORTANT: check if there is already a record for this customer in mappings
existing_row = None
for idx, mapping in enumerate(doc.customer_mappings):
if mapping.etaxes_customer_name == etaxes_customer.etaxes_party_name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_customer value
doc.customer_mappings[existing_row].erp_customer = best_match.name
doc.customer_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('customer_mappings', {
'etaxes_customer_name': etaxes_customer.etaxes_party_name,
'etaxes_tax_id': etaxes_customer.etaxes_tax_id,
'erp_customer': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Customers status only for customers that have a match
customer_doc = frappe.get_doc('E-Taxes Customers', etaxes_customer.name)
customer_doc.status = 'Mapped'
customer_doc.mapped_customer = best_match.name
customer_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} customers'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_suppliers():
"""Matching suppliers by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all suppliers with "New" status from E-Taxes Suppliers
etaxes_suppliers = frappe.get_all('E-Taxes Suppliers',
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.supplier_mappings:
existing_mappings[mapping.etaxes_supplier_name] = mapping.erp_supplier
# Get suppliers with empty mapping (exist in table but erp_supplier is None or empty)
suppliers_with_empty_mapping = []
for etaxes_id, erp_supplier in existing_mappings.items():
if not erp_supplier: # If erp_supplier is None or empty string
# Find corresponding E-Taxes supplier
for supplier in etaxes_suppliers:
if supplier.etaxes_party_name == etaxes_id:
suppliers_with_empty_mapping.append(supplier)
break
# Filter only those that are not in mappings
unmapped_suppliers = []
for supplier in etaxes_suppliers:
if supplier.etaxes_party_name not in existing_mappings:
unmapped_suppliers.append(supplier)
# Combine two lists: suppliers without mappings and suppliers with empty mappings
suppliers_to_process = unmapped_suppliers + suppliers_with_empty_mapping
# Get all suppliers from system
system_suppliers = frappe.get_all('Supplier', fields=['name', 'supplier_name'])
matched_count = 0
total_processed = len(suppliers_to_process)
# Get fresh copy of settings document
doc = frappe.get_single('E-Taxes Settings')
# Process all suppliers
for etaxes_supplier in suppliers_to_process:
# Find similar supplier
best_match = None
best_score = 0
# Normalize E-Taxes supplier name
etaxes_name = normalize_string(etaxes_supplier.etaxes_party_name, consider_azeri)
for supplier in system_suppliers:
# Normalize system supplier name
supplier_name = normalize_string(supplier.supplier_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, supplier_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = supplier
if best_match:
# IMPORTANT: check if there is already a record for this supplier in mappings
existing_row = None
for idx, mapping in enumerate(doc.supplier_mappings):
if mapping.etaxes_supplier_name == etaxes_supplier.etaxes_party_name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_supplier value
doc.supplier_mappings[existing_row].erp_supplier = best_match.name
doc.supplier_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('supplier_mappings', {
'etaxes_supplier_name': etaxes_supplier.etaxes_party_name,
'etaxes_tax_id': etaxes_supplier.etaxes_tax_id,
'erp_supplier': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Suppliers status only for suppliers that have a match
supplier_doc = frappe.get_doc('E-Taxes Suppliers', etaxes_supplier.name)
supplier_doc.status = 'Mapped'
supplier_doc.mapped_supplier = best_match.name
supplier_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} suppliers'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_customers():
"""Creating customers for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
# Get settings
settings_doc = frappe.get_single('E-Taxes Settings')
# Get elements from table that have no mapping
unmapped_customers = []
for mapping_customer in settings_doc.customer_mappings:
# Check that customer has no mapping or it's empty
if not mapping_customer.erp_customer:
# Get E-Taxes Customers document for this record
if frappe.db.exists('E-Taxes Customers', mapping_customer.etaxes_customer_name):
etaxes_customer = frappe.get_doc('E-Taxes Customers', mapping_customer.etaxes_customer_name)
unmapped_customers.append(etaxes_customer)
if len(unmapped_customers) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped customers in table to create"
}
created_count = 0
error_count = 0
# Collect individual settings for each element
mapping_settings = {}
for mapping in settings_doc.customer_mappings:
customer_settings = {}
if mapping.customer_group:
customer_settings['customer_group'] = mapping.customer_group
if mapping.territory:
customer_settings['territory'] = mapping.territory
if mapping.payment_terms:
customer_settings['payment_terms'] = mapping.payment_terms
if customer_settings:
mapping_settings[mapping.etaxes_customer_name] = customer_settings
for etaxes_customer in unmapped_customers:
try:
# Check if such customer with this name already exists
customer_exists = frappe.db.exists('Customer', {'customer_name': etaxes_customer.etaxes_party_name})
if customer_exists:
continue
# Create new customer
customer_doc = frappe.new_doc("Customer")
# Get individual settings
individual_settings = mapping_settings.get(etaxes_customer.etaxes_party_name, {})
# Set basic fields
customer_doc.customer_name = etaxes_customer.etaxes_party_name
customer_doc.customer_type = "Company" # Can be configured if needed
# Apply settings considering individual priorities.
# Only assign a Customer Group if a valid, non-group one is configured;
# otherwise leave it empty (ERPNext rejects group-type Customer Groups
# and the field is no longer mandatory).
customer_group = resolve_customer_group(
individual_settings.get('customer_group') or settings_doc.default_customer_group
)
if customer_group:
customer_doc.customer_group = customer_group
# Territory
territory = individual_settings.get('territory', "All Territories")
if not frappe.db.exists('Territory', territory):
territory = "All Territories"
customer_doc.territory = territory
# If tax_id specified, add it
if etaxes_customer.etaxes_tax_id:
customer_doc.tax_id = etaxes_customer.etaxes_tax_id
# If payment terms specified, add them
if 'payment_terms' in individual_settings:
customer_doc.payment_terms = individual_settings['payment_terms']
elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
customer_doc.payment_terms = settings_doc.default_payment_terms
# Save customer
customer_doc.insert()
# Update mapping in settings
for mapping in settings_doc.customer_mappings:
if mapping.etaxes_customer_name == etaxes_customer.etaxes_party_name:
mapping.erp_customer = customer_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Customers status
etaxes_customer.status = 'Mapped'
etaxes_customer.mapped_customer = customer_doc.name
etaxes_customer.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} customers, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_suppliers():
"""Creating suppliers for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
# Get settings
settings_doc = frappe.get_single('E-Taxes Settings')
# Get elements from table that have no mapping
unmapped_suppliers = []
for mapping_supplier in settings_doc.supplier_mappings:
# Check that supplier has no mapping or it's empty
if not mapping_supplier.erp_supplier:
# Get E-Taxes Suppliers document for this record
if frappe.db.exists('E-Taxes Suppliers', mapping_supplier.etaxes_supplier_name):
etaxes_supplier = frappe.get_doc('E-Taxes Suppliers', mapping_supplier.etaxes_supplier_name)
unmapped_suppliers.append(etaxes_supplier)
if len(unmapped_suppliers) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped suppliers in table to create"
}
created_count = 0
error_count = 0
# Collect individual settings for each element
mapping_settings = {}
for mapping in settings_doc.supplier_mappings:
supplier_settings = {}
if mapping.supplier_group:
supplier_settings['supplier_group'] = mapping.supplier_group
if mapping.payment_terms:
supplier_settings['payment_terms'] = mapping.payment_terms
if supplier_settings:
mapping_settings[mapping.etaxes_supplier_name] = supplier_settings
for etaxes_supplier in unmapped_suppliers:
try:
# ИСПРАВЛЕНИЕ: Обрезаем название поставщика до 140 символов
supplier_name = etaxes_supplier.etaxes_party_name
if len(supplier_name) > 140:
supplier_name = supplier_name[:140]
# Check if such supplier with this name already exists
supplier_exists = frappe.db.exists('Supplier', {'supplier_name': supplier_name})
if supplier_exists:
continue
# Create new supplier
supplier_doc = frappe.new_doc("Supplier")
# Get individual settings
individual_settings = mapping_settings.get(etaxes_supplier.etaxes_party_name, {})
# Set basic fields
supplier_doc.supplier_name = supplier_name
supplier_doc.supplier_type = "Company" # Can be configured if needed
# Apply settings considering individual priorities
supplier_group = individual_settings.get('supplier_group', settings_doc.default_supplier_group)
if not frappe.db.exists('Supplier Group', supplier_group):
supplier_group = "All Supplier Groups"
supplier_doc.supplier_group = supplier_group
# If tax_id specified, add it
if etaxes_supplier.etaxes_tax_id:
supplier_doc.tax_id = etaxes_supplier.etaxes_tax_id
# If payment terms specified, add them
if 'payment_terms' in individual_settings:
supplier_doc.payment_terms = individual_settings['payment_terms']
elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
supplier_doc.payment_terms = settings_doc.default_payment_terms
# Save supplier
supplier_doc.insert()
# Update mapping in settings
for mapping in settings_doc.supplier_mappings:
if mapping.etaxes_supplier_name == etaxes_supplier.etaxes_party_name:
mapping.erp_supplier = supplier_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Suppliers status
etaxes_supplier.status = 'Mapped'
etaxes_supplier.mapped_supplier = supplier_doc.name
etaxes_supplier.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} suppliers, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_customers_v2():
"""Creating customers for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
# Get settings
settings_doc = frappe.get_single('E-Taxes Settings')
# Get elements from table that have no mapping
unmapped_customers = []
for mapping_customer in settings_doc.customer_mappings:
# Check that customer has no mapping or it's empty
if not mapping_customer.erp_customer:
# Get E-Taxes Customers document for this record
if frappe.db.exists('E-Taxes Customers', mapping_customer.etaxes_customer_name):
etaxes_customer = frappe.get_doc('E-Taxes Customers', mapping_customer.etaxes_customer_name)
unmapped_customers.append(etaxes_customer)
if len(unmapped_customers) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped customers in table to create"
}
created_count = 0
error_count = 0
# Collect individual settings for each element
mapping_settings = {}
for mapping in settings_doc.customer_mappings:
customer_settings = {}
if mapping.customer_group:
customer_settings['customer_group'] = mapping.customer_group
if mapping.territory:
customer_settings['territory'] = mapping.territory
if mapping.payment_terms:
customer_settings['payment_terms'] = mapping.payment_terms
if customer_settings:
mapping_settings[mapping.etaxes_customer_name] = customer_settings
for etaxes_customer in unmapped_customers:
try:
# ИСПРАВЛЕНИЕ: Обрезаем название клиента до 140 символов
customer_name = etaxes_customer.etaxes_party_name
if len(customer_name) > 140:
customer_name = customer_name[:140]
# Check if such customer with this name already exists
customer_exists = frappe.db.exists('Customer', {'customer_name': customer_name})
if customer_exists:
continue
# Create new customer
customer_doc = frappe.new_doc("Customer")
# Get individual settings
individual_settings = mapping_settings.get(etaxes_customer.etaxes_party_name, {})
# Set basic fields
customer_doc.customer_name = customer_name
customer_doc.customer_type = "Company" # Can be configured if needed
# Apply settings considering individual priorities.
# Only assign a Customer Group if a valid, non-group one is configured;
# otherwise leave it empty (ERPNext rejects group-type Customer Groups
# and the field is no longer mandatory).
customer_group = resolve_customer_group(
individual_settings.get('customer_group') or settings_doc.default_customer_group
)
if customer_group:
customer_doc.customer_group = customer_group
# Territory
territory = individual_settings.get('territory', "All Territories")
if not frappe.db.exists('Territory', territory):
territory = "All Territories"
customer_doc.territory = territory
# If tax_id specified, add it
if etaxes_customer.etaxes_tax_id:
customer_doc.tax_id = etaxes_customer.etaxes_tax_id
# If payment terms specified, add them
if 'payment_terms' in individual_settings:
customer_doc.payment_terms = individual_settings['payment_terms']
elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
customer_doc.payment_terms = settings_doc.default_payment_terms
# Save customer
customer_doc.insert()
# Update mapping in settings
for mapping in settings_doc.customer_mappings:
if mapping.etaxes_customer_name == etaxes_customer.etaxes_party_name:
mapping.erp_customer = customer_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Customers status
etaxes_customer.status = 'Mapped'
etaxes_customer.mapped_customer = customer_doc.name
etaxes_customer.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} customers, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
# Helper functions
@frappe.whitelist()
def get_active_settings():
"""Getting active E-Taxes settings"""
return frappe.get_single('E-Taxes Settings')
@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 load_parties_from_invoices(date_from, date_to, max_count=200, offset=0, invoice_type="purchase"):
"""Загружает контрагентов из инвойсов"""
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'}
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(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response_inbox:
return {'success': False, **response_inbox}
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(asan_login_settings['main_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 = invoices_inbox + invoices_outbox
hasMore_inbox = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
hasMore_outbox = response_outbox.get('hasMore', False) or response_outbox.get('total', 0) > offset + max_count
hasMore = hasMore_inbox or hasMore_outbox
else:
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
except ImportError:
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
except Exception:
hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count
return {
'success': True,
'invoices': all_invoices,
'hasMore': hasMore,
'token': asan_login_settings['main_token'],
'total': len(all_invoices)
}
except Exception as e:
frappe.log_error(f"Error in load_parties_from_invoices: {str(e)}", "Load Parties Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_reference_data_summary():
"""Get summary of all reference data for E-Taxes Settings dashboard"""
try:
summary = {}
# Define DocTypes to check
doctypes_to_check = {
'items': 'E-Taxes Item',
'customers': 'E-Taxes Customers',
'suppliers': 'E-Taxes Suppliers',
'units': 'E-Taxes Unit'
}
for key, doctype in doctypes_to_check.items():
try:
# Check if DocType exists
if frappe.db.exists('DocType', doctype):
total = int(frappe.db.count(doctype))
# Check if status field exists before counting mapped items
meta = frappe.get_meta(doctype)
has_status_field = any(df.fieldname == 'status' for df in meta.fields)
if has_status_field:
mapped = int(frappe.db.count(doctype, {'status': 'Mapped'}))
else:
mapped = 0
summary[key] = {'total': total, 'mapped': mapped}
else:
summary[key] = {'total': 0, 'mapped': 0}
except Exception as e:
frappe.log_error(f"Error getting summary for {doctype}: {str(e)}", "Reference Data Summary Error")
summary[key] = {'total': 0, 'mapped': 0}
return {
'success': True,
'summary': summary
}
except Exception as e:
frappe.log_error(f"Error getting reference data summary: {str(e)}\n{frappe.get_traceback()}", "Reference Data Summary Error")
return {'success': False, 'message': str(e)}
@frappe.whitelist()
def get_reference_data_lists(data_type, limit=100, offset=0):
"""Get paginated list of reference data"""
try:
# Convert limit and offset to integers
try:
limit = int(limit)
offset = int(offset)
except (ValueError, TypeError):
limit = 100
offset = 0
valid_types = ['items', 'customers', 'suppliers', 'units']
if data_type not in valid_types:
return {'success': False, 'message': 'Invalid data type'}
doctype_map = {
'items': 'E-Taxes Item',
'customers': 'E-Taxes Customers',
'suppliers': 'E-Taxes Suppliers',
'units': 'E-Taxes Unit'
}
doctype = doctype_map[data_type]
# Check if DocType exists
if not frappe.db.exists('DocType', doctype):
return {
'success': True,
'data': [],
'total_count': 0,
'has_more': False,
'message': f'DocType {doctype} does not exist'
}
# Get DocType meta to check available fields
meta = frappe.get_meta(doctype)
available_fields = [df.fieldname for df in meta.fields] + ['name', 'creation', 'modified']
# Define base fields that should exist
base_fields = ['name', 'creation']
# Define fields map with fallbacks
fields_map = {
'items': {
'required': base_fields + ['etaxes_item_name'],
'optional': ['etaxes_item_code', 'status', 'mapped_item', 'is_service_item']
},
'customers': {
'required': base_fields + ['etaxes_party_name'],
'optional': ['etaxes_tax_id', 'status', 'mapped_customer']
},
'suppliers': {
'required': base_fields + ['etaxes_party_name'],
'optional': ['etaxes_tax_id', 'status', 'mapped_supplier']
},
'units': {
'required': base_fields + ['etaxes_unit_name'],
'optional': ['status', 'mapped_unit']
}
}
# Build fields list based on what's actually available
field_config = fields_map[data_type]
fields_to_fetch = []
# Add required fields
for field in field_config['required']:
if field in available_fields:
fields_to_fetch.append(field)
# Add optional fields if they exist
for field in field_config['optional']:
if field in available_fields:
fields_to_fetch.append(field)
if not fields_to_fetch:
return {
'success': True,
'data': [],
'total_count': 0,
'has_more': False,
'message': f'No valid fields found for {doctype}'
}
# Get total count first
total_count = frappe.db.count(doctype)
# Get data with pagination
data = frappe.get_all(
doctype,
fields=fields_to_fetch,
order_by='creation desc',
limit=limit,
start=offset
)
# Calculate has_more (ensure all are integers)
has_more = (int(offset) + int(limit)) < int(total_count)
return {
'success': True,
'data': data,
'total_count': int(total_count),
'has_more': has_more
}
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 process_single_invoice_for_reference_data(token, invoice_id, source_type='purchase',
load_items=1, load_units=1,
load_customers=1, load_suppliers=1):
"""Обрабатывает один инвойс и создает все 4 типа доктайпов"""
record_etaxes_activity()
try:
# Получаем детали инвойса
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,
'error': invoice_details.get('error'),
'message': invoice_details.get('message', 'Failed to get invoice details')
}
# Создаем доктайпы из одного инвойса (только выбранные типы)
result = create_reference_data_from_single_invoice(
invoice_details, source_type,
load_items=int(load_items), load_units=int(load_units),
load_customers=int(load_customers), load_suppliers=int(load_suppliers)
)
return result
except Exception as e:
frappe.log_error(f"Error in process_single_invoice_for_reference_data: {str(e)}", "Process Single Invoice Error")
return {
'success': False,
'message': "An unknown error occurred processing this invoice"
}
@frappe.whitelist()
def create_reference_data_from_single_invoice(invoice_details, source_type,
load_items=1, load_units=1,
load_customers=1, load_suppliers=1):
"""Создает доктайпы из одного инвойса с EQM кодами - оптимизированная версия"""
try:
stats = {
'items_created': 0,
'items_skipped': 0,
'units_created': 0,
'units_skipped': 0,
'customers_created': 0,
'customers_skipped': 0,
'suppliers_created': 0,
'suppliers_skipped': 0
}
serial_number = invoice_details.get('serialNumber', '')
items = invoice_details.get('items', [])
sender = invoice_details.get('sender', {})
receiver = invoice_details.get('receiver', {})
# === СОЗДАНИЕ ТОВАРОВ С EQM КОДАМИ ===
if int(load_items):
processed_items = set()
for item in items:
item_name = item.get('productName', '').strip()
if not item_name:
continue
# Обрезаем до 140 символов
if len(item_name) > 140:
item_name = item_name[:140]
if item_name in processed_items:
continue
processed_items.add(item_name)
# Проверяем существование
if frappe.db.exists('E-Taxes Item', item_name):
stats['items_skipped'] += 1
continue
try:
# Создаем товар
item_code = item.get('itemId', '') or f"CODE-{serial_number}"
# Обрабатываем группу товаров
product_group = item.get('productGroup', {})
product_group_code = product_group.get('code', '') if product_group else ''
product_group_type = product_group.get('type', '') if product_group else ''
# Получаем название группы товаров
product_group_name = ''
if product_group and product_group.get('name'):
names = product_group.get('name', {})
if isinstance(names, dict):
product_group_name = names.get('az', '') or names.get('en', '') or names.get('ru', '')
elif isinstance(names, str):
product_group_name = names
# Обрезаем название группы товаров до 1000 символов
if len(product_group_name) > 1000:
product_group_name = product_group_name[:1000]
# НОВОЕ: Поиск EQM Code по коду группы товаров - возвращаем name для Link поля
eqm_code = None
if product_group_code:
try:
frappe.log_error(f"[EQM DEBUG] Searching for product_group_code: '{product_group_code}'", "EQM Code Search")
# Ищем запись и получаем и name, и eqm_name
eqm_record = frappe.db.sql("""
SELECT name, eqm_name
FROM `tabEQM Codes`
WHERE eqm_name LIKE %s
LIMIT 1
""", (f"{product_group_code} -%",), as_dict=True)
frappe.log_error(f"[EQM DEBUG] SQL search result: {eqm_record}", "EQM Code Search")
if eqm_record:
# ИСПРАВЛЕНИЕ: Сохраняем name (ID записи) для Link поля, а не eqm_name
eqm_code = eqm_record[0].get('name') # Изменено с eqm_name на name
frappe.log_error(f"[EQM DEBUG] Found EQM Code ID: '{eqm_code}' (description: '{eqm_record[0].get('eqm_name')}') for product group '{product_group_code}' in item {item_name}", "EQM Code Assignment")
else:
frappe.log_error(f"[EQM DEBUG] EQM Code not found for product group '{product_group_code}' in item {item_name}", "EQM Code Not Found")
except Exception as e:
frappe.log_error(f"[EQM DEBUG] Error searching EQM Code for '{product_group_code}': {str(e)}", "EQM Code Search Error")
# Логируем финальное значение
frappe.log_error(f"[EQM DEBUG] Final eqm_code ID to save: '{eqm_code}' for item {item_name}", "EQM Code Final")
# Логируем финальное значение
frappe.log_error(f"[EQM DEBUG] Final eqm_code value to save: '{eqm_code}' for item {item_name}", "EQM Code Final")
# Определяем, является ли товар услугой
is_service = 1 if product_group_type == 'service' else 0
# Определяем источники
is_from_purchase = 1 if source_type == 'purchase' else 0
is_from_sales = 1 if source_type == 'sales' else 0
# Создаем документ с EQM кодом
doc_data = {
'doctype': 'E-Taxes Item',
'etaxes_item_name': item_name,
'etaxes_item_code': item_code,
'etaxes_unit': item.get('unit', ''),
'etaxes_price': item.get('pricePerUnit', 0),
'etaxes_product_group_code': product_group_code,
'etaxes_product_group_name': product_group_name,
'etaxes_product_group_type': product_group_type,
'is_service_item': is_service,
'source_invoice': serial_number,
'is_from_purchase': is_from_purchase,
'is_from_sales': is_from_sales,
'status': 'New'
}
# НОВОЕ: Добавляем EQM код если найден
if eqm_code:
doc_data['eqm_code'] = eqm_code
doc = frappe.get_doc(doc_data)
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
stats['items_created'] += 1
except frappe.DuplicateEntryError:
stats['items_skipped'] += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Item {item_name}: {str(e)}", "Create Item Error")
# === СОЗДАНИЕ ЕДИНИЦ ===
if int(load_units):
processed_units = set()
for item in items:
unit_name = item.get('unit', '').strip()
if not unit_name or unit_name in processed_units:
continue
processed_units.add(unit_name)
# Проверяем существование
if frappe.db.exists('E-Taxes Unit', unit_name):
stats['units_skipped'] += 1
continue
try:
source_type_text = 'Purchase' if source_type == 'purchase' else 'Sales'
doc = frappe.get_doc({
'doctype': 'E-Taxes Unit',
'etaxes_unit_name': unit_name,
'etaxes_unit_code': unit_name,
'source_invoice': serial_number,
'source_type': source_type_text,
'status': 'New'
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
stats['units_created'] += 1
except frappe.DuplicateEntryError:
stats['units_skipped'] += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Unit {unit_name}: {str(e)}", "Create Unit Error")
# === СОЗДАНИЕ КЛИЕНТОВ (RECEIVER) ===
if int(load_customers) and receiver:
receiver_name = (receiver.get('name') or '').strip()
receiver_tin = (receiver.get('tin') or '').strip()
receiver_address = (receiver.get('address') or '').strip()
receiver_name = ' '.join(receiver_name.split()) # Убираем лишние пробелы
if receiver_name and receiver_tin:
if not frappe.db.exists('E-Taxes Customers', receiver_name):
try:
doc = frappe.get_doc({
'doctype': 'E-Taxes Customers',
'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin,
'etaxes_address': receiver_address,
'etaxes_party_type': 'Receiver',
'source_invoice': serial_number,
'status': 'New'
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
stats['customers_created'] += 1
except frappe.DuplicateEntryError:
stats['customers_skipped'] += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Customers {receiver_name}: {str(e)}", "Create Customer Error")
else:
stats['customers_skipped'] += 1
# === СОЗДАНИЕ ПОСТАВЩИКОВ/КЛИЕНТОВ (SENDER) ===
if sender:
sender_name = (sender.get('name') or '').strip()
sender_tin = (sender.get('tin') or '').strip()
sender_address = (sender.get('address') or '').strip()
sender_name = ' '.join(sender_name.split()) # Убираем лишние пробелы
if sender_name and sender_tin:
if source_type == 'purchase' and int(load_suppliers):
# Sender = Supplier для purchase инвойсов
if not frappe.db.exists('E-Taxes Suppliers', sender_name):
try:
doc = frappe.get_doc({
'doctype': 'E-Taxes Suppliers',
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'etaxes_address': sender_address,
'etaxes_party_type': 'Sender',
'source_invoice': serial_number,
'status': 'New'
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
stats['suppliers_created'] += 1
except frappe.DuplicateEntryError:
stats['suppliers_skipped'] += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Suppliers {sender_name}: {str(e)}", "Create Supplier Error")
else:
stats['suppliers_skipped'] += 1
elif source_type != 'purchase' and int(load_customers):
# Sender = Customer для sales инвойсов
if not frappe.db.exists('E-Taxes Customers', sender_name):
try:
doc = frappe.get_doc({
'doctype': 'E-Taxes Customers',
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'etaxes_address': sender_address,
'etaxes_party_type': 'Sender',
'source_invoice': serial_number,
'status': 'New'
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
stats['customers_created'] += 1
except frappe.DuplicateEntryError:
stats['customers_skipped'] += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Customers {sender_name}: {str(e)}", "Create Customer Error")
else:
stats['customers_skipped'] += 1
return {
'success': True,
'stats': stats,
'serial_number': serial_number
}
except Exception as e:
frappe.log_error(f"Error creating reference data: {str(e)}\n{frappe.get_traceback()}", "Create Reference Data Error")
return {
'success': False,
'message': str(e)
}
# ===== НОВЫЕ PYTHON ФУНКЦИИ =====
@frappe.whitelist()
def load_single_invoice_batch(date_from, date_to, offset=0, max_count=200):
"""Загружает одну порцию инвойсов из inbox"""
record_etaxes_activity()
try:
# Получаем настройки токена
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
token = asan_login_settings['main_token']
# Преобразуем параметры
try:
max_count = int(max_count)
if max_count != 200: # Принудительно устанавливаем 200
max_count = 200
except (ValueError, TypeError):
max_count = 200
try:
offset = int(offset)
except (ValueError, TypeError):
offset = 0
# Настройки фильтров
filters = {
"actionOwner": None,
"amountFrom": None,
"amountTo": None,
"creationDateFrom": date_from,
"creationDateTo": date_to,
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
"exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"],
"maxCount": max_count,
"offset": offset,
"productCode": None,
"productName": None,
"receiverName": None,
"receiverTin": None,
"senderName": None,
"senderTin": None,
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "approvedBySystem"],
"types": ["current", "corrected"]
}
# Получаем порцию инвойсов из inbox
response = get_invoices(token, json.dumps(filters))
if 'error' in response:
return {
'success': False,
'error': response.get('error'),
'message': response.get('message', 'Failed to retrieve invoices')
}
invoices = response.get('data', []) or response.get('invoices', [])
hasMore = response.get('hasMore', False)
total = response.get('total', 0)
# Дополнительная проверка hasMore
if not hasMore and total > 0:
hasMore = (offset + max_count) < total
return {
'success': True,
'invoices': invoices,
'hasMore': hasMore,
'token': token,
'offset': offset,
'total': total,
'loaded': len(invoices)
}
except Exception as e:
frappe.log_error(f"Error in load_single_invoice_batch: {str(e)}\n{frappe.get_traceback()}", "Load Invoice Batch Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def load_single_sales_invoice_batch(date_from, date_to, offset=0, max_count=200):
"""Загружает одну порцию инвойсов из outbox (sales)"""
record_etaxes_activity()
try:
# Получаем настройки токена
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
token = asan_login_settings['main_token']
# Преобразуем параметры
try:
max_count = int(max_count)
if max_count != 200: # Принудительно устанавливаем 200
max_count = 200
except (ValueError, TypeError):
max_count = 200
try:
offset = int(offset)
except (ValueError, TypeError):
offset = 0
# Настройки фильтров
filters = {
"actionOwner": None,
"amountFrom": None,
"amountTo": None,
"creationDateFrom": date_from,
"creationDateTo": date_to,
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
"exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"],
"maxCount": max_count,
"offset": offset,
"productCode": None,
"productName": None,
"receiverName": None,
"receiverTin": None,
"senderName": None,
"senderTin": None,
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "approvedBySystem"],
"types": ["current", "corrected"]
}
# Получаем порцию инвойсов из outbox
try:
from .sales_api import get_sales_invoices
response = get_sales_invoices(token, json.dumps(filters))
if 'error' in response:
return {
'success': False,
'error': response.get('error'),
'message': response.get('message', 'Failed to retrieve sales invoices')
}
invoices = response.get('data', []) or response.get('invoices', [])
hasMore = response.get('hasMore', False)
total = response.get('total', 0)
# Дополнительная проверка hasMore
if not hasMore and total > 0:
hasMore = (offset + max_count) < total
return {
'success': True,
'invoices': invoices,
'hasMore': hasMore,
'token': token,
'offset': offset,
'total': total,
'loaded': len(invoices)
}
except ImportError:
# Sales API недоступен
return {
'success': True,
'invoices': [],
'hasMore': False,
'token': token,
'offset': offset,
'total': 0,
'loaded': 0
}
except Exception as e:
frappe.log_error(f"Error in load_single_sales_invoice_batch: {str(e)}\n{frappe.get_traceback()}", "Load Sales Invoice Batch Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def refresh_token_from_asan_login():
"""Обновляет токен из Asan Login"""
try:
asan_login = frappe.get_all('Asan Login',
filters={'is_default': 1},
fields=['name', 'main_token'],
limit=1)
if asan_login and asan_login[0].main_token:
return {
'success': True,
'token': asan_login[0].main_token
}
else:
return {
'success': False,
'message': 'No valid token found in Asan Login'
}
except Exception as e:
return {
'success': False,
'message': str(e)
}
@frappe.whitelist()
def import_bulk_purchase_invoices(invoice_ids, token, warehouse=None):
"""Enqueue bulk purchase 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
frappe.enqueue(
_process_bulk_purchase_import,
invoice_ids=invoice_ids,
token=token,
warehouse=warehouse,
user=user,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(invoice_ids)}
def _process_bulk_purchase_import(invoice_ids, token, warehouse, user):
"""Background job: fetch details and import each purchase invoice with realtime progress."""
frappe.set_user(user)
record_etaxes_activity()
total = len(invoice_ids)
imported_count = 0
errors = []
for idx, invoice_id in enumerate(invoice_ids):
try:
details = get_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":
asan_login = get_default_asan_login()
if asan_login.get("found") and asan_login.get("main_token"):
token = asan_login["main_token"]
details = get_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:
schedule_date = datetime.datetime.strptime(str(created_at)[:10], "%Y-%m-%d").strftime("%Y-%m-%d")
except Exception:
pass
result = import_invoice_with_mapping(
invoice_data=details,
purchase_order_name=None,
schedule_date=schedule_date,
warehouse=warehouse,
)
if result and result.get("success"):
try:
sender_name = details.get("sender", {}).get("name", "") if details.get("sender") else ""
total_amount = details.get("totalAmount", 0) or details.get("amount", 0)
etaxes_result = create_etaxes_purchase(invoice_id, schedule_date, sender_name, total_amount)
if etaxes_result and etaxes_result.get("success"):
link_purchase_order_to_etaxes(result["purchase_order"], etaxes_result["name"])
except Exception:
pass
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(
"purchase_import_progress",
{"current": idx + 1, "total": total},
user=user,
)
frappe.publish_realtime(
"purchase_import_complete",
{"total": total, "imported": imported_count, "errors": errors},
user=user,
)