Added purchase invoice, and uom mapping, and a lot more
This commit is contained in:
parent
b24e35071d
commit
591d54ea7b
|
|
@ -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,22 +2233,29 @@ 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
|
||||
|
||||
if not default_warehouse:
|
||||
# If warehouse not specified, try to get from settings
|
||||
# If warehouse not specified, try to get from settings
|
||||
if hasattr(settings, 'default_warehouse') and settings.default_warehouse:
|
||||
default_warehouse = settings.default_warehouse
|
||||
else:
|
||||
|
|
@ -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,11 +2517,62 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
|
|||
# Save again to ensure changes are applied
|
||||
po.save()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Invoice data imported successfully.',
|
||||
'purchase_order': po.name
|
||||
}
|
||||
# ИЗМЕНЕНИЕ: Создаем 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. 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
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"Error in import_invoice_with_mapping: {str(e)}\n{frappe.get_traceback()}", "Import Error")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue