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( __('
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( - __('This action is irreversible. Are you sure?
'), + __('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( - __('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( - __('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( + __('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( __('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: ` -${message}
- - -| Type | Name | ID | Position | Status | |
|---|---|---|---|---|---|
| ' + (cert.taxpayerType || '') + ' | ' + - '' + name + ' | ' + - '' + id + ' | ' + - '' + (cert.position || '') + ' | ' + - '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + ' | ' + - '' + - ' |
Error loading items: ${error_msg}
+This usually means there are no items to display or a field is missing.
+ +Network error loading items: ${error}
+ +| Name | Code | Status | Mapped To | Type | Created |
|---|---|---|---|---|---|
| ${item_name} | +${item_code} | +${status} | +${mapped_to} | +${item_type} | +${created} | +
Showing ${items.length} of ${total_count} items
+ View All Items +No items found. Use "Load All Data" to import items from E-Taxes.
'; + html += 'Error loading customers: ${error_msg}
+This usually means there are no customers to display or a field is missing.
+ +Network error loading customers: ${error}
+ +| Name | Tax ID | Status | Mapped To | Created |
|---|---|---|---|---|
| ${party_name} | +${tax_id} | +${status} | +${mapped_to} | +${created} | +
Showing ${customers.length} of ${total_count} customers
+ View All Customers +No customers found. Use "Load All Data" to import customers from E-Taxes.
'; + html += 'Error loading suppliers: ${error_msg}
+This usually means there are no suppliers to display or a field is missing.
+ +Network error loading suppliers: ${error}
+ +| Name | Tax ID | Status | Mapped To | Created |
|---|---|---|---|---|
| ${party_name} | +${tax_id} | +${status} | +${mapped_to} | +${created} | +
Showing ${suppliers.length} of ${total_count} suppliers
+ View All Suppliers +No suppliers found. Use "Load All Data" to import suppliers from E-Taxes.
'; + html += 'Error loading units: ${error_msg}
+This usually means there are no units to display or a field is missing.
+ +Network error loading units: ${error}
+ +| Name | Status | Mapped To | Created |
|---|---|---|---|
| ${unit_name} | +${status} | +${mapped_to} | +${created} | +
Showing ${units.length} of ${total_count} units
+ View All Units +No units found. Use "Load All Data" to import units from E-Taxes.
'; + html += '${message}
+ + +