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"
|
url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox"
|
||||||
|
|
||||||
# Base request parameters
|
# Base request parameters - изменено: только approved и approvedBySystem статусы
|
||||||
payload = {
|
payload = {
|
||||||
"sortBy": "creationDate",
|
"sortBy": "creationDate",
|
||||||
"sortAsc": True,
|
"sortAsc": True,
|
||||||
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested",
|
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
|
||||||
"cancelRequested", "approvedBySystem", "onApprovalEdited",
|
|
||||||
"deactivated", "cancelationRefused", "correctionRefused"],
|
|
||||||
"types": ["current", "corrected"],
|
"types": ["current", "corrected"],
|
||||||
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
|
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
|
||||||
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
|
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
|
||||||
|
|
@ -1170,20 +1168,49 @@ def find_or_create_supplier(supplier_name, supplier_tin):
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_uom_for_unit(unit_name):
|
def get_uom_for_unit(unit_name):
|
||||||
"""Converts unit name from invoice to system UOM"""
|
"""Converts unit name from invoice to system UOM with support for mappings"""
|
||||||
# Conversion dictionary
|
if not unit_name:
|
||||||
uom_mapping = {
|
return "Nos" # Default UOM if none provided
|
||||||
"шт": "Nos",
|
|
||||||
"кг": "Kg",
|
|
||||||
"г": "Gram",
|
|
||||||
"л": "Litre",
|
|
||||||
"м": "Meter",
|
|
||||||
"м2": "Square Meter",
|
|
||||||
"м3": "Cubic Meter"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Return corresponding UOM or "Nos" by default
|
# Try to find in settings first
|
||||||
return uom_mapping.get(unit_name.lower(), "Nos")
|
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()
|
@frappe.whitelist()
|
||||||
def load_items_from_invoices(date_from, date_to, max_count=200):
|
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):
|
except (ValueError, TypeError):
|
||||||
max_count = 200
|
max_count = 200
|
||||||
|
|
||||||
# Format filters for the API request
|
# Format filters for the API request - изменено: только approved и approvedBySystem статусы
|
||||||
filters = {
|
filters = {
|
||||||
"actionOwner": None,
|
"actionOwner": None,
|
||||||
"amountFrom": None,
|
"amountFrom": None,
|
||||||
|
|
@ -1231,7 +1258,7 @@ def load_items_from_invoices(date_from, date_to, max_count=200):
|
||||||
"serialNumber": None,
|
"serialNumber": None,
|
||||||
"sortAsc": True,
|
"sortAsc": True,
|
||||||
"sortBy": "creationDate",
|
"sortBy": "creationDate",
|
||||||
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested", "cancelRequested", "approvedBySystem", "onApprovalEdited", "deactivated", "cancelationRefused", "correctionRefused"],
|
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
|
||||||
"types": ["current", "corrected"]
|
"types": ["current", "corrected"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1334,7 +1361,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200, invoice_type="
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
max_count = 200
|
max_count = 200
|
||||||
|
|
||||||
# Format filters for the API request
|
# Format filters for the API request - изменено: только approved и approvedBySystem статусы
|
||||||
filters = {
|
filters = {
|
||||||
"actionOwner": None,
|
"actionOwner": None,
|
||||||
"amountFrom": None,
|
"amountFrom": None,
|
||||||
|
|
@ -1353,7 +1380,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200, invoice_type="
|
||||||
"serialNumber": None,
|
"serialNumber": None,
|
||||||
"sortAsc": True,
|
"sortAsc": True,
|
||||||
"sortBy": "creationDate",
|
"sortBy": "creationDate",
|
||||||
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested", "cancelRequested", "approvedBySystem", "onApprovalEdited", "deactivated", "cancelationRefused", "correctionRefused"],
|
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
|
||||||
"types": ["current", "corrected"]
|
"types": ["current", "corrected"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1771,28 +1798,6 @@ def match_similar_items():
|
||||||
# Get fresh copy of settings document
|
# Get fresh copy of settings document
|
||||||
doc = frappe.get_doc("E-Taxes Settings", settings.name)
|
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
|
# Process all items
|
||||||
for etaxes_item in items_to_process:
|
for etaxes_item in items_to_process:
|
||||||
# Log item being processed
|
# 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."
|
"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()
|
@frappe.whitelist()
|
||||||
def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None):
|
def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None):
|
||||||
"""Import invoice taking into account mappings and processing unmapped elements"""
|
"""Import invoice taking into account 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'
|
'message': 'No active E-Taxes settings found'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create mapping dictionaries
|
# Create mapping dictionaries with case-insensitive keys
|
||||||
item_mappings = {}
|
item_mappings = {}
|
||||||
for mapping in settings.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
|
item_mappings[key] = mapping.erp_item
|
||||||
|
|
||||||
party_mappings = {}
|
party_mappings = {}
|
||||||
for mapping in settings.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)
|
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
|
# Get default warehouse
|
||||||
default_warehouse = warehouse
|
default_warehouse = warehouse
|
||||||
|
|
||||||
if not default_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:
|
if hasattr(settings, 'default_warehouse') and settings.default_warehouse:
|
||||||
default_warehouse = settings.default_warehouse
|
default_warehouse = settings.default_warehouse
|
||||||
else:
|
else:
|
||||||
|
|
@ -2248,7 +2289,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule
|
||||||
|
|
||||||
# Set supplier
|
# Set supplier
|
||||||
sender = invoice_data.get('sender', {})
|
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:
|
if sender_key in party_mappings:
|
||||||
po.supplier = party_mappings[sender_key][0]
|
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
|
# Variable for tracking added items
|
||||||
added_items_count = 0
|
added_items_count = 0
|
||||||
unmatched_items = []
|
unmatched_items = []
|
||||||
|
unmatched_units = []
|
||||||
|
|
||||||
# Add items from invoice
|
# Add items from invoice
|
||||||
if invoice_data.get("items"):
|
if invoice_data.get("items"):
|
||||||
for item in 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_name = item.get("productName", "")
|
||||||
item_code = item.get("itemId", "")
|
item_code = item.get("itemId", "")
|
||||||
item_key = f"{item_name}|{item_code}"
|
item_key = f"{item_name.lower()}|{item_code.lower()}"
|
||||||
|
|
||||||
# Look for mapping
|
# Look for mapping
|
||||||
mapped_item = None
|
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:
|
if item_key in item_mappings:
|
||||||
mapped_item = item_mappings[item_key]
|
mapped_item = item_mappings[item_key]
|
||||||
else:
|
else:
|
||||||
# Try to find item by code if specified
|
# Try to find item by code if specified (case-insensitive)
|
||||||
if item_code:
|
if item_code:
|
||||||
item_list = frappe.get_all('Item', filters={'item_code': item_code}, limit=1)
|
item_list = frappe.get_all('Item',
|
||||||
if item_list:
|
filters=[['item_code', 'like', f'%{item_code}%']],
|
||||||
mapped_item = item_list[0].name
|
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:
|
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 found by code or name
|
||||||
if not mapped_item:
|
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
|
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
|
# Add position to PO
|
||||||
po_item = frappe.new_doc("Purchase Order Item")
|
po_item = frappe.new_doc("Purchase Order Item")
|
||||||
po_item.parent = po.name
|
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.qty = item.get("quantity", 0)
|
||||||
po_item.rate = item.get("pricePerUnit", 0)
|
po_item.rate = item.get("pricePerUnit", 0)
|
||||||
po_item.amount = item.get("cost", 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
|
# IMPORTANT: Set schedule_date for each row
|
||||||
po_item.schedule_date = date_to_use
|
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'
|
'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
|
# Check that there is at least one element in table
|
||||||
if added_items_count == 0:
|
if added_items_count == 0:
|
||||||
frappe.log_error(
|
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
|
# Save again to ensure changes are applied
|
||||||
po.save()
|
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 {
|
return {
|
||||||
'success': True,
|
'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
|
'purchase_order': po.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2470,37 +2631,6 @@ def normalize_string(text, consider_azeri=True):
|
||||||
|
|
||||||
return text
|
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()
|
@frappe.whitelist()
|
||||||
def create_unmapped_parties(settings_name):
|
def create_unmapped_parties(settings_name):
|
||||||
"""Creating parties for unmapped elements from E-Taxes settings table"""
|
"""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:
|
except Exception as e:
|
||||||
frappe.log_error(f"Error deleting E-Taxes Purchase {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
|
frappe.log_error(f"Error deleting E-Taxes Purchase {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
|
||||||
"Purchase Order Delete Error")
|
"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 {
|
} else {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
message: __('Import error: ') + serialNumber,
|
message: __('Import error: ') + serialNumber + ' ' + import_r.message.message,
|
||||||
indicator: 'red'
|
indicator: 'red'
|
||||||
}, 3);
|
}, 3);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,16 @@ app_email = "info@jeyerp.az"
|
||||||
app_license = "unlicense"
|
app_license = "unlicense"
|
||||||
|
|
||||||
doctype_js = {
|
doctype_js = {
|
||||||
"Purchase Order": "client/purchase_order.js"
|
"Purchase Order": "client/purchase_order.js",
|
||||||
|
"Purchase Invoice": "client/purchase_invoice.js"
|
||||||
}
|
}
|
||||||
|
|
||||||
doctype_list_js = {
|
doctype_list_js = {
|
||||||
"E-Taxes Item": "client/e_taxes_items_list.js",
|
"E-Taxes Item": "client/e_taxes_items_list.js",
|
||||||
"E-Taxes Parties": "client/e_taxes_parties_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
|
# Хуки для добавления обработчиков событий Purchase Order
|
||||||
|
|
@ -20,6 +23,9 @@ doc_events = {
|
||||||
"Purchase Order": {
|
"Purchase Order": {
|
||||||
"on_trash": "invoice_az.api.on_delete_purchase_order",
|
"on_trash": "invoice_az.api.on_delete_purchase_order",
|
||||||
"on_cancel": "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;
|
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', {
|
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) {
|
refresh: function(frm) {
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,6 +42,11 @@ frappe.ui.form.on('E-Taxes Settings', {
|
||||||
bulk_edit_table(frm, 'party_mappings');
|
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() {
|
frm.add_custom_button(__('Match items by similar name'), function() {
|
||||||
|
|
@ -398,6 +430,187 @@ frappe.ui.form.on('E-Taxes Settings', {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, __('Partners'));
|
}, __('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_section",
|
||||||
"item_mappings",
|
"item_mappings",
|
||||||
"party_mappings_section",
|
"party_mappings_section",
|
||||||
"party_mappings"
|
"party_mappings",
|
||||||
|
"unit_mappings_section",
|
||||||
|
"unit_mappings"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
|
@ -147,11 +149,22 @@
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Business Partner Mappings",
|
"label": "Business Partner Mappings",
|
||||||
"options": "E-Taxes Party Mapping"
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-06 12:00:00.000000",
|
"modified": "2025-05-21 15:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Invoice Az",
|
"module": "Invoice Az",
|
||||||
"name": "E-Taxes Settings",
|
"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