Added purchase invoice, and uom mapping, and a lot more

This commit is contained in:
Ali 2025-05-22 16:37:53 +04:00
parent b24e35071d
commit 591d54ea7b
15 changed files with 1988 additions and 103 deletions

View File

@ -440,13 +440,11 @@ def get_invoices(token, filters=None):
url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox"
# Base request parameters
# Base request parameters - изменено: только approved и approvedBySystem статусы
payload = {
"sortBy": "creationDate",
"sortAsc": True,
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested",
"cancelRequested", "approvedBySystem", "onApprovalEdited",
"deactivated", "cancelationRefused", "correctionRefused"],
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
"types": ["current", "corrected"],
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
@ -1170,20 +1168,49 @@ def find_or_create_supplier(supplier_name, supplier_tin):
@frappe.whitelist()
def get_uom_for_unit(unit_name):
"""Converts unit name from invoice to system UOM"""
# Conversion dictionary
uom_mapping = {
"шт": "Nos",
"кг": "Kg",
"г": "Gram",
"л": "Litre",
"м": "Meter",
"м2": "Square Meter",
"м3": "Cubic Meter"
}
"""Converts unit name from invoice to system UOM with support for mappings"""
if not unit_name:
return "Nos" # Default UOM if none provided
# Return corresponding UOM or "Nos" by default
return uom_mapping.get(unit_name.lower(), "Nos")
# Try to find in settings first
try:
# Get active settings
settings = get_active_settings()
if settings and hasattr(settings, 'unit_mappings'):
# First we need to find corresponding E-Taxes Unit record
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'etaxes_unit_name': unit_name},
fields=['name'],
limit=1)
if etaxes_units:
etaxes_unit_name = etaxes_units[0].name
# Check if there's a mapping for this unit (case-insensitive)
for mapping in settings.unit_mappings:
if (mapping.etaxes_unit_name == etaxes_unit_name and
mapping.erp_unit):
return mapping.erp_unit
except Exception as e:
frappe.log_error(f"Error getting UOM mapping: {str(e)}", "UOM Mapping Error")
# If no mapping found, create new E-Taxes Unit record for this unit
# so it can be mapped later
if unit_name and not frappe.db.exists('E-Taxes Unit', {'etaxes_unit_name': unit_name}):
try:
# Generate a consistent code for the unit
unit_code = "UNIT-" + ''.join(e for e in unit_name if e.isalnum()).upper()
etaxes_unit = frappe.new_doc('E-Taxes Unit')
etaxes_unit.etaxes_unit_name = unit_name
etaxes_unit.etaxes_unit_code = unit_code
etaxes_unit.status = 'New'
etaxes_unit.insert(ignore_permissions=True)
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Unit: {str(e)}", "UOM Mapping Error")
# If nothing matched, return default UOM
return "Nos"
@frappe.whitelist()
def load_items_from_invoices(date_from, date_to, max_count=200):
@ -1212,7 +1239,7 @@ def load_items_from_invoices(date_from, date_to, max_count=200):
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request
# Format filters for the API request - изменено: только approved и approvedBySystem статусы
filters = {
"actionOwner": None,
"amountFrom": None,
@ -1231,7 +1258,7 @@ def load_items_from_invoices(date_from, date_to, max_count=200):
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested", "cancelRequested", "approvedBySystem", "onApprovalEdited", "deactivated", "cancelationRefused", "correctionRefused"],
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
"types": ["current", "corrected"]
}
@ -1334,7 +1361,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200, invoice_type="
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request
# Format filters for the API request - изменено: только approved и approvedBySystem статусы
filters = {
"actionOwner": None,
"amountFrom": None,
@ -1353,7 +1380,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200, invoice_type="
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested", "cancelRequested", "approvedBySystem", "onApprovalEdited", "deactivated", "cancelationRefused", "correctionRefused"],
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
"types": ["current", "corrected"]
}
@ -1771,28 +1798,6 @@ def match_similar_items():
# Get fresh copy of settings document
doc = frappe.get_doc("E-Taxes Settings", settings.name)
def normalize_string(text, consider_azeri=False):
if not text:
return ""
# Convert text to lowercase
text = text.lower()
# If need to consider Azerbaijani characters, perform replacements
if consider_azeri:
# Replace Azerbaijani characters with Latin equivalents for comparison
replacements = {
'ə': 'e', 'ı': 'i', 'ö': 'o', 'ü': 'u', 'ş': 's', 'ç': 'c', 'ğ': 'g',
'Ə': 'e', 'I': 'i', 'Ö': 'o', 'Ü': 'u', 'Ş': 's', 'Ç': 'c', 'Ğ': 'g'
}
for az_char, lat_char in replacements.items():
text = text.replace(az_char, lat_char)
# Remove all characters except letters and numbers
text = re.sub(r'[^a-z0-9]', '', text)
return text
# Process all items
for etaxes_item in items_to_process:
# Log item being processed
@ -2180,6 +2185,35 @@ def create_unmapped_items(settings_name):
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_purchase_invoice_from_order(purchase_order_name):
"""Создает Purchase Invoice из Purchase Order используя стандартную функцию ERPNext"""
try:
# Импортируем стандартную функцию из модуля Purchase Order
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
# Создаем Purchase Invoice из Purchase Order
pi_doc = make_purchase_invoice(purchase_order_name)
# Устанавливаем даты
pi_doc.posting_date = frappe.utils.nowdate()
pi_doc.due_date = frappe.utils.add_days(frappe.utils.nowdate(), 30) # 30 дней для оплаты
# ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Purchase Invoice
pi_doc.is_taxes_doc = 1
# Сохраняем документ
pi_doc.insert(ignore_permissions=True)
frappe.log_error(f"Purchase Invoice {pi_doc.name} created from Purchase Order {purchase_order_name}", "Purchase Invoice Creation")
# Возвращаем имя созданного Purchase Invoice
return pi_doc.name
except Exception as e:
frappe.log_error(f"Error creating Purchase Invoice from PO {purchase_order_name}: {str(e)}\n{frappe.get_traceback()}", "Purchase Invoice Creation Error")
return None
@frappe.whitelist()
def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None):
"""Import invoice taking into account mappings and processing unmapped elements"""
@ -2199,17 +2233,24 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
'message': 'No active E-Taxes settings found'
}
# Create mapping dictionaries
# Create mapping dictionaries with case-insensitive keys
item_mappings = {}
for mapping in settings.item_mappings:
key = f"{mapping.etaxes_item_name}|{mapping.etaxes_item_code}"
key = f"{mapping.etaxes_item_name.lower()}|{mapping.etaxes_item_code.lower() if mapping.etaxes_item_code else ''}"
item_mappings[key] = mapping.erp_item
party_mappings = {}
for mapping in settings.party_mappings:
key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}"
key = f"{mapping.etaxes_party_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}"
party_mappings[key] = (mapping.erp_party, mapping.party_type)
unit_mappings = {}
for mapping in settings.unit_mappings:
if mapping.etaxes_unit_name and mapping.erp_unit:
# Get the unit name from E-Taxes Unit record
etaxes_unit = frappe.get_doc('E-Taxes Unit', mapping.etaxes_unit_name)
unit_mappings[etaxes_unit.etaxes_unit_name.lower()] = mapping.erp_unit
# Get default warehouse
default_warehouse = warehouse
@ -2248,7 +2289,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
# Set supplier
sender = invoice_data.get('sender', {})
sender_key = f"{sender.get('name', '')}|{sender.get('tin', '')}"
sender_key = f"{sender.get('name', '').lower()}|{sender.get('tin', '').lower()}"
if sender_key in party_mappings:
po.supplier = party_mappings[sender_key][0]
@ -2305,14 +2346,15 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
# Variable for tracking added items
added_items_count = 0
unmatched_items = []
unmatched_units = []
# Add items from invoice
if invoice_data.get("items"):
for item in invoice_data.get("items", []):
# Prepare key for mapping search
# Prepare key for mapping search (case-insensitive)
item_name = item.get("productName", "")
item_code = item.get("itemId", "")
item_key = f"{item_name}|{item_code}"
item_key = f"{item_name.lower()}|{item_code.lower()}"
# Look for mapping
mapped_item = None
@ -2321,17 +2363,32 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
if item_key in item_mappings:
mapped_item = item_mappings[item_key]
else:
# Try to find item by code if specified
# Try to find item by code if specified (case-insensitive)
if item_code:
item_list = frappe.get_all('Item', filters={'item_code': item_code}, limit=1)
if item_list:
mapped_item = item_list[0].name
item_list = frappe.get_all('Item',
filters=[['item_code', 'like', f'%{item_code}%']],
limit=1)
if not item_list:
# Try exact match but case-insensitive
item_list = frappe.db.sql("""
SELECT name FROM tabItem
WHERE LOWER(item_code) = %s
LIMIT 1
""", (item_code.lower(),), as_dict=True)
# If not found by code, look by name
if not mapped_item:
item_list = frappe.get_all('Item', filters={'item_name': item_name}, limit=1)
if item_list:
mapped_item = item_list[0].name
mapped_item = item_list[0].name if 'name' in item_list[0] else item_list[0]['name']
# If not found by code, look by name (case-insensitive)
if not mapped_item:
item_list = frappe.db.sql("""
SELECT name FROM tabItem
WHERE LOWER(item_name) = %s
LIMIT 1
""", (item_name.lower(),), as_dict=True)
if item_list:
mapped_item = item_list[0]['name']
# If not found by code or name
if not mapped_item:
@ -2352,6 +2409,51 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
})
continue # Move to next element
# Get the UOM from unit mapping or fallback (case-insensitive)
unit_name = item.get("unit", "")
mapped_uom = None
# Check if there's a mapping for this unit (case-insensitive)
if unit_name.lower() in unit_mappings:
mapped_uom = unit_mappings[unit_name.lower()]
else:
# If no direct mapping, check if UOM exists with exact name (case-insensitive)
uom_list = frappe.db.sql("""
SELECT name FROM tabUOM
WHERE LOWER(uom_name) = %s
LIMIT 1
""", (unit_name.lower(),), as_dict=True)
if uom_list:
mapped_uom = uom_list[0]['name']
# Save the unit to E-Taxes Unit if it doesn't exist yet
if unit_name and not frappe.db.exists('E-Taxes Unit', {'etaxes_unit_name': unit_name}):
# Generate a consistent code for the unit
unit_code = "UNIT-" + ''.join(e for e in unit_name if e.isalnum()).upper()
etaxes_unit = frappe.new_doc('E-Taxes Unit')
etaxes_unit.etaxes_unit_name = unit_name
etaxes_unit.etaxes_unit_code = unit_code
etaxes_unit.source_invoice = invoice_data.get('serialNumber', '')
etaxes_unit.status = 'New'
try:
etaxes_unit.insert(ignore_permissions=True)
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Unit: {str(e)}", "Import Invoice Error")
# Add to unmapped list if unit is present
if unit_name:
unmatched_units.append({
'name': unit_name
})
# If no mapping found, use default UOM from item
if not mapped_uom:
# Try to get default UOM from item master
item_doc = frappe.get_doc('Item', mapped_item)
mapped_uom = item_doc.stock_uom
# Add position to PO
po_item = frappe.new_doc("Purchase Order Item")
po_item.parent = po.name
@ -2364,7 +2466,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
po_item.qty = item.get("quantity", 0)
po_item.rate = item.get("pricePerUnit", 0)
po_item.amount = item.get("cost", 0)
po_item.uom = get_uom_for_unit(item.get("unit", ""))
po_item.uom = mapped_uom # Use the mapped UOM
# IMPORTANT: Set schedule_date for each row
po_item.schedule_date = date_to_use
@ -2383,6 +2485,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
'message': f'No mapping found for {len(unmatched_items)} items'
}
# Check if there are unmapped units
if unmatched_units:
return {
'success': False,
'unmatched_units': unmatched_units,
'message': f'No mapping found for {len(unmatched_units)} units'
}
# Check that there is at least one element in table
if added_items_count == 0:
frappe.log_error(
@ -2407,9 +2517,60 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
# Save again to ensure changes are applied
po.save()
# ИЗМЕНЕНИЕ: Создаем E-Taxes Purchase и устанавливаем связь ДО submit'а
invoice_id = invoice_data.get('id', '')
serial_number = invoice_data.get('serialNumber', '')
sender_name = invoice_data.get('sender', {}).get('name', '') if invoice_data.get('sender') else ''
total = invoice_data.get('totalAmount', 0) or invoice_data.get('amount', 0)
# Создаем E-Taxes Purchase
etaxes_purchase_result = create_etaxes_purchase(invoice_id, date_to_use, sender_name, total)
if etaxes_purchase_result and etaxes_purchase_result.get('success'):
# Устанавливаем поля E-Taxes ДО submit'а
po.is_taxes_doc = 1
po.taxes_doc = etaxes_purchase_result.get('name')
# Сохраняем изменения
po.save()
# ИЗМЕНЕНИЕ: Делаем Submit для Purchase Order
try:
po.submit()
frappe.log_error(f"Purchase Order {po.name} submitted successfully", "Import Invoice Success")
except Exception as e:
frappe.log_error(f"Error submitting Purchase Order {po.name}: {str(e)}", "Submit PO Error")
return {
'success': False,
'message': f'Failed to submit Purchase Order: {str(e)}'
}
# ИЗМЕНЕНИЕ: Создаем Purchase Invoice
try:
pi_name = create_purchase_invoice_from_order(po.name)
if pi_name:
# Делаем Submit для Purchase Invoice
pi = frappe.get_doc("Purchase Invoice", pi_name)
pi.submit()
frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success")
return {
'success': True,
'message': 'Invoice data imported successfully.',
'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.',
'purchase_order': po.name,
'purchase_invoice': pi_name
}
else:
# Если не удалось создать Purchase Invoice, всё равно возвращаем успех с Purchase Order
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
'purchase_order': po.name
}
except Exception as e:
frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}", "Submit PI Error")
# Если не удалось создать Purchase Invoice, всё равно возвращаем успех с Purchase Order
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
'purchase_order': po.name
}
@ -2470,37 +2631,6 @@ def normalize_string(text, consider_azeri=True):
return text
@frappe.whitelist()
def get_uom_for_unit(unit_name):
"""Getting UOM for unit from E-Taxes"""
if not unit_name:
return 'Nos' # Default
# Unit mapping
uom_mappings = {
'ədəd': 'Nos',
'kq': 'Kg',
'litr': 'Litre',
'metr': 'Meter',
'm': 'Meter',
'km': 'Km',
'saat': 'Hour',
'dəst': 'Set',
'dəfə': 'Time'
}
normalized_unit = unit_name.lower().strip()
if normalized_unit in uom_mappings:
return uom_mappings[normalized_unit]
# Check if such unit exists in system
if frappe.db.exists('UOM', {'uom_name': unit_name}):
return unit_name
# If no match found, return Nos by default
return 'Nos'
@frappe.whitelist()
def create_unmapped_parties(settings_name):
"""Creating parties for unmapped elements from E-Taxes settings table"""
@ -2869,3 +2999,530 @@ def on_delete_purchase_order(doc, method):
except Exception as e:
frappe.log_error(f"Error deleting E-Taxes Purchase {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
"Purchase Order Delete Error")
@frappe.whitelist()
def get_unmapped_units():
"""Getting unmapped units from E-Taxes"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped units from E-Taxes
unmapped_units = []
# 1. Get all units with 'New' status
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'New'},
fields=['name', 'etaxes_unit_name', 'etaxes_unit_code'])
# 2. Check if they are already in settings
existing_mappings = {}
for mapping in settings.unit_mappings:
existing_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# 3. Add only those that are not in settings
for unit in etaxes_units:
if unit.name not in existing_mappings:
# Create unit object with default settings
unit_data = {
'name': unit.name,
'etaxes_unit_name': unit.etaxes_unit_name,
'etaxes_unit_code': unit.etaxes_unit_code,
}
unmapped_units.append(unit_data)
return {
'success': True,
'units': unmapped_units
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_units():
"""Matching units by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all units with "New" status from E-Taxes
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'New'},
fields=['name', 'etaxes_unit_name', 'etaxes_unit_code'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.unit_mappings:
existing_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# Get units with empty mapping (exist in table but erp_unit is None or empty)
units_with_empty_mapping = []
for etaxes_id, erp_unit in existing_mappings.items():
if not erp_unit: # If erp_unit is None or empty string
# Find corresponding E-Taxes unit
for unit in etaxes_units:
if unit.name == etaxes_id:
units_with_empty_mapping.append(unit)
break
# Filter only those that are not in mappings
unmapped_units = []
for unit in etaxes_units:
if unit.name not in existing_mappings:
unmapped_units.append(unit)
# Combine two lists: units without mappings and units with empty mappings
units_to_process = unmapped_units + units_with_empty_mapping
# Get all UOMs from system
system_units = frappe.get_all('UOM', fields=['name', 'uom_name'])
matched_count = 0
total_processed = len(units_to_process)
# Get fresh copy of settings document
doc = frappe.get_doc("E-Taxes Settings", settings.name)
# Process all units
for etaxes_unit in units_to_process:
# Find similar unit
best_match = None
best_score = 0
# Normalize E-Taxes unit name
etaxes_name = normalize_string(etaxes_unit.etaxes_unit_name, consider_azeri)
for unit in system_units:
# Normalize system unit name
unit_name = normalize_string(unit.uom_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, unit_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = unit
if best_match:
# IMPORTANT: check if there is already a record for this unit in mappings
existing_row = None
for idx, mapping in enumerate(doc.unit_mappings):
if mapping.etaxes_unit_name == etaxes_unit.name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_unit value
doc.unit_mappings[existing_row].erp_unit = best_match.name
doc.unit_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('unit_mappings', {
'etaxes_unit_name': etaxes_unit.name,
'erp_unit': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Unit status only for units that have a match
unit_doc = frappe.get_doc('E-Taxes Unit', etaxes_unit.name)
unit_doc.status = 'Mapped'
unit_doc.mapped_unit = best_match.name
unit_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} units'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_units(settings_name):
"""Creating units for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
import re
# Get settings
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Get elements from table that have no mapping
unmapped_units = []
for mapping_unit in settings_doc.unit_mappings:
# Check that unit has no mapping or it's empty
if not mapping_unit.erp_unit:
# Get E-Taxes Unit document for this record
if frappe.db.exists('E-Taxes Unit', mapping_unit.etaxes_unit_name):
etaxes_unit = frappe.get_doc('E-Taxes Unit', mapping_unit.etaxes_unit_name)
unmapped_units.append(etaxes_unit)
if len(unmapped_units) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped units in table to create"
}
created_count = 0
error_count = 0
for etaxes_unit in unmapped_units:
try:
# Check if a UOM with this name already exists
uom_exists = frappe.db.exists('UOM', {'uom_name': etaxes_unit.etaxes_unit_name})
if uom_exists:
continue
# Create new UOM
uom_doc = frappe.new_doc("UOM")
# Standard fields for UOM
uom_doc.uom_name = etaxes_unit.etaxes_unit_name
uom_doc.enabled = 1
# Set a default conversion factor if needed
uom_doc.must_be_whole_number = 0
# Save UOM
uom_doc.insert()
# Update mapping in settings
for mapping in settings_doc.unit_mappings:
if mapping.etaxes_unit_name == etaxes_unit.name:
mapping.erp_unit = uom_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Unit status
etaxes_unit.status = 'Mapped'
etaxes_unit.mapped_unit = uom_doc.name
etaxes_unit.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} units, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def load_units_from_invoices(date_from, date_to, max_count=200, offset=0):
"""Loading units from invoices for a period"""
# Записываем активность
record_etaxes_activity()
try:
# Counters for tracking
created_count = 0
skipped_count = 0
failed_count = 0
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request - изменено: только approved и approvedBySystem статусы
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"]
}
response = get_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response:
return {
'success': False,
'message': response['message'] if 'message' in response else 'Failed to retrieve invoices'
}
# Extract data from response
items_data = response.get('data', []) or response.get('invoices', [])
# Check if more data is available
hasMore = False
if 'hasMore' in response:
hasMore = response.get('hasMore')
elif response.get('total', 0) > offset + max_count:
hasMore = True
# Get details for each invoice and collect unique units
processed_count = 0
unique_units = {}
for invoice in items_data:
invoice_id = invoice.get('id', '')
if invoice_id:
# Get detailed information about the invoice
invoice_details = get_invoice_details(asan_login_settings['main_token'], invoice_id)
# Check for errors
if isinstance(invoice_details, dict) and 'error' in invoice_details:
continue
serial_number = invoice_details.get('serialNumber', '')
items = invoice_details.get('items', [])
for item in items:
unit_name = item.get('unit', '')
# Skip empty units
if not unit_name:
continue
# Create key for uniqueness check
if unit_name not in unique_units:
# Generate a consistent code for the unit
unit_code = "UNIT-" + ''.join(e for e in unit_name if e.isalnum()).upper()
unique_units[unit_name] = {
'etaxes_unit_name': unit_name,
'etaxes_unit_code': unit_code,
'source_invoice': serial_number
}
processed_count += 1
# Now create E-Taxes Unit records for each unique unit
for unit_name, unit_data in unique_units.items():
# Check if such a unit already exists
if frappe.db.exists('E-Taxes Unit', {'etaxes_unit_name': unit_name}):
skipped_count += 1
continue
# Create new unit
try:
doc = frappe.get_doc({
'doctype': 'E-Taxes Unit',
'etaxes_unit_name': unit_data['etaxes_unit_name'],
'etaxes_unit_code': unit_data['etaxes_unit_code'],
'source_invoice': unit_data['source_invoice'],
'status': 'New'
})
doc.insert(ignore_permissions=True)
created_count += 1
except Exception as e:
failed_count += 1
return {
'success': True,
'created_count': created_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'total_invoices': processed_count,
'unique_units': len(unique_units),
'hasMore': hasMore
}
except Exception as e:
frappe.log_error(f"Error in load_units_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Units Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
# Обработчик для обновления статусов сопоставленных элементов
def update_mapped_statuses(doc, method=None):
"""Update mapped statuses in E-Taxes DocTypes when settings are saved"""
try:
# 1. Handle E-Taxes Unit mappings
if hasattr(doc, 'unit_mappings'):
# Create a dictionary of current mappings
current_unit_mappings = {}
for mapping in doc.unit_mappings:
if mapping.etaxes_unit_name:
current_unit_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# Update statuses for all mapped units
for etaxes_unit_name, erp_unit in current_unit_mappings.items():
if frappe.db.exists('E-Taxes Unit', etaxes_unit_name):
if erp_unit: # If erp_unit has a value, set as Mapped
frappe.db.set_value('E-Taxes Unit', etaxes_unit_name, {
'status': 'Mapped',
'mapped_unit': erp_unit
})
else: # If erp_unit is empty/None, set status back to New
frappe.db.set_value('E-Taxes Unit', etaxes_unit_name, {
'status': 'New',
'mapped_unit': None
})
# Find units that were previously mapped but are no longer in the mappings
mapped_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'Mapped'},
fields=['name', 'mapped_unit'])
for unit in mapped_units:
if unit.name not in current_unit_mappings:
# This unit was mapped before but is no longer in mappings
frappe.db.set_value('E-Taxes Unit', unit.name, {
'status': 'New',
'mapped_unit': None
})
# 2. Handle E-Taxes Item mappings
if hasattr(doc, 'item_mappings'):
# Create a dictionary of current mappings
current_item_mappings = {}
for mapping in doc.item_mappings:
if mapping.etaxes_item_name:
current_item_mappings[mapping.etaxes_item_name] = mapping.erp_item
# Update statuses for all mapped items
for etaxes_item_name, erp_item in current_item_mappings.items():
if frappe.db.exists('E-Taxes Item', etaxes_item_name):
if erp_item: # If erp_item has a value, set as Mapped
frappe.db.set_value('E-Taxes Item', etaxes_item_name, {
'status': 'Mapped',
'mapped_item': erp_item
})
else: # If erp_item is empty/None, set status back to New
frappe.db.set_value('E-Taxes Item', etaxes_item_name, {
'status': 'New',
'mapped_item': None
})
# Find items that were previously mapped but are no longer in the mappings
mapped_items = frappe.get_all('E-Taxes Item',
filters={'status': 'Mapped'},
fields=['name', 'mapped_item'])
for item in mapped_items:
if item.name not in current_item_mappings:
# This item was mapped before but is no longer in mappings
frappe.db.set_value('E-Taxes Item', item.name, {
'status': 'New',
'mapped_item': None
})
# 3. Handle E-Taxes Parties mappings
if hasattr(doc, 'party_mappings'):
# Create lookup for current party mappings
current_party_mappings = {}
for mapping in doc.party_mappings:
if mapping.etaxes_party_name:
current_party_mappings[mapping.etaxes_party_name] = mapping.erp_party
# Get all parties with their actual docname
all_parties = frappe.get_all('E-Taxes Parties',
fields=['name', 'etaxes_party_name', 'status', 'mapped_party'])
# Create a lookup from etaxes_party_name to actual docname
party_name_to_docname = {}
for party in all_parties:
party_name_to_docname[party.etaxes_party_name] = party.name
# Update statuses for all parties in current mappings
for etaxes_party_name, erp_party in current_party_mappings.items():
if etaxes_party_name in party_name_to_docname:
docname = party_name_to_docname[etaxes_party_name]
if erp_party: # If mapping exists
frappe.db.set_value('E-Taxes Parties', docname, {
'status': 'Mapped',
'mapped_party': erp_party
})
else: # If mapping was cleared
frappe.db.set_value('E-Taxes Parties', docname, {
'status': 'New',
'mapped_party': None
})
# Reset status for parties that were mapped but are no longer in mappings
for party in all_parties:
if party.status == 'Mapped' and party.etaxes_party_name not in current_party_mappings:
frappe.db.set_value('E-Taxes Parties', party.name, {
'status': 'New',
'mapped_party': None
})
# Commit changes
frappe.db.commit()
except Exception as e:
frappe.log_error(f"Error updating mapped statuses: {str(e)}\n{frappe.get_traceback()}",
"E-Taxes Settings Error")

View File

@ -0,0 +1,749 @@
frappe.listview_settings['E-Taxes Unit'] = {
add_fields: ['status', 'mapped_unit'],
get_indicator: function(doc) {
if (doc.status === 'Mapped') {
return [__('Mapped'), 'green', 'status,=,Mapped'];
} else if (doc.status === 'Processing') {
return [__('Processing'), 'orange', 'status,=,Processing'];
} else {
return [__('New'), 'red', 'status,=,New'];
}
},
onload: function(listview) {
// Add button to load units from E-Taxes
listview.page.add_menu_item(__('Load from E-Taxes'), function() {
// Сначала проверяем актуальность токена без анимации загрузки
check_and_process_token(function() {
show_unit_filter_dialog();
});
});
}
};
// Глобальная переменная для хранения диалога загрузки
var loading_dialog = null;
// Функция для отображения диалога загрузки с анимацией
function show_loading_dialog(title, message, submessage) {
// Если диалог уже открыт, обновляем только сообщение
if (loading_dialog) {
$('#loading_title').text(title);
$('#loading_message').text(message);
if (submessage) {
$('#loading_submessage').text(submessage).show();
} else {
$('#loading_submessage').hide();
}
return;
}
// Создаем диалог с анимацией загрузки
loading_dialog = new frappe.ui.Dialog({
title: title,
fields: [
{
fieldname: 'loading_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="lds-dual-ring" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
</div>
<style>
@keyframes lds-dual-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3">
<h4 id="loading_title">${title}</h4>
<p id="loading_message">${message}</p>
<p id="loading_submessage" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
<div id="verification_code_container" style="display:none; margin-top: 15px;">
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
<span id="verification_code">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
</div>
</div>
`
}
]
});
loading_dialog.show();
loading_dialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
}
// Функция для отображения кода верификации
function show_verification_code(code) {
if (loading_dialog && code) {
$('#verification_code').text(code);
$('#verification_code_container').show();
}
}
// Функция для скрытия диалога загрузки
function hide_loading_dialog() {
if (loading_dialog) {
loading_dialog.hide();
loading_dialog = null;
}
}
// Функция для обновления сообщения в диалоге загрузки
function update_loading_message(message, submessage) {
if (loading_dialog) {
$('#loading_message').text(message);
if (submessage !== undefined) {
$('#loading_submessage').text(submessage);
$('#loading_submessage').toggle(!!submessage);
}
}
}
// Функция для изменения статуса загрузки на успешный с автоматическим закрытием
function set_loading_success(message, submessage, callback, delay = 1500) {
if (loading_dialog) {
// Меняем анимацию на галочку
loading_dialog.$wrapper.find('.lds-dual-ring').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
// Обновляем сообщения
$('#loading_message').text(message);
if (submessage !== undefined) {
$('#loading_submessage').text(submessage);
$('#loading_submessage').toggle(!!submessage);
}
// Скрываем код верификации, если он был отображен
$('#verification_code_container').hide();
// Удаляем все существующие кнопки действий
loading_dialog.set_primary_action(null);
loading_dialog.set_secondary_action(null);
// Автоматически закрываем через указанную задержку
setTimeout(function() {
if (loading_dialog) {
loading_dialog.hide();
loading_dialog = null;
}
if (callback) callback();
}, delay);
} else if (callback) {
// Если диалог не открыт, но есть callback, вызываем его
callback();
}
}
// Функция для изменения статуса загрузки на ошибку
function set_loading_error(message, submessage, show_close_button = true) {
if (loading_dialog) {
// Меняем анимацию на крестик
loading_dialog.$wrapper.find('.lds-dual-ring').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
</div>
`);
// Обновляем сообщения
$('#loading_message').text(message);
if (submessage !== undefined) {
$('#loading_submessage').text(submessage);
$('#loading_submessage').toggle(!!submessage);
}
// Скрываем код верификации, если он был отображен
$('#verification_code_container').hide();
// При необходимости добавляем кнопку закрытия
if (show_close_button) {
loading_dialog.set_primary_action(__('Close'), function() {
hide_loading_dialog();
});
}
}
}
// Функция для проверки токена и обработки аутентификации при необходимости
function check_and_process_token(callback) {
// Проверка токена без отображения загрузки
frappe.call({
method: 'invoice_az.api.check_token_validity',
callback: function(r) {
if (r.message && r.message.valid) {
// Токен действителен, продолжаем с callback-функцией сразу
callback();
} else {
// Токен недействителен, спрашиваем пользователя о ре-аутентификации
frappe.confirm(
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
function() {
// Если пользователь согласен, запускаем процесс аутентификации
start_authentication_process(callback);
},
function() {
// Если пользователь отказался
frappe.show_alert({
message: __('Authentication canceled. Operation cannot be completed.'),
indicator: 'red'
}, 5);
}
);
}
},
error: function() {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Failed to check authentication status')
});
}
});
}
// Функция для запуска полного процесса аутентификации
function start_authentication_process(callback) {
show_loading_dialog(
__('Starting Authentication'),
__('Getting Asan Login settings...'),
__('Please wait')
);
frappe.call({
method: 'invoice_az.api.get_default_asan_login',
callback: function(r) {
if (r.message && r.message.found) {
var asan_login_name = r.message.name;
update_loading_message(
__('Sending authentication request...'),
__('This will send a request to your Asan Imza mobile app')
);
// Запускаем процесс аутентификации
frappe.call({
method: 'invoice_az.api.handle_authentication',
args: {
'asan_login_name': asan_login_name
},
callback: function(r) {
if (r.message && r.message.success) {
const bearer_token = r.message.bearer_token;
// Отображаем код верификации, если он есть в ответе
if (r.message.verification_code) {
show_verification_code(r.message.verification_code);
}
// Изменяем сообщение для ожидания подтверждения
update_loading_message(
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request')
);
// Опрашиваем статус аутентификации
poll_authentication_status(asan_login_name, bearer_token, function(success) {
if (success) {
// Если аутентификация успешна, продолжаем процесс
complete_authentication(asan_login_name, callback);
} else {
// Если аутентификация не удалась, показываем сообщение об ошибке
set_loading_error(
__('Authentication Failed'),
__('Could not authenticate with Asan Imza')
);
}
});
} else {
// Если запрос не удался, показываем сообщение об ошибке
set_loading_error(
__('Authentication Error'),
r.message ? r.message.message : __('Authentication request failed')
);
}
}
});
} else {
// Если настройки не найдены, показываем сообщение об ошибке
set_loading_error(
__('No Asan Login Settings'),
__('Please configure Asan Login settings first')
);
}
}
});
}
// Функция для опроса статуса аутентификации
function poll_authentication_status(asan_login_name, bearer_token, callback) {
// Счетчик попыток и флаг для остановки опроса
let attempts = 0;
const maxAttempts = 20; // 20 попыток с интервалом 6 секунд
let stopPolling = false;
// Функция опроса
function pollStatus() {
// Если достигнуто максимальное количество попыток или установлен флаг остановки
if (attempts >= maxAttempts || stopPolling) {
if (attempts >= maxAttempts) {
// Показываем сообщение о таймауте
set_loading_error(
__('Authentication Timeout'),
__('The authentication request has timed out. Please try again.')
);
callback(false);
}
return;
}
attempts++;
// Проверяем статус авторизации
frappe.call({
method: 'invoice_az.api.poll_auth_status',
args: {
'asan_login_name': asan_login_name,
'bearer_token': bearer_token
},
callback: function(r) {
if (r.message && r.message.success) {
if (r.message.authenticated) {
// Авторизация успешна
stopPolling = true;
set_loading_success(
__('Authentication Successful'),
__('You are now authenticated with Asan Imza'),
function() {
callback(true);
}
);
} else {
// Обновляем счетчик попыток в сообщении
update_loading_message(
__('Waiting for confirmation on your phone'),
__('Please check your phone and confirm the authentication request') +
' (' + attempts + '/' + maxAttempts + ')'
);
// Продолжаем опрос
setTimeout(pollStatus, 6000);
}
} else {
// Если ошибка, останавливаем опрос и показываем сообщение
stopPolling = true;
set_loading_error(
__('Authentication Error'),
r.message ? r.message.message : __('An unknown error occurred')
);
callback(false);
}
},
error: function(err) {
// В случае ошибки продолжаем опрос
console.error('Error during status check:', err);
setTimeout(pollStatus, 6000);
}
});
}
// Начинаем опрос
pollStatus();
}
// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
function complete_authentication(asan_login_name, callback) {
show_loading_dialog(
__('Completing Authentication'),
__('Getting certificates...'),
__('Please wait')
);
// Получаем сертификаты
frappe.call({
method: 'invoice_az.api.get_auth_certificates',
args: {
'asan_login_name': asan_login_name
},
callback: function(cert_r) {
if (cert_r.message && cert_r.message.success) {
update_loading_message(
__('Certificates retrieved'),
__('Selecting certificate and taxpayer')
);
// Получаем текущий документ для проверки выбранного сертификата
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Asan Login',
name: asan_login_name
},
callback: function(r) {
if (r.message && r.message.selected_certificate_json) {
try {
const cert_data = JSON.parse(r.message.selected_certificate_json);
const cert_name = r.message.selected_certificate;
update_loading_message(
__('Selecting certificate'),
cert_name
);
// Выбираем сертификат
frappe.call({
method: 'invoice_az.api.select_certificate',
args: {
'asan_login_name': asan_login_name,
'certificate_data': cert_data,
'certificate_name': cert_name
},
callback: function(select_r) {
if (select_r.message && select_r.message.success) {
update_loading_message(
__('Selecting taxpayer'),
__('Using certificate: ') + cert_name
);
// Выбираем налогоплательщика
frappe.call({
method: 'invoice_az.api.select_taxpayer',
args: {
'asan_login_name': asan_login_name
},
callback: function(tax_r) {
if (tax_r.message && tax_r.message.success) {
set_loading_success(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback,
2000 // Автоматически закроется через 2 секунды
);
} else {
hide_loading_dialog();
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} else {
hide_loading_dialog();
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} catch (e) {
console.error('Error parsing certificate:', e);
hide_loading_dialog();
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
}
} else {
hide_loading_dialog();
show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
}
}
});
} else {
set_loading_error(
__('Error'),
cert_r.message ? cert_r.message.message : __('Failed to get certificates')
);
}
}
});
}
// Функция для отображения селектора сертификатов
function show_certificate_selector(certificates, asan_login_name, callback) {
if (!certificates || !certificates.length) {
frappe.msgprint(__('No certificates available'));
return;
}
// Создаем таблицу сертификатов
var cert_html = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
cert_html += '<thead><tr><th>Type</th><th>Name</th><th>ID</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>';
certificates.forEach(function(cert, index) {
var name = '';
var id = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
name = cert.individualInfo.name || '';
id = cert.individualInfo.fin || '';
} else if (cert.legalInfo) {
name = cert.legalInfo.name || '';
id = cert.legalInfo.tin || cert.legalInfo.voen || '';
}
cert_html += '<tr>' +
'<td>' + (cert.taxpayerType || '') + '</td>' +
'<td>' + name + '</td>' +
'<td>' + id + '</td>' +
'<td>' + (cert.position || '') + '</td>' +
'<td>' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + '</td>' +
'<td><button class="btn btn-xs btn-primary select-cert" data-index="' + index + '">Select</button></td>' +
'</tr>';
});
cert_html += '</tbody></table></div>';
var dialog = new frappe.ui.Dialog({
title: __('Select Certificate'),
fields: [{
fieldtype: 'HTML',
fieldname: 'certificates',
options: cert_html
}]
});
dialog.show();
// Обработчик нажатия на кнопку выбора сертификата
dialog.$wrapper.find('.select-cert').on('click', function() {
var index = $(this).data('index');
var cert = certificates[index];
// Формируем имя для отображения
let certName = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
} else if (cert.legalInfo) {
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
} else {
certName = `Certificate ${index + 1}`;
}
dialog.hide();
// Показываем прогресс
show_loading_dialog(
__('Selecting Certificate'),
__('Processing your selection...'),
certName
);
// Отправляем запрос на выбор сертификата
frappe.call({
method: 'invoice_az.api.select_certificate',
args: {
'asan_login_name': asan_login_name,
'certificate_data': cert,
'certificate_name': certName
},
callback: function(r) {
if (r.message && r.message.success) {
update_loading_message(
__('Certificate selected'),
__('Now selecting taxpayer information')
);
// Выбираем налогоплательщика
frappe.call({
method: 'invoice_az.api.select_taxpayer',
args: {
'asan_login_name': asan_login_name
},
callback: function(r) {
if (r.message && r.message.success) {
set_loading_success(
__('Authentication Complete'),
__('You can now access E-Taxes services'),
callback,
2000 // Автоматически закроется через 2 секунды
);
} else {
set_loading_error(
__('Error'),
r.message ? r.message.message : __('Failed to select taxpayer')
);
}
}
});
} else {
set_loading_error(
__('Error'),
r.message ? r.message.message : __('Failed to select certificate')
);
}
}
});
});
}
function show_unit_filter_dialog() {
// Get previous year for start date and current date for end date
const minDate = moment("2020-01-01", "YYYY-MM-DD"); // Minimum date - January 1st, 2020
const prevYear = moment().subtract(1, 'year').year();
const startDate = prevYear + "-01-01"; // January 1st of previous year
const endDate = moment().format('YYYY-MM-DD'); // Today
// Create simple dialog for selecting period
var d = new frappe.ui.Dialog({
title: __('Invoice Filters for Units'),
fields: [
{
fieldname: 'date_range_section',
fieldtype: 'Section Break',
label: __('Date Range')
},
{
fieldname: 'creationDateFrom',
fieldtype: 'Date',
label: __('From Date'),
default: startDate
},
{
fieldname: 'creationDateTo',
fieldtype: 'Date',
label: __('To Date'),
default: endDate
}
// Убираем фильтр по максимальному количеству инвойсов, т.к. загружаем все через пагинацию
],
primary_action_label: __('Load Units'),
primary_action: function() {
var values = d.get_values();
// Validate date range
const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD");
const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD");
const todayMoment = moment();
// Check if the dates are within the allowed range
if (fromDateMoment.isBefore(minDate)) {
frappe.msgprint(__('From Date cannot be earlier than January 1, 2020'));
return;
}
if (toDateMoment.isAfter(todayMoment)) {
frappe.msgprint(__('To Date cannot be later than today'));
return;
}
// Close dialog
d.hide();
// Format dates correctly - convert from YYYY-MM-DD to DD-MM-YYYY HH:MM
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00');
let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59');
// Показываем анимацию загрузки
show_loading_dialog(
__('Loading Units from E-Taxes'),
__('Retrieving units from invoices for the selected period...'),
`${fromDate} - ${toDate}`
);
// Начинаем загрузку с пагинацией
load_units_with_pagination(fromDate, toDate, 0, {
created_count: 0,
skipped_count: 0,
failed_count: 0,
total_invoices: 0
});
}
});
d.show();
}
// Функция для загрузки единиц измерения с поддержкой пагинации
function load_units_with_pagination(fromDate, toDate, offset = 0, accumulated_data = null) {
// Используем фиксированный размер партии
const maxCount = 200;
// Обновляем сообщение о загрузке с информацией о текущей партии
if (offset > 0) {
update_loading_message(
__('Loading units from E-Taxes (batch {0})', [(offset / maxCount) + 1]),
__('Processing invoices starting from {0}...', [offset])
);
}
// Загружаем единицы измерения из E-Taxes
frappe.call({
method: 'invoice_az.api.load_units_from_invoices',
args: {
'date_from': fromDate,
'date_to': toDate,
'max_count': maxCount, // Всегда используем фиксированный размер
'offset': offset // Указываем текущее смещение
},
callback: function(r) {
if (r.message && r.message.success) {
// Обновляем накопленные данные
accumulated_data.created_count += r.message.created_count || 0;
accumulated_data.skipped_count += r.message.skipped_count || 0;
accumulated_data.failed_count += r.message.failed_count || 0;
accumulated_data.total_invoices += r.message.total_invoices || 0;
// Проверяем, есть ли еще данные для загрузки
let hasMore = r.message.hasMore || false;
if (hasMore) {
// Если есть еще данные, увеличиваем смещение и загружаем следующую партию
let newOffset = offset + maxCount;
// Показываем промежуточный результат
update_loading_message(
__('Loaded {0} units so far...', [accumulated_data.created_count]),
__('Loading more data...')
);
// Рекурсивно вызываем функцию для следующей партии
load_units_with_pagination(fromDate, toDate, newOffset, accumulated_data);
} else {
// Если больше нет данных, показываем итоговый результат
set_loading_success(
__('Units Loaded Successfully'),
__('Created {0} unique units. Skipped {1} duplicates.',
[accumulated_data.created_count, accumulated_data.skipped_count]),
function() {
// Refresh list
cur_list.refresh();
},
2000 // Автоматически закроется через 2 секунды
);
}
} else if (r.message && r.message.error === 'unauthorized') {
// Если ошибка авторизации, предлагаем авторизоваться заново
set_loading_error(
__('Authentication Required'),
__('Your session has expired. Please authenticate again.'),
false // Не показываем кнопку закрытия
);
// Добавляем кнопку "Authenticate"
loading_dialog.set_primary_action(__('Authenticate'), function() {
hide_loading_dialog();
// Запускаем процесс аутентификации и после него повторяем загрузку
check_and_process_token(function() {
show_unit_filter_dialog();
});
});
// Добавляем кнопку "Cancel"
loading_dialog.set_secondary_action(__('Cancel'), function() {
hide_loading_dialog();
});
} else {
set_loading_error(
__('Error'),
r.message ? r.message.message : __('An error occurred while loading units')
);
}
},
error: function(xhr, status, error) {
set_loading_error(
__('Network Error'),
__('Failed to connect to server: ') + (error || 'Unknown error')
);
}
});
}

View File

@ -0,0 +1,45 @@
frappe.ui.form.on('Purchase Invoice', {
refresh: function(frm) {
// Проверяем, установлена ли галочка is_taxes_doc
if (frm.doc.is_taxes_doc) {
// Делаем все поля read-only
frm.set_read_only(true);
// Альтернативно, можно добавить визуальное предупреждение
frm.page.set_indicator(__('Tax Document'), 'blue');
}
},
// Делаем поле is_taxes_doc тоже read-only при открытии документа
onload: function(frm) {
frm.set_df_property('is_taxes_doc', 'read_only', 1);
}
});
frappe.listview_settings['Purchase Invoice'] = {
onload: function(listview) {
// Добавляем поле как колонку (правильный способ)
listview.columns.push({
type: 'Check',
df: {
label: __('Tax Doc'),
fieldname: 'is_taxes_doc'
},
width: 120
});
// Принудительно обновляем список с новой колонкой
listview.refresh();
},
// Форматируем отображение поля is_taxes_doc
formatters: {
is_taxes_doc: function(value) {
return value ?
`<span class="indicator-pill green"></span>` :
`<span class="indicator-pill gray"></span>`;
}
}
};

View File

@ -1268,7 +1268,7 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co
});
} else {
frappe.show_alert({
message: __('Import error: ') + serialNumber,
message: __('Import error: ') + serialNumber + ' ' + import_r.message.message,
indicator: 'red'
}, 3);

View File

@ -6,13 +6,16 @@ app_email = "info@jeyerp.az"
app_license = "unlicense"
doctype_js = {
"Purchase Order": "client/purchase_order.js"
"Purchase Order": "client/purchase_order.js",
"Purchase Invoice": "client/purchase_invoice.js"
}
doctype_list_js = {
"E-Taxes Item": "client/e_taxes_items_list.js",
"E-Taxes Parties": "client/e_taxes_parties_list.js",
"Purchase Order": "client/purchase_order.js"
"E-Taxes Unit": "client/e_taxes_unit_list.js",
"Purchase Order": "client/purchase_order.js",
"Purchase Invoice": "client/purchase_invoice.js"
}
# Хуки для добавления обработчиков событий Purchase Order
@ -20,6 +23,9 @@ doc_events = {
"Purchase Order": {
"on_trash": "invoice_az.api.on_delete_purchase_order",
"on_cancel": "invoice_az.api.on_delete_purchase_order"
},
"E-Taxes Settings": {
"on_update": "invoice_az.api.update_mapped_statuses"
}
}

View File

@ -2,7 +2,34 @@ frappe.form.link_formatters['E-Taxes Item'] = function(value, doc) {
return doc.etaxes_item_name || value;
};
frappe.form.link_formatters['E-Taxes Unit'] = function(value, doc) {
return doc.etaxes_unit_name || value;
};
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);
},
refresh: function(frm) {
@ -15,6 +42,11 @@ frappe.ui.form.on('E-Taxes Settings', {
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');
});
// Кнопка для сопоставления товаров по похожему названию
frm.add_custom_button(__('Match items by similar name'), function() {
@ -398,6 +430,187 @@ frappe.ui.form.on('E-Taxes Settings', {
});
}
}, __('Partners'));
// Кнопки для единиц измерения
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'
}, 5);
}
);
} else {
// Если документ не изменен, просто выполняем действие
match_similar_units();
}
function match_similar_units() {
frappe.confirm(
__('This operation will automatically match unmapped units with similar names. Continue?'),
function() {
frappe.show_alert({
message: __('Matching units...'),
indicator: 'blue'
});
frappe.call({
method: 'invoice_az.api.match_similar_units',
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Matched {0} of {1} units',
[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 units')
});
}
}
});
}
);
}
}, __('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'
}, 5);
}
);
} else {
// Если документ не изменен, просто выполняем действие
create_unmapped_units();
}
function create_unmapped_units() {
frappe.confirm(
__('<div style="color: red; font-weight: bold;">WARNING! This action will create new UOM units in the system for all unmapped elements!</div><p>This action is irreversible. Are you sure?</p>'),
function() {
// Дополнительное подтверждение
frappe.confirm(
__('Are you really sure you want to create new UOM units? This cannot be undone.'),
function() {
frappe.call({
method: 'invoice_az.api.create_unmapped_units',
args: {
'settings_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Created {0} new UOM units', [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 UOM units')
});
}
}
});
}
);
}
);
}
}, __('Units'));
// Кнопка для добавления несопоставленных единиц измерения в таблицу
frm.add_custom_button(__('Add unmapped units'), function() {
if(frm.is_dirty()) {
frappe.confirm(
__('Document contains unsaved changes. Save before performing the operation?'),
function() {
// Если "Да", сохраняем и выполняем действие
frm.save().then(function() {
add_unmapped_units();
});
},
function() {
// Если "Нет", не делаем ничего
frappe.show_alert({
message: __('Operation canceled. Please save the document first.'),
indicator: 'red'
}, 5);
}
);
} else {
// Если документ не изменен, просто выполняем действие
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');
// Используем ID документа (name) вместо отображаемого имени
row.etaxes_unit_name = unit.name; // Должен быть ID документа
// Тип сопоставления
row.mapping_type = 'Manual';
});
frm.refresh_field('unit_mappings');
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')
});
}
}
});
}
}, __('Units'));
}
});

View File

@ -25,7 +25,9 @@
"item_mappings_section",
"item_mappings",
"party_mappings_section",
"party_mappings"
"party_mappings",
"unit_mappings_section",
"unit_mappings"
],
"fields": [
{
@ -147,11 +149,22 @@
"fieldtype": "Table",
"label": "Business Partner Mappings",
"options": "E-Taxes Party Mapping"
},
{
"fieldname": "unit_mappings_section",
"fieldtype": "Section Break",
"label": "Unit of Measure Mappings"
},
{
"fieldname": "unit_mappings",
"fieldtype": "Table",
"label": "Unit of Measure Mappings",
"options": "E-Taxes Unit Mapping"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-06 12:00:00.000000",
"modified": "2025-05-21 15:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Settings",

View File

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

View File

@ -0,0 +1,95 @@
{
"actions": [],
"autoname": "field:etaxes_unit_code",
"creation": "2025-05-21 12:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_unit_name",
"etaxes_unit_code",
"source_invoice",
"status",
"mapped_unit"
],
"fields": [
{
"fieldname": "etaxes_unit_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Unit Name",
"read_only": 1,
"reqd": 1,
"unique": 1
},
{
"fieldname": "etaxes_unit_code",
"fieldtype": "Data",
"label": "Unit Code",
"read_only": 1,
"unique": 1
},
{
"description": "Source invoice number",
"fieldname": "source_invoice",
"fieldtype": "Data",
"label": "Source",
"read_only": 1
},
{
"default": "New",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "New\nMapped\nProcessing",
"read_only": 1
},
{
"fieldname": "mapped_unit",
"fieldtype": "Link",
"label": "Mapped UOM",
"options": "UOM",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-21 14:25:13.578790",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Unit",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"search_fields": "etaxes_unit_name",
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "etaxes_unit_name",
"track_changes": 1
}

View File

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

View File

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

View File

@ -0,0 +1,51 @@
{
"actions": [],
"creation": "2025-05-21 12:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_unit_name",
"erp_unit",
"mapping_type"
],
"fields": [
{
"fieldname": "etaxes_unit_name",
"fieldtype": "Link",
"in_list_view": 1,
"in_preview": 1,
"label": "E-Taxes Unit Name",
"options": "E-Taxes Unit",
"reqd": 1
},
{
"fieldname": "erp_unit",
"fieldtype": "Link",
"in_list_view": 1,
"in_preview": 1,
"label": "System UOM",
"options": "UOM"
},
{
"default": "Manual",
"fieldname": "mapping_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_preview": 1,
"label": "Mapping Type",
"options": "Manual\nAutomatic"
}
],
"istable": 1,
"links": [],
"modified": "2025-05-21 12:44:49.793210",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Unit Mapping",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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