From f0c19854fedcae3a7eb09fc29df3803e4eccd797 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 23 Jul 2025 12:39:40 +0400 Subject: [PATCH] Fixed bugs, added new tabs and subtabs, load from everywhere and etc --- invoice_az/api.py | 1723 +++++++---- invoice_az/client/e_taxes_items_list.js | 21 +- .../doctype/e_taxes_item/e_taxes_item.json | 63 +- .../e_taxes_item_mapping.json | 10 +- .../doctype/e_taxes_sales/e_taxes_sales.json | 11 +- .../e_taxes_settings/e_taxes_settings.js | 2686 +++++++++-------- .../e_taxes_settings/e_taxes_settings.json | 501 +-- invoice_az/sales_api.py | 24 +- test_invoice.git/hooks/applypatch-msg.sample | 0 test_invoice.git/hooks/commit-msg.sample | 0 .../hooks/fsmonitor-watchman.sample | 0 test_invoice.git/hooks/post-update.sample | 0 test_invoice.git/hooks/pre-applypatch.sample | 0 test_invoice.git/hooks/pre-commit.sample | 0 .../hooks/pre-merge-commit.sample | 0 test_invoice.git/hooks/pre-push.sample | 0 test_invoice.git/hooks/pre-rebase.sample | 0 test_invoice.git/hooks/pre-receive.sample | 0 .../hooks/prepare-commit-msg.sample | 0 .../hooks/push-to-checkout.sample | 0 test_invoice.git/hooks/update.sample | 0 21 files changed, 2882 insertions(+), 2157 deletions(-) mode change 100755 => 100644 test_invoice.git/hooks/applypatch-msg.sample mode change 100755 => 100644 test_invoice.git/hooks/commit-msg.sample mode change 100755 => 100644 test_invoice.git/hooks/fsmonitor-watchman.sample mode change 100755 => 100644 test_invoice.git/hooks/post-update.sample mode change 100755 => 100644 test_invoice.git/hooks/pre-applypatch.sample mode change 100755 => 100644 test_invoice.git/hooks/pre-commit.sample mode change 100755 => 100644 test_invoice.git/hooks/pre-merge-commit.sample mode change 100755 => 100644 test_invoice.git/hooks/pre-push.sample mode change 100755 => 100644 test_invoice.git/hooks/pre-rebase.sample mode change 100755 => 100644 test_invoice.git/hooks/pre-receive.sample mode change 100755 => 100644 test_invoice.git/hooks/prepare-commit-msg.sample mode change 100755 => 100644 test_invoice.git/hooks/push-to-checkout.sample mode change 100755 => 100644 test_invoice.git/hooks/update.sample diff --git a/invoice_az/api.py b/invoice_az/api.py index b054b70..a2b8870 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -310,101 +310,6 @@ def load_items_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 load_parties_from_invoices(date_from, date_to, max_count=200, offset=0, invoice_type="purchase"): - """Загружает контрагентов из инвойсов""" - record_etaxes_activity() - - try: - asan_login_settings = get_default_asan_login() - - if not asan_login_settings.get('found'): - return {'success': False, 'message': 'No Asan Login settings found'} - - try: - max_count = int(max_count) - except (ValueError, TypeError): - max_count = 200 - - try: - offset = int(offset) - except (ValueError, TypeError): - offset = 0 - - filters = { - "actionOwner": None, - "amountFrom": None, - "amountTo": None, - "creationDateFrom": date_from, - "creationDateTo": date_to, - "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], - "maxCount": max_count, - "offset": offset, - "productCode": None, - "productName": None, - "receiverName": None, - "receiverTin": None, - "senderName": None, - "senderTin": None, - "serialNumber": None, - "sortAsc": True, - "sortBy": "creationDate", - "statuses": ["approved", "approvedBySystem"], - "types": ["current", "corrected"] - } - - # Загружаем из inbox - response_inbox = get_invoices(asan_login_settings['main_token'], json.dumps(filters)) - - if 'error' in response_inbox: - return {'success': False, **response_inbox} - - invoices_inbox = response_inbox.get('data', []) or response_inbox.get('invoices', []) - - for invoice in invoices_inbox: - invoice['_source'] = 'inbox' - - all_invoices = invoices_inbox - - # Попытка загрузить из outbox (если доступно) - try: - from .sales_api import get_sales_invoices - response_outbox = get_sales_invoices(asan_login_settings['main_token'], json.dumps(filters)) - - if 'error' not in response_outbox: - invoices_outbox = response_outbox.get('data', []) or response_outbox.get('invoices', []) - - for invoice in invoices_outbox: - invoice['_source'] = 'outbox' - - all_invoices = invoices_inbox + invoices_outbox - - hasMore_inbox = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count - hasMore_outbox = response_outbox.get('hasMore', False) or response_outbox.get('total', 0) > offset + max_count - hasMore = hasMore_inbox or hasMore_outbox - else: - hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count - - except ImportError: - hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count - except Exception: - hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count - - return { - 'success': True, - 'invoices': all_invoices, - 'hasMore': hasMore, - 'token': asan_login_settings['main_token'], - 'total': len(all_invoices) - } - - except Exception as e: - frappe.log_error(f"Error in load_parties_from_invoices: {str(e)}", "Load Parties Error") - return { - 'success': False, - 'message': "An unknown error occurred, please try again in a few minutes." - } @frappe.whitelist() def get_default_settings(): @@ -487,51 +392,87 @@ def get_unmapped_items(): 'message': 'No active E-Taxes settings found' } - # Get all unmapped items from E-Taxes - unmapped_items = [] - - # 1. Get all items with 'New' status + # Get all unmapped items from E-Taxes (single query) etaxes_items = frappe.get_all('E-Taxes Item', filters={'status': 'New'}, - fields=['name', 'etaxes_item_name', 'etaxes_item_code', 'etaxes_unit', 'etaxes_price']) + fields=['name', 'etaxes_item_name', 'etaxes_item_code', 'etaxes_unit', + 'etaxes_price', 'is_service_item', 'is_from_purchase', 'is_from_sales']) - # 2. Check if they are already in settings - existing_mappings = {} - for mapping in settings.item_mappings: - existing_mappings[mapping.etaxes_item_name] = mapping.erp_item + # Build existing mappings dict (single loop) + existing_mappings = {mapping.etaxes_item_name: mapping.erp_item for mapping in settings.item_mappings} - # 3. Add only those that are not in settings + # Cache UOM mappings to avoid repeated DB queries + unit_mappings = {} + if etaxes_items: + unique_units = {item.etaxes_unit for item in etaxes_items if item.etaxes_unit} + if unique_units: + unit_results = frappe.get_all('E-Taxes Unit', + filters={'etaxes_unit_name': ['in', list(unique_units)]}, + fields=['etaxes_unit_name', 'mapped_unit']) + unit_mappings = {r.etaxes_unit_name: r.mapped_unit for r in unit_results if r.mapped_unit} + + # Cache item group existence checks + services_group_exists = frappe.db.exists('Item Group', 'Services') + services_az_group_exists = frappe.db.exists('Item Group', 'Услуги') + + unmapped_items = [] + + # Process items (optimized loop) for item in etaxes_items: if item.name not in existing_mappings: - # Определяем UOM для товара - uom_to_use = settings.default_uom + # Get UOM efficiently + uom_to_use = unit_mappings.get(item.etaxes_unit, settings.default_uom) - # Если у товара есть etaxes_unit, пытаемся найти соответствие - if item.etaxes_unit: - try: - mapped_uom = frappe.db.get_value('E-Taxes Unit', - filters={'etaxes_unit_name': item.etaxes_unit}, - fieldname='mapped_unit') - if mapped_uom: - uom_to_use = mapped_uom - except Exception: - pass # Используем default_uom при ошибке + # Extract values once + is_service = item.get('is_service_item', 0) + is_from_purchase = item.get('is_from_purchase', 0) + is_from_sales = item.get('is_from_sales', 0) - # Create item object with default settings + # Calculate flags efficiently + is_stock_item = 0 if is_service else 1 + + # Purchase item logic + if is_from_purchase: + is_purchase_item = 1 + elif is_service and not is_from_purchase and not is_from_sales: + is_purchase_item = 1 + else: + is_purchase_item = 0 + + # Sales item logic + if is_from_sales: + is_sales_item = 1 + elif is_service and not is_from_purchase and not is_from_sales: + is_sales_item = 1 + else: + is_sales_item = 0 + + # Item group logic + item_group = settings.default_item_group + if is_service: + if services_group_exists: + item_group = 'Services' + elif services_az_group_exists: + item_group = 'Услуги' + + # Build item data item_data = { 'name': item.name, 'etaxes_item_name': item.etaxes_item_name, 'etaxes_item_code': item.etaxes_item_code, - # Add default values - 'item_group': settings.default_item_group, + 'item_group': item_group, 'uom': uom_to_use, - 'is_stock_item': settings.is_stock_item, - 'is_purchase_item': settings.is_purchase_item, - 'item_tax_template': settings.default_item_tax_template + 'is_stock_item': is_stock_item, + 'is_purchase_item': is_purchase_item, + 'is_sales_item': is_sales_item, + 'item_tax_template': settings.default_item_tax_template, + 'is_service_item': is_service, + 'is_from_purchase': is_from_purchase, + 'is_from_sales': is_from_sales } - # Add price if it exists - if hasattr(item, 'etaxes_price') and item.etaxes_price: + # Add price if exists + if item.etaxes_price: item_data['standard_rate'] = item.etaxes_price unmapped_items.append(item_data) @@ -541,81 +482,12 @@ def get_unmapped_items(): 'items': unmapped_items } except Exception as e: + frappe.log_error(f"Error in get_unmapped_items: {str(e)}\n{frappe.get_traceback()}", "Get Unmapped Items Error") return { 'success': False, 'message': "An unknown error occurred, please try again in a few minutes." } - -@frappe.whitelist() -def get_unmapped_parties(): - """Getting unmapped parties from E-Taxes Parties""" - try: - # Get active settings - settings = get_active_settings() - if not settings: - return { - 'success': False, - 'message': 'No active E-Taxes settings found' - } - - # Get all unmapped parties from E-Taxes Parties - unmapped_parties = [] - - # Get list of existing mappings - existing_mappings = {} - for mapping in settings.party_mappings: - existing_mappings[mapping.etaxes_party_name] = mapping.erp_party - - # Get unmapped parties from E-Taxes Parties - etaxes_parties = frappe.get_all('E-Taxes Parties', - filters={'status': 'New'}, - fields=['name', 'etaxes_party_name', 'etaxes_tax_id', - 'etaxes_party_type', 'erp_party_type', 'etaxes_address', 'source_invoice']) - - # Filter only those that are not in mappings - for party in etaxes_parties: - if party.etaxes_party_name not in existing_mappings: - # Determine party type (Customer/Supplier) - party_type = party.erp_party_type or ('Supplier' if party.etaxes_party_type == 'Sender' else 'Customer') - - # Create base object with common data - party_data = { - 'name': party.name, - 'etaxes_party_name': party.etaxes_party_name, - 'etaxes_tax_id': party.etaxes_tax_id, - 'etaxes_party_type': party.etaxes_party_type, - 'erp_party_type': party_type, - # Common settings - 'payment_terms': settings.default_payment_terms - } - - # Add address if it exists - if party.etaxes_address: - party_data['address'] = party.etaxes_address - - # Add source if it exists - if party.source_invoice: - party_data['source'] = party.source_invoice - - # Add specific fields depending on type - 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' # Default value - - unmapped_parties.append(party_data) - - return { - 'success': True, - 'parties': unmapped_parties - } - except Exception as e: - return { - 'success': False, - 'message': "An unknown error occurred, please try again in a few minutes." - } - + @frappe.whitelist() def match_similar_items(): """Matching items by similar names""" @@ -742,134 +614,6 @@ def match_similar_items(): 'message': "An unknown error occurred, please try again in a few minutes." } -@frappe.whitelist() -def match_similar_parties(): - """Matching parties 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 parties with "New" status from E-Taxes Parties - etaxes_parties = frappe.get_all('E-Taxes Parties', - filters={'status': 'New'}, - fields=['name', 'etaxes_party_name', 'etaxes_tax_id', - 'etaxes_party_type', 'erp_party_type']) - - # Get existing mappings - existing_mappings = {} - for mapping in settings.party_mappings: - existing_mappings[mapping.etaxes_party_name] = mapping.erp_party - - # Filter only those that are not in mappings or have empty mappings - parties_to_process = [] - for party in etaxes_parties: - if party.etaxes_party_name not in existing_mappings or not existing_mappings[party.etaxes_party_name]: - parties_to_process.append(party) - - matched_count = 0 - total_processed = len(parties_to_process) - - # Get fresh copy of settings document - doc = frappe.get_doc("E-Taxes Settings", settings.name) - - # Process all parties - for etaxes_party in parties_to_process: - # Determine ERP party type based on etaxes_party_type - erp_party_type = etaxes_party.erp_party_type - - # Get all parties of corresponding type from system - system_parties = frappe.get_all(erp_party_type, - fields=['name', - 'supplier_name' if erp_party_type == 'Supplier' else 'customer_name']) - - # Find similar party - best_match = None - best_score = 0 - - # Normalize E-Taxes party name - etaxes_name = normalize_string(etaxes_party.etaxes_party_name, consider_azeri) - - for party in system_parties: - # Get party name depending on type - party_name = party.supplier_name if erp_party_type == 'Supplier' else party.customer_name - - # Normalize system party name - party_name_normalized = normalize_string(party_name, consider_azeri) - - # Calculate similarity coefficient - name_score = SequenceMatcher(None, etaxes_name, party_name_normalized).ratio() - - # Check if this is a better match - if name_score > best_score and name_score >= similarity_threshold: - best_score = name_score - best_match = party - - if best_match: - # IMPORTANT: check if there is already a record for this party in mappings - existing_row = None - for idx, mapping in enumerate(doc.party_mappings): - if mapping.etaxes_party_name == etaxes_party.etaxes_party_name: - existing_row = idx - break - - if existing_row is not None: - # If row already exists, update its erp_party value - doc.party_mappings[existing_row].erp_party = best_match.name - doc.party_mappings[existing_row].party_type = erp_party_type - doc.party_mappings[existing_row].mapping_type = 'Automatic' - else: - # If no row, add a new one - doc.append('party_mappings', { - 'etaxes_party_name': etaxes_party.etaxes_party_name, - 'etaxes_tax_id': etaxes_party.etaxes_tax_id, - 'party_type': erp_party_type, - 'erp_party': best_match.name, - 'mapping_type': 'Automatic' - }) - - matched_count += 1 - - # Update record in E-Taxes Parties - try: - party_doc = frappe.get_doc('E-Taxes Parties', etaxes_party.name) - party_doc.status = 'Mapped' - party_doc.mapped_party = best_match.name - party_doc.save() - except Exception as e: - pass - - # Save settings after adding all mappings - 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} parties' - } - - except Exception as e: - return { - 'success': False, - 'message': "An unknown error occurred, please try again in a few minutes." - } - @frappe.whitelist() def create_unmapped_items(settings_name): """Creating items for unmapped elements from E-Taxes settings table""" @@ -882,18 +626,13 @@ def create_unmapped_items(settings_name): # Get settings settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) - # Get elements from table that have no mapping - unmapped_items = [] - + # Get unmapped items efficiently + unmapped_etaxes_items = [] for mapping_item in settings_doc.item_mappings: - # Check that item has no mapping or it's empty - if not mapping_item.erp_item: - # Get E-Taxes Item document for this record - 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 not mapping_item.erp_item and frappe.db.exists('E-Taxes Item', mapping_item.etaxes_item_name): + unmapped_etaxes_items.append(mapping_item.etaxes_item_name) - if len(unmapped_items) == 0: + if not unmapped_etaxes_items: return { "success": True, "created_count": 0, @@ -901,114 +640,156 @@ def create_unmapped_items(settings_name): "message": "No unmapped items in table to create" } + # Batch load E-Taxes Items + etaxes_items_data = frappe.get_all('E-Taxes Item', + filters={'name': ['in', unmapped_etaxes_items]}, + fields=['name', 'etaxes_item_name', 'etaxes_item_code', + 'etaxes_product_group_code', 'etaxes_product_group_name', + 'is_service_item', 'is_from_purchase', 'is_from_sales']) + + # Build mapping settings dict + mapping_settings = {} + for mapping in settings_doc.item_mappings: + if any([mapping.item_group, mapping.uom, mapping.is_stock_item is not None, + mapping.is_purchase_item is not None, mapping.is_sales_item is not None, + mapping.item_tax_template]): + mapping_settings[mapping.etaxes_item_name] = { + 'item_group': mapping.item_group, + 'uom': mapping.uom, + 'is_stock_item': mapping.is_stock_item, + 'is_purchase_item': mapping.is_purchase_item, + 'is_sales_item': mapping.is_sales_item, + 'item_tax_template': mapping.item_tax_template + } + + # Cache existence checks + services_group_exists = frappe.db.exists('Item Group', 'Services') + services_az_group_exists = frappe.db.exists('Item Group', 'Услуги') + default_group_exists = frappe.db.exists('Item Group', settings_doc.default_item_group) + default_uom_exists = frappe.db.exists('UOM', settings_doc.default_uom) + + # Check for custom fields once + product_group_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_code'}) + product_group_name_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_name'}) + product_group_name_field_length = 140 + if product_group_name_field_exists: + custom_field = frappe.get_doc('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_name'}) + product_group_name_field_length = custom_field.length or 140 + created_count = 0 error_count = 0 - # Collect individual settings for each element - mapping_settings = {} - for mapping in settings_doc.item_mappings: - item_settings = {} - if mapping.item_group: - item_settings['item_group'] = mapping.item_group - if mapping.uom: - item_settings['uom'] = mapping.uom - if mapping.is_stock_item is not None: - item_settings['is_stock_item'] = mapping.is_stock_item - if mapping.is_purchase_item is not None: - item_settings['is_purchase_item'] = mapping.is_purchase_item - if mapping.item_tax_template: - item_settings['item_tax_template'] = mapping.item_tax_template - - if item_settings: - mapping_settings[mapping.etaxes_item_name] = item_settings - - for etaxes_item in unmapped_items: + for etaxes_item_data in etaxes_items_data: try: - # Check if an item with this name already exists - item_exists = frappe.db.exists('Item', {'item_name': etaxes_item.etaxes_item_name}) - - if item_exists: + # Skip if item already exists + if frappe.db.exists('Item', {'item_name': etaxes_item_data.etaxes_item_name}): continue # Create new item item_doc = frappe.new_doc("Item") - # Ensure item_code uniqueness, removing invalid characters - item_code = etaxes_item.etaxes_item_code or etaxes_item.etaxes_item_name + # Generate unique item_code + item_code = etaxes_item_data.etaxes_item_code or etaxes_item_data.etaxes_item_name item_code = re.sub(r'[^a-zA-Z0-9_.-]', '', item_code) if not item_code or len(item_code) < 3: item_code = "ITEM-" + frappe.generate_hash(length=8) - # Check that item_code is unique if frappe.db.exists('Item', {'item_code': item_code}): item_code = item_code + "-" + frappe.generate_hash(length=5) item_doc.item_code = item_code - item_doc.item_name = etaxes_item.etaxes_item_name - item_doc.description = etaxes_item.etaxes_item_name + item_doc.item_name = etaxes_item_data.etaxes_item_name + item_doc.description = etaxes_item_data.etaxes_item_name - # Check if there are individual settings for this element - individual_settings = mapping_settings.get(etaxes_item.name, {}) + # Get individual settings + individual_settings = mapping_settings.get(etaxes_item_data.name, {}) - # Apply settings considering individual priorities - item_group = individual_settings.get('item_group', settings_doc.default_item_group) - stock_uom = individual_settings.get('uom', settings_doc.default_uom) - - # Check item_group existence - if not frappe.db.exists('Item Group', item_group): + # Set item group + item_group = individual_settings.get('item_group') or settings_doc.default_item_group + if not default_group_exists and item_group == settings_doc.default_item_group: item_group = "All Item Groups" - - # Check UOM existence - if not frappe.db.exists('UOM', stock_uom): - stock_uom = "Nos" - item_doc.item_group = item_group + + # Set UOM + stock_uom = individual_settings.get('uom') or settings_doc.default_uom + if not default_uom_exists and stock_uom == settings_doc.default_uom: + stock_uom = "Nos" item_doc.stock_uom = stock_uom - # For boolean fields check that they are not None - if 'is_stock_item' in individual_settings: + # Set item flags based on logic + is_service = etaxes_item_data.get('is_service_item', 0) + is_from_purchase = etaxes_item_data.get('is_from_purchase', 0) + is_from_sales = etaxes_item_data.get('is_from_sales', 0) + + # is_stock_item + if individual_settings.get('is_stock_item') is not None: item_doc.is_stock_item = individual_settings['is_stock_item'] else: - item_doc.is_stock_item = getattr(settings_doc, 'is_stock_item', 1) - - if 'is_purchase_item' in individual_settings: + item_doc.is_stock_item = 0 if is_service else 1 + + # is_purchase_item + if individual_settings.get('is_purchase_item') is not None: item_doc.is_purchase_item = individual_settings['is_purchase_item'] else: - item_doc.is_purchase_item = getattr(settings_doc, 'is_purchase_item', 1) + if is_from_purchase or (is_service and not is_from_purchase and not is_from_sales): + item_doc.is_purchase_item = 1 + else: + item_doc.is_purchase_item = 0 - # Correctly add tax template - if 'item_tax_template' in individual_settings: - 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: - 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 + # is_sales_item + if individual_settings.get('is_sales_item') is not None: + item_doc.is_sales_item = individual_settings['is_sales_item'] + else: + if is_from_sales or (is_service and not is_from_purchase and not is_from_sales): + item_doc.is_sales_item = 1 + else: + item_doc.is_sales_item = 0 + + # Auto-set service item group + if is_service and not individual_settings.get('item_group'): + if services_group_exists: + item_doc.item_group = 'Services' + elif services_az_group_exists: + item_doc.item_group = 'Услуги' + + # Add product group info to custom fields + if product_group_code_field_exists and etaxes_item_data.etaxes_product_group_code: + item_doc.etaxes_product_group_code = etaxes_item_data.etaxes_product_group_code + + if product_group_name_field_exists and etaxes_item_data.etaxes_product_group_name: + product_group_name = etaxes_item_data.etaxes_product_group_name + if len(product_group_name) > product_group_name_field_length: + product_group_name = product_group_name[:product_group_name_field_length] + item_doc.etaxes_product_group_name = product_group_name + + # Add tax template + tax_template = individual_settings.get('item_tax_template') or settings_doc.default_item_tax_template + if tax_template and frappe.db.exists('Item Tax Template', tax_template): + tax_row = item_doc.append('taxes', {}) + tax_row.item_tax_template = tax_template # Save item item_doc.insert() # Update mapping in settings for mapping in settings_doc.item_mappings: - if mapping.etaxes_item_name == etaxes_item.name: + if mapping.etaxes_item_name == etaxes_item_data.name: mapping.erp_item = item_doc.name mapping.mapping_type = 'Automatic' break # Update E-Taxes Item status - etaxes_item.status = 'Mapped' - etaxes_item.mapped_item = item_doc.name - etaxes_item.save() + frappe.db.set_value('E-Taxes Item', etaxes_item_data.name, { + 'status': 'Mapped', + 'mapped_item': item_doc.name + }) created_count += 1 except Exception as e: error_count += 1 - # Save settings after adding all mappings + # Save settings if created_count > 0: settings_doc.save() @@ -1020,11 +801,12 @@ def create_unmapped_items(settings_name): } except Exception as e: + frappe.log_error(f"Error in create_unmapped_items: {str(e)}\n{frappe.get_traceback()}", "Create Items Error") return { "success": False, "message": "An unknown error occurred, please try again in a few minutes." } - + @frappe.whitelist() def create_purchase_invoice_from_order(purchase_order_name): """Создает Purchase Invoice из Purchase Order используя стандартную функцию ERPNext""" @@ -1067,7 +849,7 @@ def create_purchase_invoice_from_order(purchase_order_name): except Exception as e: frappe.log_error(f"Error creating Purchase Invoice from PO {purchase_order_name}: {str(e)}\n{frappe.get_traceback()}", "Purchase Invoice Creation Error") return None - + @frappe.whitelist() def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None): """Import invoice taking into account NEW supplier mappings""" @@ -1097,7 +879,9 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 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 ''}" + # ИСПРАВЛЕНИЕ: Обрезаем ключ до 140 символов + supplier_name = mapping.etaxes_supplier_name[:140] if len(mapping.etaxes_supplier_name) > 140 else mapping.etaxes_supplier_name + key = f"{supplier_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}" supplier_mappings[key] = mapping.erp_supplier # Get default warehouse @@ -1131,16 +915,19 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule else: po = frappe.new_doc('Purchase Order') - # ИЗМЕНЕНО: Set supplier используя supplier_mappings + # ИЗМЕНЕНО: Set supplier используя supplier_mappings с обрезкой sender = invoice_data.get('sender', {}) - sender_key = f"{sender.get('name', '').lower()}|{sender.get('tin', '').lower()}" + sender_name = sender.get('name', '') + # ИСПРАВЛЕНИЕ: Обрезаем название поставщика до 140 символов для поиска + sender_name_truncated = sender_name[:140] if len(sender_name) > 140 else sender_name + sender_key = f"{sender_name_truncated.lower()}|{sender.get('tin', '').lower()}" if sender_key in supplier_mappings and supplier_mappings[sender_key]: po.supplier = supplier_mappings[sender_key] else: return { 'success': False, - 'unmatched_parties': [{ + 'unmatched_suppliers': [{ 'name': sender.get('name', ''), 'tin': sender.get('tin', ''), 'type': 'Sender' @@ -1188,12 +975,15 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule item_name = item.get("productName", "").strip() item_code = item.get("itemId", "") or f"CODE-{invoice_data.get('serialNumber', '')}" - # Ищем соответствие товара - mapped_item = item_mappings.get(item_name) + # ИСПРАВЛЕНИЕ: Обрезаем название товара до 140 символов для поиска + item_name_truncated = item_name[:140] if len(item_name) > 140 else item_name + + # Ищем соответствие товара сначала по обрезанному названию + mapped_item = item_mappings.get(item_name_truncated) # Если не найдено, ищем без учета регистра if not mapped_item: - item_name_lower = item_name.lower() + item_name_lower = item_name_truncated.lower() for mapping_key, mapping_value in item_mappings.items(): if mapping_key.lower() == item_name_lower: mapped_item = mapping_value @@ -1201,7 +991,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule # Если все еще не найдено, ищем с нормализацией азербайджанских символов if not mapped_item: - normalized_input = normalize_string(item_name, consider_azeri=True) + normalized_input = normalize_string(item_name_truncated, consider_azeri=True) for mapping_key, mapping_value in item_mappings.items(): normalized_key = normalize_string(mapping_key, consider_azeri=True) if normalized_key == normalized_input: @@ -1398,167 +1188,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""" - # Записываем активность - 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_parties = [] - - for mapping_party in settings_doc.party_mappings: - # Check that party has no mapping or it's empty - if not mapping_party.erp_party: - # Get E-Taxes Parties document for this record - 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) - # Add party type from settings - etaxes_party.erp_party_type = mapping_party.party_type - unmapped_parties.append(etaxes_party) - - if len(unmapped_parties) == 0: - return { - "success": True, - "created_count": 0, - "error_count": 0, - "message": "No unmapped partners in table to create" - } - - created_count = 0 - error_count = 0 - - # Collect individual settings for each element - mapping_settings = {} - for mapping in settings_doc.party_mappings: - party_settings = {} - if mapping.supplier_group: - party_settings['supplier_group'] = mapping.supplier_group - if mapping.customer_group: - party_settings['customer_group'] = mapping.customer_group - if mapping.territory: - party_settings['territory'] = mapping.territory - if mapping.payment_terms: - party_settings['payment_terms'] = mapping.payment_terms - - if party_settings: - key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}" - mapping_settings[key] = party_settings - - for etaxes_party in unmapped_parties: - try: - # Determine party type (Customer/Supplier) - party_type = etaxes_party.erp_party_type - - # Check if such party with this name and type already exists - 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}) - - if party_exists: - continue - - # Create new party - party_doc = frappe.new_doc(party_type) - - # Get individual settings - key = f"{etaxes_party.etaxes_party_name}|{etaxes_party.etaxes_tax_id}" - individual_settings = mapping_settings.get(key, {}) - - # Set basic fields - if party_type == "Supplier": - party_doc.supplier_name = etaxes_party.etaxes_party_name - party_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" - party_doc.supplier_group = supplier_group - - # If tax_id specified, add it - if etaxes_party.etaxes_tax_id: - party_doc.tax_id = etaxes_party.etaxes_tax_id - - # If payment terms specified, add them - if 'payment_terms' in individual_settings: - party_doc.payment_terms = individual_settings['payment_terms'] - elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms: - party_doc.payment_terms = settings_doc.default_payment_terms - else: # party_type == "Customer" - party_doc.customer_name = etaxes_party.etaxes_party_name - party_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" - party_doc.customer_group = customer_group - - # Territory - territory = individual_settings.get('territory', "All Territories") - if not frappe.db.exists('Territory', territory): - territory = "All Territories" - party_doc.territory = territory - - # If tax_id specified, add it - if etaxes_party.etaxes_tax_id: - party_doc.tax_id = etaxes_party.etaxes_tax_id - - # If payment terms specified, add them - if 'payment_terms' in individual_settings: - party_doc.payment_terms = individual_settings['payment_terms'] - elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms: - party_doc.payment_terms = settings_doc.default_payment_terms - - # Save party - party_doc.insert() - - # Update mapping in settings - 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 - mapping.party_type = party_type - mapping.mapping_type = 'Automatic' - break - - # Update E-Taxes Parties status - etaxes_party.status = 'Mapped' - etaxes_party.mapped_party = party_doc.name - etaxes_party.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} parties, 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 get_etaxes_purchases(): """Gets list of all E-Taxes Purchase records""" @@ -2285,68 +1915,9 @@ def refresh_item_mappings_display(settings_name): 'message': "An error occurred while refreshing display" } -# Helper functions - @frappe.whitelist() -def get_active_settings(): - """Getting active E-Taxes settings""" - settings_list = frappe.get_all('E-Taxes Settings', - filters={'is_active': 1}, - order_by='creation desc', - limit=1) - - if not settings_list: - return None - - return frappe.get_doc('E-Taxes Settings', settings_list[0].name) - -@frappe.whitelist() -def normalize_string(text, consider_azeri=True): - """Normalizing string for comparison""" - import re - - if not text: - return '' - - # ДОБАВЛЕНО: Сначала заменяем азербайджанские/турецкие символы ДО приведения к нижнему регистру - if consider_azeri: - azeri_replacements = { - # Заглавные буквы - 'Ə': 'E', - 'Ü': 'U', - 'Ö': 'O', - 'Ğ': 'G', - 'İ': 'I', - 'Ç': 'C', - 'Ş': 'S', - # Строчные буквы - 'ə': 'e', - 'ü': 'u', - 'ö': 'o', - 'ğ': 'g', - 'ı': 'i', - 'ç': 'c', - 'ş': 's' - } - - for azeri_char, latin_char in azeri_replacements.items(): - text = text.replace(azeri_char, latin_char) - - # Convert to lowercase ПОСЛЕ замены символов - text = text.lower() - - # Remove all special characters and numbers - text = re.sub(r'[^\w\s]', '', text) - text = re.sub(r'\d+', '', text) - - # Remove extra spaces - text = ' '.join(text.split()) - - return text - -@frappe.whitelist() -def process_single_invoice_for_items(token, invoice_id): - """Processing a single invoice for item extraction with progress support""" +def process_single_invoice_for_items(token, invoice_id, source_type='purchase'): + """Processing a single invoice for item extraction with name length handling""" # Записываем активность record_etaxes_activity() @@ -2354,6 +1925,7 @@ def process_single_invoice_for_items(token, invoice_id): created_count = 0 skipped_count = 0 failed_count = 0 + updated_count = 0 # Получаем детали инвойса invoice_details = get_invoice_details(token, invoice_id) @@ -2369,6 +1941,10 @@ def process_single_invoice_for_items(token, invoice_id): serial_number = invoice_details.get('serialNumber', '') items = invoice_details.get('items', []) + # Определяем тип источника + is_from_purchase = 1 if source_type == 'purchase' else 0 + is_from_sales = 1 if source_type == 'sales' else 0 + # Словарь для предотвращения дубликатов в пределах одного инвойса unique_items = {} @@ -2381,42 +1957,89 @@ def process_single_invoice_for_items(token, invoice_id): failed_count += 1 continue - # ИСПРАВЛЕНО: Проверяем существование по name документа (etaxes_item_name) - # так как autoname = "field:etaxes_item_name" - existing_check = frappe.db.exists('E-Taxes Item', item_name) + # ИСПРАВЛЕНИЕ: Просто обрезаем до 140 символов + if len(item_name) > 140: + item_name = item_name[:140] - if existing_check: + # Проверяем существование по обрезанному имени + existing_item = frappe.db.get_value('E-Taxes Item', item_name, + ['name', 'is_from_purchase', 'is_from_sales'], as_dict=True) + + if existing_item: + # Если товар уже существует, обновляем флаги источника при необходимости + need_update = False + update_data = {} + + if is_from_purchase and not existing_item.is_from_purchase: + update_data['is_from_purchase'] = 1 + need_update = True + + if is_from_sales and not existing_item.is_from_sales: + update_data['is_from_sales'] = 1 + need_update = True + + if need_update: + try: + frappe.db.set_value('E-Taxes Item', item_name, update_data) + updated_count += 1 + except Exception as e: + frappe.log_error(f"Error updating E-Taxes Item {item_name}: {str(e)}", "Update Item Source Error") + skipped_count += 1 continue - # Используем только имя товара как ключ для предотвращения дубликатов в одном инвойсе - # так как name документа = etaxes_item_name + # Используем обрезанное имя товара как ключ if item_name in unique_items: continue + # Обрабатываем информацию о группе товаров + product_group = item.get('productGroup', {}) + product_group_code = product_group.get('code', '') if product_group else '' + product_group_type = product_group.get('type', '') if product_group else '' + + # Получаем название группы товаров (предпочитаем азербайджанский язык) + product_group_name = '' + if product_group and product_group.get('name'): + names = product_group.get('name', {}) + if isinstance(names, dict): + product_group_name = names.get('az', '') or names.get('en', '') or names.get('ru', '') + elif isinstance(names, str): + product_group_name = names + + # НОВОЕ: Обрезаем название группы товаров до 1000 символов + if len(product_group_name) > 1000: + product_group_name = product_group_name[:1000] + frappe.log_error(f"Product group name truncated for item {item_name}: original length {len(product_group.get('name', {}).get('az', ''))}", "Product Group Name Truncation") + + # Определяем, является ли товар услугой + is_service = 1 if product_group_type == 'service' else 0 + # Добавляем в словарь уникальных товаров unique_items[item_name] = { 'etaxes_item_name': item_name, 'etaxes_item_code': item_code, 'etaxes_unit': item.get('unit', ''), 'etaxes_price': item.get('pricePerUnit', 0), + 'etaxes_product_group_code': product_group_code, + 'etaxes_product_group_name': product_group_name, + 'etaxes_product_group_type': product_group_type, + 'is_service_item': is_service, 'source_invoice': serial_number, - 'source_type': 'Purchase', # ДОБАВЛЕНО: помечаем тип источника + 'is_from_purchase': is_from_purchase, + 'is_from_sales': is_from_sales, 'status': 'New' } # Создаём записи для уникальных товаров for item_name, item_data in unique_items.items(): try: - # Финальная проверка перед созданием (защита от race condition) - # ИСПРАВЛЕНО: Проверяем по name документа + # Финальная проверка перед созданием existing_check = frappe.db.exists('E-Taxes Item', item_name) if existing_check: skipped_count += 1 continue - # ИСПРАВЛЕНО: Используем insert с ignore_if_duplicate=True doc = frappe.get_doc({ 'doctype': 'E-Taxes Item', **item_data @@ -2425,7 +2048,6 @@ def process_single_invoice_for_items(token, invoice_id): created_count += 1 except frappe.DuplicateEntryError: - # Если всё-таки произошло дублирование, считаем как пропущенный skipped_count += 1 except Exception as e: frappe.log_error(f"Error creating E-Taxes Item {item_data['etaxes_item_name']}: {str(e)}", "Process Single Invoice Error") @@ -2434,10 +2056,12 @@ def process_single_invoice_for_items(token, invoice_id): return { 'success': True, 'created_count': created_count, + 'updated_count': updated_count, 'skipped_count': skipped_count, 'failed_count': failed_count, 'invoice_id': invoice_id, - 'serial_number': serial_number + 'serial_number': serial_number, + 'source_type': source_type } except Exception as e: @@ -2446,9 +2070,9 @@ def process_single_invoice_for_items(token, invoice_id): 'success': False, 'message': "An unknown error occurred processing this invoice" } - + @frappe.whitelist() -def process_single_invoice_for_units(token, invoice_id): +def process_single_invoice_for_units(token, invoice_id, source_type='purchase'): """Processing a single invoice for unit extraction with progress support""" # Записываем активность record_etaxes_activity() @@ -2457,6 +2081,7 @@ def process_single_invoice_for_units(token, invoice_id): created_count = 0 skipped_count = 0 failed_count = 0 + updated_count = 0 # Получаем детали инвойса invoice_details = get_invoice_details(token, invoice_id) @@ -2472,6 +2097,9 @@ def process_single_invoice_for_units(token, invoice_id): serial_number = invoice_details.get('serialNumber', '') items = invoice_details.get('items', []) + # Определяем тип источника + source_type_text = 'Purchase' if source_type == 'purchase' else 'Sales' + # Словарь для предотвращения дубликатов в пределах одного инвойса unique_units = {} @@ -2501,7 +2129,7 @@ def process_single_invoice_for_units(token, invoice_id): 'etaxes_unit_name': unit_name, 'etaxes_unit_code': unit_code, 'source_invoice': serial_number, - 'source_type': 'Purchase', # ДОБАВЛЕНО: помечаем тип источника + 'source_type': source_type_text, 'status': 'New' } @@ -2534,11 +2162,13 @@ def process_single_invoice_for_units(token, invoice_id): return { 'success': True, 'created_count': created_count, + 'updated_count': updated_count, 'skipped_count': skipped_count, 'failed_count': failed_count, 'invoice_id': invoice_id, 'serial_number': serial_number, - 'unique_units': len(unique_units) + 'unique_units': len(unique_units), + 'source_type': source_type } except Exception as e: @@ -2547,7 +2177,7 @@ def process_single_invoice_for_units(token, invoice_id): 'success': False, 'message': "An unknown error occurred processing this invoice" } - + @frappe.whitelist() def process_invoice_parties_from_list(invoice_list_data, token): """Обрабатывает контрагентов из списка инвойсов с разделением на customers и suppliers""" @@ -2890,7 +2520,6 @@ def load_combined_data_from_etaxes(date_from, date_to, load_items=False, load_pa '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""" @@ -3012,7 +2641,6 @@ def get_unmapped_suppliers(): 'message': "An unknown error occurred, please try again in a few minutes." } - @frappe.whitelist() def match_similar_customers(): """Matching customers by similar names""" @@ -3267,8 +2895,6 @@ def match_similar_suppliers(): '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""" @@ -3438,8 +3064,13 @@ def create_unmapped_suppliers(settings_name): for etaxes_supplier in unmapped_suppliers: try: + # ИСПРАВЛЕНИЕ: Обрезаем название поставщика до 140 символов + supplier_name = etaxes_supplier.etaxes_party_name + if len(supplier_name) > 140: + supplier_name = supplier_name[:140] + # Check if such supplier with this name already exists - supplier_exists = frappe.db.exists('Supplier', {'supplier_name': etaxes_supplier.etaxes_party_name}) + supplier_exists = frappe.db.exists('Supplier', {'supplier_name': supplier_name}) if supplier_exists: continue @@ -3451,7 +3082,7 @@ def create_unmapped_suppliers(settings_name): 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_name = supplier_name supplier_doc.supplier_type = "Company" # Can be configured if needed # Apply settings considering individual priorities @@ -3507,3 +3138,793 @@ def create_unmapped_suppliers(settings_name): "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: + # ИСПРАВЛЕНИЕ: Обрезаем название клиента до 140 символов + customer_name = etaxes_customer.etaxes_party_name + if len(customer_name) > 140: + customer_name = customer_name[:140] + + # Check if such customer with this name already exists + customer_exists = frappe.db.exists('Customer', {'customer_name': customer_name}) + + if customer_exists: + continue + + # Create new customer + customer_doc = frappe.new_doc("Customer") + + # Get individual settings + individual_settings = mapping_settings.get(etaxes_customer.etaxes_party_name, {}) + + # Set basic fields + customer_doc.customer_name = customer_name + customer_doc.customer_type = "Company" # Can be configured if needed + + # Apply settings considering individual priorities + 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." + } + +# Helper functions + +@frappe.whitelist() +def get_active_settings(): + """Getting active E-Taxes settings""" + settings_list = frappe.get_all('E-Taxes Settings', + filters={'is_active': 1}, + order_by='creation desc', + limit=1) + + if not settings_list: + return None + + return frappe.get_doc('E-Taxes Settings', settings_list[0].name) + +@frappe.whitelist() +def normalize_string(text, consider_azeri=True): + """Normalizing string for comparison""" + import re + + if not text: + return '' + + # ДОБАВЛЕНО: Сначала заменяем азербайджанские/турецкие символы ДО приведения к нижнему регистру + if consider_azeri: + azeri_replacements = { + # Заглавные буквы + 'Ə': 'E', + 'Ü': 'U', + 'Ö': 'O', + 'Ğ': 'G', + 'İ': 'I', + 'Ç': 'C', + 'Ş': 'S', + # Строчные буквы + 'ə': 'e', + 'ü': 'u', + 'ö': 'o', + 'ğ': 'g', + 'ı': 'i', + 'ç': 'c', + 'ş': 's' + } + + for azeri_char, latin_char in azeri_replacements.items(): + text = text.replace(azeri_char, latin_char) + + # Convert to lowercase ПОСЛЕ замены символов + text = text.lower() + + # Remove all special characters and numbers + text = re.sub(r'[^\w\s]', '', text) + text = re.sub(r'\d+', '', text) + + # Remove extra spaces + text = ' '.join(text.split()) + + return text + +@frappe.whitelist() +def load_parties_from_invoices(date_from, date_to, max_count=200, offset=0, invoice_type="purchase"): + """Загружает контрагентов из инвойсов""" + record_etaxes_activity() + + try: + asan_login_settings = get_default_asan_login() + + if not asan_login_settings.get('found'): + return {'success': False, 'message': 'No Asan Login settings found'} + + try: + max_count = int(max_count) + except (ValueError, TypeError): + max_count = 200 + + try: + offset = int(offset) + except (ValueError, TypeError): + offset = 0 + + filters = { + "actionOwner": None, + "amountFrom": None, + "amountTo": None, + "creationDateFrom": date_from, + "creationDateTo": date_to, + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "maxCount": max_count, + "offset": offset, + "productCode": None, + "productName": None, + "receiverName": None, + "receiverTin": None, + "senderName": None, + "senderTin": None, + "serialNumber": None, + "sortAsc": True, + "sortBy": "creationDate", + "statuses": ["approved", "approvedBySystem"], + "types": ["current", "corrected"] + } + + # Загружаем из inbox + response_inbox = get_invoices(asan_login_settings['main_token'], json.dumps(filters)) + + if 'error' in response_inbox: + return {'success': False, **response_inbox} + + invoices_inbox = response_inbox.get('data', []) or response_inbox.get('invoices', []) + + for invoice in invoices_inbox: + invoice['_source'] = 'inbox' + + all_invoices = invoices_inbox + + # Попытка загрузить из outbox (если доступно) + try: + from .sales_api import get_sales_invoices + response_outbox = get_sales_invoices(asan_login_settings['main_token'], json.dumps(filters)) + + if 'error' not in response_outbox: + invoices_outbox = response_outbox.get('data', []) or response_outbox.get('invoices', []) + + for invoice in invoices_outbox: + invoice['_source'] = 'outbox' + + all_invoices = invoices_inbox + invoices_outbox + + hasMore_inbox = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count + hasMore_outbox = response_outbox.get('hasMore', False) or response_outbox.get('total', 0) > offset + max_count + hasMore = hasMore_inbox or hasMore_outbox + else: + hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count + + except ImportError: + hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count + except Exception: + hasMore = response_inbox.get('hasMore', False) or response_inbox.get('total', 0) > offset + max_count + + return { + 'success': True, + 'invoices': all_invoices, + 'hasMore': hasMore, + 'token': asan_login_settings['main_token'], + 'total': len(all_invoices) + } + + except Exception as e: + frappe.log_error(f"Error in load_parties_from_invoices: {str(e)}", "Load Parties Error") + return { + 'success': False, + 'message': "An unknown error occurred, please try again in a few minutes." + } + +@frappe.whitelist() +def get_reference_data_summary(): + """Get summary of all reference data for E-Taxes Settings dashboard""" + try: + summary = {} + + # Define DocTypes to check + doctypes_to_check = { + 'items': 'E-Taxes Item', + 'customers': 'E-Taxes Customers', + 'suppliers': 'E-Taxes Suppliers', + 'units': 'E-Taxes Unit' + } + + for key, doctype in doctypes_to_check.items(): + try: + # Check if DocType exists + if frappe.db.exists('DocType', doctype): + total = int(frappe.db.count(doctype)) + + # Check if status field exists before counting mapped items + meta = frappe.get_meta(doctype) + has_status_field = any(df.fieldname == 'status' for df in meta.fields) + + if has_status_field: + mapped = int(frappe.db.count(doctype, {'status': 'Mapped'})) + else: + mapped = 0 + + summary[key] = {'total': total, 'mapped': mapped} + else: + summary[key] = {'total': 0, 'mapped': 0} + + except Exception as e: + frappe.log_error(f"Error getting summary for {doctype}: {str(e)}", "Reference Data Summary Error") + summary[key] = {'total': 0, 'mapped': 0} + + return { + 'success': True, + 'summary': summary + } + + except Exception as e: + frappe.log_error(f"Error getting reference data summary: {str(e)}\n{frappe.get_traceback()}", "Reference Data Summary Error") + return {'success': False, 'message': str(e)} + +@frappe.whitelist() +def get_reference_data_lists(data_type, limit=100, offset=0): + """Get paginated list of reference data""" + try: + # Convert limit and offset to integers + try: + limit = int(limit) + offset = int(offset) + except (ValueError, TypeError): + limit = 100 + offset = 0 + + valid_types = ['items', 'customers', 'suppliers', 'units'] + if data_type not in valid_types: + return {'success': False, 'message': 'Invalid data type'} + + doctype_map = { + 'items': 'E-Taxes Item', + 'customers': 'E-Taxes Customers', + 'suppliers': 'E-Taxes Suppliers', + 'units': 'E-Taxes Unit' + } + + doctype = doctype_map[data_type] + + # Check if DocType exists + if not frappe.db.exists('DocType', doctype): + return { + 'success': True, + 'data': [], + 'total_count': 0, + 'has_more': False, + 'message': f'DocType {doctype} does not exist' + } + + # Get DocType meta to check available fields + meta = frappe.get_meta(doctype) + available_fields = [df.fieldname for df in meta.fields] + ['name', 'creation', 'modified'] + + # Define base fields that should exist + base_fields = ['name', 'creation'] + + # Define fields map with fallbacks + fields_map = { + 'items': { + 'required': base_fields + ['etaxes_item_name'], + 'optional': ['etaxes_item_code', 'status', 'mapped_item', 'is_service_item'] + }, + 'customers': { + 'required': base_fields + ['etaxes_party_name'], + 'optional': ['etaxes_tax_id', 'status', 'mapped_customer'] + }, + 'suppliers': { + 'required': base_fields + ['etaxes_party_name'], + 'optional': ['etaxes_tax_id', 'status', 'mapped_supplier'] + }, + 'units': { + 'required': base_fields + ['etaxes_unit_name'], + 'optional': ['status', 'mapped_unit'] + } + } + + # Build fields list based on what's actually available + field_config = fields_map[data_type] + fields_to_fetch = [] + + # Add required fields + for field in field_config['required']: + if field in available_fields: + fields_to_fetch.append(field) + + # Add optional fields if they exist + for field in field_config['optional']: + if field in available_fields: + fields_to_fetch.append(field) + + if not fields_to_fetch: + return { + 'success': True, + 'data': [], + 'total_count': 0, + 'has_more': False, + 'message': f'No valid fields found for {doctype}' + } + + # Get total count first + total_count = frappe.db.count(doctype) + + # Get data with pagination + data = frappe.get_all( + doctype, + fields=fields_to_fetch, + order_by='creation desc', + limit=limit, + start=offset + ) + + # Calculate has_more (ensure all are integers) + has_more = (int(offset) + int(limit)) < int(total_count) + + return { + 'success': True, + 'data': data, + 'total_count': int(total_count), + 'has_more': has_more + } + + except Exception as e: + frappe.log_error(f"Error getting reference data list for {data_type}: {str(e)}\n{frappe.get_traceback()}", "Reference Data List Error") + return {'success': False, 'message': str(e)} +@frappe.whitelist() +def load_reference_data_from_invoices(date_from, date_to, max_count=200, offset=0): + """Загружает только список инвойсов, без обработки""" + record_etaxes_activity() + + try: + # Получаем настройки токена + asan_login_settings = get_default_asan_login() + if not asan_login_settings.get('found'): + return { + 'success': False, + 'message': 'No Asan Login settings found' + } + + token = asan_login_settings['main_token'] + + # Преобразуем параметры + try: + max_count = int(max_count) + except (ValueError, TypeError): + max_count = 200 + + try: + offset = int(offset) + except (ValueError, TypeError): + offset = 0 + + # Настройки фильтров + filters = { + "actionOwner": None, + "amountFrom": None, + "amountTo": None, + "creationDateFrom": date_from, + "creationDateTo": date_to, + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "maxCount": max_count, + "offset": offset, + "productCode": None, + "productName": None, + "receiverName": None, + "receiverTin": None, + "senderName": None, + "senderTin": None, + "serialNumber": None, + "sortAsc": True, + "sortBy": "creationDate", + "statuses": ["approved", "approvedBySystem"], + "types": ["current", "corrected"] + } + + # Получаем список инвойсов из inbox + response_inbox = get_invoices(token, json.dumps(filters)) + + if 'error' in response_inbox: + return { + 'success': False, + 'error': response_inbox.get('error'), + 'message': response_inbox.get('message', 'Failed to retrieve invoices') + } + + invoices_inbox = response_inbox.get('data', []) or response_inbox.get('invoices', []) + + for invoice in invoices_inbox: + invoice['_source'] = 'inbox' + + all_invoices = invoices_inbox + + # Пытаемся получить инвойсы из outbox + try: + from .sales_api import get_sales_invoices + response_outbox = get_sales_invoices(token, json.dumps(filters)) + + if 'error' not in response_outbox: + invoices_outbox = response_outbox.get('data', []) or response_outbox.get('invoices', []) + + for invoice in invoices_outbox: + invoice['_source'] = 'outbox' + + all_invoices.extend(invoices_outbox) + + except (ImportError, Exception) as e: + frappe.log_error(f"Could not load outbox invoices: {str(e)}", "Load Reference Data Warning") + + return { + 'success': True, + 'invoices': all_invoices, + 'token': token, + 'total': len(all_invoices), + 'inbox_count': len(invoices_inbox), + 'outbox_count': len(all_invoices) - len(invoices_inbox) + } + + except Exception as e: + frappe.log_error(f"Error in load_reference_data_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Reference Data Error") + return { + 'success': False, + 'message': "An unknown error occurred, please try again in a few minutes." + } + +@frappe.whitelist() +def process_single_invoice_for_reference_data(token, invoice_id, source_type='purchase'): + """Обрабатывает один инвойс и создает все 4 типа доктайпов""" + record_etaxes_activity() + + try: + # Получаем детали инвойса + invoice_details = get_invoice_details(token, invoice_id) + + if isinstance(invoice_details, dict) and 'error' in invoice_details: + return { + 'success': False, + 'error': invoice_details.get('error'), + 'message': invoice_details.get('message', 'Failed to get invoice details') + } + + # Создаем все доктайпы из одного инвойса + result = create_reference_data_from_single_invoice(invoice_details, source_type) + + return result + + except Exception as e: + frappe.log_error(f"Error in process_single_invoice_for_reference_data: {str(e)}", "Process Single Invoice Error") + return { + 'success': False, + 'message': "An unknown error occurred processing this invoice" + } + +@frappe.whitelist() +def create_reference_data_from_single_invoice(invoice_details, source_type): + """Создает все 4 типа доктайпов из одного инвойса""" + try: + stats = { + 'items_created': 0, + 'items_skipped': 0, + 'units_created': 0, + 'units_skipped': 0, + 'customers_created': 0, + 'customers_skipped': 0, + 'suppliers_created': 0, + 'suppliers_skipped': 0 + } + + serial_number = invoice_details.get('serialNumber', '') + items = invoice_details.get('items', []) + sender = invoice_details.get('sender', {}) + receiver = invoice_details.get('receiver', {}) + + # === СОЗДАНИЕ ТОВАРОВ === + processed_items = set() + for item in items: + item_name = item.get('productName', '').strip() + if not item_name: + continue + + # Обрезаем до 140 символов + if len(item_name) > 140: + item_name = item_name[:140] + + if item_name in processed_items: + continue + + processed_items.add(item_name) + + # Проверяем существование + if frappe.db.exists('E-Taxes Item', item_name): + stats['items_skipped'] += 1 + continue + + try: + # Создаем товар + item_code = item.get('itemId', '') or f"CODE-{serial_number}" + + # Обрабатываем группу товаров + product_group = item.get('productGroup', {}) + product_group_code = product_group.get('code', '') if product_group else '' + product_group_type = product_group.get('type', '') if product_group else '' + + # Получаем название группы товаров + product_group_name = '' + if product_group and product_group.get('name'): + names = product_group.get('name', {}) + if isinstance(names, dict): + product_group_name = names.get('az', '') or names.get('en', '') or names.get('ru', '') + elif isinstance(names, str): + product_group_name = names + + # Обрезаем название группы товаров до 1000 символов + if len(product_group_name) > 1000: + product_group_name = product_group_name[:1000] + + # Определяем, является ли товар услугой + is_service = 1 if product_group_type == 'service' else 0 + + # Определяем источники + is_from_purchase = 1 if source_type == 'purchase' else 0 + is_from_sales = 1 if source_type == 'sales' else 0 + + doc = frappe.get_doc({ + 'doctype': 'E-Taxes Item', + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code, + 'etaxes_unit': item.get('unit', ''), + 'etaxes_price': item.get('pricePerUnit', 0), + 'etaxes_product_group_code': product_group_code, + 'etaxes_product_group_name': product_group_name, + 'etaxes_product_group_type': product_group_type, + 'is_service_item': is_service, + 'source_invoice': serial_number, + 'is_from_purchase': is_from_purchase, + 'is_from_sales': is_from_sales, + 'status': 'New' + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + stats['items_created'] += 1 + + except frappe.DuplicateEntryError: + stats['items_skipped'] += 1 + except Exception as e: + frappe.log_error(f"Error creating E-Taxes Item {item_name}: {str(e)}", "Create Item Error") + + # === СОЗДАНИЕ ЕДИНИЦ === + processed_units = set() + for item in items: + unit_name = item.get('unit', '').strip() + if not unit_name or unit_name in processed_units: + continue + + processed_units.add(unit_name) + + # Проверяем существование + if frappe.db.exists('E-Taxes Unit', unit_name): + stats['units_skipped'] += 1 + continue + + try: + source_type_text = 'Purchase' if source_type == 'purchase' else 'Sales' + + doc = frappe.get_doc({ + 'doctype': 'E-Taxes Unit', + 'etaxes_unit_name': unit_name, + 'etaxes_unit_code': unit_name, + 'source_invoice': serial_number, + 'source_type': source_type_text, + 'status': 'New' + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + stats['units_created'] += 1 + + except frappe.DuplicateEntryError: + stats['units_skipped'] += 1 + except Exception as e: + frappe.log_error(f"Error creating E-Taxes Unit {unit_name}: {str(e)}", "Create Unit Error") + + # === СОЗДАНИЕ КЛИЕНТОВ (RECEIVER) === + if receiver: + receiver_name = (receiver.get('name') or '').strip() + receiver_tin = (receiver.get('tin') or '').strip() + receiver_address = (receiver.get('address') or '').strip() + + receiver_name = ' '.join(receiver_name.split()) # Убираем лишние пробелы + + if receiver_name and receiver_tin: + if not frappe.db.exists('E-Taxes Customers', receiver_name): + try: + doc = frappe.get_doc({ + 'doctype': 'E-Taxes Customers', + 'etaxes_party_name': receiver_name, + 'etaxes_tax_id': receiver_tin, + 'etaxes_address': receiver_address, + 'etaxes_party_type': 'Receiver', + 'source_invoice': serial_number, + 'status': 'New' + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + stats['customers_created'] += 1 + + except frappe.DuplicateEntryError: + stats['customers_skipped'] += 1 + except Exception as e: + frappe.log_error(f"Error creating E-Taxes Customers {receiver_name}: {str(e)}", "Create Customer Error") + else: + stats['customers_skipped'] += 1 + + # === СОЗДАНИЕ ПОСТАВЩИКОВ/КЛИЕНТОВ (SENDER) === + if sender: + sender_name = (sender.get('name') or '').strip() + sender_tin = (sender.get('tin') or '').strip() + sender_address = (sender.get('address') or '').strip() + + sender_name = ' '.join(sender_name.split()) # Убираем лишние пробелы + + if sender_name and sender_tin: + if source_type == 'purchase': + # Sender = Supplier для purchase инвойсов + if not frappe.db.exists('E-Taxes Suppliers', sender_name): + try: + doc = frappe.get_doc({ + 'doctype': 'E-Taxes Suppliers', + 'etaxes_party_name': sender_name, + 'etaxes_tax_id': sender_tin, + 'etaxes_address': sender_address, + 'etaxes_party_type': 'Sender', + 'source_invoice': serial_number, + 'status': 'New' + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + stats['suppliers_created'] += 1 + + except frappe.DuplicateEntryError: + stats['suppliers_skipped'] += 1 + except Exception as e: + frappe.log_error(f"Error creating E-Taxes Suppliers {sender_name}: {str(e)}", "Create Supplier Error") + else: + stats['suppliers_skipped'] += 1 + else: + # Sender = Customer для sales инвойсов + if not frappe.db.exists('E-Taxes Customers', sender_name): + try: + doc = frappe.get_doc({ + 'doctype': 'E-Taxes Customers', + 'etaxes_party_name': sender_name, + 'etaxes_tax_id': sender_tin, + 'etaxes_address': sender_address, + 'etaxes_party_type': 'Sender', + 'source_invoice': serial_number, + 'status': 'New' + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + stats['customers_created'] += 1 + + except frappe.DuplicateEntryError: + stats['customers_skipped'] += 1 + except Exception as e: + frappe.log_error(f"Error creating E-Taxes Customers {sender_name}: {str(e)}", "Create Customer Error") + else: + stats['customers_skipped'] += 1 + + return { + 'success': True, + 'stats': stats, + 'serial_number': serial_number + } + + except Exception as e: + frappe.log_error(f"Error creating reference data: {str(e)}\n{frappe.get_traceback()}", "Create Reference Data Error") + return { + 'success': False, + 'message': str(e) + } + diff --git a/invoice_az/client/e_taxes_items_list.js b/invoice_az/client/e_taxes_items_list.js index e421723..c27953e 100644 --- a/invoice_az/client/e_taxes_items_list.js +++ b/invoice_az/client/e_taxes_items_list.js @@ -851,12 +851,19 @@ function process_invoices_for_items(invoices, token, accumulated_data, current_i const outboxText = accumulated_data.outbox_count ? __(' and {0} sales invoices', [accumulated_data.outbox_count]) : ''; + let message = __('Created {0} unique items. Skipped {1} duplicates. Total invoices processed: {2}{3}{4}', + [accumulated_data.created_count, accumulated_data.skipped_count, + accumulated_data.total_invoices, inboxText, outboxText]); + + // Добавляем информацию об обновлениях, если есть + if (accumulated_data.updated_count > 0) { + message += __(' Updated {0} existing items with new source information.', [accumulated_data.updated_count]); + } + frappe.msgprint({ title: __('Items Loaded Successfully'), indicator: 'green', - message: __('Created {0} unique items. Skipped {1} duplicates. Total invoices processed: {2}{3}{4}', - [accumulated_data.created_count, accumulated_data.skipped_count, - accumulated_data.total_invoices, inboxText, outboxText]) + message: message }); // ИСПРАВЛЕНО: Refresh list с задержкой для уверенности в очистке состояния @@ -875,13 +882,17 @@ function process_invoices_for_items(invoices, token, accumulated_data, current_i // Улучшенная логика определения источника let sourceLabel; + let sourceType; if (source === 'inbox') { sourceLabel = __('Purchase'); + sourceType = 'purchase'; } else if (source === 'outbox') { sourceLabel = __('Sales'); + sourceType = 'sales'; } else { // Если _source не установлен или имеет неожиданное значение, считаем как Purchase sourceLabel = __('Purchase'); + sourceType = 'purchase'; // Добавляем отладочную информацию в консоль console.log('Warning: Invoice', serial_number, 'has unexpected _source:', source); } @@ -899,12 +910,14 @@ function process_invoices_for_items(invoices, token, accumulated_data, current_i method: 'invoice_az.api.process_single_invoice_for_items', args: { 'token': token, - 'invoice_id': invoice_id + 'invoice_id': invoice_id, + 'source_type': sourceType // НОВОЕ: передаем тип источника }, callback: function(r) { if (r.message && r.message.success) { // Обновляем накопленные данные accumulated_data.created_count += r.message.created_count || 0; + accumulated_data.updated_count += r.message.updated_count || 0; // НОВОЕ: учитываем обновления accumulated_data.skipped_count += r.message.skipped_count || 0; accumulated_data.failed_count += r.message.failed_count || 0; diff --git a/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json index 9b669f1..732d44b 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json +++ b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json @@ -9,7 +9,15 @@ "etaxes_item_code", "etaxes_unit", "etaxes_price", + "product_group_section", + "etaxes_product_group_code", + "etaxes_product_group_name", + "etaxes_product_group_type", + "is_service_item", + "source_section", "source_invoice", + "is_from_purchase", + "is_from_sales", "status", "mapped_item", "item_creation_section", @@ -50,11 +58,62 @@ "options": "AZN", "read_only": 1 }, + { + "fieldname": "product_group_section", + "fieldtype": "Section Break", + "label": "Product Group Information" + }, + { + "fieldname": "etaxes_product_group_code", + "fieldtype": "Data", + "label": "Product Group Code", + "length": 50, + "read_only": 1 + }, + { + "fieldname": "etaxes_product_group_name", + "fieldtype": "Data", + "label": "Product Group Name", + "length": 1000, + "read_only": 1 + }, + { + "fieldname": "etaxes_product_group_type", + "fieldtype": "Data", + "label": "Product Group Type", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_service_item", + "fieldtype": "Check", + "label": "Is Service Item", + "read_only": 1 + }, + { + "fieldname": "source_section", + "fieldtype": "Section Break", + "label": "Source Information" + }, { "description": "Source invoice number", "fieldname": "source_invoice", "fieldtype": "Data", - "label": "Source", + "label": "Source Invoice", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_from_purchase", + "fieldtype": "Check", + "label": "From Purchase Invoice", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_from_sales", + "fieldtype": "Check", + "label": "From Sales Invoice", "read_only": 1 }, { @@ -101,7 +160,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-07-09 20:19:56.319865", + "modified": "2025-07-16 20:19:56.319865", "modified_by": "Administrator", "module": "Invoice Az", "name": "E-Taxes Item", diff --git a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json index ac6170a..84ea7c0 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json +++ b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json @@ -14,6 +14,7 @@ "uom", "is_stock_item", "is_purchase_item", + "is_sales_item", "item_tax_template" ], "fields": [ @@ -83,6 +84,13 @@ "fieldtype": "Check", "label": "Is Purchase Item" }, + { + "default": "0", + "description": "Overrides default Is Sales Item setting", + "fieldname": "is_sales_item", + "fieldtype": "Check", + "label": "Is Sales Item" + }, { "description": "Overrides default Item Tax Template setting", "fieldname": "item_tax_template", @@ -93,7 +101,7 @@ ], "istable": 1, "links": [], - "modified": "2025-05-23 16:00:32.090354", + "modified": "2025-07-16 16:00:32.090354", "modified_by": "Administrator", "module": "Invoice Az", "name": "E-Taxes Item Mapping", diff --git a/invoice_az/invoice_az/doctype/e_taxes_sales/e_taxes_sales.json b/invoice_az/invoice_az/doctype/e_taxes_sales/e_taxes_sales.json index d2ef64f..61f5e40 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_sales/e_taxes_sales.json +++ b/invoice_az/invoice_az/doctype/e_taxes_sales/e_taxes_sales.json @@ -8,7 +8,8 @@ "date", "total", "party", - "etaxes_id" + "etaxes_id", + "type" ], "fields": [ { @@ -30,11 +31,17 @@ "fieldname": "etaxes_id", "fieldtype": "Data", "label": "etaxes_id" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "type", + "options": "Sales\nPurchase" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-05-31 14:13:09.870421", + "modified": "2025-07-22 18:16:56.347313", "modified_by": "Administrator", "module": "Invoice Az", "name": "E-Taxes Sales", diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js index 7c6a17d..3331f27 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js @@ -7,88 +7,41 @@ frappe.form.link_formatters['E-Taxes Unit'] = function(value, doc) { }; frappe.ui.form.on('E-Taxes Settings', { - - // validate: function(frm) { - // // Показываем индикатор загрузки при сохранении - // frm.page.set_indicator(__('Updating mappings...'), 'blue'); - // }, - after_save: function(frm) { - // Обновляем индикатор после сохранения - // frm.page.set_indicator(__('Mappings updated'), 'green'); - - // Показываем уведомление об успешном обновлении - // frappe.show_alert({ - // message: __('Mappings updated successfully'), - // indicator: 'green' - // }, 5); - - // Снимаем индикатор через 2 секунды - // setTimeout(function() { - // frm.page.set_indicator(); - // }, 2000); - }, - + // After save operations can be added here if needed + }, refresh: function(frm) { - - // Кнопка для комбинированной загрузки данных из E-Taxes - frm.add_custom_button(__('Load data from E-Taxes'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - // Если "Да", сохраняем и выполняем действие - frm.save().then(function() { - check_and_process_token_combined(function() { - show_combined_load_dialog(); - }); - }); - }, - function() { - // Если "Нет", не делаем ничего - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - // Если документ не изменен, просто выполняем действие - check_and_process_token_combined(function() { - show_combined_load_dialog(); - }); - } + // Main Load All Data button + frm.add_custom_button(__('Load All Data'), function() { + show_load_all_data_dialog(frm); }, __('Data Loading')); + // Load reference data lists for tabs if document exists + if (frm.doc.name) { + load_reference_data_tabs(frm); + } + + // Add bulk edit buttons for mapping tables frm.fields_dict['item_mappings'].grid.add_custom_button(__('Bulk Edit'), function() { bulk_edit_table(frm, 'item_mappings'); }); - // Добавляем кнопку "Bulk Edit" для табличной части party_mappings - frm.fields_dict['party_mappings'].grid.add_custom_button(__('Bulk Edit'), function() { - bulk_edit_table(frm, 'party_mappings'); - }); - - // Добавляем кнопку "Bulk Edit" для табличной части unit_mappings frm.fields_dict['unit_mappings'].grid.add_custom_button(__('Bulk Edit'), function() { bulk_edit_table(frm, 'unit_mappings'); }); - - // Кнопка для сопоставления товаров по похожему названию + // Items section buttons frm.add_custom_button(__('Match items by similar name'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { match_similar_items(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -96,7 +49,6 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } else { - // Если документ не изменен, просто выполняем действие match_similar_items(); } @@ -133,19 +85,16 @@ frappe.ui.form.on('E-Taxes Settings', { } }, __('Items')); - // Кнопка для создания товаров-соответствий frm.add_custom_button(__('Create matching items'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { create_unmapped_items(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -153,7 +102,6 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } else { - // Если документ не изменен, просто выполняем действие create_unmapped_items(); } @@ -161,7 +109,6 @@ frappe.ui.form.on('E-Taxes Settings', { frappe.confirm( __('
WARNING! This action will create new items in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), function() { - // Дополнительное подтверждение frappe.confirm( __('Are you really sure you want to create new items? This cannot be undone.'), function() { @@ -193,19 +140,16 @@ frappe.ui.form.on('E-Taxes Settings', { } }, __('Items')); - // Кнопка для добавления несопоставленных товаров в таблицу frm.add_custom_button(__('Add unmapped items'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { add_unmapped_items(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -213,7 +157,6 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } else { - // Если документ не изменен, просто выполняем действие add_unmapped_items(); } @@ -223,29 +166,24 @@ frappe.ui.form.on('E-Taxes Settings', { callback: function(r) { if (r.message && r.message.success) { if (r.message.items && r.message.items.length > 0) { - // Добавляем несопоставленные товары в таблицу r.message.items.forEach(function(item) { let row = frm.add_child('item_mappings'); - // Используем ID документа (name) row.etaxes_item_name = item.name; - // Добавляем все значения по умолчанию 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.is_sales_item !== undefined) row.is_sales_item = item.is_sales_item; if (item.item_tax_template) row.item_tax_template = item.item_tax_template; - // Тип сопоставления row.mapping_type = 'Manual'; }); frm.refresh_field('item_mappings'); - // СНАЧАЛА СОХРАНЯЕМ документ с новыми строками frm.save().then(function() { - // СРАЗУ вызываем операцию для обновления отображения frappe.call({ method: 'invoice_az.api.refresh_item_mappings_display', args: { @@ -253,32 +191,26 @@ frappe.ui.form.on('E-Taxes Settings', { }, callback: function(refresh_r) { if (refresh_r.message && refresh_r.message.success && refresh_r.message.updated_mappings) { - // Обновляем отображение Link полей на клиенте без перезагрузки refresh_r.message.updated_mappings.forEach(function(mapping) { - // Находим строку в grid и обновляем отображение let grid_row = frm.fields_dict['item_mappings'].grid.grid_rows.find( row => row.doc.name === mapping.name ); if (grid_row && grid_row.columns.etaxes_item_name) { - // Обновляем отображаемый текст в Link поле - let link_field = grid_row.columns.etaxes_item_name.df; - if (link_field && grid_row.columns.etaxes_item_name.$input) { - // Устанавливаем правильное отображение - grid_row.columns.etaxes_item_name.$input.val(mapping.etaxes_item_name_display); - grid_row.columns.etaxes_item_name.set_value(mapping.etaxes_item_name_id); + let link_field = grid_row.columns.etaxes_item_name; + if (link_field.$input) { + link_field.$input.val(mapping.etaxes_item_name_display); + link_field.set_value(mapping.etaxes_item_name_id); } } }); } + + showFinalMessage(r.message.items); } }); }); - frappe.show_alert({ - message: __('Added {0} unmapped items', [r.message.items.length]), - indicator: 'green' - }, 5); } else { frappe.show_alert({ message: __('No unmapped items'), @@ -294,22 +226,59 @@ frappe.ui.form.on('E-Taxes Settings', { } } }); + + function showFinalMessage(items) { + let services_count = items.filter(item => item.is_service_item).length; + let goods_count = items.length - services_count; + let purchase_count = items.filter(item => item.is_from_purchase).length; + let sales_count = items.filter(item => item.is_from_sales).length; + let both_count = items.filter(item => item.is_from_purchase && item.is_from_sales).length; + + let message = __('Added {0} unmapped items', [items.length]); + + let details = []; + if (services_count > 0 && goods_count > 0) { + details.push(`${goods_count} goods, ${services_count} services`); + } else if (services_count > 0) { + details.push(`${services_count} services`); + } else { + details.push(`${goods_count} goods`); + } + + if (both_count > 0) { + details.push(`${both_count} from both purchase & sales`); + } else { + if (purchase_count > 0) details.push(`${purchase_count} from purchases`); + if (sales_count > 0) details.push(`${sales_count} from sales`); + } + + if (details.length > 0) { + message += ` (${details.join(', ')})`; + } + + frappe.show_alert({ + message: message, + indicator: 'green' + }, 6); + } } + }, __('Items')); - // Аналогичные кнопки для контрагентов - frm.add_custom_button(__('Match partners by similar name'), function() { + // ================================================================= + // КНОПКИ ДЛЯ CUSTOMERS + // ================================================================= + + frm.add_custom_button(__('Match customers by similar name'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { - match_similar_parties(); + match_similar_customers(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -317,25 +286,24 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } else { - // Если документ не изменен, просто выполняем действие - match_similar_parties(); + match_similar_customers(); } - function match_similar_parties() { + function match_similar_customers() { frappe.confirm( - __('This operation will automatically match unmapped partners with similar names. Continue?'), + __('This operation will automatically match unmapped customers with similar names. Continue?'), function() { frappe.show_alert({ - message: __('Matching partners...'), + message: __('Matching customers...'), indicator: 'blue' }); frappe.call({ - method: 'invoice_az.api.match_similar_parties', + method: 'invoice_az.api.match_similar_customers', callback: function(r) { if (r.message && r.message.success) { frappe.show_alert({ - message: __('Matched {0} of {1} partners', + message: __('Matched {0} of {1} customers', [r.message.matched_count, r.message.total_processed]), indicator: 'green' }, 5); @@ -344,7 +312,7 @@ frappe.ui.form.on('E-Taxes Settings', { frappe.msgprint({ title: __('Error'), indicator: 'red', - message: r.message ? r.message.message : __('Error matching partners') + message: r.message ? r.message.message : __('Error matching customers') }); } } @@ -352,20 +320,18 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } - }, __('Partners')); - - frm.add_custom_button(__('Create matching partners'), function() { + }, __('Customers')); + + frm.add_custom_button(__('Create matching customers'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { - create_unmapped_parties(); + create_unmapped_customers(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -373,27 +339,25 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } else { - // Если документ не изменен, просто выполняем действие - create_unmapped_parties(); + create_unmapped_customers(); } - function create_unmapped_parties() { + function create_unmapped_customers() { frappe.confirm( - __('
WARNING! This action will create new partners in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), + __('
WARNING! This action will create new customers in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), function() { - // Дополнительное подтверждение frappe.confirm( - __('Are you really sure you want to create new partners? This cannot be undone.'), + __('Are you really sure you want to create new customers? This cannot be undone.'), function() { frappe.call({ - method: 'invoice_az.api.create_unmapped_parties', + method: 'invoice_az.api.create_unmapped_customers', args: { 'settings_name': frm.doc.name }, callback: function(r) { if (r.message && r.message.success) { frappe.show_alert({ - message: __('Created {0} new partners', [r.message.created_count]), + message: __('Created {0} new customers', [r.message.created_count]), indicator: 'green' }, 5); frm.reload_doc(); @@ -401,7 +365,7 @@ frappe.ui.form.on('E-Taxes Settings', { frappe.msgprint({ title: __('Error'), indicator: 'red', - message: r.message ? r.message.message : __('Error creating partners') + message: r.message ? r.message.message : __('Error creating customers') }); } } @@ -411,20 +375,18 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } - }, __('Partners')); - - frm.add_custom_button(__('Add unmapped partners'), function() { + }, __('Customers')); + + frm.add_custom_button(__('Add unmapped customers'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { - add_unmapped_parties(); + add_unmapped_customers(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -432,60 +394,45 @@ frappe.ui.form.on('E-Taxes Settings', { } ); } else { - // Если документ не изменен, просто выполняем действие - add_unmapped_parties(); + add_unmapped_customers(); } - function add_unmapped_parties() { + function add_unmapped_customers() { frappe.call({ - method: 'invoice_az.api.get_unmapped_parties', + method: 'invoice_az.api.get_unmapped_customers', callback: function(r) { if (r.message && r.message.success) { - if (r.message.parties && r.message.parties.length > 0) { - // Добавляем несопоставленных контрагентов в таблицу - r.message.parties.forEach(function(party) { - let row = frm.add_child('party_mappings'); + if (r.message.customers && r.message.customers.length > 0) { + r.message.customers.forEach(function(customer) { + let row = frm.add_child('customer_mappings'); - // Основные поля - row.etaxes_party_name = party.etaxes_party_name; - row.etaxes_tax_id = party.etaxes_tax_id; + row.etaxes_customer_name = customer.etaxes_party_name; + row.etaxes_tax_id = customer.etaxes_tax_id; - // Тип контрагента - row.party_type = party.erp_party_type || - (party.etaxes_party_type === 'Sender' ? 'Supplier' : 'Customer'); + if (customer.customer_group) + row.customer_group = customer.customer_group; - // Настройки по умолчанию в зависимости от типа - if (party.supplier_group && row.party_type === 'Supplier') - row.supplier_group = party.supplier_group; + if (customer.territory) + row.territory = customer.territory; - if (party.customer_group && row.party_type === 'Customer') - row.customer_group = party.customer_group; + if (customer.payment_terms) + row.payment_terms = customer.payment_terms; - 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'; }); - frm.refresh_field('party_mappings'); + frm.refresh_field('customer_mappings'); - // СНАЧАЛА СОХРАНЯЕМ документ с новыми строками frm.save().then(function() { - // Для parties обновление отображения не требуется, - // так как etaxes_party_name это обычное текстовое поле frappe.show_alert({ - message: __('Added {0} unmapped partners', [r.message.parties.length]), + message: __('Added {0} unmapped customers', [r.message.customers.length]), indicator: 'green' }, 5); }); } else { frappe.show_alert({ - message: __('No unmapped partners'), + message: __('No unmapped customers'), indicator: 'blue' }, 5); } @@ -493,402 +440,207 @@ frappe.ui.form.on('E-Taxes Settings', { frappe.msgprint({ title: __('Error'), indicator: 'red', - message: r.message ? r.message.message : __('Error retrieving partners') + message: r.message ? r.message.message : __('Error retrieving customers') }); } } }); } - }, __('Partners')); - + }, __('Customers')); + // ================================================================= + // КНОПКИ ДЛЯ SUPPLIERS + // ================================================================= - - -// ================================================================= -// КНОПКИ ДЛЯ CUSTOMERS -// ================================================================= - -frm.add_custom_button(__('Match customers by similar name'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - frm.save().then(function() { - match_similar_customers(); - }); - }, - function() { - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - match_similar_customers(); - } - - function match_similar_customers() { - frappe.confirm( - __('This operation will automatically match unmapped customers with similar names. Continue?'), - function() { - frappe.show_alert({ - message: __('Matching customers...'), - indicator: 'blue' - }); - - frappe.call({ - method: 'invoice_az.api.match_similar_customers', - callback: function(r) { - if (r.message && r.message.success) { - frappe.show_alert({ - message: __('Matched {0} of {1} customers', - [r.message.matched_count, r.message.total_processed]), - indicator: 'green' - }, 5); - frm.reload_doc(); - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error matching customers') - }); - } - } - }); - } - ); - } -}, __('Customers')); - -frm.add_custom_button(__('Create matching customers'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - frm.save().then(function() { - create_unmapped_customers(); - }); - }, - function() { - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - create_unmapped_customers(); - } - - function create_unmapped_customers() { - frappe.confirm( - __('
WARNING! This action will create new customers in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), - function() { - frappe.confirm( - __('Are you really sure you want to create new customers? This cannot be undone.'), - function() { - frappe.call({ - method: 'invoice_az.api.create_unmapped_customers', - args: { - 'settings_name': frm.doc.name - }, - callback: function(r) { - if (r.message && r.message.success) { - frappe.show_alert({ - message: __('Created {0} new customers', [r.message.created_count]), - indicator: 'green' - }, 5); - frm.reload_doc(); - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error creating customers') - }); - } - } - }); - } - ); - } - ); - } -}, __('Customers')); - -frm.add_custom_button(__('Add unmapped customers'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - frm.save().then(function() { - add_unmapped_customers(); - }); - }, - function() { - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - add_unmapped_customers(); - } - - function add_unmapped_customers() { - frappe.call({ - method: 'invoice_az.api.get_unmapped_customers', - callback: function(r) { - if (r.message && r.message.success) { - if (r.message.customers && r.message.customers.length > 0) { - r.message.customers.forEach(function(customer) { - let row = frm.add_child('customer_mappings'); - - row.etaxes_customer_name = customer.etaxes_party_name; - row.etaxes_tax_id = customer.etaxes_tax_id; - - if (customer.customer_group) - row.customer_group = customer.customer_group; - - if (customer.territory) - row.territory = customer.territory; - - if (customer.payment_terms) - row.payment_terms = customer.payment_terms; - - row.mapping_type = 'Manual'; - }); - - frm.refresh_field('customer_mappings'); - - frm.save().then(function() { - frappe.show_alert({ - message: __('Added {0} unmapped customers', [r.message.customers.length]), - indicator: 'green' - }, 5); - }); - - } else { - frappe.show_alert({ - message: __('No unmapped customers'), - indicator: 'blue' - }, 5); - } - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error retrieving customers') - }); - } - } - }); - } -}, __('Customers')); - -// ================================================================= -// КНОПКИ ДЛЯ SUPPLIERS -// ================================================================= - -frm.add_custom_button(__('Match suppliers by similar name'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - frm.save().then(function() { - match_similar_suppliers(); - }); - }, - function() { - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - match_similar_suppliers(); - } - - function match_similar_suppliers() { - frappe.confirm( - __('This operation will automatically match unmapped suppliers with similar names. Continue?'), - function() { - frappe.show_alert({ - message: __('Matching suppliers...'), - indicator: 'blue' - }); - - frappe.call({ - method: 'invoice_az.api.match_similar_suppliers', - callback: function(r) { - if (r.message && r.message.success) { - frappe.show_alert({ - message: __('Matched {0} of {1} suppliers', - [r.message.matched_count, r.message.total_processed]), - indicator: 'green' - }, 5); - frm.reload_doc(); - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error matching suppliers') - }); - } - } - }); - } - ); - } -}, __('Suppliers')); - -frm.add_custom_button(__('Create matching suppliers'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - frm.save().then(function() { - create_unmapped_suppliers(); - }); - }, - function() { - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - create_unmapped_suppliers(); - } - - function create_unmapped_suppliers() { - frappe.confirm( - __('
WARNING! This action will create new suppliers in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), - function() { - frappe.confirm( - __('Are you really sure you want to create new suppliers? This cannot be undone.'), - function() { - frappe.call({ - method: 'invoice_az.api.create_unmapped_suppliers', - args: { - 'settings_name': frm.doc.name - }, - callback: function(r) { - if (r.message && r.message.success) { - frappe.show_alert({ - message: __('Created {0} new suppliers', [r.message.created_count]), - indicator: 'green' - }, 5); - frm.reload_doc(); - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error creating suppliers') - }); - } - } - }); - } - ); - } - ); - } -}, __('Suppliers')); - -frm.add_custom_button(__('Add unmapped suppliers'), function() { - if(frm.is_dirty()) { - frappe.confirm( - __('Document contains unsaved changes. Save before performing the operation?'), - function() { - frm.save().then(function() { - add_unmapped_suppliers(); - }); - }, - function() { - frappe.show_alert({ - message: __('Operation canceled. Please save the document first.'), - indicator: 'red' - }, 5); - } - ); - } else { - add_unmapped_suppliers(); - } - - function add_unmapped_suppliers() { - frappe.call({ - method: 'invoice_az.api.get_unmapped_suppliers', - callback: function(r) { - if (r.message && r.message.success) { - if (r.message.suppliers && r.message.suppliers.length > 0) { - r.message.suppliers.forEach(function(supplier) { - let row = frm.add_child('supplier_mappings'); - - row.etaxes_supplier_name = supplier.etaxes_party_name; - row.etaxes_tax_id = supplier.etaxes_tax_id; - - if (supplier.supplier_group) - row.supplier_group = supplier.supplier_group; - - if (supplier.payment_terms) - row.payment_terms = supplier.payment_terms; - - row.mapping_type = 'Manual'; - }); - - frm.refresh_field('supplier_mappings'); - - frm.save().then(function() { - frappe.show_alert({ - message: __('Added {0} unmapped suppliers', [r.message.suppliers.length]), - indicator: 'green' - }, 5); - }); - - } else { - frappe.show_alert({ - message: __('No unmapped suppliers'), - indicator: 'blue' - }, 5); - } - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error retrieving suppliers') - }); - } - } - }); - } -}, __('Suppliers')); - - - - - - - - - // Кнопки для единиц измерения - frm.add_custom_button(__('Match units by similar name'), function() { + frm.add_custom_button(__('Match suppliers by similar name'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { - match_similar_units(); + match_similar_suppliers(); + }); + }, + function() { + frappe.show_alert({ + message: __('Operation canceled. Please save the document first.'), + indicator: 'red' + }, 5); + } + ); + } else { + match_similar_suppliers(); + } + + function match_similar_suppliers() { + frappe.confirm( + __('This operation will automatically match unmapped suppliers with similar names. Continue?'), + function() { + frappe.show_alert({ + message: __('Matching suppliers...'), + indicator: 'blue' + }); + + frappe.call({ + method: 'invoice_az.api.match_similar_suppliers', + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ + message: __('Matched {0} of {1} suppliers', + [r.message.matched_count, r.message.total_processed]), + indicator: 'green' + }, 5); + frm.reload_doc(); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Error matching suppliers') + }); + } + } + }); + } + ); + } + }, __('Suppliers')); + + frm.add_custom_button(__('Create matching suppliers'), function() { + if(frm.is_dirty()) { + frappe.confirm( + __('Document contains unsaved changes. Save before performing the operation?'), + function() { + frm.save().then(function() { + create_unmapped_suppliers(); + }); + }, + function() { + frappe.show_alert({ + message: __('Operation canceled. Please save the document first.'), + indicator: 'red' + }, 5); + } + ); + } else { + create_unmapped_suppliers(); + } + + function create_unmapped_suppliers() { + frappe.confirm( + __('
WARNING! This action will create new suppliers in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), + function() { + frappe.confirm( + __('Are you really sure you want to create new suppliers? This cannot be undone.'), + function() { + frappe.call({ + method: 'invoice_az.api.create_unmapped_suppliers', + args: { + 'settings_name': frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ + message: __('Created {0} new suppliers', [r.message.created_count]), + indicator: 'green' + }, 5); + frm.reload_doc(); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Error creating suppliers') + }); + } + } + }); + } + ); + } + ); + } + }, __('Suppliers')); + + frm.add_custom_button(__('Add unmapped suppliers'), function() { + if(frm.is_dirty()) { + frappe.confirm( + __('Document contains unsaved changes. Save before performing the operation?'), + function() { + frm.save().then(function() { + add_unmapped_suppliers(); + }); + }, + function() { + frappe.show_alert({ + message: __('Operation canceled. Please save the document first.'), + indicator: 'red' + }, 5); + } + ); + } else { + add_unmapped_suppliers(); + } + + function add_unmapped_suppliers() { + frappe.call({ + method: 'invoice_az.api.get_unmapped_suppliers', + callback: function(r) { + if (r.message && r.message.success) { + if (r.message.suppliers && r.message.suppliers.length > 0) { + r.message.suppliers.forEach(function(supplier) { + let row = frm.add_child('supplier_mappings'); + + row.etaxes_supplier_name = supplier.etaxes_party_name; + row.etaxes_tax_id = supplier.etaxes_tax_id; + + if (supplier.supplier_group) + row.supplier_group = supplier.supplier_group; + + if (supplier.payment_terms) + row.payment_terms = supplier.payment_terms; + + row.mapping_type = 'Manual'; + }); + + frm.refresh_field('supplier_mappings'); + + frm.save().then(function() { + frappe.show_alert({ + message: __('Added {0} unmapped suppliers', [r.message.suppliers.length]), + indicator: 'green' + }, 5); + }); + + } else { + frappe.show_alert({ + message: __('No unmapped suppliers'), + indicator: 'blue' + }, 5); + } + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Error retrieving suppliers') + }); + } + } + }); + } + + }, __('Suppliers')); + + // Кнопки для единиц измерения + frm.add_custom_button(__('Match units by similar name'), function() { + if(frm.is_dirty()) { + frappe.confirm( + __('Document contains unsaved changes. Save before performing the operation?'), + function() { + frm.save().then(function() { + match_similar_units(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -896,7 +648,6 @@ frm.add_custom_button(__('Add unmapped suppliers'), function() { } ); } else { - // Если документ не изменен, просто выполняем действие match_similar_units(); } @@ -933,19 +684,16 @@ frm.add_custom_button(__('Add unmapped suppliers'), function() { } }, __('Units')); - // Кнопка для создания единиц измерения frm.add_custom_button(__('Create matching units'), function() { if(frm.is_dirty()) { frappe.confirm( __('Document contains unsaved changes. Save before performing the operation?'), function() { - // Если "Да", сохраняем и выполняем действие frm.save().then(function() { create_unmapped_units(); }); }, function() { - // Если "Нет", не делаем ничего frappe.show_alert({ message: __('Operation canceled. Please save the document first.'), indicator: 'red' @@ -953,7 +701,6 @@ frm.add_custom_button(__('Add unmapped suppliers'), function() { } ); } else { - // Если документ не изменен, просто выполняем действие create_unmapped_units(); } @@ -961,7 +708,6 @@ frm.add_custom_button(__('Add unmapped suppliers'), function() { frappe.confirm( __('
WARNING! This action will create new UOM units in the system for all unmapped elements!

This action is irreversible. Are you sure?

'), function() { - // Дополнительное подтверждение frappe.confirm( __('Are you really sure you want to create new UOM units? This cannot be undone.'), function() { @@ -993,7 +739,6 @@ frm.add_custom_button(__('Add unmapped suppliers'), function() { } }, __('Units')); - // Кнопка для добавления несопоставленных единиц измерения в таблицу frm.add_custom_button(__('Add unmapped units'), function() { if(frm.is_dirty()) { frappe.confirm( @@ -1014,629 +759,125 @@ frm.add_custom_button(__('Add unmapped suppliers'), function() { add_unmapped_units(); } - function add_unmapped_units() { - frappe.call({ - method: 'invoice_az.api.get_unmapped_units', - callback: function(r) { - if (r.message && r.message.success) { - if (r.message.units && r.message.units.length > 0) { - // Добавляем несопоставленные единицы в таблицу - r.message.units.forEach(function(unit) { - let row = frm.add_child('unit_mappings'); - - // ПРАВИЛЬНОЕ решение: используем frappe.model.set_value - // который корректно обрабатывает Link поля - frappe.model.set_value(row.doctype, row.name, 'etaxes_unit_name', unit.value); - row.mapping_type = 'Manual'; - }); - - // Обновляем таблицу и сохраняем - frm.refresh_field('unit_mappings'); - frm.save(); + function add_unmapped_units() { + frappe.call({ + method: 'invoice_az.api.get_unmapped_units', + callback: function(r) { + if (r.message && r.message.success) { + if (r.message.units && r.message.units.length > 0) { + r.message.units.forEach(function(unit) { + let row = frm.add_child('unit_mappings'); + frappe.model.set_value(row.doctype, row.name, 'etaxes_unit_name', unit.value); + row.mapping_type = 'Manual'; + }); + + frm.refresh_field('unit_mappings'); + + frm.save().then(function() { frappe.show_alert({ message: __('Added {0} unmapped units', [r.message.units.length]), indicator: 'green' }, 5); - } else { - frappe.show_alert({ - message: __('No unmapped units'), - indicator: 'blue' - }, 5); - } - } else { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: r.message ? r.message.message : __('Error retrieving units') }); + + } else { + frappe.show_alert({ + message: __('No unmapped units'), + indicator: 'blue' + }, 5); } + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Error retrieving units') + }); } - }); - } + } + }); + } + }, __('Units')); - } }); +// ================================================================= +// NEW FUNCTIONS FOR LOAD ALL DATA FUNCTIONALITY +// ================================================================= -// Глобальная переменная для хранения диалога загрузки для комбинированной загрузки -var loading_dialog_combined = null; - -// Функция для отображения диалога загрузки с анимацией для комбинированной загрузки -function show_loading_dialog_combined(title, message, submessage) { - // Если диалог уже открыт, обновляем только сообщение - if (loading_dialog_combined) { - $('#loading_title_combined').text(title); - $('#loading_message_combined').text(message); - if (submessage) { - $('#loading_submessage_combined').text(submessage).show(); - } else { - $('#loading_submessage_combined').hide(); - } - return; - } - - // Создаем диалог с анимацией загрузки - loading_dialog_combined = new frappe.ui.Dialog({ - title: title, - fields: [ - { - fieldname: 'loading_html', - fieldtype: 'HTML', - options: ` -
-
-
-
- -
-

${title}

-

${message}

-

${submessage || ''}

- -
-
- ` - } - ] +function show_load_all_data_dialog(frm) { + // Check authentication first + check_and_process_token_settings(function() { + open_load_dialog(frm); }); - - loading_dialog_combined.show(); - loading_dialog_combined.$wrapper.find('.modal-dialog').css('max-width', '450px'); } -// Функция для отображения кода верификации для комбинированной загрузки -function show_verification_code_combined(code) { - if (loading_dialog_combined && code) { - $('#verification_code_combined').text(code); - $('#verification_code_container_combined').show(); - } -} - -// Функция для скрытия диалога загрузки для комбинированной загрузки -function hide_loading_dialog_combined() { - if (loading_dialog_combined) { - try { - loading_dialog_combined.hide(); - loading_dialog_combined = null; - } catch (e) { - console.error('Error hiding loading dialog:', e); - loading_dialog_combined = null; - } - } -} - -// Функция для обновления сообщения в диалоге загрузки для комбинированной загрузки -function update_loading_message_combined(message, submessage) { - if (loading_dialog_combined) { - $('#loading_message_combined').text(message); - if (submessage !== undefined) { - $('#loading_submessage_combined').text(submessage); - $('#loading_submessage_combined').toggle(!!submessage); - } - } -} - -// Функция для изменения статуса загрузки на успешный с автоматическим закрытием для комбинированной загрузки -function set_loading_success_combined(message, submessage, callback, delay = 1500) { - if (loading_dialog_combined) { - // Меняем анимацию на галочку - loading_dialog_combined.$wrapper.find('.lds-dual-ring').html(` -
- -
- `); - - // Обновляем сообщения - $('#loading_message_combined').text(message); - if (submessage !== undefined) { - $('#loading_submessage_combined').text(submessage); - $('#loading_submessage_combined').toggle(!!submessage); - } - - // Скрываем код верификации, если он был отображен - $('#verification_code_container_combined').hide(); - - // Удаляем все существующие кнопки действий - loading_dialog_combined.set_primary_action(null); - loading_dialog_combined.set_secondary_action(null); - - // Автоматически закрываем через указанную задержку - setTimeout(function() { - if (loading_dialog_combined) { - loading_dialog_combined.hide(); - loading_dialog_combined = null; - } - if (callback) callback(); - }, delay); - } else if (callback) { - // Если диалог не открыт, но есть callback, вызываем его - callback(); - } -} - -// Функция для изменения статуса загрузки на ошибку для комбинированной загрузки -function set_loading_error_combined(message, submessage, show_close_button = true) { - if (loading_dialog_combined) { - // Меняем анимацию на крестик - loading_dialog_combined.$wrapper.find('.lds-dual-ring').html(` -
- -
- `); - - // Обновляем сообщения - $('#loading_message_combined').text(message); - if (submessage !== undefined) { - $('#loading_submessage_combined').text(submessage); - $('#loading_submessage_combined').toggle(!!submessage); - } - - // Скрываем код верификации, если он был отображен - $('#verification_code_container_combined').hide(); - - // При необходимости добавляем кнопку закрытия - if (show_close_button) { - loading_dialog_combined.set_primary_action(__('Close'), function() { - hide_loading_dialog_combined(); - }); - } - } -} - -// Функция для проверки токена и обработки аутентификации при необходимости для комбинированной загрузки -function check_and_process_token_combined(callback) { - // Проверяем, доступен ли новый модуль ETaxes - if (typeof ETaxes !== 'undefined' && ETaxes.auth) { - ETaxes.auth.checkAndProcess(callback); - return; - } - - // Fallback на старый код +function open_load_dialog(frm) { + // Get current data summary frappe.call({ - method: 'invoice_az.auth.check_token_validity', + method: 'invoice_az.api.get_reference_data_summary', callback: function(r) { - if (r.message && r.message.valid) { - callback(); + if (r.message && r.message.success) { + show_load_dialog_with_summary(frm, r.message.summary); } else { - frappe.confirm( - __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'), - function() { - start_authentication_process_combined(callback); - }, - function() { - frappe.show_alert({ - message: __('Authentication canceled. Operation cannot be completed.'), - indicator: 'red' - }, 5); - } - ); - } - }, - error: function() { - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: __('Failed to check authentication status') - }); - } - }); -} - -// Функция для запуска полного процесса аутентификации для комбинированной загрузки -function start_authentication_process_combined(callback) { - show_loading_dialog_combined( - __('Starting Authentication'), - __('Getting Asan Login settings...'), - __('Please wait') - ); - - frappe.call({ - method: 'invoice_az.auth.get_default_asan_login', - callback: function(r) { - if (r.message && r.message.found) { - var asan_login_name = r.message.name; - update_loading_message_combined( - __('Sending authentication request...'), - __('This will send a request to your Asan Imza mobile app') - ); - - // Запускаем процесс аутентификации - frappe.call({ - method: 'invoice_az.auth.handle_authentication', - args: { - 'asan_login_name': asan_login_name - }, - callback: function(r) { - if (r.message && r.message.success) { - const bearer_token = r.message.bearer_token; - - // Отображаем код верификации, если он есть в ответе - if (r.message.verification_code) { - show_verification_code_combined(r.message.verification_code); - } - - // Изменяем сообщение для ожидания подтверждения - update_loading_message_combined( - __('Waiting for confirmation on your phone'), - __('Please check your phone and confirm the authentication request') - ); - - // Опрашиваем статус аутентификации - poll_authentication_status_combined(asan_login_name, bearer_token, function(success) { - if (success) { - // Если аутентификация успешна, продолжаем процесс - complete_authentication_combined(asan_login_name, callback); - } else { - // Если аутентификация не удалась, показываем сообщение об ошибке - set_loading_error_combined( - __('Authentication Failed'), - __('Could not authenticate with Asan Imza') - ); - } - }); - } else { - // Если запрос не удался, показываем сообщение об ошибке - set_loading_error_combined( - __('Authentication Error'), - r.message ? r.message.message : __('Authentication request failed') - ); - } - } - }); - } else { - // Если настройки не найдены, показываем сообщение об ошибке - set_loading_error_combined( - __('No Asan Login Settings'), - __('Please configure Asan Login settings first') - ); + show_load_dialog_with_summary(frm, {}); } } }); } -// Функция для опроса статуса аутентификации для комбинированной загрузки -function poll_authentication_status_combined(asan_login_name, bearer_token, callback) { - // Счетчик попыток и флаг для остановки опроса - let attempts = 0; - const maxAttempts = 20; // 20 попыток с интервалом 6 секунд - let stopPolling = false; - - // Функция опроса - function pollStatus() { - // Если достигнуто максимальное количество попыток или установлен флаг остановки - if (attempts >= maxAttempts || stopPolling) { - if (attempts >= maxAttempts) { - // Показываем сообщение о таймауте - set_loading_error_combined( - __('Authentication Timeout'), - __('The authentication request has timed out. Please try again.') - ); - callback(false); - } - return; - } - - attempts++; - - // Проверяем статус авторизации - frappe.call({ - method: 'invoice_az.auth.poll_auth_status', - args: { - 'asan_login_name': asan_login_name, - 'bearer_token': bearer_token - }, - callback: function(r) { - if (r.message && r.message.success) { - if (r.message.authenticated) { - // Авторизация успешна - stopPolling = true; - set_loading_success_combined( - __('Authentication Successful'), - __('You are now authenticated with Asan Imza'), - function() { - callback(true); - } - ); - } else { - // Обновляем счетчик попыток в сообщении - update_loading_message_combined( - __('Waiting for confirmation on your phone'), - __('Please check your phone and confirm the authentication request') + - ' (' + attempts + '/' + maxAttempts + ')' - ); - // Продолжаем опрос - setTimeout(pollStatus, 6000); - } - } else { - // Если ошибка, останавливаем опрос и показываем сообщение - stopPolling = true; - set_loading_error_combined( - __('Authentication Error'), - r.message ? r.message.message : __('An unknown error occurred') - ); - callback(false); - } - }, - error: function(err) { - // В случае ошибки продолжаем опрос - console.error('Error during status check:', err); - setTimeout(pollStatus, 6000); - } - }); - } - - // Начинаем опрос - pollStatus(); -} - -// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика для комбинированной загрузки -function complete_authentication_combined(asan_login_name, callback) { - show_loading_dialog_combined( - __('Completing Authentication'), - __('Getting certificates...'), - __('Please wait') - ); - - // Получаем сертификаты - frappe.call({ - method: 'invoice_az.auth.get_auth_certificates', - args: { - 'asan_login_name': asan_login_name - }, - callback: function(cert_r) { - if (cert_r.message && cert_r.message.success) { - update_loading_message_combined( - __('Certificates retrieved'), - __('Selecting certificate and taxpayer') - ); - - // Получаем текущий документ для проверки выбранного сертификата - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: 'Asan Login', - name: asan_login_name - }, - callback: function(r) { - if (r.message && r.message.selected_certificate_json) { - try { - const cert_data = JSON.parse(r.message.selected_certificate_json); - const cert_name = r.message.selected_certificate; - - update_loading_message_combined( - __('Selecting certificate'), - cert_name - ); - - // Выбираем сертификат - frappe.call({ - method: 'invoice_az.auth.select_certificate', - args: { - 'asan_login_name': asan_login_name, - 'certificate_data': cert_data, - 'certificate_name': cert_name - }, - callback: function(select_r) { - if (select_r.message && select_r.message.success) { - update_loading_message_combined( - __('Selecting taxpayer'), - __('Using certificate: ') + cert_name - ); - - // Выбираем налогоплательщика - frappe.call({ - method: 'invoice_az.auth.select_taxpayer', - args: { - 'asan_login_name': asan_login_name - }, - callback: function(tax_r) { - if (tax_r.message && tax_r.message.success) { - set_loading_success_combined( - __('Authentication Complete'), - __('You can now access E-Taxes services'), - callback, - 2000 // Автоматически закроется через 2 секунды - ); - } else { - hide_loading_dialog_combined(); - show_certificate_selector_combined(cert_r.message.certificates, asan_login_name, callback); - } - } - }); - } else { - hide_loading_dialog_combined(); - show_certificate_selector_combined(cert_r.message.certificates, asan_login_name, callback); - } - } - }); - } catch (e) { - console.error('Error parsing certificate:', e); - hide_loading_dialog_combined(); - show_certificate_selector_combined(cert_r.message.certificates, asan_login_name, callback); - } - } else { - hide_loading_dialog_combined(); - show_certificate_selector_combined(cert_r.message.certificates, asan_login_name, callback); - } - } - }); - } else { - set_loading_error_combined( - __('Error'), - cert_r.message ? cert_r.message.message : __('Failed to get certificates') - ); - } - } - }); -} - -// Функция для отображения селектора сертификатов для комбинированной загрузки -function show_certificate_selector_combined(certificates, asan_login_name, callback) { - if (!certificates || !certificates.length) { - frappe.msgprint(__('No certificates available')); - return; - } - - // Создаем таблицу сертификатов - var cert_html = '
'; - cert_html += ''; - - certificates.forEach(function(cert, index) { - var name = ''; - var id = ''; - - if (cert.taxpayerType === 'individual' && cert.individualInfo) { - name = cert.individualInfo.name || ''; - id = cert.individualInfo.fin || ''; - } else if (cert.legalInfo) { - name = cert.legalInfo.name || ''; - id = cert.legalInfo.tin || cert.legalInfo.voen || ''; - } - - cert_html += '' + - '' + - '' + - '' + - '' + - '' + - '' + - ''; - }); - - cert_html += '
TypeNameIDPositionStatus
' + (cert.taxpayerType || '') + '' + name + '' + id + '' + (cert.position || '') + '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + '
'; - - var dialog = new frappe.ui.Dialog({ - title: __('Select Certificate'), - fields: [{ - fieldtype: 'HTML', - fieldname: 'certificates', - options: cert_html - }] - }); - - dialog.show(); - - // Обработчик нажатия на кнопку выбора сертификата - dialog.$wrapper.find('.select-cert-combined').on('click', function() { - var index = $(this).data('index'); - var cert = certificates[index]; - - // Формируем имя для отображения - let certName = ''; - if (cert.taxpayerType === 'individual' && cert.individualInfo) { - certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`; - } else if (cert.legalInfo) { - certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`; - } else { - certName = `Certificate ${index + 1}`; - } - - dialog.hide(); - - // Показываем прогресс - show_loading_dialog_combined( - __('Selecting Certificate'), - __('Processing your selection...'), - certName - ); - - // Отправляем запрос на выбор сертификата - frappe.call({ - method: 'invoice_az.auth.select_certificate', - args: { - 'asan_login_name': asan_login_name, - 'certificate_data': cert, - 'certificate_name': certName - }, - callback: function(r) { - if (r.message && r.message.success) { - update_loading_message_combined( - __('Certificate selected'), - __('Now selecting taxpayer information') - ); - - // Выбираем налогоплательщика - frappe.call({ - method: 'invoice_az.auth.select_taxpayer', - args: { - 'asan_login_name': asan_login_name - }, - callback: function(r) { - if (r.message && r.message.success) { - set_loading_success_combined( - __('Authentication Complete'), - __('You can now access E-Taxes services'), - callback, - 2000 // Автоматически закроется через 2 секунды - ); - } else { - set_loading_error_combined( - __('Error'), - r.message ? r.message.message : __('Failed to select taxpayer') - ); - } - } - }); - } else { - set_loading_error_combined( - __('Error'), - r.message ? r.message.message : __('Failed to select certificate') - ); - } - } - }); - }); -} - -function show_combined_load_dialog() { - // Получаем даты по умолчанию (текущий месяц) +function show_load_dialog_with_summary(frm, summary) { const today = frappe.datetime.get_today(); const first_day_of_month = frappe.datetime.add_months(frappe.datetime.month_start(today), 0); const last_day_of_month = frappe.datetime.add_months(frappe.datetime.month_end(today), 0); + // Build summary HTML + let summary_html = '
'; + summary_html += '
Current Reference Data
'; + summary_html += '
'; + + const data_types = [ + {key: 'items', label: 'Items'}, + {key: 'customers', label: 'Customers'}, + {key: 'suppliers', label: 'Suppliers'}, + {key: 'units', label: 'Units'} + ]; + + data_types.forEach(function(type) { + const data = summary[type.key] || {total: 0, mapped: 0}; + const mapped_percent = data.total > 0 ? Math.round((data.mapped / data.total) * 100) : 0; + const unmapped = data.total - data.mapped; + + summary_html += `
+
+
${type.label}
+
${data.total} total
+
${data.mapped} mapped
+
${unmapped} unmapped
+
${mapped_percent}% mapped
+
+
`; + }); + + summary_html += '
'; + const d = new frappe.ui.Dialog({ - title: __('Load Data from E-Taxes'), + title: __('Load All Reference Data'), fields: [ { - label: __('Period'), - fieldname: 'period_section', - fieldtype: 'Section Break' + fieldname: 'summary_section', + fieldtype: 'HTML', + options: summary_html + }, + { + fieldname: 'period_section', + fieldtype: 'Section Break', + label: __('Period') }, { - label: __('Date From'), fieldname: 'date_from', fieldtype: 'Date', + label: __('Date From'), default: first_day_of_month, reqd: 1 }, @@ -1645,30 +886,56 @@ function show_combined_load_dialog() { fieldtype: 'Column Break' }, { - label: __('Date To'), fieldname: 'date_to', - fieldtype: 'Date', + fieldtype: 'Date', + label: __('Date To'), default: last_day_of_month, reqd: 1 }, { - label: __('Data Types to Load'), fieldname: 'data_types_section', - fieldtype: 'Section Break' + fieldtype: 'Section Break', + label: __('Data Types to Load') }, { - label: __('Load Items'), fieldname: 'load_items', fieldtype: 'Check', + label: __('Load Items'), default: 1, - description: __('Load items and units of measurement (units load automatically with items)') + description: __('Load items from invoices (includes service/product classification)') }, { - label: __('Load Parties'), - fieldname: 'load_parties', + fieldname: 'col_break_2', + fieldtype: 'Column Break' + }, + { + fieldname: 'load_units', fieldtype: 'Check', + label: __('Load Units'), default: 1, - description: __('Load suppliers and customers') + description: __('Load units of measurement from invoices') + }, + { + fieldname: 'col_break_3', + fieldtype: 'Column Break' + }, + { + fieldname: 'load_customers', + fieldtype: 'Check', + label: __('Load Customers'), + default: 1, + description: __('Load customers from invoice receivers') + }, + { + fieldname: 'col_break_4', + fieldtype: 'Column Break' + }, + { + fieldname: 'load_suppliers', + fieldtype: 'Check', + label: __('Load Suppliers'), + default: 1, + description: __('Load suppliers from invoice senders') } ], size: 'large', @@ -1676,29 +943,30 @@ function show_combined_load_dialog() { primary_action: function() { const values = d.get_values(); - // Проверяем, что выбран хотя бы один тип данных - if (!values.load_items && !values.load_parties) { + // Validate selections + if (!values.load_items && !values.load_customers && !values.load_suppliers && !values.load_units) { frappe.msgprint(__('Please select at least one data type to load.')); return; } - // Проверяем корректность дат if (values.date_from > values.date_to) { frappe.msgprint(__('Date From cannot be later than Date To.')); return; } - // Показываем предупреждение и подтверждение + // Show confirmation with selected types const selected_types = []; - if (values.load_items) selected_types.push(__('Items & Units')); - if (values.load_parties) selected_types.push(__('Parties')); + if (values.load_items) selected_types.push(__('Items')); + if (values.load_customers) selected_types.push(__('Customers')); + if (values.load_suppliers) selected_types.push(__('Suppliers')); + if (values.load_units) selected_types.push(__('Units')); frappe.confirm( __('This will load {0} from E-Taxes for the period {1} to {2}.

This operation may take several minutes depending on the number of invoices.

Continue?', - [selected_types.join(' and '), values.date_from, values.date_to]), + [selected_types.join(', '), values.date_from, values.date_to]), function() { d.hide(); - start_combined_loading(values); + start_reference_data_loading(frm, values); } ); } @@ -1707,129 +975,659 @@ function show_combined_load_dialog() { d.show(); } -function start_combined_loading(values) { - // Показываем диалог загрузки - show_loading_dialog_combined( - __('Loading Data from E-Taxes'), - __('Preparing request...'), - __('Please wait') - ); +function start_reference_data_loading(frm, values) { + // frappe.show_progress(__('Loading'), 0, 100, __('Loading invoices...')); - // Форматируем даты в нужном формате - const date_from_formatted = frappe.datetime.str_to_user(values.date_from) + ' 00:00'; - const date_to_formatted = frappe.datetime.str_to_user(values.date_to) + ' 23:59'; - - // Обновляем сообщение - update_loading_message_combined( - __('Sending request to E-Taxes...'), - __('This may take several minutes') - ); - - // Добавляем таймаут для обновления сообщения - setTimeout(function() { - if (loading_dialog_combined) { - update_loading_message_combined( - __('Processing data from E-Taxes...'), - __('Loading invoices and extracting data') - ); - } - }, 3000); - - // Еще один таймаут для дополнительного обновления - setTimeout(function() { - if (loading_dialog_combined) { - update_loading_message_combined( - __('Still processing...'), - __('This operation processes each invoice individually') - ); - } - }, 10000); - - // Вызываем API функцию с увеличенным таймаутом frappe.call({ - method: 'invoice_az.api.load_combined_data_from_etaxes', + method: 'invoice_az.api.load_reference_data_from_invoices', args: { - 'date_from': date_from_formatted, - 'date_to': date_to_formatted, - 'load_items': values.load_items, - 'load_parties': values.load_parties, - 'max_count': 200 + date_from: moment(values.date_from).format("DD-MM-YYYY 00:00"), + date_to: moment(values.date_to).format("DD-MM-YYYY 23:59") }, - timeout: 300, // 5 минут таймаут callback: function(r) { - console.log('API Response:', r); // Для отладки - - if (r.message && r.message.success) { - const stats = r.message.stats || {}; - - // Формируем подробное сообщение об итогах - let details = []; - if (values.load_items) { - details.push(__('Items: {0} created, {1} skipped', [stats.items_created || 0, stats.items_skipped || 0])); - details.push(__('Units: {0} created, {1} skipped', [stats.units_created || 0, stats.units_skipped || 0])); - } - if (values.load_parties) { - details.push(__('Parties: {0} created, {1} skipped', [stats.parties_created || 0, stats.parties_skipped || 0])); - } - - // Показываем успешное завершение - set_loading_success_combined( - __('Data Loading Complete'), - details.join('; '), - null, - 3000 // Показываем дольше для чтения деталей - ); - - } else if (r.message && r.message.error === 'unauthorized') { - // Обработка ошибки аутентификации - перезапускаем аутентификацию - hide_loading_dialog_combined(); - - frappe.confirm( - __('Your E-Taxes session has expired. Would you like to authenticate now and retry?'), - function() { - check_and_process_token_combined(function() { - // После успешной аутентификации снова показываем диалог - show_combined_load_dialog(); - }); - }, - function() { - frappe.show_alert({ - message: __('Authentication cancelled'), - indicator: 'red' - }, 5); - } - ); - } else { - // Другие ошибки - let error_message = r.message ? r.message.message : __('Unknown error occurred'); - console.error('API Error:', r.message); // Для отладки - - set_loading_error_combined( - __('Error Loading Data'), - error_message - ); + if (!r.message || !r.message.success) { + frappe.hide_progress(); + frappe.msgprint(__('Failed to load invoices: ') + (r.message?.message || 'Unknown error')); + return; } + + const invoices = r.message.invoices || []; + const token = r.message.token; + + if (invoices.length === 0) { + frappe.hide_progress(); + frappe.msgprint(__('No invoices found for the selected period.')); + return; + } + + // Начинаем обработку инвойсов + process_invoices_for_reference_data(invoices, token, { + items_created: 0, + items_skipped: 0, + units_created: 0, + units_skipped: 0, + customers_created: 0, + customers_skipped: 0, + suppliers_created: 0, + suppliers_skipped: 0, + total_invoices: invoices.length, + inbox_count: r.message.inbox_count, + outbox_count: r.message.outbox_count + }, 0, frm); }, - error: function(xhr, status, error) { - console.error('Network Error:', {xhr, status, error}); // Для отладки - - let error_msg = __('Network Error'); - let error_detail = __('Failed to connect to server'); - - if (status === 'timeout') { - error_msg = __('Timeout Error'); - error_detail = __('The operation took too long. Please try with a smaller date range.'); - } else if (xhr && xhr.responseJSON && xhr.responseJSON.message) { - error_detail = xhr.responseJSON.message; - } else if (error) { - error_detail = error; - } - - set_loading_error_combined(error_msg, error_detail); + error: function(r) { + frappe.hide_progress(); + frappe.msgprint(__('Error loading invoices: ') + (r.message || 'Unknown error')); } }); } +function process_invoices_for_reference_data(invoices, token, accumulated_data, current_index = 0, frm) { + // Инициализация при первом вызове + if (current_index === 0) { + window.cancelReferenceLoading = false; + + frappe.show_progress(__('Processing Reference Data'), 0, invoices.length, + __('Processing {0} invoices...', [invoices.length]), null, true); + } + + // Проверка отмены + if (window.cancelReferenceLoading) { + frappe.hide_progress(); + frappe.show_alert({ + message: __('Reference data loading cancelled'), + indicator: 'red' + }, 3); + return; + } + + // Если все инвойсы обработаны + if (current_index >= invoices.length) { + frappe.hide_progress(); + + const msg = []; + if (accumulated_data.items_created > 0) { + msg.push(`${accumulated_data.items_created} items`); + } + if (accumulated_data.units_created > 0) { + msg.push(`${accumulated_data.units_created} units`); + } + if (accumulated_data.customers_created > 0) { + msg.push(`${accumulated_data.customers_created} customers`); + } + if (accumulated_data.suppliers_created > 0) { + msg.push(`${accumulated_data.suppliers_created} suppliers`); + } + + frappe.show_alert({ + message: __('Created: ') + msg.join(', ') + '. Use "Add unmapped" buttons to add them to mappings.', + indicator: 'green' + }, 10); + + frm.reload_doc(); + return; + } + + const invoice = invoices[current_index]; + const invoice_id = invoice.id; + const serial_number = invoice.serialNumber || invoice_id; + const source = invoice._source || 'inbox'; + const source_type = source === 'outbox' ? 'sales' : 'purchase'; + + // Обновляем прогресс + frappe.show_progress( + __('Processing Reference Data'), + current_index, + invoices.length, + __('Processing invoice {0} of {1}: {2}', [current_index + 1, invoices.length, serial_number]) + ); + + // Обрабатываем текущий инвойс + frappe.call({ + method: 'invoice_az.api.process_single_invoice_for_reference_data', + args: { + 'token': token, + 'invoice_id': invoice_id, + 'source_type': source_type + }, + callback: function(r) { + if (r.message && r.message.success && r.message.stats) { + // Обновляем накопленные данные + const stats = r.message.stats; + accumulated_data.items_created += stats.items_created || 0; + accumulated_data.items_skipped += stats.items_skipped || 0; + accumulated_data.units_created += stats.units_created || 0; + accumulated_data.units_skipped += stats.units_skipped || 0; + accumulated_data.customers_created += stats.customers_created || 0; + accumulated_data.customers_skipped += stats.customers_skipped || 0; + accumulated_data.suppliers_created += stats.suppliers_created || 0; + accumulated_data.suppliers_skipped += stats.suppliers_skipped || 0; + } + + // Переходим к следующему инвойсу + setTimeout(function() { + if (!window.cancelReferenceLoading) { + process_invoices_for_reference_data(invoices, token, accumulated_data, current_index + 1, frm); + } + }, 50); + + }, + error: function(xhr, status, error) { + // При ошибке переходим к следующему инвойсу + setTimeout(function() { + if (!window.cancelReferenceLoading) { + process_invoices_for_reference_data(invoices, token, accumulated_data, current_index + 1, frm); + } + }, 100); + } + }); +} + +function show_loading_results(stats, selected_types) { + let details = []; + + if (stats.items_created !== undefined || stats.items_updated !== undefined) { + const created = stats.items_created || 0; + const updated = stats.items_updated || 0; + const skipped = stats.items_skipped || 0; + details.push(__('Items: {0} created, {1} updated, {2} skipped', [created, updated, skipped])); + } + if (stats.customers_created !== undefined) { + details.push(__('Customers: {0} created, {1} skipped', + [stats.customers_created, stats.customers_skipped || 0])); + } + if (stats.suppliers_created !== undefined) { + details.push(__('Suppliers: {0} created, {1} skipped', + [stats.suppliers_created, stats.suppliers_skipped || 0])); + } + if (stats.units_created !== undefined) { + details.push(__('Units: {0} created, {1} skipped', + [stats.units_created, stats.units_skipped || 0])); + } + + const message = details.length > 0 ? details.join('
') : __('No new data loaded'); + + let additional_info = ''; + if (stats.invoices_processed && stats.invoices_failed) { + const total_processed = stats.invoices_processed + stats.invoices_failed; + additional_info = `

Processed ${stats.invoices_processed} of ${total_processed} invoices successfully.`; + } + + frappe.msgprint({ + title: __('Data Loading Complete'), + indicator: 'green', + message: message + additional_info + }); +} + +function load_reference_data_tabs(frm) { + // Load data for each tab + load_items_tab(); + load_customers_tab(); + load_suppliers_tab(); + load_units_tab(); +} + +function load_items_tab() { + console.log('Loading items tab...'); + frappe.call({ + method: 'invoice_az.api.get_reference_data_lists', + args: {'data_type': 'items', 'limit': 50, 'offset': 0}, + callback: function(r) { + console.log('Items response:', r); + if (r.message && r.message.success) { + render_items_list(r.message.data, r.message.total_count); + } else { + const error_msg = r.message ? r.message.message : 'Unknown error'; + console.error('Items loading error:', error_msg); + $('#items-list-container').html(`
+
+
+

Error loading items: ${error_msg}

+

This usually means there are no items to display or a field is missing.

+ +
+
+
`); + + // Add retry handler + $('#items-list-container').off('click', '.retry-items-btn').on('click', '.retry-items-btn', function() { + load_items_tab(); + }); + } + }, + error: function(xhr, status, error) { + console.error('Items network error:', xhr, status, error); + $('#items-list-container').html(`
+
+
+

Network error loading items: ${error}

+ +
+
+
`); + + // Add retry handler + $('#items-list-container').off('click', '.retry-items-btn').on('click', '.retry-items-btn', function() { + load_items_tab(); + }); + } + }); +} + +function render_items_list(items, total_count) { + let html = `
+
+
+
Items (${total_count} total)
+
+
+ +
+
+
`; + + if (items && items.length > 0) { + html += '
'; + html += '
'; + html += ''; + html += ''; + html += ''; + + items.forEach(function(item) { + const status = item.status || 'New'; + const status_indicator = status === 'Mapped' ? 'green' : 'red'; + const item_type = item.is_service_item ? 'Service' : 'Product'; + const mapped_to = item.mapped_item ? `${item.mapped_item}` : '-'; + const created = item.creation ? frappe.datetime.str_to_user(item.creation) : '-'; + const item_name = item.etaxes_item_name || 'Unknown'; + const item_code = item.etaxes_item_code || '-'; + + html += ` + + + + + + + `; + }); + + html += '
NameCodeStatusMapped ToTypeCreated
${item_name}${item_code}${status}${mapped_to}${item_type}${created}
'; + html += '
'; // section-body + html += '
'; // form-section + + if (total_count > items.length) { + html += `
+

Showing ${items.length} of ${total_count} items

+ View All Items +
`; + } + } else { + html += '
'; + html += '
'; + html += '
'; + html += '

No items found. Use "Load All Data" to import items from E-Taxes.

'; + html += '
'; + html += '
'; + html += '
'; + } + + $('#items-list-container').html(html); + + // Add event handler for refresh button + $('#items-list-container').off('click', '.refresh-items-btn').on('click', '.refresh-items-btn', function() { + $('#items-list-container').html('
Loading...
'); + load_items_tab(); + }); +} + +function load_customers_tab() { + console.log('Loading customers tab...'); + frappe.call({ + method: 'invoice_az.api.get_reference_data_lists', + args: {'data_type': 'customers', 'limit': 50, 'offset': 0}, + callback: function(r) { + console.log('Customers response:', r); + if (r.message && r.message.success) { + render_customers_list(r.message.data, r.message.total_count); + } else { + const error_msg = r.message ? r.message.message : 'Unknown error'; + console.error('Customers loading error:', error_msg); + $('#customers-list-container').html(`
+
+
+

Error loading customers: ${error_msg}

+

This usually means there are no customers to display or a field is missing.

+ +
+
+
`); + + // Add retry handler + $('#customers-list-container').off('click', '.retry-customers-btn').on('click', '.retry-customers-btn', function() { + load_customers_tab(); + }); + } + }, + error: function(xhr, status, error) { + console.error('Customers network error:', xhr, status, error); + $('#customers-list-container').html(`
+
+
+

Network error loading customers: ${error}

+ +
+
+
`); + + // Add retry handler + $('#customers-list-container').off('click', '.retry-customers-btn').on('click', '.retry-customers-btn', function() { + load_customers_tab(); + }); + } + }); +} + +function render_customers_list(customers, total_count) { + let html = `
+
+
+
Customers (${total_count} total)
+
+
+ +
+
+
`; + + if (customers && customers.length > 0) { + html += '
'; + html += '
'; + html += ''; + html += ''; + html += ''; + + customers.forEach(function(customer) { + const status = customer.status || 'New'; + const status_indicator = status === 'Mapped' ? 'green' : 'red'; + const mapped_to = customer.mapped_customer ? `${customer.mapped_customer}` : '-'; + const created = customer.creation ? frappe.datetime.str_to_user(customer.creation) : '-'; + const party_name = customer.etaxes_party_name || 'Unknown'; + const tax_id = customer.etaxes_tax_id || '-'; + + html += ` + + + + + + `; + }); + + html += '
NameTax IDStatusMapped ToCreated
${party_name}${tax_id}${status}${mapped_to}${created}
'; + html += '
'; // section-body + html += '
'; // form-section + + if (total_count > customers.length) { + html += `
+

Showing ${customers.length} of ${total_count} customers

+ View All Customers +
`; + } + } else { + html += '
'; + html += '
'; + html += '
'; + html += '

No customers found. Use "Load All Data" to import customers from E-Taxes.

'; + html += '
'; + html += '
'; + html += '
'; + } + + $('#customers-list-container').html(html); + + // Add event handler for refresh button + $('#customers-list-container').off('click', '.refresh-customers-btn').on('click', '.refresh-customers-btn', function() { + $('#customers-list-container').html('
Loading...
'); + load_customers_tab(); + }); +} + +function load_suppliers_tab() { + console.log('Loading suppliers tab...'); + frappe.call({ + method: 'invoice_az.api.get_reference_data_lists', + args: {'data_type': 'suppliers', 'limit': 50, 'offset': 0}, + callback: function(r) { + console.log('Suppliers response:', r); + if (r.message && r.message.success) { + render_suppliers_list(r.message.data, r.message.total_count); + } else { + const error_msg = r.message ? r.message.message : 'Unknown error'; + console.error('Suppliers loading error:', error_msg); + $('#suppliers-list-container').html(`
+
+
+

Error loading suppliers: ${error_msg}

+

This usually means there are no suppliers to display or a field is missing.

+ +
+
+
`); + + // Add retry handler + $('#suppliers-list-container').off('click', '.retry-suppliers-btn').on('click', '.retry-suppliers-btn', function() { + load_suppliers_tab(); + }); + } + }, + error: function(xhr, status, error) { + console.error('Suppliers network error:', xhr, status, error); + $('#suppliers-list-container').html(`
+
+
+

Network error loading suppliers: ${error}

+ +
+
+
`); + + // Add retry handler + $('#suppliers-list-container').off('click', '.retry-suppliers-btn').on('click', '.retry-suppliers-btn', function() { + load_suppliers_tab(); + }); + } + }); +} + +function render_suppliers_list(suppliers, total_count) { + let html = `
+
+
+
Suppliers (${total_count} total)
+
+
+ +
+
+
`; + + if (suppliers && suppliers.length > 0) { + html += '
'; + html += '
'; + html += ''; + html += ''; + html += ''; + + suppliers.forEach(function(supplier) { + const status = supplier.status || 'New'; + const status_indicator = status === 'Mapped' ? 'green' : 'red'; + const mapped_to = supplier.mapped_supplier ? `${supplier.mapped_supplier}` : '-'; + const created = supplier.creation ? frappe.datetime.str_to_user(supplier.creation) : '-'; + const party_name = supplier.etaxes_party_name || 'Unknown'; + const tax_id = supplier.etaxes_tax_id || '-'; + + html += ` + + + + + + `; + }); + + html += '
NameTax IDStatusMapped ToCreated
${party_name}${tax_id}${status}${mapped_to}${created}
'; + html += '
'; // section-body + html += '
'; // form-section + + if (total_count > suppliers.length) { + html += `
+

Showing ${suppliers.length} of ${total_count} suppliers

+ View All Suppliers +
`; + } + } else { + html += '
'; + html += '
'; + html += '
'; + html += '

No suppliers found. Use "Load All Data" to import suppliers from E-Taxes.

'; + html += '
'; + html += '
'; + html += '
'; + } + + $('#suppliers-list-container').html(html); + + // Add event handler for refresh button + $('#suppliers-list-container').off('click', '.refresh-suppliers-btn').on('click', '.refresh-suppliers-btn', function() { + $('#suppliers-list-container').html('
Loading...
'); + load_suppliers_tab(); + }); +} + +function load_units_tab() { + console.log('Loading units tab...'); + frappe.call({ + method: 'invoice_az.api.get_reference_data_lists', + args: {'data_type': 'units', 'limit': 50, 'offset': 0}, + callback: function(r) { + console.log('Units response:', r); + if (r.message && r.message.success) { + render_units_list(r.message.data, r.message.total_count); + } else { + const error_msg = r.message ? r.message.message : 'Unknown error'; + console.error('Units loading error:', error_msg); + $('#units-list-container').html(`
+
+
+

Error loading units: ${error_msg}

+

This usually means there are no units to display or a field is missing.

+ +
+
+
`); + + // Add retry handler + $('#units-list-container').off('click', '.retry-units-btn').on('click', '.retry-units-btn', function() { + load_units_tab(); + }); + } + }, + error: function(xhr, status, error) { + console.error('Units network error:', xhr, status, error); + $('#units-list-container').html(`
+
+
+

Network error loading units: ${error}

+ +
+
+
`); + + // Add retry handler + $('#units-list-container').off('click', '.retry-units-btn').on('click', '.retry-units-btn', function() { + load_units_tab(); + }); + } + }); +} + +function render_units_list(units, total_count) { + let html = `
+
+
+
Units (${total_count} total)
+
+
+ +
+
+
`; + + if (units && units.length > 0) { + html += '
'; + html += '
'; + html += ''; + html += ''; + html += ''; + + units.forEach(function(unit) { + const status = unit.status || 'New'; + const status_indicator = status === 'Mapped' ? 'green' : 'red'; + const mapped_to = unit.mapped_unit ? `${unit.mapped_unit}` : '-'; + const created = unit.creation ? frappe.datetime.str_to_user(unit.creation) : '-'; + const unit_name = unit.etaxes_unit_name || 'Unknown'; + + html += ` + + + + + `; + }); + + html += '
NameStatusMapped ToCreated
${unit_name}${status}${mapped_to}${created}
'; + html += '
'; // section-body + html += '
'; // form-section + + if (total_count > units.length) { + html += `
+

Showing ${units.length} of ${total_count} units

+ View All Units +
`; + } + } else { + html += '
'; + html += '
'; + html += '
'; + html += '

No units found. Use "Load All Data" to import units from E-Taxes.

'; + html += '
'; + html += '
'; + html += '
'; + } + + $('#units-list-container').html(html); + + // Add event handler for refresh button + $('#units-list-container').off('click', '.refresh-units-btn').on('click', '.refresh-units-btn', function() { + $('#units-list-container').html('
Loading...
'); + load_units_tab(); + }); +} + +// ================================================================= +// EXISTING UTILITY FUNCTIONS +// ================================================================= + function bulk_edit_table(frm, table_field) { // Получаем выбранные строки const selected_rows = frm.fields_dict[table_field].grid.get_selected_children(); @@ -1969,4 +1767,376 @@ function bulk_edit_table(frm, table_field) { }); d.show(); +} + +// Authentication functions for Load All Data +function check_and_process_token_settings(callback) { + // Проверяем, доступен ли новый модуль ETaxes + if (typeof ETaxes !== 'undefined' && ETaxes.auth) { + ETaxes.auth.checkAndProcess(callback); + return; + } + + // Fallback на старый код + frappe.call({ + method: 'invoice_az.auth.check_token_validity', + callback: function(r) { + if (r.message && r.message.valid) { + callback(); + } else { + frappe.confirm( + __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'), + function() { + start_authentication_process_settings(callback); + }, + function() { + frappe.show_alert({ + message: __('Authentication canceled. Operation cannot be completed.'), + indicator: 'red' + }, 5); + } + ); + } + }, + error: function() { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('Failed to check authentication status') + }); + } + }); +} + +function start_authentication_process_settings(callback) { + show_loading_dialog_settings( + __('Starting Authentication'), + __('Getting Asan Login settings...'), + __('Please wait') + ); + + frappe.call({ + method: 'invoice_az.auth.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + var asan_login_name = r.message.name; + update_loading_message_settings( + __('Sending authentication request...'), + __('This will send a request to your Asan Imza mobile app') + ); + + frappe.call({ + method: 'invoice_az.auth.handle_authentication', + args: { + 'asan_login_name': asan_login_name + }, + callback: function(r) { + if (r.message && r.message.success) { + const bearer_token = r.message.bearer_token; + + if (r.message.verification_code) { + show_verification_code_settings(r.message.verification_code); + } + + update_loading_message_settings( + __('Waiting for confirmation on your phone'), + __('Please check your phone and confirm the authentication request') + ); + + poll_authentication_status_settings(asan_login_name, bearer_token, function(success) { + if (success) { + complete_authentication_settings(asan_login_name, callback); + } else { + set_loading_error_settings( + __('Authentication Failed'), + __('Could not authenticate with Asan Imza') + ); + } + }); + } else { + set_loading_error_settings( + __('Authentication Error'), + r.message ? r.message.message : __('Authentication request failed') + ); + } + } + }); + } else { + set_loading_error_settings( + __('No Asan Login Settings'), + __('Please configure Asan Login settings first') + ); + } + } + }); +} + +// Простые функции диалогов для settings +var loading_dialog_settings = null; + +function show_loading_dialog_settings(title, message, submessage) { + if (loading_dialog_settings) { + $('#loading_title_settings').text(title); + $('#loading_message_settings').text(message); + if (submessage) { + $('#loading_submessage_settings').text(submessage).show(); + } else { + $('#loading_submessage_settings').hide(); + } + return; + } + + loading_dialog_settings = new frappe.ui.Dialog({ + title: title, + fields: [{ + fieldname: 'loading_html', + fieldtype: 'HTML', + options: ` +
+
+
+
+ +
+

${title}

+

${message}

+

${submessage || ''}

+ +
+
+ ` + }] + }); + + loading_dialog_settings.show(); + loading_dialog_settings.$wrapper.find('.modal-dialog').css('max-width', '450px'); +} + +function update_loading_message_settings(message, submessage) { + if (loading_dialog_settings) { + $('#loading_message_settings').text(message); + if (submessage !== undefined) { + $('#loading_submessage_settings').text(submessage); + $('#loading_submessage_settings').toggle(!!submessage); + } + } +} + +function show_verification_code_settings(code) { + if (loading_dialog_settings && code) { + $('#verification_code_settings').text(code); + $('#verification_code_container_settings').show(); + } +} + +function set_loading_error_settings(message, submessage) { + if (loading_dialog_settings) { + loading_dialog_settings.$wrapper.find('.lds-dual-ring').html(` +
+ +
+ `); + + $('#loading_message_settings').text(message); + if (submessage !== undefined) { + $('#loading_submessage_settings').text(submessage); + $('#loading_submessage_settings').toggle(!!submessage); + } + + $('#verification_code_container_settings').hide(); + + loading_dialog_settings.set_primary_action(__('Close'), function() { + hide_loading_dialog_settings(); + }); + } +} + +function hide_loading_dialog_settings() { + if (loading_dialog_settings) { + try { + loading_dialog_settings.hide(); + } catch (e) { + console.error('Error hiding loading dialog:', e); + } + loading_dialog_settings = null; + } +} + +function poll_authentication_status_settings(asan_login_name, bearer_token, callback) { + let attempts = 0; + const maxAttempts = 20; + let stopPolling = false; + + function pollStatus() { + if (attempts >= maxAttempts || stopPolling) { + if (attempts >= maxAttempts) { + set_loading_error_settings( + __('Authentication Timeout'), + __('The authentication request has timed out. Please try again.') + ); + callback(false); + } + return; + } + + attempts++; + + frappe.call({ + method: 'invoice_az.auth.poll_auth_status', + args: { + 'asan_login_name': asan_login_name, + 'bearer_token': bearer_token + }, + callback: function(r) { + if (r.message && r.message.success) { + if (r.message.authenticated) { + stopPolling = true; + loading_dialog_settings.$wrapper.find('.lds-dual-ring').html(` +
+ +
+ `); + update_loading_message_settings( + __('Authentication Successful'), + __('You are now authenticated with Asan Imza') + ); + setTimeout(function() { + hide_loading_dialog_settings(); + callback(true); + }, 1500); + } else { + update_loading_message_settings( + __('Waiting for confirmation on your phone'), + __('Please check your phone and confirm the authentication request') + + ' (' + attempts + '/' + maxAttempts + ')' + ); + setTimeout(pollStatus, 6000); + } + } else { + stopPolling = true; + set_loading_error_settings( + __('Authentication Error'), + r.message ? r.message.message : __('An unknown error occurred') + ); + callback(false); + } + }, + error: function(err) { + console.error('Error during status check:', err); + setTimeout(pollStatus, 6000); + } + }); + } + + pollStatus(); +} + +function complete_authentication_settings(asan_login_name, callback) { + show_loading_dialog_settings( + __('Completing Authentication'), + __('Getting certificates...'), + __('Please wait') + ); + + frappe.call({ + method: 'invoice_az.auth.get_auth_certificates', + args: { + 'asan_login_name': asan_login_name + }, + callback: function(cert_r) { + if (cert_r.message && cert_r.message.success) { + update_loading_message_settings( + __('Certificates retrieved'), + __('Selecting certificate and taxpayer') + ); + + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Asan Login', + name: asan_login_name + }, + callback: function(r) { + if (r.message && r.message.selected_certificate_json) { + try { + const cert_data = JSON.parse(r.message.selected_certificate_json); + const cert_name = r.message.selected_certificate; + + frappe.call({ + method: 'invoice_az.auth.select_certificate', + args: { + 'asan_login_name': asan_login_name, + 'certificate_data': cert_data, + 'certificate_name': cert_name + }, + callback: function(select_r) { + if (select_r.message && select_r.message.success) { + frappe.call({ + method: 'invoice_az.auth.select_taxpayer', + args: { + 'asan_login_name': asan_login_name + }, + callback: function(tax_r) { + if (tax_r.message && tax_r.message.success) { + loading_dialog_settings.$wrapper.find('.lds-dual-ring').html(` +
+ +
+ `); + update_loading_message_settings( + __('Authentication Complete'), + __('You can now access E-Taxes services') + ); + setTimeout(function() { + hide_loading_dialog_settings(); + callback(); + }, 2000); + } else { + set_loading_error_settings( + __('Error'), + tax_r.message ? tax_r.message.message : __('Failed to select taxpayer') + ); + } + } + }); + } else { + set_loading_error_settings( + __('Error'), + select_r.message ? select_r.message.message : __('Failed to select certificate') + ); + } + } + }); + } catch (e) { + set_loading_error_settings( + __('Error'), + __('Error parsing certificate data') + ); + } + } else { + set_loading_error_settings( + __('Error'), + __('No certificate configuration found') + ); + } + } + }); + } else { + set_loading_error_settings( + __('Error'), + cert_r.message ? cert_r.message.message : __('Failed to get certificates') + ); + } + } + }); } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json index 2d0e9fd..1e0935f 100644 --- a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json @@ -1,233 +1,272 @@ { - "actions": [], - "allow_rename": 1, - "autoname": "field:settings_name", - "creation": "2025-05-06 12:00:00.000000", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "main_tab", - "settings_name", - "is_default", - "is_active", - "general_section", - "similarity_threshold", - "consider_azeri_chars", - "default_item_settings_section", - "default_item_group", - "default_uom", - "is_stock_item", - "is_purchase_item", - "default_item_tax_template", - "default_party_settings_section", - "default_supplier_group", - "default_customer_group", - "default_payment_terms", - "data_upload_tab", - "mappings_tab", - "item_mappings_section", - "item_mappings", - "customer_mappings_section", - "customer_mappings", - "supplier_mappings_section", - "supplier_mappings", - "party_mappings_section", - "party_mappings", - "unit_mappings_section", - "unit_mappings" - ], - "fields": [ - { - "fieldname": "main_tab", - "fieldtype": "Tab Break", - "label": "Settings" - }, - { - "fieldname": "settings_name", - "fieldtype": "Data", - "label": "Settings Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "is_default", - "fieldtype": "Check", - "label": "Is Default", - "default": 0 - }, - { - "fieldname": "is_active", - "fieldtype": "Check", - "label": "Is Active", - "default": 1 - }, - { - "fieldname": "general_section", - "fieldtype": "Section Break", - "label": "General Settings" - }, - { - "fieldname": "similarity_threshold", - "fieldtype": "Percent", - "label": "Similarity Threshold (%)", - "default": 95, - "description": "Minimum similarity percentage for automatic mapping" - }, - { - "fieldname": "consider_azeri_chars", - "fieldtype": "Check", - "label": "Consider Azerbaijani Characters", - "default": 1, - "description": "Consider replacement of Azerbaijani letters with Latin equivalents when matching" - }, - { - "fieldname": "default_item_settings_section", - "fieldtype": "Section Break", - "label": "Default Item Settings" - }, - { - "fieldname": "default_item_group", - "fieldtype": "Link", - "label": "Default Item Group", - "options": "Item Group", - "reqd": 1 - }, - { - "fieldname": "default_uom", - "fieldtype": "Link", - "label": "Default Unit of Measure", - "options": "UOM", - "reqd": 1 - }, - { - "fieldname": "is_stock_item", - "fieldtype": "Check", - "label": "Is Stock Item", - "default": 1 - }, - { - "fieldname": "is_purchase_item", - "fieldtype": "Check", - "label": "Is Purchase Item", - "default": 1 - }, - { - "fieldname": "default_item_tax_template", - "fieldtype": "Link", - "label": "Default Item Tax Template", - "options": "Item Tax Template" - }, - { - "fieldname": "default_party_settings_section", - "fieldtype": "Section Break", - "label": "Default Business Partner Settings" - }, - { - "fieldname": "default_supplier_group", - "fieldtype": "Link", - "label": "Default Supplier Group", - "options": "Supplier Group" - }, - { - "fieldname": "default_customer_group", - "fieldtype": "Link", - "label": "Default Customer Group", - "options": "Customer Group" - }, - { - "fieldname": "default_payment_terms", - "fieldtype": "Link", - "label": "Default Payment Terms", - "options": "Payment Terms Template" - }, - { - "fieldname": "data_upload_tab", - "fieldtype": "Tab Break", - "label": "Data Upload from Website" - }, - { - "fieldname": "mappings_tab", - "fieldtype": "Tab Break", - "label": "Mappings" - }, - { - "fieldname": "item_mappings_section", - "fieldtype": "Section Break", - "label": "Item Mappings" - }, - { - "fieldname": "item_mappings", - "fieldtype": "Table", - "label": "Item Mappings", - "options": "E-Taxes Item Mapping" - }, - { - "fieldname": "customer_mappings_section", - "fieldtype": "Section Break", - "label": "Customer Mappings" - }, - { - "fieldname": "customer_mappings", - "fieldtype": "Table", - "label": "Customer Mappings", - "options": "E-Taxes Customer Mappings" - }, - { - "fieldname": "supplier_mappings_section", - "fieldtype": "Section Break", - "label": "Supplier Mappings" - }, - { - "fieldname": "supplier_mappings", - "fieldtype": "Table", - "label": "Supplier Mappings", - "options": "E-Taxes Supplier Mappings" - }, - { - "fieldname": "party_mappings_section", - "fieldtype": "Section Break", - "label": "Business Partner Mappings (Legacy)" - }, - { - "fieldname": "party_mappings", - "fieldtype": "Table", - "label": "Business Partner Mappings", - "options": "E-Taxes Party Mapping" - }, - { - "fieldname": "unit_mappings_section", - "fieldtype": "Section Break", - "label": "Unit of Measure Mappings" - }, - { - "fieldname": "unit_mappings", - "fieldtype": "Table", - "label": "Unit of Measure Mappings", - "options": "E-Taxes Unit Mapping" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-07-11 14:00:00.000000", - "modified_by": "Administrator", - "module": "Invoice Az", - "name": "E-Taxes Settings", - "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": "modified", - "sort_order": "DESC", - "track_changes": 1 + "actions": [], + "allow_rename": 1, + "autoname": "field:settings_name", + "creation": "2025-05-06 12:00:00", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "main_tab", + "settings_name", + "is_default", + "is_active", + "general_section", + "similarity_threshold", + "consider_azeri_chars", + "default_item_settings_section", + "default_item_group", + "default_uom", + "default_item_tax_template", + "default_party_settings_section", + "default_supplier_group", + "default_customer_group", + "default_payment_terms", + "items_tab", + "items_list_html", + "customers_tab", + "customers_list_html", + "suppliers_tab", + "suppliers_list_html", + "units_tab", + "units_list_html", + "mappings_tab", + "item_mappings_section", + "item_mappings", + "customer_mappings_section", + "customer_mappings", + "supplier_mappings_section", + "supplier_mappings", + "unit_mappings_section", + "unit_mappings", + "data_tab", + "html_nbtb" + ], + "fields": [ + { + "fieldname": "main_tab", + "fieldtype": "Tab Break", + "label": "Settings" + }, + { + "fieldname": "settings_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Settings Name", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + }, + { + "default": "1", + "fieldname": "is_active", + "fieldtype": "Check", + "label": "Is Active" + }, + { + "fieldname": "general_section", + "fieldtype": "Section Break", + "label": "General Settings" + }, + { + "default": "95", + "description": "Minimum similarity percentage for automatic mapping", + "fieldname": "similarity_threshold", + "fieldtype": "Percent", + "label": "Similarity Threshold (%)" + }, + { + "default": "1", + "description": "Consider replacement of Azerbaijani letters with Latin equivalents when matching", + "fieldname": "consider_azeri_chars", + "fieldtype": "Check", + "label": "Consider Azerbaijani Characters" + }, + { + "fieldname": "default_item_settings_section", + "fieldtype": "Section Break", + "label": "Default Item Settings" + }, + { + "fieldname": "default_item_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Item Group", + "options": "Item Group", + "reqd": 1 + }, + { + "fieldname": "default_uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Unit of Measure", + "options": "UOM", + "reqd": 1 + }, + { + "fieldname": "default_item_tax_template", + "fieldtype": "Link", + "label": "Default Item Tax Template", + "options": "Item Tax Template" + }, + { + "fieldname": "default_party_settings_section", + "fieldtype": "Section Break", + "label": "Default Business Partner Settings" + }, + { + "fieldname": "default_supplier_group", + "fieldtype": "Link", + "label": "Default Supplier Group", + "options": "Supplier Group" + }, + { + "fieldname": "default_customer_group", + "fieldtype": "Link", + "label": "Default Customer Group", + "options": "Customer Group" + }, + { + "fieldname": "default_payment_terms", + "fieldtype": "Link", + "label": "Default Payment Terms", + "options": "Payment Terms Template" + }, + { + "fieldname": "items_tab", + "fieldtype": "Tab Break", + "js_parent_subtab": "Data tab", + "label": "Items" + }, + { + "fieldname": "items_list_html", + "fieldtype": "HTML", + "label": "Items List", + "options": "
Loading items...
" + }, + { + "fieldname": "customers_tab", + "fieldtype": "Tab Break", + "js_parent_subtab": "Data tab", + "label": "Customers" + }, + { + "fieldname": "customers_list_html", + "fieldtype": "HTML", + "label": "Customers List", + "options": "
Loading customers...
" + }, + { + "fieldname": "suppliers_tab", + "fieldtype": "Tab Break", + "js_parent_subtab": "Data tab", + "label": "Suppliers" + }, + { + "fieldname": "suppliers_list_html", + "fieldtype": "HTML", + "label": "Suppliers List", + "options": "
Loading suppliers...
" + }, + { + "fieldname": "units_tab", + "fieldtype": "Tab Break", + "js_parent_subtab": "Data tab", + "label": "Units" + }, + { + "fieldname": "units_list_html", + "fieldtype": "HTML", + "label": "Units List", + "options": "
Loading units...
" + }, + { + "fieldname": "mappings_tab", + "fieldtype": "Tab Break", + "label": "Mappings" + }, + { + "fieldname": "item_mappings_section", + "fieldtype": "Section Break", + "label": "Item Mappings" + }, + { + "fieldname": "item_mappings", + "fieldtype": "Table", + "label": "Item Mappings", + "options": "E-Taxes Item Mapping" + }, + { + "fieldname": "customer_mappings_section", + "fieldtype": "Section Break", + "label": "Customer Mappings" + }, + { + "fieldname": "customer_mappings", + "fieldtype": "Table", + "label": "Customer Mappings", + "options": "E-Taxes Customer Mappings" + }, + { + "fieldname": "supplier_mappings_section", + "fieldtype": "Section Break", + "label": "Supplier Mappings" + }, + { + "fieldname": "supplier_mappings", + "fieldtype": "Table", + "label": "Supplier Mappings", + "options": "E-Taxes Supplier Mappings" + }, + { + "fieldname": "unit_mappings_section", + "fieldtype": "Section Break", + "label": "Unit of Measure Mappings" + }, + { + "fieldname": "unit_mappings", + "fieldtype": "Table", + "label": "Unit of Measure Mappings", + "options": "E-Taxes Unit Mapping" + }, + { + "fieldname": "html_nbtb", + "fieldtype": "HTML" + }, + { + "fieldname": "data_tab", + "fieldtype": "Tab Break", + "label": "Data tab" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-07-18 18:13:40.328160", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "E-Taxes Settings", + "naming_rule": "By fieldname", + "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": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/invoice_az/sales_api.py b/invoice_az/sales_api.py index 9cf28dc..c7dd8bd 100644 --- a/invoice_az/sales_api.py +++ b/invoice_az/sales_api.py @@ -244,7 +244,9 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched customer_mappings = {} for mapping in settings.customer_mappings: if mapping.etaxes_customer_name and mapping.erp_customer: - key = f"{mapping.etaxes_customer_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}" + # ИСПРАВЛЕНИЕ: Обрезаем ключ до 140 символов + customer_name = mapping.etaxes_customer_name[:140] if len(mapping.etaxes_customer_name) > 140 else mapping.etaxes_customer_name + key = f"{customer_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}" customer_mappings[key] = mapping.erp_customer # Get default warehouse @@ -278,9 +280,12 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched else: so = frappe.new_doc('Sales Order') - # ИЗМЕНЕНО: Set customer используя customer_mappings + # ИЗМЕНЕНО: Set customer используя customer_mappings с обрезкой receiver = invoice_data.get('receiver', {}) - receiver_key = f"{receiver.get('name', '').lower()}|{receiver.get('tin', '').lower()}" + receiver_name = receiver.get('name', '') + # ИСПРАВЛЕНИЕ: Обрезаем название клиента до 140 символов для поиска + receiver_name_truncated = receiver_name[:140] if len(receiver_name) > 140 else receiver_name + receiver_key = f"{receiver_name_truncated.lower()}|{receiver.get('tin', '').lower()}" if receiver_key in customer_mappings and customer_mappings[receiver_key]: so.customer = customer_mappings[receiver_key] @@ -335,12 +340,15 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched item_name = re.sub(r'\s+', ' ', item.get("productName", "")).strip() item_code = item.get("itemId", "") or f"CODE-{invoice_data.get('serialNumber', '')}" - # Ищем соответствие товара - mapped_item = item_mappings.get(item_name) + # ИСПРАВЛЕНИЕ: Обрезаем название товара до 140 символов для поиска + item_name_truncated = item_name[:140] if len(item_name) > 140 else item_name + + # Ищем соответствие товара сначала по обрезанному названию + mapped_item = item_mappings.get(item_name_truncated) # Если не найдено, ищем без учета регистра if not mapped_item: - item_name_lower = item_name.lower() + item_name_lower = item_name_truncated.lower() for mapping_key, mapping_value in item_mappings.items(): if mapping_key.lower() == item_name_lower: mapped_item = mapping_value @@ -348,7 +356,7 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched # Если все еще не найдено, ищем с нормализацией азербайджанских символов if not mapped_item: - normalized_input = normalize_string(item_name, consider_azeri=True) + normalized_input = normalize_string(item_name_truncated, consider_azeri=True) for mapping_key, mapping_value in item_mappings.items(): normalized_key = normalize_string(mapping_key, consider_azeri=True) if normalized_key == normalized_input: @@ -546,7 +554,7 @@ def import_sales_invoice_with_mapping(invoice_data, sales_order_name=None, sched 'success': False, 'message': "An unknown error occurred, please try again in a few minutes." } - + @frappe.whitelist() def create_sales_invoice_from_order(sales_order_name): """Создает Sales Invoice из Sales Order используя стандартную функцию ERPNext""" diff --git a/test_invoice.git/hooks/applypatch-msg.sample b/test_invoice.git/hooks/applypatch-msg.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/commit-msg.sample b/test_invoice.git/hooks/commit-msg.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/fsmonitor-watchman.sample b/test_invoice.git/hooks/fsmonitor-watchman.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/post-update.sample b/test_invoice.git/hooks/post-update.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/pre-applypatch.sample b/test_invoice.git/hooks/pre-applypatch.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/pre-commit.sample b/test_invoice.git/hooks/pre-commit.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/pre-merge-commit.sample b/test_invoice.git/hooks/pre-merge-commit.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/pre-push.sample b/test_invoice.git/hooks/pre-push.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/pre-rebase.sample b/test_invoice.git/hooks/pre-rebase.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/pre-receive.sample b/test_invoice.git/hooks/pre-receive.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/prepare-commit-msg.sample b/test_invoice.git/hooks/prepare-commit-msg.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/push-to-checkout.sample b/test_invoice.git/hooks/push-to-checkout.sample old mode 100755 new mode 100644 diff --git a/test_invoice.git/hooks/update.sample b/test_invoice.git/hooks/update.sample old mode 100755 new mode 100644