From 85f37a3f67316afbf016d0ca3b542d65b0e762f8 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Tue, 15 Jul 2025 17:19:29 +0400 Subject: [PATCH] Parties got separated to customers and suppliers --- invoice_az/api.py | 912 ++++++++++++++--- ...ties_list.js => e_taxes_customers_list.js} | 302 +++--- invoice_az/client/e_taxes_suppliers_list.js | 916 ++++++++++++++++++ invoice_az/client/sales_order.js | 21 +- invoice_az/hooks.py | 6 + .../e_taxes_customer_mappings/__init__.py | 0 .../e_taxes_customer_mappings.json | 76 ++ .../e_taxes_customer_mappings.py | 9 + .../doctype/e_taxes_customers/__init__.py | 0 .../e_taxes_customers/e_taxes_customers.js | 8 + .../e_taxes_customers/e_taxes_customers.json | 139 +++ .../e_taxes_customers/e_taxes_customers.py | 9 + .../test_e_taxes_customers.py | 30 + .../e_taxes_settings/e_taxes_settings.js | 375 +++++++ .../e_taxes_settings/e_taxes_settings.json | 30 +- .../e_taxes_supplier_mappings/__init__.py | 0 .../e_taxes_supplier_mappings.json | 69 ++ .../e_taxes_supplier_mappings.py | 9 + .../doctype/e_taxes_suppliers/__init__.py | 0 .../e_taxes_suppliers/e_taxes_suppliers.js | 8 + .../e_taxes_suppliers/e_taxes_suppliers.json | 131 +++ .../e_taxes_suppliers/e_taxes_suppliers.py | 9 + .../test_e_taxes_suppliers.py | 30 + invoice_az/sales_api.py | 229 +++-- 24 files changed, 2931 insertions(+), 387 deletions(-) rename invoice_az/client/{e_taxes_parties_list.js => e_taxes_customers_list.js} (77%) create mode 100644 invoice_az/client/e_taxes_suppliers_list.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customer_mappings/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customer_mappings/e_taxes_customer_mappings.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customer_mappings/e_taxes_customer_mappings.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customers/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customers/e_taxes_customers.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customers/e_taxes_customers.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customers/e_taxes_customers.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_customers/test_e_taxes_customers.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_supplier_mappings/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_supplier_mappings/e_taxes_supplier_mappings.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_supplier_mappings/e_taxes_supplier_mappings.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_suppliers/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_suppliers/e_taxes_suppliers.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_suppliers/e_taxes_suppliers.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_suppliers/e_taxes_suppliers.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_suppliers/test_e_taxes_suppliers.py diff --git a/invoice_az/api.py b/invoice_az/api.py index a0259d5..b054b70 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -1070,7 +1070,7 @@ def create_purchase_invoice_from_order(purchase_order_name): @frappe.whitelist() def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None): - """Import invoice taking into account mappings and processing unmapped elements""" + """Import invoice taking into account NEW supplier mappings""" # Записываем активность record_etaxes_activity() @@ -1090,30 +1090,27 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule # Create mapping dictionaries item_mappings = {} for mapping in settings.item_mappings: - # mapping.etaxes_item_name содержит name документа E-Taxes Item if mapping.etaxes_item_name and mapping.erp_item: item_mappings[mapping.etaxes_item_name] = mapping.erp_item - party_mappings = {} - for mapping in settings.party_mappings: - if mapping.etaxes_party_name and mapping.erp_party: - key = f"{mapping.etaxes_party_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}" - party_mappings[key] = (mapping.erp_party, mapping.party_type) + # ИЗМЕНЕНО: Используем supplier_mappings вместо party_mappings + supplier_mappings = {} + for mapping in settings.supplier_mappings: + if mapping.etaxes_supplier_name and mapping.erp_supplier: + key = f"{mapping.etaxes_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 warehouse not specified, try to get from settings if hasattr(settings, 'default_warehouse') and settings.default_warehouse: default_warehouse = settings.default_warehouse else: - # If warehouse not specified in settings, try to get default warehouse for company company = frappe.defaults.get_user_default('Company') if company: default_warehouse = frappe.db.get_value('Company', company, 'default_warehouse') - # If still no warehouse, take first active warehouse if not default_warehouse: warehouses = frappe.get_all('Warehouse', filters={'is_group': 0, 'disabled': 0}, @@ -1122,7 +1119,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule if warehouses: default_warehouse = warehouses[0].name - # Check warehouse existence if not default_warehouse: return { 'success': False, @@ -1133,15 +1129,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule if purchase_order_name: po = frappe.get_doc('Purchase Order', purchase_order_name) else: - # Create new Purchase Order po = frappe.new_doc('Purchase Order') - # Set supplier + # ИЗМЕНЕНО: Set supplier используя supplier_mappings sender = invoice_data.get('sender', {}) sender_key = f"{sender.get('name', '').lower()}|{sender.get('tin', '').lower()}" - if sender_key in party_mappings and party_mappings[sender_key][0]: - po.supplier = party_mappings[sender_key][0] + if sender_key in supplier_mappings and supplier_mappings[sender_key]: + po.supplier = supplier_mappings[sender_key] else: return { 'success': False, @@ -1156,7 +1151,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule # Set dates po.transaction_date = frappe.utils.today() - # Get date from invoice or parameter if specified if schedule_date: po.schedule_date = schedule_date elif invoice_data.get("creationDate"): @@ -1215,7 +1209,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule break if not mapped_item: - # Если соответствие не найдено, добавляем в список несопоставленных unmatched_items.append({ 'name': item_name, 'code': item_code @@ -1227,7 +1220,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule mapped_uom = None if unit_name: - # Ищем существующую запись E-Taxes Unit и берем mapped_unit try: mapped_uom = frappe.db.get_value('E-Taxes Unit', filters={'etaxes_unit_name': unit_name}, @@ -1238,17 +1230,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule # Если соответствие единицы не найдено, используем UOM из товара или настроек if not mapped_uom: if unit_name: - # Добавляем в список несопоставленных единиц unmatched_units.append({ 'name': unit_name }) - # Используем UOM из настроек товара или default try: item_doc = frappe.get_doc('Item', mapped_item) mapped_uom = item_doc.stock_uom except: - # Fallback на default_uom из настроек mapped_uom = settings.default_uom if settings.default_uom else "Nos" # Add position to PO @@ -1259,13 +1248,11 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule po_item.item_code = mapped_item - # ИСПРАВЛЕНИЕ: Получаем название и описание из базы данных 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: - # Fallback на название из E-Taxes если не удалось получить из базы po_item.item_name = item.get("productName", "").strip() po_item.description = item.get("productName", "").strip() @@ -1273,11 +1260,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule po_item.rate = item.get("pricePerUnit", 0) po_item.amount = item.get("cost", 0) po_item.uom = mapped_uom - - # IMPORTANT: Set schedule_date for each row po_item.schedule_date = date_to_use - - # IMPORTANT: Set default warehouse po_item.warehouse = default_warehouse po.append("items", po_item) @@ -1312,17 +1295,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule # ДОБАВЛЕНО: Создание строки ƏDV в Purchase Taxes and Charges ТОЛЬКО если есть vat в данных try: - # Получаем VAT из E-Taxes данных vat_amount = invoice_data.get('vat', 0) - # Создаем строку ƏDV ТОЛЬКО если есть 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 для определения ставки 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 @@ -1399,7 +1379,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'purchase_invoice': pi_name } else: - # Если не удалось создать Purchase Invoice, всё равно возвращаем успех с Purchase Order return { 'success': True, 'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.', @@ -1407,7 +1386,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule } except Exception as e: frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}", "Submit PI Error") - # Если не удалось создать Purchase Invoice, всё равно возвращаем успех с Purchase Order return { 'success': True, 'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.', @@ -1420,7 +1398,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'success': False, 'message': "An unknown error occurred, please try again in a few minutes." } - + @frappe.whitelist() def create_unmapped_parties(settings_name): """Creating parties for unmapped elements from E-Taxes settings table""" @@ -2092,7 +2070,7 @@ def load_units_from_invoices(date_from, date_to, max_count=200, offset=0): '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""" @@ -2181,57 +2159,89 @@ def update_mapped_statuses(doc, method=None): except Exception as e: frappe.log_error(f"Error resetting unmapped items: {str(e)}", "Status Update Error") - # 3. Handle E-Taxes Parties mappings - if hasattr(doc, 'party_mappings'): + # 3. Handle E-Taxes Customers mappings + if hasattr(doc, 'customer_mappings'): # Создаем словарь текущих соответствий - current_party_mappings = {} - for mapping in doc.party_mappings: - if mapping.etaxes_party_name: - # Проверяем что erp_party не None и не пустая строка - erp_party = getattr(mapping, 'erp_party', None) - current_party_mappings[mapping.etaxes_party_name] = erp_party if erp_party else None + 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 - # Получаем все контрагенты с их фактическими именами документов - try: - all_parties = frappe.get_all('E-Taxes Parties', - fields=['name', 'etaxes_party_name', 'status', 'mapped_party']) - - # Создаем lookup от etaxes_party_name к фактическому имени документа - party_name_to_docname = {} - for party in all_parties: - party_name_to_docname[party.etaxes_party_name] = party.name - - # Обновляем статусы для всех контрагентов в mappings - for etaxes_party_name, erp_party in current_party_mappings.items(): - try: - if etaxes_party_name in party_name_to_docname: - docname = party_name_to_docname[etaxes_party_name] - if erp_party: # Если есть соответствие - frappe.db.set_value('E-Taxes Parties', docname, { - 'status': 'Mapped', - 'mapped_party': erp_party - }, update_modified=False) - else: # Если соответствие пустое - frappe.db.set_value('E-Taxes Parties', docname, { - 'status': 'New', - 'mapped_party': None - }, update_modified=False) - except Exception as e: - frappe.log_error(f"Error updating E-Taxes Party {etaxes_party_name}: {str(e)}", "Status Update Error") - - # Сбрасываем статус для контрагентов, которые были удалены из mappings - for party in all_parties: - try: - if party.status == 'Mapped' and party.etaxes_party_name not in current_party_mappings: - frappe.db.set_value('E-Taxes Parties', party.name, { - 'status': 'New', - 'mapped_party': 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) - except Exception as e: - frappe.log_error(f"Error resetting unmapped party {party.name}: {str(e)}", "Status Update Error") - + 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 processing parties mappings: {str(e)}", "Status Update Error") + 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() @@ -2540,20 +2550,22 @@ def process_single_invoice_for_units(token, invoice_id): @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_created = 0 + total_customers_created = 0 + total_suppliers_created = 0 total_skipped = 0 total_failed = 0 processed_invoices = 0 # Для отслеживания уникальных контрагентов в рамках батча - batch_unique_parties = {} + batch_unique_customers = {} + batch_unique_suppliers = {} for invoice in invoices: try: @@ -2561,10 +2573,6 @@ def process_invoice_parties_from_list(invoice_list_data, token): serial_number = invoice.get('serialNumber', invoice.get('id', '')) source = invoice.get('_source', 'inbox') - created_count = 0 - skipped_count = 0 - failed_count = 0 - # Обработка отправителя sender = invoice.get('sender', {}) if sender: @@ -2574,28 +2582,39 @@ def process_invoice_parties_from_list(invoice_list_data, token): sender_name = ' '.join(sender_name.split()) - if sender_name and sender_tin and source == 'inbox': - # ИСПРАВЛЕНО: Используем sender_name как ключ, так как autoname = "field:etaxes_party_name" + if sender_name and sender_tin: + # Логика определения типа: + # Purchase invoices (inbox): Sender -> Supplier + # Sales invoices (outbox): Sender -> Customer - # Проверяем в базе данных и в локальном кэше батча - if sender_name not in batch_unique_parties: - # ИСПРАВЛЕНО: Проверяем по name документа (etaxes_party_name) - existing = frappe.db.exists('E-Taxes Parties', sender_name) - - if not existing: - batch_unique_parties[sender_name] = { - 'etaxes_party_name': sender_name, - 'etaxes_tax_id': sender_tin, - 'etaxes_address': sender_address, - 'etaxes_party_type': 'Sender', - 'erp_party_type': 'Supplier', - 'source_invoice': serial_number, - 'status': 'New' - } - else: - skipped_count += 1 + 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: - skipped_count += 1 + # 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', {}) @@ -2607,54 +2626,63 @@ def process_invoice_parties_from_list(invoice_list_data, token): receiver_name = ' '.join(receiver_name.split()) if receiver_name and receiver_tin: - # ИСПРАВЛЕНО: Используем receiver_name как ключ - - # Проверяем в базе данных и в локальном кэше батча - if receiver_name not in batch_unique_parties: - # ИСПРАВЛЕНО: Проверяем по name документа (etaxes_party_name) - existing = frappe.db.exists('E-Taxes Parties', receiver_name) + # Логика: 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_parties[receiver_name] = { + batch_unique_customers[receiver_name] = { 'etaxes_party_name': receiver_name, 'etaxes_tax_id': receiver_tin, 'etaxes_address': receiver_address, 'etaxes_party_type': 'Receiver', - 'erp_party_type': 'Customer', 'source_invoice': serial_number, 'status': 'New' } - else: - skipped_count += 1 - else: - skipped_count += 1 - total_skipped += skipped_count processed_invoices += 1 except Exception: total_failed += 1 processed_invoices += 1 - # Создание записей для всех уникальных контрагентов в батче - for party_name, party_data in batch_unique_parties.items(): + # Создание записей для всех уникальных customers в батче + for customer_name, customer_data in batch_unique_customers.items(): try: - # Финальная проверка на дубликаты перед созданием - # ИСПРАВЛЕНО: Проверяем по name документа - existing = frappe.db.exists('E-Taxes Parties', party_name) + existing = frappe.db.exists('E-Taxes Customers', customer_name) if existing: total_skipped += 1 continue - # ИСПРАВЛЕНО: Используем insert с ignore_if_duplicate=True doc = frappe.get_doc({ - 'doctype': 'E-Taxes Parties', - **party_data + 'doctype': 'E-Taxes Customers', + **customer_data }) doc.insert(ignore_permissions=True, ignore_if_duplicate=True) - total_created += 1 + 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: @@ -2662,12 +2690,14 @@ def process_invoice_parties_from_list(invoice_list_data, token): return { 'success': True, - 'created_count': total_created, + '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_parties_found': len(batch_unique_parties) + 'unique_customers_found': len(batch_unique_customers), + 'unique_suppliers_found': len(batch_unique_suppliers) } except Exception as e: @@ -2859,3 +2889,621 @@ def load_combined_data_from_etaxes(date_from, date_to, load_items=False, load_pa '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_doc("E-Taxes Settings", settings.name) + + # 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_doc("E-Taxes Settings", settings.name) + + # 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(settings_name): + """Creating customers for unmapped elements from E-Taxes settings table""" + # Записываем активность + record_etaxes_activity() + + try: + # Get settings + settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) + + # 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 + 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" + 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(settings_name): + """Creating suppliers for unmapped elements from E-Taxes settings table""" + # Записываем активность + record_etaxes_activity() + + try: + # Get settings + settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) + + # 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: + # Check if such supplier with this name already exists + supplier_exists = frappe.db.exists('Supplier', {'supplier_name': etaxes_supplier.etaxes_party_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 = etaxes_supplier.etaxes_party_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." + } + diff --git a/invoice_az/client/e_taxes_parties_list.js b/invoice_az/client/e_taxes_customers_list.js similarity index 77% rename from invoice_az/client/e_taxes_parties_list.js rename to invoice_az/client/e_taxes_customers_list.js index 0a86d13..d60ef87 100644 --- a/invoice_az/client/e_taxes_parties_list.js +++ b/invoice_az/client/e_taxes_customers_list.js @@ -1,6 +1,6 @@ -// Обновленные настройки списка E-Taxes Parties с прогресс баром -frappe.listview_settings['E-Taxes Parties'] = { - add_fields: ['status', 'mapped_party'], +// Настройки списка E-Taxes Customers с прогресс баром +frappe.listview_settings['E-Taxes Customers'] = { + add_fields: ['status', 'mapped_customer'], get_indicator: function(doc) { if (doc.status === 'Mapped') { @@ -13,48 +13,48 @@ frappe.listview_settings['E-Taxes Parties'] = { }, onload: function(listview) { - // Add button to load parties from E-Taxes + // Add button to load customers from E-Taxes listview.page.add_menu_item(__('Load from E-Taxes'), function() { // Сначала проверяем актуальность токена перед загрузкой - check_and_process_token_parties(function() { - show_invoice_filter_dialog(); + check_and_process_token_customers(function() { + show_invoice_filter_dialog_customers(); }); }); } }; // Глобальная переменная для хранения диалога загрузки -var loading_dialog_parties = null; +var loading_dialog_customers = null; // Очистка состояния при закрытии/перезагрузке страницы $(window).on('beforeunload', function() { - cleanup_parties_loading_state(); + cleanup_customers_loading_state(); }); // Очистка состояния при переходе на другую страницу $(document).on('page:before-change', function() { - cleanup_parties_loading_state(); + cleanup_customers_loading_state(); }); -// НОВАЯ ФУНКЦИЯ: Очистка состояния загрузки контрагентов -function cleanup_parties_loading_state() { +// НОВАЯ ФУНКЦИЯ: Очистка состояния загрузки customers +function cleanup_customers_loading_state() { try { // Скрываем прогресс бар frappe.hide_progress(); // Удаляем event listeners - $(document).off('progress-cancel.loading_parties'); + $(document).off('progress-cancel.loading_customers'); // Очищаем глобальные переменные - if (typeof window.cancelPartiesLoading !== 'undefined') { - delete window.cancelPartiesLoading; + if (typeof window.cancelCustomersLoading !== 'undefined') { + delete window.cancelCustomersLoading; } - if (typeof window.totalPartiesToProcess !== 'undefined') { - delete window.totalPartiesToProcess; + if (typeof window.totalCustomersToProcess !== 'undefined') { + delete window.totalCustomersToProcess; } // ВАЖНО: Скрываем все возможные диалоги загрузки - hide_loading_dialog_parties(); + hide_loading_dialog_customers(); // Дополнительная очистка - удаляем все модальные окна, которые могут висеть $('.modal-backdrop').remove(); @@ -63,9 +63,9 @@ function cleanup_parties_loading_state() { // Очищаем любые блокировки интерфейса frappe.dom.unfreeze(); - console.log('Parties loading state cleaned up'); + console.log('Customers loading state cleaned up'); } catch (e) { - console.error('Error during parties cleanup:', e); + console.error('Error during customers cleanup:', e); // В крайнем случае, принудительно разблокируем интерфейс frappe.dom.unfreeze(); $('.modal-backdrop').remove(); @@ -74,21 +74,21 @@ function cleanup_parties_loading_state() { } // Функция для отображения диалога загрузки с анимацией -function show_loading_dialog_parties(title, message, submessage) { +function show_loading_dialog_customers(title, message, submessage) { // Если диалог уже открыт, обновляем только сообщение - if (loading_dialog_parties) { - $('#loading_title_parties').text(title); - $('#loading_message_parties').text(message); + if (loading_dialog_customers) { + $('#loading_title_customers').text(title); + $('#loading_message_customers').text(message); if (submessage) { - $('#loading_submessage_parties').text(submessage).show(); + $('#loading_submessage_customers').text(submessage).show(); } else { - $('#loading_submessage_parties').hide(); + $('#loading_submessage_customers').hide(); } return; } // Создаем диалог с анимацией загрузки - loading_dialog_parties = new frappe.ui.Dialog({ + loading_dialog_customers = new frappe.ui.Dialog({ title: title, fields: [ { @@ -106,13 +106,13 @@ function show_loading_dialog_parties(title, message, submessage) { }
-

${title}

-

${message}

-

${submessage || ''}

+

${title}

+

${message}

+

${submessage || ''}

-