Small edits to make app good (read only, anti dublicate and etc)

This commit is contained in:
Ali 2025-05-15 15:53:52 +04:00
parent e12efa781a
commit 905cfeac0c
11 changed files with 985 additions and 443 deletions

View File

@ -1049,8 +1049,6 @@ def load_items_from_invoices(date_from, date_to, max_count=200):
def load_parties_from_invoices(date_from, date_to, max_count=200): def load_parties_from_invoices(date_from, date_to, max_count=200):
"""Loading parties from invoices for a period""" """Loading parties from invoices for a period"""
try: try:
frappe.log_error(f"=== НАЧАЛО ЗАГРУЗКИ КОНТРАГЕНТОВ ===\nПараметры: date_from={date_from}, date_to={date_to}, max_count={max_count}", "Party Load Start")
# Счетчики для отслеживания # Счетчики для отслеживания
created_count = 0 created_count = 0
skipped_count = 0 skipped_count = 0
@ -1058,10 +1056,8 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
# Получаем настройки по умолчанию # Получаем настройки по умолчанию
asan_login_settings = get_default_asan_login() asan_login_settings = get_default_asan_login()
frappe.log_error(f"Asan Login settings: {asan_login_settings}", "Asan Login Details")
if not asan_login_settings.get('found'): if not asan_login_settings.get('found'):
frappe.log_error("Asan Login settings not found!", "Party Load Error")
return { return {
'success': False, 'success': False,
'message': 'No Asan Login settings found' 'message': 'No Asan Login settings found'
@ -1096,11 +1092,9 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
"types": ["current", "corrected"] "types": ["current", "corrected"]
} }
frappe.log_error(f"Отправляем запрос на API с фильтрами: {json.dumps(filters)}", "API Request Filters")
response = get_invoices(asan_login_settings['main_token'], json.dumps(filters)) response = get_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response: if 'error' in response:
frappe.log_error(f"API вернул ошибку: {response['error']}", "API Error")
return { return {
'success': False, 'success': False,
'message': response['error'] 'message': response['error']
@ -1108,10 +1102,8 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
# Extract data from response # Extract data from response
invoices_data = response.get('data', []) or response.get('invoices', []) invoices_data = response.get('data', []) or response.get('invoices', [])
frappe.log_error(f"Получено инвойсов: {len(invoices_data)}", "Invoices Count")
if len(invoices_data) == 0: if len(invoices_data) == 0:
frappe.log_error(f"API вернул пустой список инвойсов! Полный ответ API: {response}", "Empty Invoices List")
return { return {
'success': True, 'success': True,
'created_count': 0, 'created_count': 0,
@ -1121,10 +1113,6 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
'message': 'No invoices found for the specified period' 'message': 'No invoices found for the specified period'
} }
# Логируем структуру первого инвойса для отладки
if invoices_data and len(invoices_data) > 0:
frappe.log_error(f"Структура первого инвойса: {json.dumps(invoices_data[0])}", "First Invoice Structure")
# Для хранения уникальных контрагентов (поставщики и клиенты) # Для хранения уникальных контрагентов (поставщики и клиенты)
unique_parties = {} unique_parties = {}
@ -1133,21 +1121,18 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
for invoice in invoices_data: for invoice in invoices_data:
processed_count += 1 processed_count += 1
invoice_id = invoice.get('id', 'Нет ID') invoice_id = invoice.get('id', 'Нет ID')
frappe.log_error(f"Обработка инвойса #{processed_count}, ID: {invoice_id}", f"Invoice {invoice_id}")
serial_number = invoice.get('serialNumber', '') serial_number = invoice.get('serialNumber', '')
# Обработка отправителя (Sender -> Supplier) # Обработка отправителя (Sender -> Supplier)
sender = invoice.get('sender', {}) sender = invoice.get('sender', {})
frappe.log_error(f"Поля sender в инвойсе {invoice_id}: {json.dumps(sender)}", f"Sender Data {invoice_id}")
if sender: if sender:
sender_name = sender.get('name', '') sender_name = sender.get('name', '')
sender_tin = sender.get('tin', '') sender_tin = sender.get('tin', '')
sender_pin = sender.get('pin', '') sender_address = sender.get('address', '')
if not sender_name or not sender_tin: if not sender_name or not sender_tin:
frappe.log_error(f"Пропуск sender с неполными данными: name={sender_name}, tin={sender_tin}", f"Skip Sender {invoice_id}")
continue continue
# Создаем ключ для проверки уникальности # Создаем ключ для проверки уникальности
@ -1157,27 +1142,22 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
unique_parties[sender_key] = { unique_parties[sender_key] = {
'etaxes_party_name': sender_name, 'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin, 'etaxes_tax_id': sender_tin,
'etaxes_pin': sender_pin, 'etaxes_address': sender_address,
'etaxes_party_type': 'Sender', 'etaxes_party_type': 'Sender',
'erp_party_type': 'Supplier', 'erp_party_type': 'Supplier',
'source_invoice': serial_number, 'source_invoice': serial_number,
'status': 'New' 'status': 'New'
} }
frappe.log_error(f"Добавлен новый уникальный sender: {sender_key}", f"New Supplier {invoice_id}")
else:
frappe.log_error(f"Инвойс {invoice_id} не содержит данных sender!", f"No Sender {invoice_id}")
# Обработка получателя (Receiver -> Customer) # Обработка получателя (Receiver -> Customer)
receiver = invoice.get('receiver', {}) receiver = invoice.get('receiver', {})
frappe.log_error(f"Поля receiver в инвойсе {invoice_id}: {json.dumps(receiver)}", f"Receiver Data {invoice_id}")
if receiver: if receiver:
receiver_name = receiver.get('name', '') receiver_name = receiver.get('name', '')
receiver_tin = receiver.get('tin', '') receiver_tin = receiver.get('tin', '')
receiver_pin = receiver.get('pin', '') receiver_address = receiver.get('address', '')
if not receiver_name or not receiver_tin: if not receiver_name or not receiver_tin:
frappe.log_error(f"Пропуск receiver с неполными данными: name={receiver_name}, tin={receiver_tin}", f"Skip Receiver {invoice_id}")
continue continue
# Создаем ключ для проверки уникальности # Создаем ключ для проверки уникальности
@ -1187,27 +1167,12 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
unique_parties[receiver_key] = { unique_parties[receiver_key] = {
'etaxes_party_name': receiver_name, 'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin, 'etaxes_tax_id': receiver_tin,
'etaxes_pin': receiver_pin, 'etaxes_address': receiver_address,
'etaxes_party_type': 'Receiver', 'etaxes_party_type': 'Receiver',
'erp_party_type': 'Customer', 'erp_party_type': 'Customer',
'source_invoice': serial_number, 'source_invoice': serial_number,
'status': 'New' 'status': 'New'
} }
frappe.log_error(f"Добавлен новый уникальный receiver: {receiver_key}", f"New Customer {invoice_id}")
else:
frappe.log_error(f"Инвойс {invoice_id} не содержит данных receiver!", f"No Receiver {invoice_id}")
frappe.log_error(f"Обработано инвойсов: {processed_count}, Найдено уникальных контрагентов: {len(unique_parties)}", "Processing Summary")
# Вывод список найденных контрагентов с детальной информацией
party_list = []
for key, data in unique_parties.items():
party_list.append(f"{key}: {data['etaxes_party_name']}/{data['etaxes_tax_id']} - {data['erp_party_type']}")
if party_list:
frappe.log_error(f"Список найденных контрагентов:\n" + "\n".join(party_list), "Parties List")
else:
frappe.log_error("Не найдено ни одного контрагента в инвойсах!", "No Parties Found")
# Добавляем контрагентов в систему # Добавляем контрагентов в систему
for party_key, party_data in unique_parties.items(): for party_key, party_data in unique_parties.items():
@ -1218,7 +1183,6 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
existing = frappe.db.exists('E-Taxes Parties', {'etaxes_party_name': party_name}) existing = frappe.db.exists('E-Taxes Parties', {'etaxes_party_name': party_name})
if existing: if existing:
frappe.log_error(f"Контрагент уже существует: {party_name} ({party_tin})", "Existing Party")
skipped_count += 1 skipped_count += 1
continue continue
@ -1228,26 +1192,18 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
'doctype': 'E-Taxes Parties', 'doctype': 'E-Taxes Parties',
'etaxes_party_name': party_name, 'etaxes_party_name': party_name,
'etaxes_tax_id': party_tin, 'etaxes_tax_id': party_tin,
'etaxes_pin': party_data.get('etaxes_pin', ''), 'etaxes_address': party_data.get('etaxes_address', ''),
'etaxes_party_type': party_data.get('etaxes_party_type', 'Sender'), 'etaxes_party_type': party_data.get('etaxes_party_type', 'Sender'),
'erp_party_type': party_data.get('erp_party_type', 'Supplier'), 'erp_party_type': party_data.get('erp_party_type', 'Supplier'),
'source_invoice': party_data.get('source_invoice', ''), 'source_invoice': party_data.get('source_invoice', ''),
'status': 'New' 'status': 'New'
}) })
frappe.log_error(f"Создание контрагента с данными: {json.dumps(doc.as_dict())}", f"Create Party {party_tin}")
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
frappe.log_error(f"Контрагент успешно создан: {party_name} ({party_tin})", "Party Created")
created_count += 1 created_count += 1
except Exception as e: except Exception as e:
error_msg = f"Ошибка создания контрагента {party_name} ({party_tin}): {str(e)}\n{frappe.get_traceback()}"
frappe.log_error(error_msg, "Party Creation Error")
failed_count += 1 failed_count += 1
frappe.log_error(f"=== ИТОГ ЗАГРУЗКИ КОНТРАГЕНТОВ ===\nСоздано: {created_count}\nПропущено: {skipped_count}\nОшибки: {failed_count}", "Loading Results")
return { return {
'success': True, 'success': True,
'created_count': created_count, 'created_count': created_count,
@ -1258,15 +1214,12 @@ def load_parties_from_invoices(date_from, date_to, max_count=200):
} }
except Exception as e: except Exception as e:
error_msg = f"=== КРИТИЧЕСКАЯ ОШИБКА ===\nError in load_parties_from_invoices: {str(e)}\n{frappe.get_traceback()}"
frappe.log_error(error_msg, "Critical Error")
return { return {
'success': False, 'success': False,
'message': str(e) 'message': str(e)
} }
@frappe.whitelist() @frappe.whitelist()
def get_default_settings(): def get_default_settings():
"""Получение активных настроек E-Taxes""" """Получение активных настроек E-Taxes"""
@ -1376,22 +1329,34 @@ def get_unmapped_items():
# 1. Получаем все товары со статусом 'New' # 1. Получаем все товары со статусом 'New'
etaxes_items = frappe.get_all('E-Taxes Item', etaxes_items = frappe.get_all('E-Taxes Item',
filters={'status': 'New'}, filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code']) fields=['name', 'etaxes_item_name', 'etaxes_item_code', 'etaxes_unit', 'etaxes_price'])
# 2. Проверяем, нет ли их уже в настройках # 2. Проверяем, нет ли их уже в настройках
existing_mappings = {} existing_mappings = {}
for mapping in settings.item_mappings: for mapping in settings.item_mappings:
existing_mappings[mapping.etaxes_item_name] = True existing_mappings[mapping.etaxes_item_name] = mapping.erp_item
# 3. Добавляем только те, которых нет в настройках # 3. Добавляем только те, которых нет в настройках
for item in etaxes_items: for item in etaxes_items:
# В E-Taxes Item name равен etaxes_item_code из-за настройки autoname
if item.name not in existing_mappings: if item.name not in existing_mappings:
unmapped_items.append({ # Создаем объект товара с настройками по умолчанию
'name': item.name, # Это etaxes_item_code (ID документа) item_data = {
'name': item.name,
'etaxes_item_name': item.etaxes_item_name, 'etaxes_item_name': item.etaxes_item_name,
'etaxes_item_code': item.etaxes_item_code 'etaxes_item_code': item.etaxes_item_code,
}) # Добавляем значения по умолчанию
'item_group': settings.default_item_group,
'uom': settings.default_uom if not item.etaxes_unit else get_uom_for_unit(item.etaxes_unit),
'is_stock_item': settings.is_stock_item,
'is_purchase_item': settings.is_purchase_item,
'item_tax_template': settings.default_item_tax_template
}
# Добавляем цену, если она есть
if hasattr(item, 'etaxes_price') and item.etaxes_price:
item_data['standard_rate'] = item.etaxes_price
unmapped_items.append(item_data)
return { return {
'success': True, 'success': True,
@ -1407,9 +1372,6 @@ def get_unmapped_items():
def get_unmapped_parties(): def get_unmapped_parties():
"""Получение несопоставленных контрагентов из E-Taxes Parties""" """Получение несопоставленных контрагентов из E-Taxes Parties"""
try: try:
# Получаем всех контрагентов из E-Taxes Parties без сопоставления
unmapped_parties = []
# Получаем активные настройки # Получаем активные настройки
settings = get_active_settings() settings = get_active_settings()
if not settings: if not settings:
@ -1418,6 +1380,9 @@ def get_unmapped_parties():
'message': 'Не найдены активные настройки E-Taxes' 'message': 'Не найдены активные настройки E-Taxes'
} }
# Получаем всех контрагентов из E-Taxes Parties без сопоставления
unmapped_parties = []
# Получаем список существующих сопоставлений # Получаем список существующих сопоставлений
existing_mappings = {} existing_mappings = {}
for mapping in settings.party_mappings: for mapping in settings.party_mappings:
@ -1426,18 +1391,42 @@ def get_unmapped_parties():
# Получаем несопоставленные контрагенты из E-Taxes Parties # Получаем несопоставленные контрагенты из E-Taxes Parties
etaxes_parties = frappe.get_all('E-Taxes Parties', etaxes_parties = frappe.get_all('E-Taxes Parties',
filters={'status': 'New'}, filters={'status': 'New'},
fields=['etaxes_party_name', 'etaxes_tax_id', fields=['name', 'etaxes_party_name', 'etaxes_tax_id',
'etaxes_party_type', 'erp_party_type']) 'etaxes_party_type', 'erp_party_type', 'etaxes_address', 'source_invoice'])
# Фильтруем только те, которых нет в маппингах # Фильтруем только те, которых нет в маппингах
for party in etaxes_parties: for party in etaxes_parties:
if party.etaxes_party_name not in existing_mappings: if party.etaxes_party_name not in existing_mappings:
unmapped_parties.append({ # Определяем тип контрагента (Customer/Supplier)
party_type = party.erp_party_type or ('Supplier' if party.etaxes_party_type == 'Sender' else 'Customer')
# Создаем базовый объект с общими данными
party_data = {
'name': party.name,
'etaxes_party_name': party.etaxes_party_name, 'etaxes_party_name': party.etaxes_party_name,
'etaxes_tax_id': party.etaxes_tax_id, 'etaxes_tax_id': party.etaxes_tax_id,
'etaxes_party_type': party.etaxes_party_type, 'etaxes_party_type': party.etaxes_party_type,
'erp_party_type': party.erp_party_type 'erp_party_type': party_type,
}) # Общие настройки
'payment_terms': settings.default_payment_terms
}
# Добавляем адрес, если он есть
if party.etaxes_address:
party_data['address'] = party.etaxes_address
# Добавляем источник, если есть
if party.source_invoice:
party_data['source'] = party.source_invoice
# Добавляем специфические поля в зависимости от типа
if party_type == 'Supplier':
party_data['supplier_group'] = settings.default_supplier_group
else: # Customer
party_data['customer_group'] = settings.default_customer_group
party_data['territory'] = 'All Territories' # Значение по умолчанию
unmapped_parties.append(party_data)
return { return {
'success': True, 'success': True,
@ -1449,7 +1438,6 @@ def get_unmapped_parties():
'message': str(e) 'message': str(e)
} }
@frappe.whitelist() @frappe.whitelist()
def match_similar_items(): def match_similar_items():
"""Сопоставление товаров по похожим названиям""" """Сопоставление товаров по похожим названиям"""
@ -1767,58 +1755,53 @@ def match_similar_parties():
@frappe.whitelist() @frappe.whitelist()
def create_unmapped_items(settings_name): def create_unmapped_items(settings_name):
"""Создание товаров для всех несопоставленных элементов из E-Taxes""" """Создание товаров для несопоставленных элементов из табличной части настроек E-Taxes"""
try: try:
import re import re
# Получаем настройки # Получаем настройки
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Получаем несопоставленные товары # Получаем элементы из табличной части, у которых нет сопоставления
etaxes_items = frappe.get_all('E-Taxes Item', unmapped_items = []
filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code'])
# Логируем полученные товары for mapping_item in settings_doc.item_mappings:
# Проверяем, что у товара нет сопоставления или оно пустое
if not mapping_item.erp_item:
# Получаем документ E-Taxes Item для этой записи
if frappe.db.exists('E-Taxes Item', mapping_item.etaxes_item_name):
etaxes_item = frappe.get_doc('E-Taxes Item', mapping_item.etaxes_item_name)
unmapped_items.append(etaxes_item)
# Получаем существующие маппинги if len(unmapped_items) == 0:
existing_mappings = {} return {
mapping_settings = {} # Для хранения индивидуальных настроек "success": True,
"created_count": 0,
"error_count": 0,
"message": "Нет несопоставленных товаров в табличной части для создания"
}
created_count = 0
error_count = 0
# Собираем индивидуальные настройки для каждого элемента
mapping_settings = {}
for mapping in settings_doc.item_mappings: for mapping in settings_doc.item_mappings:
# Используем name (id) документа E-Taxes Item как ключ
existing_mappings[mapping.etaxes_item_name] = mapping.erp_item
# Сохраняем индивидуальные настройки для этого элемента, если они есть
item_settings = {} item_settings = {}
if mapping.item_group: if mapping.item_group:
item_settings['item_group'] = mapping.item_group item_settings['item_group'] = mapping.item_group
if mapping.uom: if mapping.uom:
item_settings['uom'] = mapping.uom item_settings['uom'] = mapping.uom
if mapping.is_stock_item is not None: # Проверяем, что поле не None if mapping.is_stock_item is not None:
item_settings['is_stock_item'] = mapping.is_stock_item item_settings['is_stock_item'] = mapping.is_stock_item
if mapping.is_purchase_item is not None: # Проверяем, что поле не None if mapping.is_purchase_item is not None:
item_settings['is_purchase_item'] = mapping.is_purchase_item item_settings['is_purchase_item'] = mapping.is_purchase_item
if mapping.item_tax_template: if mapping.item_tax_template:
item_settings['item_tax_template'] = mapping.item_tax_template item_settings['item_tax_template'] = mapping.item_tax_template
if item_settings: # Если есть индивидуальные настройки if item_settings:
mapping_settings[mapping.etaxes_item_name] = item_settings mapping_settings[mapping.etaxes_item_name] = item_settings
# Логируем существующие маппинги
# Фильтруем только те, которых нет в маппингах ИЛИ у которых erp_item пустой
unmapped_items = []
for item in etaxes_items:
# Проверяем, что либо товара нет в маппингах, либо соответствующее поле erp_item пустое
if item.name not in existing_mappings or not existing_mappings[item.name]:
unmapped_items.append(item)
# Логируем отфильтрованные товары
created_count = 0
error_count = 0
for etaxes_item in unmapped_items: for etaxes_item in unmapped_items:
try: try:
# Проверяем, существует ли уже товар с таким именем # Проверяем, существует ли уже товар с таким именем
@ -1836,7 +1819,9 @@ def create_unmapped_items(settings_name):
if not item_code or len(item_code) < 3: if not item_code or len(item_code) < 3:
item_code = "ITEM-" + frappe.generate_hash(length=8) item_code = "ITEM-" + frappe.generate_hash(length=8)
# Логируем создаваемый товар # Проверяем, что item_code уникален
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_code = item_code
item_doc.item_name = etaxes_item.etaxes_item_name item_doc.item_name = etaxes_item.etaxes_item_name
@ -1846,8 +1831,19 @@ def create_unmapped_items(settings_name):
individual_settings = mapping_settings.get(etaxes_item.name, {}) individual_settings = mapping_settings.get(etaxes_item.name, {})
# Применяем настройки с учетом индивидуальных приоритетов # Применяем настройки с учетом индивидуальных приоритетов
item_doc.item_group = individual_settings.get('item_group', settings_doc.default_item_group) item_group = individual_settings.get('item_group', settings_doc.default_item_group)
item_doc.stock_uom = individual_settings.get('uom', settings_doc.default_uom) stock_uom = individual_settings.get('uom', settings_doc.default_uom)
# Проверяем существование item_group
if not frappe.db.exists('Item Group', item_group):
item_group = "All Item Groups"
# Проверяем существование UOM
if not frappe.db.exists('UOM', stock_uom):
stock_uom = "Nos"
item_doc.item_group = item_group
item_doc.stock_uom = stock_uom
# Для булевых полей проверяем, что они не None # Для булевых полей проверяем, что они не None
if 'is_stock_item' in individual_settings: if 'is_stock_item' in individual_settings:
@ -1860,39 +1856,32 @@ def create_unmapped_items(settings_name):
else: else:
item_doc.is_purchase_item = getattr(settings_doc, 'is_purchase_item', 1) item_doc.is_purchase_item = getattr(settings_doc, 'is_purchase_item', 1)
# Если есть шаблон налогов, применяем его # Правильно добавляем налоговый шаблон
if 'item_tax_template' in individual_settings: if 'item_tax_template' in individual_settings:
item_doc.taxes = [{ tax_template = individual_settings['item_tax_template']
"item_tax_template": individual_settings['item_tax_template'] if frappe.db.exists('Item Tax Template', tax_template):
}] tax_row = item_doc.append('taxes', {})
tax_row.item_tax_template = tax_template
elif hasattr(settings_doc, 'default_item_tax_template') and settings_doc.default_item_tax_template: elif hasattr(settings_doc, 'default_item_tax_template') and settings_doc.default_item_tax_template:
item_doc.taxes = [{ tax_template = settings_doc.default_item_tax_template
"item_tax_template": settings_doc.default_item_tax_template if frappe.db.exists('Item Tax Template', tax_template):
}] tax_row = item_doc.append('taxes', {})
tax_row.item_tax_template = tax_template
# Сохраняем товар # Сохраняем товар
item_doc.insert() item_doc.insert()
# Логируем созданный товар # Обновляем маппинг в настройках
for mapping in settings_doc.item_mappings:
# Проверяем, существует ли уже маппинг для этого товара
existing_row = None
for idx, mapping in enumerate(settings_doc.item_mappings):
if mapping.etaxes_item_name == etaxes_item.name: if mapping.etaxes_item_name == etaxes_item.name:
existing_row = idx mapping.erp_item = item_doc.name
mapping.mapping_type = 'Automatic'
break break
if existing_row is not None: # Обновляем статус E-Taxes Item
# Если строка уже существует, обновляем её значение erp_item etaxes_item.status = 'Mapped'
settings_doc.item_mappings[existing_row].erp_item = item_doc.name etaxes_item.mapped_item = item_doc.name
settings_doc.item_mappings[existing_row].mapping_type = 'Automatic' etaxes_item.save()
else:
# Если строки нет, добавляем новую
settings_doc.append('item_mappings', {
'etaxes_item_name': etaxes_item.name, # Используем ID документа
'erp_item': item_doc.name,
'mapping_type': 'Automatic'
})
created_count += 1 created_count += 1
@ -1903,18 +1892,6 @@ def create_unmapped_items(settings_name):
if created_count > 0: if created_count > 0:
settings_doc.save() settings_doc.save()
# Обновляем статусы E-Taxes Item
for etaxes_item in unmapped_items:
# Ищем сопоставление по ID документа
for mapping in settings_doc.item_mappings:
if mapping.etaxes_item_name == etaxes_item.name and mapping.erp_item:
item_doc = frappe.get_doc('E-Taxes Item', etaxes_item.name)
item_doc.status = 'Mapped'
item_doc.mapped_item = mapping.erp_item
item_doc.save()
break
return { return {
"success": True, "success": True,
"created_count": created_count, "created_count": created_count,
@ -1928,51 +1905,6 @@ def create_unmapped_items(settings_name):
"message": str(e) "message": str(e)
} }
@frappe.whitelist()
def get_unmapped_items():
"""Получение несопоставленных товаров из E-Taxes"""
try:
# Получаем активные настройки
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'Не найдены активные настройки E-Taxes'
}
# Получаем все товары из E-Taxes без сопоставления
unmapped_items = []
# 1. Получаем все товары со статусом 'New'
etaxes_items = frappe.get_all('E-Taxes Item',
filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code'])
# 2. Проверяем, нет ли их уже в настройках
existing_mappings = {}
for mapping in settings.item_mappings:
existing_mappings[mapping.etaxes_item_name] = True
# 3. Добавляем только те, которых нет в настройках
# КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: проверяем по etaxes_item_name, а не по item.name
for item in etaxes_items:
if item.etaxes_item_name not in existing_mappings:
unmapped_items.append({
'name': item.name, # ID документа (равен etaxes_item_code из-за autoname)
'etaxes_item_name': item.etaxes_item_name,
'etaxes_item_code': item.etaxes_item_code
})
return {
'success': True,
'items': unmapped_items
}
except Exception as e:
return {
'success': False,
'message': str(e)
}
@frappe.whitelist() @frappe.whitelist()
def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None): def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None):
"""Импорт инвойса с учетом сопоставлений и обработкой несопоставленных элементов""" """Импорт инвойса с учетом сопоставлений и обработкой несопоставленных элементов"""
@ -2360,31 +2292,44 @@ def debug_azeri_matching(search_term, compare_with):
@frappe.whitelist() @frappe.whitelist()
def create_unmapped_parties(settings_name): def create_unmapped_parties(settings_name):
"""Создание контрагентов для всех несопоставленных элементов из E-Taxes Parties""" """Создание контрагентов для несопоставленных элементов из табличной части настроек E-Taxes"""
try: try:
import re
# Получаем настройки # Получаем настройки
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Получаем несопоставленные контрагенты # Получаем элементы из табличной части, у которых нет сопоставления
etaxes_parties = frappe.get_all('E-Taxes Parties', unmapped_parties = []
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id',
'etaxes_party_type', 'erp_party_type'])
# Логируем полученные контрагенты for mapping_party in settings_doc.party_mappings:
frappe.log_error(f"E-Taxes parties with status New: {etaxes_parties}", "Create Parties Debug") # Проверяем, что у контрагента нет сопоставления или оно пустое
if not mapping_party.erp_party:
# Получаем документ E-Taxes Parties для этой записи
parties = frappe.get_all('E-Taxes Parties',
filters={
'etaxes_party_name': mapping_party.etaxes_party_name,
'etaxes_tax_id': mapping_party.etaxes_tax_id
},
fields=['name'])
if parties:
etaxes_party = frappe.get_doc('E-Taxes Parties', parties[0].name)
# Добавляем тип контрагента из настроек
etaxes_party.erp_party_type = mapping_party.party_type
unmapped_parties.append(etaxes_party)
# Получаем существующие маппинги if len(unmapped_parties) == 0:
existing_mappings = {} return {
mapping_settings = {} # Для хранения индивидуальных настроек "success": True,
"created_count": 0,
"error_count": 0,
"message": "Нет несопоставленных контрагентов в табличной части для создания"
}
created_count = 0
error_count = 0
# Собираем индивидуальные настройки для каждого элемента
mapping_settings = {}
for mapping in settings_doc.party_mappings: for mapping in settings_doc.party_mappings:
# Используем name (id) документа E-Taxes Parties как ключ
existing_mappings[mapping.etaxes_party_name] = mapping.erp_party
# Сохраняем индивидуальные настройки для этого элемента, если они есть
party_settings = {} party_settings = {}
if mapping.supplier_group: if mapping.supplier_group:
party_settings['supplier_group'] = mapping.supplier_group party_settings['supplier_group'] = mapping.supplier_group
@ -2395,54 +2340,39 @@ def create_unmapped_parties(settings_name):
if mapping.payment_terms: if mapping.payment_terms:
party_settings['payment_terms'] = mapping.payment_terms party_settings['payment_terms'] = mapping.payment_terms
if party_settings: # Если есть индивидуальные настройки if party_settings:
mapping_settings[mapping.etaxes_party_name] = party_settings key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}"
mapping_settings[key] = party_settings
# Логируем существующие маппинги
frappe.log_error(f"Existing mappings: {existing_mappings}", "Create Parties Debug")
# Фильтруем только те, которых нет в маппингах ИЛИ у которых erp_party пустой
unmapped_parties = []
for party in etaxes_parties:
# Проверяем, что либо контрагента нет в маппингах, либо соответствующее поле erp_party пустое
if party.etaxes_party_name not in existing_mappings or not existing_mappings[party.etaxes_party_name]:
unmapped_parties.append(party)
# Логируем отфильтрованных контрагентов
frappe.log_error(f"Unmapped parties to create: {unmapped_parties}", "Create Parties Debug")
created_count = 0
error_count = 0
for etaxes_party in unmapped_parties: for etaxes_party in unmapped_parties:
try: try:
# Определяем тип контрагента (Customer/Supplier) на основе erp_party_type # Определяем тип контрагента (Customer/Supplier)
party_type = etaxes_party.erp_party_type party_type = etaxes_party.erp_party_type
# Получаем индивидуальные настройки для этого контрагента
individual_settings = mapping_settings.get(etaxes_party.etaxes_party_name, {})
# Проверяем, существует ли уже контрагент с таким названием и типом # Проверяем, существует ли уже контрагент с таким названием и типом
party_field = 'supplier_name' if party_type == 'Supplier' else 'customer_name' party_field = 'supplier_name' if party_type == 'Supplier' else 'customer_name'
party_exists = frappe.db.exists(party_type, {party_field: etaxes_party.etaxes_party_name}) party_exists = frappe.db.exists(party_type, {party_field: etaxes_party.etaxes_party_name})
if party_exists: if party_exists:
frappe.log_error(f"{party_type} already exists: {etaxes_party.etaxes_party_name}", "Create Parties Debug")
continue continue
# Логируем создаваемого контрагента # Создаем нового контрагента
frappe.log_error(f"Creating {party_type}: {etaxes_party.etaxes_party_name}", "Create Parties Debug")
# Создаем новый контрагент
party_doc = frappe.new_doc(party_type) party_doc = frappe.new_doc(party_type)
# Получаем индивидуальные настройки
key = f"{etaxes_party.etaxes_party_name}|{etaxes_party.etaxes_tax_id}"
individual_settings = mapping_settings.get(key, {})
# Задаем основные поля # Задаем основные поля
if party_type == "Supplier": if party_type == "Supplier":
party_doc.supplier_name = etaxes_party.etaxes_party_name party_doc.supplier_name = etaxes_party.etaxes_party_name
party_doc.supplier_type = "Company" # Можно настроить при необходимости party_doc.supplier_type = "Company" # Можно настроить при необходимости
# Применяем настройки с учетом индивидуальных приоритетов # Применяем настройки с учетом индивидуальных приоритетов
party_doc.supplier_group = individual_settings.get('supplier_group', settings_doc.default_supplier_group) 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"
party_doc.supplier_group = supplier_group
# Если указан tax_id, добавляем его # Если указан tax_id, добавляем его
if etaxes_party.etaxes_tax_id: if etaxes_party.etaxes_tax_id:
@ -2458,14 +2388,16 @@ def create_unmapped_parties(settings_name):
party_doc.customer_type = "Company" # Можно настроить при необходимости party_doc.customer_type = "Company" # Можно настроить при необходимости
# Применяем настройки с учетом индивидуальных приоритетов # Применяем настройки с учетом индивидуальных приоритетов
party_doc.customer_group = individual_settings.get('customer_group', settings_doc.default_customer_group) customer_group = individual_settings.get('customer_group', settings_doc.default_customer_group)
if not frappe.db.exists('Customer Group', customer_group):
customer_group = "All Customer Groups"
party_doc.customer_group = customer_group
# Территория # Территория
if 'territory' in individual_settings: territory = individual_settings.get('territory', "All Territories")
party_doc.territory = individual_settings['territory'] if not frappe.db.exists('Territory', territory):
else: territory = "All Territories"
# Можно использовать "All Territories" как дефолтное значение party_doc.territory = territory
party_doc.territory = "All Territories"
# Если указан tax_id, добавляем его # Если указан tax_id, добавляем его
if etaxes_party.etaxes_tax_id: if etaxes_party.etaxes_tax_id:
@ -2480,49 +2412,28 @@ def create_unmapped_parties(settings_name):
# Сохраняем контрагента # Сохраняем контрагента
party_doc.insert() party_doc.insert()
# Логируем созданного контрагента # Обновляем маппинг в настройках
frappe.log_error(f"{party_type} created successfully: {party_doc.name}", "Create Parties Debug") for mapping in settings_doc.party_mappings:
if mapping.etaxes_party_name == etaxes_party.etaxes_party_name and mapping.etaxes_tax_id == etaxes_party.etaxes_tax_id:
# Проверяем, существует ли уже маппинг для этого контрагента mapping.erp_party = party_doc.name
existing_row = None mapping.party_type = party_type
for idx, mapping in enumerate(settings_doc.party_mappings): mapping.mapping_type = 'Automatic'
if mapping.etaxes_party_name == etaxes_party.etaxes_party_name:
existing_row = idx
break break
if existing_row is not None: # Обновляем статус E-Taxes Parties
# Если строка уже существует, обновляем её значение erp_party etaxes_party.status = 'Mapped'
settings_doc.party_mappings[existing_row].erp_party = party_doc.name etaxes_party.mapped_party = party_doc.name
settings_doc.party_mappings[existing_row].party_type = party_type etaxes_party.save()
settings_doc.party_mappings[existing_row].mapping_type = 'Automatic'
else:
# Если строки нет, добавляем новую
settings_doc.append('party_mappings', {
'etaxes_party_name': etaxes_party.etaxes_party_name,
'etaxes_tax_id': etaxes_party.etaxes_tax_id,
'party_type': party_type,
'erp_party': party_doc.name,
'mapping_type': 'Automatic'
})
# Обновляем запись в E-Taxes Parties
party_doc_etaxes = frappe.get_doc('E-Taxes Parties', etaxes_party.name)
party_doc_etaxes.status = 'Mapped'
party_doc_etaxes.mapped_party = party_doc.name
party_doc_etaxes.save()
created_count += 1 created_count += 1
except Exception as e: except Exception as e:
frappe.log_error(f"Error creating party {etaxes_party.etaxes_party_name}: {str(e)}", "Create Parties Debug")
error_count += 1 error_count += 1
# Сохраняем настройки после добавления всех сопоставлений # Сохраняем настройки после добавления всех сопоставлений
if created_count > 0: if created_count > 0:
settings_doc.save() settings_doc.save()
frappe.log_error(f"Final result: created {created_count} parties, errors: {error_count}", "Create Parties Debug")
return { return {
"success": True, "success": True,
"created_count": created_count, "created_count": created_count,
@ -2531,7 +2442,6 @@ def create_unmapped_parties(settings_name):
} }
except Exception as e: except Exception as e:
frappe.log_error(f"Error in create_unmapped_parties: {str(e)}\n{frappe.get_traceback()}", "Create Parties Debug")
return { return {
"success": False, "success": False,
"message": str(e) "message": str(e)
@ -2585,3 +2495,144 @@ def check_token_validity(asan_login_name=None):
except Exception as e: except Exception as e:
frappe.log_error(f"Error in check_token_validity: {str(e)}\n{frappe.get_traceback()}", "Token Validity Check") frappe.log_error(f"Error in check_token_validity: {str(e)}\n{frappe.get_traceback()}", "Token Validity Check")
return {"valid": False, "message": f"Error: {str(e)}"} return {"valid": False, "message": f"Error: {str(e)}"}
@frappe.whitelist()
def check_invoice_exists(serial_number):
"""Проверяет, существует ли уже инвойс с указанным серийным номером"""
try:
# Проверяем наличие заказа с указанным серийным номером
exists = frappe.db.exists('Purchase Order', {'invoice_serial_number': serial_number, 'docstatus': ['!=', 2]})
return {
'exists': bool(exists),
'serial_number': serial_number
}
except Exception as e:
frappe.log_error(f"Error in check_invoice_exists: {str(e)}", "API Error")
return {
'exists': False,
'error': str(e)
}
@frappe.whitelist()
def get_etaxes_purchases():
"""Получает список всех записей E-Taxes Purchase"""
try:
purchases = frappe.get_all('E-Taxes Purchase',
fields=['name', 'etaxes_id', 'date', 'party', 'total'])
# Логирование для отладки
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': str(e)
}
@frappe.whitelist()
def create_etaxes_purchase(etaxes_id, date, party, total):
"""Создает запись E-Taxes Purchase для отслеживания импортированных инвойсов"""
try:
# Нормализуем ID для хранения
etaxes_id = str(etaxes_id).strip() if etaxes_id else ""
# Логирование для отладки
frappe.logger().info(f"Creating E-Taxes Purchase: ID={etaxes_id}, date={date}, party={party}, total={total}")
# Проверяем, существует ли уже запись с таким etaxes_id
existing = frappe.db.get_value('E-Taxes Purchase', {'etaxes_id': etaxes_id}, 'name')
if existing:
# Логирование для отладки
frappe.logger().info(f"E-Taxes Purchase already exists: {existing}")
return {
'success': True,
'message': 'Record already exists',
'name': existing
}
# Создаем новую запись
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() # Явный commit
# Логирование для отладки
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': str(e)
}
@frappe.whitelist()
def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase):
"""Устанавливает связь между Purchase Order и E-Taxes Purchase"""
try:
# Логирование для отладки
frappe.logger().info(f"Linking Purchase Order {purchase_order} to E-Taxes Purchase {etaxes_purchase}")
# Проверяем, существуют ли оба документа
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'
}
# Получаем документ Purchase Order
po_doc = frappe.get_doc('Purchase Order', purchase_order)
# Устанавливаем связь с E-Taxes Purchase
po_doc.is_taxes_doc = 1
po_doc.taxes_doc = etaxes_purchase
# Сохраняем изменения
po_doc.save()
frappe.db.commit() # Явный commit
# Логирование для отладки
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': str(e)
}
def on_delete_purchase_order(doc, method):
"""Удаляет связанную запись E-Taxes Purchase при удалении Purchase Order"""
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")
# Проверяем, существует ли запись E-Taxes Purchase
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")

View File

@ -1,16 +1,25 @@
frappe.ui.form.on('Purchase Order', { frappe.ui.form.on('Purchase Order', {
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.docstatus === 0) { // Проверяем, установлена ли галочка is_taxes_doc
frm.add_custom_button(__('Импорт из E-Taxes'), function() { if (frm.doc.is_taxes_doc) {
show_etaxes_import_dialog(frm); // Делаем все поля read-only
}, __('Создать')); frm.set_read_only(true);
// Альтернативно, можно добавить визуальное предупреждение
frm.page.set_indicator(__('Налоговый документ'), 'blue');
} }
},
// Делаем поле is_taxes_doc тоже read-only при открытии документа
onload: function(frm) {
frm.set_df_property('is_taxes_doc', 'read_only', 1);
} }
}); });
frappe.listview_settings['Purchase Order'] = { frappe.listview_settings['Purchase Order'] = {
// Добавляем необходимые поля в запрос // Добавляем необходимые поля в запрос
add_fields: ['supplier', 'transaction_date', 'status', 'docstatus'], add_fields: ['supplier', 'transaction_date', 'status', 'docstatus', 'is_taxes_doc', 'taxes_doc'],
// Настраиваем индикаторы статусов // Настраиваем индикаторы статусов
get_indicator: function(doc) { get_indicator: function(doc) {
@ -42,8 +51,11 @@ frappe.listview_settings['Purchase Order'] = {
// Глобальная переменная для хранения диалога загрузки // Глобальная переменная для хранения диалога загрузки
var loading_dialog = null; var loading_dialog = null;
// Функция для отображения диалога загрузки // Глобальная переменная для отмены процесса загрузки
function show_loading_dialog(message) { var cancel_loading = false;
// Функция для отображения диалога загрузки с прогресс-баром
function show_loading_dialog(message, show_progress = false) {
if (!loading_dialog) { if (!loading_dialog) {
loading_dialog = new frappe.ui.Dialog({ loading_dialog = new frappe.ui.Dialog({
title: __('Загрузка'), title: __('Загрузка'),
@ -53,7 +65,7 @@ function show_loading_dialog(message) {
fieldtype: 'HTML', fieldtype: 'HTML',
options: ` options: `
<div class="text-center"> <div class="text-center">
<div class="lds-dual-ring" style="display: inline-block; width: 64px; height: 64px;"> <div class="loading-animation" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div> <div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
</div> </div>
<style> <style>
@ -61,6 +73,31 @@ function show_loading_dialog(message) {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
.progress-container {
width: 100%;
margin-top: 20px;
display: none;
}
.progress-bar {
width: 100%;
background-color: #f3f3f3;
border-radius: 4px;
height: 20px;
position: relative;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #4e73df;
border-radius: 4px;
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
text-align: center;
font-weight: bold;
margin-top: 5px;
}
</style> </style>
<div class="mt-3" id="status_message"> <div class="mt-3" id="status_message">
<h4>${message || __('Пожалуйста, подождите...')}</h4> <h4>${message || __('Пожалуйста, подождите...')}</h4>
@ -73,29 +110,79 @@ function show_loading_dialog(message) {
</div> </div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p> <p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div> </div>
<!-- Прогресс-бар -->
<div class="progress-container" id="progress_container">
<div class="progress-bar">
<div class="progress-bar-fill" id="progress_bar_fill"></div>
</div>
<div class="progress-text" id="progress_text">0%</div>
</div>
</div> </div>
` `
} }
], ],
primary_action_label: __('Отмена'), primary_action_label: __('Отмена'),
primary_action: function() { primary_action: function() {
hide_loading_dialog(); cancel_loading = true;
loading_dialog.$wrapper.find('.primary-action').prop('disabled', true);
loading_dialog.$wrapper.find('.primary-action').html(__('Отмена в процессе...'));
$('#status_message p').text(__('Отмена загрузки. Уже загруженные инвойсы сохранятся.'));
} }
}); });
} else { } else {
// Обновляем сообщение // Обновляем сообщение
$('#status_message h4').text(message || __('Пожалуйста, подождите...')); $('#status_message h4').text(message || __('Пожалуйста, подождите...'));
$('#status_message p').text(__('Операция может занять некоторое время'));
} }
// Показываем или скрываем прогресс-бар
if (show_progress) {
$('#progress_container').show();
// Сбрасываем прогресс
$('#progress_bar_fill').css('width', '0%');
$('#progress_text').text('0%');
} else {
$('#progress_container').hide();
}
// Сбрасываем флаг отмены
cancel_loading = false;
loading_dialog.show(); loading_dialog.show();
} }
// Функция для обновления прогресса
function update_progress(current, total) {
if (loading_dialog) {
var percent = Math.round((current / total) * 100);
$('#progress_bar_fill').css('width', percent + '%');
$('#progress_text').text(current + ' из ' + total + ' (' + percent + '%)');
}
}
// Функция для скрытия диалога загрузки // Функция для скрытия диалога загрузки
function hide_loading_dialog() { function hide_loading_dialog() {
if (loading_dialog) { if (loading_dialog) {
loading_dialog.hide(); try {
loading_dialog.hide();
} catch (e) {
console.error("Error hiding loading dialog:", e);
}
loading_dialog = null; loading_dialog = null;
} }
// Сбрасываем флаг отмены
cancel_loading = false;
}
function ensure_loading_dialog_closed(timeout = 10000) {
// Устанавливаем таймаут для закрытия диалога
if (loading_dialog) {
setTimeout(function() {
if (loading_dialog) {
hide_loading_dialog();
}
}, timeout);
}
} }
// Функция для обновления сообщения в диалоге загрузки // Функция для обновления сообщения в диалоге загрузки
@ -121,7 +208,7 @@ function show_verification_code(code) {
function set_loading_success(message, submessage, callback, delay = 2000) { function set_loading_success(message, submessage, callback, delay = 2000) {
if (loading_dialog) { if (loading_dialog) {
// Меняем анимацию на галочку // Меняем анимацию на галочку
loading_dialog.$wrapper.find('.lds-dual-ring').html(` loading_dialog.$wrapper.find('.loading-animation').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;"> <div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i> <i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div> </div>
@ -133,8 +220,9 @@ function set_loading_success(message, submessage, callback, delay = 2000) {
$('#status_message p').text(submessage); $('#status_message p').text(submessage);
} }
// Скрываем код верификации // Скрываем код верификации и прогресс
$('#verification_code_container').hide(); $('#verification_code_container').hide();
$('#progress_container').hide();
// Удаляем кнопки // Удаляем кнопки
loading_dialog.set_primary_action(null); loading_dialog.set_primary_action(null);
@ -157,7 +245,7 @@ function set_loading_success(message, submessage, callback, delay = 2000) {
function set_loading_error(message, submessage, show_close_button = true) { function set_loading_error(message, submessage, show_close_button = true) {
if (loading_dialog) { if (loading_dialog) {
// Меняем анимацию на крестик // Меняем анимацию на крестик
loading_dialog.$wrapper.find('.lds-dual-ring').html(` loading_dialog.$wrapper.find('.loading-animation').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;"> <div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i> <i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
</div> </div>
@ -169,8 +257,9 @@ function set_loading_error(message, submessage, show_close_button = true) {
$('#status_message p').text(submessage); $('#status_message p').text(submessage);
} }
// Скрываем код верификации // Скрываем код верификации и прогресс
$('#verification_code_container').hide(); $('#verification_code_container').hide();
$('#progress_container').hide();
// При необходимости добавляем кнопку закрытия // При необходимости добавляем кнопку закрытия
if (show_close_button) { if (show_close_button) {
@ -353,8 +442,7 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) {
}, },
error: function(err) { error: function(err) {
// В случае ошибки продолжаем опрос // В случае ошибки продолжаем опрос
console.error('Error during status check:', err); setTimeout(pollStatus, 6000);
setTimeout(pollStatus, 6000);
} }
}); });
} }
@ -440,8 +528,7 @@ function complete_authentication(asan_login_name, callback) {
} }
}); });
} catch (e) { } catch (e) {
console.error('Error parsing certificate:', e); hide_loading_dialog();
hide_loading_dialog();
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback); show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
} }
} else { } else {
@ -651,94 +738,6 @@ function show_etaxes_import_dialog(frm) {
}); });
} }
// Новая функция для загрузки инвойсов с обработкой ошибок авторизации
function load_etaxes_invoices(frm, fromDate, toDate, warehouse) {
// Сначала получаем настройки авторизации
frappe.call({
method: 'invoice_az.api.get_default_asan_login',
callback: function(login_response) {
if (login_response.message && login_response.message.found) {
// Получаем токен из настроек
let main_token = login_response.message.main_token;
if (!main_token) {
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка авторизации'),
indicator: 'red',
message: __('Не найден токен авторизации. Пожалуйста, завершите процесс авторизации в настройках E-Taxes.')
});
return;
}
// Загружаем счета-фактуры с использованием полученного токена
frappe.call({
method: 'invoice_az.api.get_invoices',
args: {
'token': main_token,
'filters': JSON.stringify({
"creationDateFrom": fromDate,
"creationDateTo": toDate
})
},
callback: function(r) {
// Закрываем диалог загрузки
hide_loading_dialog();
if (r.message && !r.message.error) {
// Получаем данные о счетах-фактурах
let invoices = r.message.data || r.message.invoices || [];
if (invoices.length > 0) {
// Передаем токен, frm и warehouse в следующий диалог
show_invoice_selection_dialog(frm, invoices, main_token, warehouse);
} else {
frappe.msgprint({
title: __('Информация'),
indicator: 'blue',
message: __('За указанный период не найдено счетов-фактур')
});
}
} else if (r.message && r.message.error === 'unauthorized') {
// Если ошибка авторизации, предлагаем авторизоваться
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
// Запускаем процесс аутентификации и после успеха повторяем загрузку
start_authentication_process(function() {
// После успешной аутентификации повторяем загрузку
load_etaxes_invoices(frm, fromDate, toDate, warehouse);
});
},
function() {
// Если пользователь отказался, показываем сообщение
frappe.show_alert({
message: __('Authentication canceled. Operation cannot be completed.'),
indicator: 'red'
}, 5);
}
);
} else {
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: r.message ? r.message.message : __('Ошибка загрузки счетов-фактур')
});
}
}
});
} else {
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: __('Не найдены настройки авторизации E-Taxes')
});
}
}
});
}
// Функция для отображения диалога импорта при работе из списка // Функция для отображения диалога импорта при работе из списка
function show_etaxes_import_dialog_for_list() { function show_etaxes_import_dialog_for_list() {
// Сначала проверяем токен // Сначала проверяем токен
@ -816,6 +815,215 @@ function show_etaxes_import_dialog_for_list() {
}); });
} }
// Функция для загрузки инвойсов с фильтрацией дубликатов
function load_etaxes_invoices(frm, fromDate, toDate, warehouse) {
// Устанавливаем таймаут для автоматического закрытия диалога
ensure_loading_dialog_closed(30000); // 30 секунд максимум
// Сначала получаем настройки авторизации
frappe.call({
method: 'invoice_az.api.get_default_asan_login',
callback: function(login_response) {
if (!loading_dialog) {
// Если диалог был закрыт (пользователь ушел со страницы), прекращаем операцию
return;
}
if (login_response.message && login_response.message.found) {
// Получаем токен из настроек
let main_token = login_response.message.main_token;
if (!main_token) {
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка авторизации'),
indicator: 'red',
message: __('Не найден токен авторизации. Пожалуйста, завершите процесс авторизации в настройках E-Taxes.')
});
return;
}
// Загружаем счета-фактуры с использованием полученного токена
frappe.call({
method: 'invoice_az.api.get_invoices',
args: {
'token': main_token,
'filters': JSON.stringify({
"creationDateFrom": fromDate,
"creationDateTo": toDate
})
},
callback: function(r) {
if (!loading_dialog) {
// Если диалог был закрыт, прекращаем операцию
return;
}
if (r.message && !r.message.error) {
// Получаем данные о счетах-фактурах
let invoices = r.message.data || r.message.invoices || [];
if (invoices.length > 0) {
// Показываем диалог загрузки для проверки дубликатов
update_loading_message(
__('Проверка на дубликаты...'),
__('Фильтрация уже импортированных счетов-фактур')
);
// Получаем список уже импортированных инвойсов из E-Taxes Purchase
frappe.call({
method: 'invoice_az.api.get_etaxes_purchases',
callback: function(etaxes_r) {
// Проверяем, что диалог все еще открыт
if (!loading_dialog) {
return;
}
try {
// Преобразуем данные E-Taxes Purchase в хеш-таблицу для быстрого поиска
let importedInvoices = {};
let importedCount = 0;
if (etaxes_r.message && etaxes_r.message.success && etaxes_r.message.purchases) {
importedCount = etaxes_r.message.purchases.length;
etaxes_r.message.purchases.forEach(function(purchase) {
// Проверяем и нормализуем ID
if (purchase.etaxes_id) {
// Нормализуем ID для более надежного сравнения
let normId = String(purchase.etaxes_id).trim();
importedInvoices[normId] = true;
}
});
}
// Фильтруем список инвойсов, исключая дубликаты
let filteredInvoices = [];
let skippedCount = 0;
for (let i = 0; i < invoices.length; i++) {
const invoice = invoices[i];
// Нормализуем ID инвойса из API
let invoiceId = String(invoice.id).trim();
// Проверяем, есть ли запись с таким ID в E-Taxes Purchase
if (!importedInvoices.hasOwnProperty(invoiceId)) {
filteredInvoices.push(invoice);
} else {
skippedCount++;
}
}
// Скрываем диалог загрузки
hide_loading_dialog();
if (filteredInvoices.length > 0) {
// Если есть не импортированные инвойсы, показываем их для выбора
show_invoice_selection_dialog(frm, filteredInvoices, main_token, warehouse);
} else {
frappe.msgprint({
title: __('Информация'),
indicator: 'blue',
message: __('За указанный период не найдено новых счетов-фактур (все ' +
invoices.length + ' уже импортированы)')
});
}
} catch (e) {
console.error("Error processing purchase data:", e);
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: __('Произошла ошибка при обработке данных: ') + e.message
});
}
},
error: function(err) {
console.error("Error fetching purchases:", err);
hide_loading_dialog();
// В случае ошибки показываем все инвойсы без фильтрации
show_invoice_selection_dialog(frm, invoices, main_token, warehouse);
frappe.show_alert({
message: __('Не удалось выполнить проверку дубликатов. Показаны все инвойсы.'),
indicator: 'orange'
}, 5);
}
});
} else {
// Скрываем диалог загрузки
hide_loading_dialog();
frappe.msgprint({
title: __('Информация'),
indicator: 'blue',
message: __('За указанный период не найдено счетов-фактур')
});
}
} else if (r.message && r.message.error === 'unauthorized') {
// Если ошибка авторизации, предлагаем авторизоваться
hide_loading_dialog();
frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
// Запускаем процесс аутентификации и после успеха повторяем загрузку
start_authentication_process(function() {
// После успешной аутентификации повторяем загрузку
load_etaxes_invoices(frm, fromDate, toDate, warehouse);
});
},
function() {
// Если пользователь отказался, показываем сообщение
frappe.show_alert({
message: __('Authentication canceled. Operation cannot be completed.'),
indicator: 'red'
}, 5);
}
);
} else {
// Скрываем диалог загрузки
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: r.message ? r.message.message : __('Ошибка загрузки счетов-фактур')
});
}
},
error: function(xhr, status, error) {
console.error("Error fetching invoices:", error);
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка сети'),
indicator: 'red',
message: __('Ошибка при получении данных с сервера: ') + (error || 'Unknown error')
});
}
});
} else {
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: __('Не найдены настройки авторизации E-Taxes')
});
}
},
error: function(xhr, status, error) {
console.error("Error fetching Asan login settings:", error);
hide_loading_dialog();
frappe.msgprint({
title: __('Ошибка сети'),
indicator: 'red',
message: __('Ошибка при получении настроек авторизации: ') + (error || 'Unknown error')
});
}
});
}
// Функция для отображения диалога с выбором счета-фактуры // Функция для отображения диалога с выбором счета-фактуры
function show_invoice_selection_dialog(frm, invoices, token, warehouse) { function show_invoice_selection_dialog(frm, invoices, token, warehouse) {
// Создаем таблицу с инвойсами // Создаем таблицу с инвойсами
@ -830,7 +1038,15 @@ function show_invoice_selection_dialog(frm, invoices, token, warehouse) {
invoices.forEach(function(invoice) { invoices.forEach(function(invoice) {
let serialNumber = invoice.serialNumber || ''; let serialNumber = invoice.serialNumber || '';
let creationDate = invoice.creationDate ? moment(invoice.creationDate).format('DD.MM.YYYY') : '';
// Используем createdAt для отображения даты, с запасным вариантом creationDate
let creationDate = '';
if (invoice.createdAt) {
creationDate = moment(invoice.createdAt).format('DD.MM.YYYY');
} else if (invoice.creationDate) {
creationDate = moment(invoice.creationDate).format('DD.MM.YYYY');
}
let senderName = invoice.sender ? invoice.sender.name : (invoice.senderName || ''); let senderName = invoice.sender ? invoice.sender.name : (invoice.senderName || '');
let amount = invoice.totalAmount || invoice.amount || 0; let amount = invoice.totalAmount || invoice.amount || 0;
@ -845,10 +1061,20 @@ function show_invoice_selection_dialog(frm, invoices, token, warehouse) {
invoice_table += '</tbody></table></div>'; invoice_table += '</tbody></table></div>';
// Информация о количестве найденных инвойсов
let info_message = '<div class="alert alert-info">' +
__('Найдено новых счетов-фактур: ') + invoices.length +
'</div>';
// Создаем диалог // Создаем диалог
var d = new frappe.ui.Dialog({ var d = new frappe.ui.Dialog({
title: __('Выбор счета-фактуры'), title: __('Выбор счета-фактуры'),
fields: [ fields: [
{
fieldname: 'info_html',
fieldtype: 'HTML',
options: info_message
},
{ {
fieldname: 'invoices_html', fieldname: 'invoices_html',
fieldtype: 'HTML', fieldtype: 'HTML',
@ -882,7 +1108,12 @@ function show_invoice_selection_dialog(frm, invoices, token, warehouse) {
d.hide(); d.hide();
// Загружаем выбранные инвойсы последовательно // Загружаем выбранные инвойсы последовательно
load_selected_invoices(frm, selected_invoice_ids, token, warehouse); // Показываем диалог с прогрессом
show_loading_dialog(__('Загрузка инвойсов...'), true);
update_progress(0, selected_invoice_ids.length);
// Загружаем выбранные инвойсы последовательно
load_selected_invoices(frm, selected_invoice_ids, token, warehouse, 0);
}, },
secondary_action_label: __('Отмена'), secondary_action_label: __('Отмена'),
secondary_action: function() { secondary_action: function() {
@ -899,14 +1130,36 @@ function show_invoice_selection_dialog(frm, invoices, token, warehouse) {
}); });
} }
// Функция для загрузки выбранных инвойсов // Функция для загрузки выбранных инвойсов с прогресс-баром и механизмом отмены
function load_selected_invoices(frm, invoice_ids, token, warehouse) { function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count = 0) {
if (invoice_ids.length === 0) { // Если все инвойсы обработаны или был запрос на отмену
// Если все инвойсы обработаны, обновляем документ if (invoice_ids.length === 0 || cancel_loading) {
if (frm) { // Если был запрос на отмену, показываем соответствующее сообщение
frm.reload_doc(); if (cancel_loading) {
set_loading_success(
__('Загрузка отменена'),
__('Загружено ' + processed_count + ' инвойсов из ' + (processed_count + invoice_ids.length)),
function() {
if (frm) {
frm.reload_doc();
} else {
cur_list.refresh();
}
}
);
} else { } else {
cur_list.refresh(); // Все успешно загружено
set_loading_success(
__('Загрузка завершена'),
__('Загружено ' + processed_count + ' инвойсов'),
function() {
if (frm) {
frm.reload_doc();
} else {
cur_list.refresh();
}
}
);
} }
return; return;
} }
@ -914,8 +1167,14 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
// Берем первый ID из списка // Берем первый ID из списка
var invoice_id = invoice_ids.shift(); var invoice_id = invoice_ids.shift();
// Показываем диалог загрузки // Обновляем прогресс-бар
show_loading_dialog(__('Загрузка данных счета-фактуры (' + (invoice_ids.length + 1) + ' осталось)...')); update_progress(processed_count + 1, processed_count + invoice_ids.length + 1);
// Обновляем текст загрузки
update_loading_message(
__('Загрузка инвойса (' + (processed_count + 1) + ' из ' + (processed_count + invoice_ids.length + 1) + ')'),
__('Получение данных счета-фактуры...')
);
// Получаем детали счета-фактуры // Получаем детали счета-фактуры
frappe.call({ frappe.call({
@ -925,13 +1184,24 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
'invoice_id': invoice_id 'invoice_id': invoice_id
}, },
callback: function(r) { callback: function(r) {
// Закрываем диалог загрузки if (cancel_loading) {
hide_loading_dialog(); // Если был запрос на отмену во время запроса, прекращаем загрузку
load_selected_invoices(frm, [], token, warehouse, processed_count);
return;
}
if (r.message && !r.message.error) { if (r.message && !r.message.error) {
// Получаем дату создания инвойса // Получаем дату создания инвойса
let creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate(); let creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate();
let scheduleDate = moment(creationDate).format('YYYY-MM-DD'); let scheduleDate = moment(creationDate).format('YYYY-MM-DD');
let serialNumber = r.message.serialNumber || '';
let senderName = r.message.sender ? r.message.sender.name : '';
let total = r.message.totalAmount || r.message.amount || 0;
update_loading_message(
__('Загрузка инвойса (' + (processed_count + 1) + ' из ' + (processed_count + invoice_ids.length + 1) + ')'),
__('Импорт данных инвойса: ') + serialNumber
);
// Импортируем данные инвойса с указанием даты и склада // Импортируем данные инвойса с указанием даты и склада
frappe.call({ frappe.call({
@ -943,23 +1213,80 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
'warehouse': warehouse // Передаем склад в API 'warehouse': warehouse // Передаем склад в API
}, },
callback: function(import_r) { callback: function(import_r) {
if (cancel_loading) {
// Если был запрос на отмену во время запроса, прекращаем загрузку
load_selected_invoices(frm, [], token, warehouse, processed_count);
return;
}
if (import_r.message && import_r.message.success) { if (import_r.message && import_r.message.success) {
// Показываем уведомление об успешном импорте // Показываем уведомление об успешном импорте
frappe.show_alert({ frappe.show_alert({
message: __('Инвойс успешно импортирован: ' + r.message.serialNumber), message: __('Инвойс успешно импортирован: ') + serialNumber,
indicator: 'green' indicator: 'green'
}, 3); }, 3);
// Продолжаем с обработкой следующего инвойса // Получаем имя созданного Purchase Order
setTimeout(function() { let purchase_order_name = import_r.message.purchase_order;
// Если создан новый заказ, обновляем ссылку на документ
if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) { // Сохраняем информацию в E-Taxes Purchase
frm = null;
frappe.call({
method: 'invoice_az.api.create_etaxes_purchase',
args: {
'etaxes_id': invoice_id,
'date': scheduleDate,
'party': senderName,
'total': total
},
callback: function(etaxes_r) {
if (etaxes_r.message && etaxes_r.message.success) {
// Устанавливаем связь между Purchase Order и E-Taxes Purchase
frappe.call({
method: 'invoice_az.api.link_purchase_order_to_etaxes',
args: {
'purchase_order': purchase_order_name,
'etaxes_purchase': etaxes_r.message.name
},
callback: function(link_r) {
// Увеличиваем счетчик обработанных инвойсов
processed_count++;
// Продолжаем с обработкой следующего инвойса
setTimeout(function() {
// Если создан новый заказ, обновляем ссылку на документ
if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) {
frm = null;
}
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count);
}, 300);
}
});
} else {
// Обрабатываем ошибку создания E-Taxes Purchase
frappe.show_alert({
message: __('Ошибка при создании E-Taxes Purchase'),
indicator: 'red'
}, 3);
// Всё равно увеличиваем счетчик и переходим к следующему
processed_count++;
setTimeout(function() {
if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) {
frm = null;
}
load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count);
}, 300);
}
} }
load_selected_invoices(frm, invoice_ids, token, warehouse); });
}, 1000);
} else if (import_r.message && (import_r.message.unmatched_items || import_r.message.unmatched_parties)) { } else if (import_r.message && (import_r.message.unmatched_items || import_r.message.unmatched_parties)) {
// Если есть несопоставленные элементы или контрагенты // Если есть несопоставленные элементы или контрагенты
hide_loading_dialog();
if (import_r.message.unmatched_items) { if (import_r.message.unmatched_items) {
show_unmatched_items_dialog(r.message, import_r.message.unmatched_items); show_unmatched_items_dialog(r.message, import_r.message.unmatched_items);
} else if (import_r.message.unmatched_parties) { } else if (import_r.message.unmatched_parties) {
@ -973,21 +1300,22 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
message: __('Для продолжения необходимо создать недостающие сопоставления') message: __('Для продолжения необходимо создать недостающие сопоставления')
}); });
} else { } else {
frappe.msgprint({ frappe.show_alert({
title: __('Ошибка'), message: __('Ошибка импорта: ') + serialNumber,
indicator: 'red', indicator: 'red'
message: import_r.message ? import_r.message.message : __('Ошибка импорта данных счета-фактуры') }, 3);
});
// Переходим к следующему инвойсу // Переходим к следующему инвойсу, не увеличивая счетчик успешно обработанных
setTimeout(function() { setTimeout(function() {
load_selected_invoices(frm, invoice_ids, token, warehouse); load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count);
}, 1000); }, 300);
} }
} }
}); });
} else if (r.message && r.message.error === 'unauthorized') { } else if (r.message && r.message.error === 'unauthorized') {
// Если ошибка авторизации, предлагаем авторизоваться заново // Если ошибка авторизации, предлагаем авторизоваться заново
hide_loading_dialog();
frappe.confirm( frappe.confirm(
__('Your E-Taxes session has expired. Would you like to authenticate now?'), __('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() { function() {
@ -998,8 +1326,15 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
method: 'invoice_az.api.get_default_asan_login', method: 'invoice_az.api.get_default_asan_login',
callback: function(login_r) { callback: function(login_r) {
if (login_r.message && login_r.message.found && login_r.message.main_token) { if (login_r.message && login_r.message.found && login_r.message.main_token) {
// Показываем диалог с прогрессом снова
show_loading_dialog(__('Возобновление загрузки инвойсов...'), true);
update_progress(processed_count, processed_count + invoice_ids.length + 1);
// Возвращаем текущий инвойс в начало списка
invoice_ids.unshift(invoice_id);
// Используем новый токен для загрузки // Используем новый токен для загрузки
load_selected_invoices(frm, invoice_ids, login_r.message.main_token, warehouse); load_selected_invoices(frm, invoice_ids, login_r.message.main_token, warehouse, processed_count);
} else { } else {
frappe.msgprint({ frappe.msgprint({
title: __('Ошибка'), title: __('Ошибка'),
@ -1020,16 +1355,15 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
} }
); );
} else { } else {
frappe.msgprint({ frappe.show_alert({
title: __('Ошибка'), message: __('Ошибка загрузки данных инвойса'),
indicator: 'red', indicator: 'red'
message: r.message ? r.message.message : __('Ошибка загрузки данных счета-фактуры') }, 3);
});
// Переходим к следующему инвойсу // Переходим к следующему инвойсу
setTimeout(function() { setTimeout(function() {
load_selected_invoices(frm, invoice_ids, token, warehouse); load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count);
}, 1000); }, 300);
} }
} }
}); });

View File

@ -6,23 +6,23 @@ app_email = "info@jeyerp.az"
app_license = "unlicense" app_license = "unlicense"
doctype_js = { doctype_js = {
"Purchase Order": "public/js/purchase_order.js" "Purchase Order": "client/purchase_order.js"
} }
page_js = {
"purchase-order": "public/js/purchase_order.js"
}
app_include_js = [
"/assets/invoice_az/js/purchase_order.js"
]
doctype_list_js = { doctype_list_js = {
"E-Taxes Item": "client/e_taxes_items_list.js", "E-Taxes Item": "client/e_taxes_items_list.js",
"E-Taxes Parties": "client/e_taxes_parties_list.js", "E-Taxes Parties": "client/e_taxes_parties_list.js",
"Purchase Order": "client/purchase_order.js" "Purchase Order": "client/purchase_order.js"
} }
# Хуки для добавления обработчиков событий Purchase Order
doc_events = {
"Purchase Order": {
"on_trash": "invoice_az.api.on_delete_purchase_order",
"on_cancel": "invoice_az.api.on_delete_purchase_order"
}
}
# Apps # Apps
# ------------------ # ------------------

View File

@ -24,6 +24,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Item Name", "label": "Item Name",
"read_only": 1,
"reqd": 1, "reqd": 1,
"unique": 1 "unique": 1
}, },
@ -31,37 +32,43 @@
"fieldname": "etaxes_item_code", "fieldname": "etaxes_item_code",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Item Code", "label": "Item Code",
"read_only": 1,
"unique": 1 "unique": 1
}, },
{ {
"fieldname": "etaxes_unit", "fieldname": "etaxes_unit",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Unit of Measure" "label": "Unit of Measure",
"read_only": 1
}, },
{ {
"fieldname": "etaxes_price", "fieldname": "etaxes_price",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Price", "label": "Price",
"options": "AZN" "options": "AZN",
"read_only": 1
}, },
{ {
"description": "Source invoice number", "description": "Source invoice number",
"fieldname": "source_invoice", "fieldname": "source_invoice",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Source" "label": "Source",
"read_only": 1
}, },
{ {
"default": "New", "default": "New",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "New\nMapped\nProcessing" "options": "New\nMapped\nProcessing",
"read_only": 1
}, },
{ {
"fieldname": "mapped_item", "fieldname": "mapped_item",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Mapped Item", "label": "Mapped Item",
"options": "Item" "options": "Item",
"read_only": 1
}, },
{ {
"fieldname": "item_creation_section", "fieldname": "item_creation_section",
@ -72,24 +79,27 @@
"fieldname": "item_group", "fieldname": "item_group",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Item Group", "label": "Item Group",
"options": "Item Group" "options": "Item Group",
"read_only": 1
}, },
{ {
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Unit of Measure", "label": "Unit of Measure",
"options": "UOM" "options": "UOM",
"read_only": 1
}, },
{ {
"fieldname": "item_tax_template", "fieldname": "item_tax_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Tax Template", "label": "Tax Template",
"options": "Item Tax Template" "options": "Item Tax Template",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-05-06 19:24:47.211911", "modified": "2025-05-15 14:25:13.578790",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Invoice Az", "module": "Invoice Az",
"name": "E-Taxes Item", "name": "E-Taxes Item",

View File

@ -24,51 +24,59 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Party Name", "label": "Party Name",
"read_only": 1,
"reqd": 1, "reqd": 1,
"unique": 1 "unique": 1
}, },
{ {
"fieldname": "etaxes_tax_id", "fieldname": "etaxes_tax_id",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Tax ID" "label": "Tax ID",
"read_only": 1
}, },
{ {
"default": "Sender", "default": "Sender",
"fieldname": "etaxes_party_type", "fieldname": "etaxes_party_type",
"fieldtype": "Select", "fieldtype": "Select",
"label": "E-Taxes Type", "label": "E-Taxes Type",
"options": "Sender\nReceiver" "options": "Sender\nReceiver",
"read_only": 1
}, },
{ {
"fieldname": "etaxes_address", "fieldname": "etaxes_address",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Address" "label": "Address",
"read_only": 1
}, },
{ {
"description": "Source invoice number", "description": "Source invoice number",
"fieldname": "source_invoice", "fieldname": "source_invoice",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Source" "label": "Source",
"read_only": 1
}, },
{ {
"default": "New", "default": "New",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "New\nMapped\nProcessing" "options": "New\nMapped\nProcessing",
"read_only": 1
}, },
{ {
"default": "Supplier", "default": "Supplier",
"fieldname": "erp_party_type", "fieldname": "erp_party_type",
"fieldtype": "Select", "fieldtype": "Select",
"label": "System Party Type", "label": "System Party Type",
"options": "Supplier\nCustomer" "options": "Supplier\nCustomer",
"read_only": 1
}, },
{ {
"fieldname": "mapped_party", "fieldname": "mapped_party",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"label": "Mapped Party", "label": "Mapped Party",
"options": "erp_party_type" "options": "erp_party_type",
"read_only": 1
}, },
{ {
"fieldname": "party_creation_section", "fieldname": "party_creation_section",
@ -79,7 +87,8 @@
"fieldname": "party_group", "fieldname": "party_group",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"label": "Party Group", "label": "Party Group",
"options": "party_group_type" "options": "party_group_type",
"read_only": 1
}, },
{ {
"default": "Supplier Group", "default": "Supplier Group",
@ -87,18 +96,20 @@
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1, "hidden": 1,
"label": "Party Group Type", "label": "Party Group Type",
"options": "Supplier Group\nCustomer Group" "options": "Supplier Group\nCustomer Group",
"read_only": 1
}, },
{ {
"fieldname": "payment_terms", "fieldname": "payment_terms",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payment Terms", "label": "Payment Terms",
"options": "Payment Terms Template" "options": "Payment Terms Template",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-05-09 15:46:11.179417", "modified": "2025-05-15 14:25:54.517365",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Invoice Az", "module": "Invoice Az",
"name": "E-Taxes Parties", "name": "E-Taxes Parties",

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Jey ERP and contributors
// For license information, please see license.txt
// frappe.ui.form.on("E-Taxes Purchase", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,59 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-05-14 16:07:06.007893",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"date",
"total",
"party",
"etaxes_id"
],
"fields": [
{
"fieldname": "date",
"fieldtype": "Data",
"label": "date"
},
{
"fieldname": "total",
"fieldtype": "Data",
"label": "total"
},
{
"fieldname": "party",
"fieldtype": "Data",
"label": "party"
},
{
"fieldname": "etaxes_id",
"fieldtype": "Data",
"label": "etaxes_id"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-14 16:14:55.664290",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Purchase",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ETaxesPurchase(Document):
pass

View File

@ -0,0 +1,30 @@
# Copyright (c) 2025, Jey ERP and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestETaxesPurchase(UnitTestCase):
"""
Unit tests for ETaxesPurchase.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestETaxesPurchase(IntegrationTestCase):
"""
Integration tests for ETaxesPurchase.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -154,8 +154,18 @@ frappe.ui.form.on('E-Taxes Settings', {
// Добавляем несопоставленные товары в таблицу // Добавляем несопоставленные товары в таблицу
r.message.items.forEach(function(item) { r.message.items.forEach(function(item) {
let row = frm.add_child('item_mappings'); let row = frm.add_child('item_mappings');
// ИСПРАВЛЕНИЕ: Используем ID документа (name) вместо отображаемого имени
// Используем ID документа (name) вместо отображаемого имени
row.etaxes_item_name = item.name; // Должен быть ID документа row.etaxes_item_name = item.name; // Должен быть ID документа
// Добавляем все значения по умолчанию
if (item.item_group) row.item_group = item.item_group;
if (item.uom) row.uom = item.uom;
if (item.is_stock_item !== undefined) row.is_stock_item = item.is_stock_item;
if (item.is_purchase_item !== undefined) row.is_purchase_item = item.is_purchase_item;
if (item.item_tax_template) row.item_tax_template = item.item_tax_template;
// Тип сопоставления
row.mapping_type = 'Manual'; row.mapping_type = 'Manual';
}); });
frm.refresh_field('item_mappings'); frm.refresh_field('item_mappings');
@ -329,9 +339,29 @@ frappe.ui.form.on('E-Taxes Settings', {
// Добавляем несопоставленных контрагентов в таблицу // Добавляем несопоставленных контрагентов в таблицу
r.message.parties.forEach(function(party) { r.message.parties.forEach(function(party) {
let row = frm.add_child('party_mappings'); let row = frm.add_child('party_mappings');
// Основные поля
row.etaxes_party_name = party.etaxes_party_name; row.etaxes_party_name = party.etaxes_party_name;
row.etaxes_tax_id = party.etaxes_tax_id; row.etaxes_tax_id = party.etaxes_tax_id;
row.party_type = party.etaxes_party_type === 'Sender' ? 'Supplier' : 'Customer';
// Тип контрагента
row.party_type = party.erp_party_type ||
(party.etaxes_party_type === 'Sender' ? 'Supplier' : 'Customer');
// Настройки по умолчанию в зависимости от типа
if (party.supplier_group && row.party_type === 'Supplier')
row.supplier_group = party.supplier_group;
if (party.customer_group && row.party_type === 'Customer')
row.customer_group = party.customer_group;
if (party.territory && row.party_type === 'Customer')
row.territory = party.territory;
if (party.payment_terms)
row.payment_terms = party.payment_terms;
// Тип сопоставления
row.mapping_type = 'Manual'; row.mapping_type = 'Manual';
}); });
frm.refresh_field('party_mappings'); frm.refresh_field('party_mappings');