4425 lines
196 KiB
Python
4425 lines
196 KiB
Python
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,
|
||
)
|