added create if not mapped

This commit is contained in:
Ali 2026-01-22 20:30:37 +04:00
parent c037dfe45c
commit f1ae840d88
2 changed files with 357 additions and 33 deletions

View File

@ -82,6 +82,7 @@ VATETaxes.utils = {
humanizeErrorType: function(errorType) {
const errorTypeMap = {
'customer_not_found': __('Customer Not Found'),
'customer_creation_failed': __('Customer Creation Failed'),
'account_not_found': __('Account Not Found'),
'mapping_not_found': __('VAT Account Mapping Not Found'),
'import_error': __('Import Error'),
@ -892,13 +893,6 @@ VATETaxes.import = {
options: 'Company',
reqd: true,
default: frappe.defaults.get_user_default('Company')
},
{
fieldname: 'create_as_draft',
fieldtype: 'Check',
label: __('Create as Draft'),
default: 0,
description: __('If checked, Journal Entries will be created in Draft status instead of being submitted automatically')
}
],
primary_action_label: __('Search'),
@ -934,7 +928,7 @@ VATETaxes.import = {
const createAsDraft = values.create_as_draft || 0;
d.hide();
VATETaxes.import.loadOperations(fromDate, toDate, values.company, [], 0, createAsDraft);
VATETaxes.import.loadOperations(fromDate, toDate, values.company, [], 0, createAsDraft, 0);
}
});
@ -943,7 +937,7 @@ VATETaxes.import = {
},
// Загрузить операции с фильтрацией дубликатов
loadOperations: function(fromDate, toDate, company, accumulatedOperations = [], offset = 0, createAsDraft = 0) {
loadOperations: function(fromDate, toDate, company, accumulatedOperations = [], offset = 0, createAsDraft = 0, autoCreateCustomers = 0) {
VATETaxes.cancelLoading = false;
$(document).off('progress-cancel.vat_import');
@ -990,7 +984,7 @@ VATETaxes.import = {
if (hasMore && currentOperations.length > 0) {
const newOffset = offset + currentOperations.length;
VATETaxes.import.loadOperations(fromDate, toDate, company, allOperations, newOffset, createAsDraft);
VATETaxes.import.loadOperations(fromDate, toDate, company, allOperations, newOffset, createAsDraft, autoCreateCustomers);
} else {
if (allOperations.length > 0) {
frappe.show_alert({
@ -998,7 +992,7 @@ VATETaxes.import = {
indicator: 'blue'
}, 3);
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft);
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft, autoCreateCustomers);
} else {
frappe.msgprint({
title: __('Information'),
@ -1012,7 +1006,7 @@ VATETaxes.import = {
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
function() {
VATETaxes.auth.startProcess(function() {
VATETaxes.import.loadOperations(fromDate, toDate, company, [], 0, createAsDraft);
VATETaxes.import.loadOperations(fromDate, toDate, company, [], 0, createAsDraft, autoCreateCustomers);
});
},
function() {
@ -1050,7 +1044,7 @@ VATETaxes.import = {
},
// Фильтрация дубликатов
filterDuplicates: function(allOperations, token, company, createAsDraft = 0) {
filterDuplicates: function(allOperations, token, company, createAsDraft = 0, autoCreateCustomers = 0) {
frappe.call({
method: 'frappe.client.get_list',
args: {
@ -1085,7 +1079,7 @@ VATETaxes.import = {
}
if (filteredOperations.length > 0) {
VATETaxes.import.showOperationSelection(filteredOperations, token, company, createAsDraft);
VATETaxes.import.showOperationSelection(filteredOperations, token, company, createAsDraft, autoCreateCustomers);
} else {
frappe.msgprint({
title: __('Information'),
@ -1105,7 +1099,7 @@ VATETaxes.import = {
},
error: function(err) {
console.error("Error fetching VAT operations:", err);
VATETaxes.import.showOperationSelection(allOperations, token, company, createAsDraft);
VATETaxes.import.showOperationSelection(allOperations, token, company, createAsDraft, autoCreateCustomers);
frappe.show_alert({
message: __('Failed to check for duplicates. Showing all operations.'),
indicator: 'orange'
@ -1170,6 +1164,30 @@ VATETaxes.import = {
fieldtype: 'HTML',
options: infoMessage
},
{
fieldname: 'settings_section',
fieldtype: 'Section Break',
label: __('Import Settings')
},
{
fieldname: 'create_as_draft',
fieldtype: 'Check',
label: __('Create as Draft'),
default: createAsDraft,
description: __('If checked, Journal Entries will be created in Draft status instead of being submitted automatically')
},
{
fieldname: 'auto_create_customers',
fieldtype: 'Check',
label: __('Auto-create missing customers'),
default: 0,
description: __('If checked, will automatically create Customer records and mappings for TINs not found in the system')
},
{
fieldname: 'operations_section',
fieldtype: 'Section Break',
label: __('Operations')
},
{
fieldname: 'operations_html',
fieldtype: 'HTML',
@ -1198,6 +1216,11 @@ VATETaxes.import = {
return;
}
// Получить значения настроек из диалога
const values = d.get_values();
const createAsDraftValue = values.create_as_draft || 0;
const autoCreateCustomers = values.auto_create_customers || 0;
// Найти полные данные операций
const selectedOperations = [];
selectedOperationIds.forEach(function(id) {
@ -1209,7 +1232,7 @@ VATETaxes.import = {
d.hide();
VATETaxes.cancelLoading = false;
VATETaxes.import.loadSelectedOperations(selectedOperations, token, company, 0, 0, createAsDraft);
VATETaxes.import.loadSelectedOperations(selectedOperations, token, company, 0, 0, createAsDraftValue, autoCreateCustomers);
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
@ -1237,7 +1260,7 @@ VATETaxes.import = {
},
// Загрузка выбранных операций
loadSelectedOperations: function(operations, token, company, processedCount = 0, currentIndex = 0, createAsDraft = 0) {
loadSelectedOperations: function(operations, token, company, processedCount = 0, currentIndex = 0, createAsDraft = 0, autoCreateCustomers = 0) {
if (processedCount === 0 && currentIndex === 0) {
VATETaxes.loadingErrors = [];
window.totalOperationsToProcess = operations.length;
@ -1271,7 +1294,7 @@ VATETaxes.import = {
if (VATETaxes.cancelLoading) {
operations = [];
frappe.hide_progress();
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft);
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers);
return;
}
@ -1299,11 +1322,12 @@ VATETaxes.import = {
args: {
'operations_data': JSON.stringify([operation]),
'company': company,
'create_as_draft': createAsDraft
'create_as_draft': createAsDraft,
'auto_create_customers': autoCreateCustomers
},
callback: function(r) {
VATETaxes.import._handleImportResult(r, operation,
operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft);
operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
},
error: function(xhr, status, error) {
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
@ -1312,13 +1336,13 @@ VATETaxes.import = {
xhr_response: xhr.responseText
});
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft);
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
}
});
},
// Обработка результата импорта
_handleImportResult: function(importR, operation, operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft = 0) {
_handleImportResult: function(importR, operation, operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft = 0, autoCreateCustomers = 0) {
if (importR.message && importR.message.success) {
if (importR.message.imported > 0) {
// НЕ показываем индивидуальные алерты во время массового импорта
@ -1330,19 +1354,19 @@ VATETaxes.import = {
VATETaxes.errors.add(operation, error.error_type || 'Import Error', error.message, error);
}
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft);
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
} else {
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
VATETaxes.errors.add(operation, 'Import Error', errorMessage, {
server_response: importR.message
});
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft);
VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft, autoCreateCustomers);
}
},
// Продолжить или завершить обработку
_continueOrFinish: function(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft = 0) {
_continueOrFinish: function(operations, token, company, processedCount, currentIndex, isLastOperation, createAsDraft = 0, autoCreateCustomers = 0) {
if (isLastOperation) {
$(document).off('progress-cancel.loading_operations');
@ -1372,7 +1396,7 @@ VATETaxes.import = {
frappe.hide_progress();
}
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft);
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers);
}, VATETaxes.PROGRESS_UPDATE_DELAY);
}
};

View File

@ -186,6 +186,29 @@ def get_vat_operations(token, date_from, date_to, max_count=50, offset=0):
return {"error": "unknown_error", "message": "An unknown error occurred, please try again."}
def normalize_customer_name(name):
"""
Нормализует имя клиента для поиска
Использует normalize_string() из sales_api.py
Args:
name (str): Имя клиента
Returns:
str: Нормализованное имя
"""
if not name:
return ''
try:
from invoice_az.sales_api import normalize_string
return normalize_string(name, consider_azeri=True)
except Exception as e:
frappe.log_error(f"Error normalizing customer name '{name}': {str(e)}", "VAT Customer Normalize Error")
# Fallback to simple lowercase if normalize_string fails
return name.lower().strip()
def find_customer_by_tin(tin):
"""
Поиск клиента по TIN (напрямую в Customer.tax_id)
@ -224,6 +247,248 @@ def find_customer_by_tin(tin):
return None
def find_or_create_customer_from_vat_operation(tin, name, settings_doc, auto_create=False):
"""
Найти или создать клиента из VAT операции с автосозданием маппингов
Логика поиска (ПРАВИЛЬНЫЙ ПРИОРИТЕТ):
0. Проверка маппингов (НАИВЫСШИЙ ПРИОРИТЕТ) - что указано в маппинге, то и используется
- Если найден маппинг с заполненным erp_customer возвращаем его
- Если найден маппинг с пустым erp_customer запоминаем и продолжаем поиск
1. Если auto_create=False return None
2. Поиск Customer в базе (только если маппинг пустой или отсутствует):
- По tax_id
- По точному имени
- По нормализованному имени
- Если найден создать/обновить маппинг
3. Создание нового Customer + создание/обновление маппинга
Args:
tin (str): TIN клиента
name (str): Имя клиента из операции
settings_doc (Document): Объект E-Taxes Settings
auto_create (bool): Разрешить автосоздание
Returns:
str: Имя клиента или None
"""
if not tin:
return None
try:
# Шаг 0: НАИВЫСШИЙ ПРИОРИТЕТ - Проверка маппингов
existing_mapping_row = None
if settings_doc and hasattr(settings_doc, 'customer_mappings'):
for mapping in settings_doc.customer_mappings:
if mapping.etaxes_tax_id == tin:
if mapping.erp_customer:
# Маппинг с заполненным erp_customer - это источник истины!
return mapping.erp_customer
else:
# Маппинг существует, но erp_customer пустой - запоминаем для обновления
existing_mapping_row = mapping
break
# Если auto_create=False, завершаем поиск
if not auto_create:
return None
# Шаг 1: Поиск Customer по tax_id в базе
customer_name = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
if customer_name:
# Найден Customer - создаем/обновляем маппинг
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_name, settings_doc, existing_mapping_row)
return customer_name
# Шаг 2: Поиск Customer по точному имени
if name:
customer_name = frappe.db.get_value('Customer', {'customer_name': name}, 'name')
if customer_name:
# Обновляем tax_id если нужно (с защитой от конфликта)
try:
customer_doc = frappe.get_doc('Customer', customer_name)
if not customer_doc.tax_id or customer_doc.tax_id != tin:
# Проверяем нет ли уже другого Customer с таким tax_id
existing_customer_with_tin = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
if not existing_customer_with_tin or existing_customer_with_tin == customer_name:
customer_doc.tax_id = tin
customer_doc.save(ignore_permissions=True)
frappe.db.commit()
except Exception:
# Игнорируем ошибки обновления tax_id
pass
# Создаем/обновляем маппинг
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_name, settings_doc, existing_mapping_row)
return customer_name
# Шаг 3: Поиск по нормализованному имени
if name:
normalized_search_name = normalize_customer_name(name)
if normalized_search_name:
all_customers = frappe.get_all('Customer', fields=['name', 'customer_name'])
for customer in all_customers:
customer_normalized = normalize_customer_name(customer.get('customer_name', ''))
if customer_normalized == normalized_search_name:
# Обновляем tax_id если нужно (с защитой от конфликта)
try:
customer_doc = frappe.get_doc('Customer', customer['name'])
if not customer_doc.tax_id or customer_doc.tax_id != tin:
# Проверяем нет ли уже другого Customer с таким tax_id
existing_customer_with_tin = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
if not existing_customer_with_tin or existing_customer_with_tin == customer['name']:
customer_doc.tax_id = tin
customer_doc.save(ignore_permissions=True)
frappe.db.commit()
except Exception:
# Игнорируем ошибки обновления tax_id
pass
# Создаем/обновляем маппинг
_create_or_update_etaxes_customer_and_mapping(tin, name, customer['name'], settings_doc, existing_mapping_row)
return customer['name']
# Шаг 4: Создание нового Customer
# Получаем default customer group
default_customer_group = settings_doc.default_customer_group if settings_doc else None
if not default_customer_group or not frappe.db.exists('Customer Group', default_customer_group):
default_customer_group = 'All Customer Groups'
# Создаем Customer (с защитой от race condition)
try:
customer_doc = frappe.new_doc('Customer')
customer_doc.customer_name = name or f"Customer-{tin}"
customer_doc.customer_type = 'Company'
customer_doc.customer_group = default_customer_group
customer_doc.territory = 'All Territories'
customer_doc.tax_id = tin
# Payment terms если есть
if settings_doc and hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
customer_doc.payment_terms = settings_doc.default_payment_terms
customer_doc.insert(ignore_permissions=True)
frappe.db.commit()
# Создаем/обновляем маппинг
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_doc.name, settings_doc, existing_mapping_row)
return customer_doc.name
except frappe.exceptions.DuplicateEntryError:
# Race condition: кто-то уже создал Customer с таким именем или tax_id
# Попробуем найти снова по tax_id
customer_name = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
if customer_name:
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_name, settings_doc, existing_mapping_row)
return customer_name
# Если не нашли - возвращаем None
return None
except Exception as e:
frappe.log_error(
f"Error finding/creating customer for TIN {tin}, name {name}: {str(e)}\n{frappe.get_traceback()}",
"VAT Customer Auto-Create Error"
)
return None
def _create_or_update_etaxes_customer_and_mapping(tin, etaxes_name, erp_customer, settings_doc, existing_mapping_row=None):
"""
Вспомогательная функция для создания/обновления E-Taxes Customers и маппинга
Работает ТИХО - без сообщений пользователю
Args:
tin (str): TIN клиента
etaxes_name (str): Имя из e-taxes
erp_customer (str): Имя Customer в ERP
settings_doc (Document): Объект E-Taxes Settings
existing_mapping_row (object): Существующая строка маппинга с пустым erp_customer (если есть)
"""
# Сначала обновляем маппинг (это главное!)
try:
if settings_doc:
if existing_mapping_row:
# Используем прямое обновление child table через SQL
default_customer_group = settings_doc.default_customer_group if settings_doc.default_customer_group else 'All Customer Groups'
# Обновляем через frappe.db.sql напрямую
frappe.db.sql("""
UPDATE `tabE-Taxes Customer Mappings`
SET erp_customer = %s,
customer_group = COALESCE(NULLIF(customer_group, ''), %s),
territory = COALESCE(NULLIF(territory, ''), 'All Territories'),
mapping_type = COALESCE(NULLIF(mapping_type, ''), 'Automatic')
WHERE parent = %s
AND etaxes_tax_id = %s
""", (erp_customer, default_customer_group, settings_doc.name, tin))
frappe.db.commit()
else:
# Проверяем, нет ли уже маппинга с таким TIN (на всякий случай)
mapping_exists = False
if hasattr(settings_doc, 'customer_mappings'):
for mapping in settings_doc.customer_mappings:
if mapping.etaxes_tax_id == tin:
mapping_exists = True
break
if not mapping_exists:
# Создаем новую строку маппинга
settings_doc.append('customer_mappings', {
'etaxes_customer_name': etaxes_name or f"Customer-{tin}",
'etaxes_tax_id': tin,
'erp_customer': erp_customer,
'customer_group': settings_doc.default_customer_group if settings_doc.default_customer_group else 'All Customer Groups',
'territory': 'All Territories',
'mapping_type': 'Automatic'
})
settings_doc.save(ignore_permissions=True)
frappe.db.commit()
except Exception as e:
# Тихая обработка ошибок маппинга - только в лог
frappe.log_error(
f"Error updating mapping for TIN {tin}: {str(e)}\n{frappe.get_traceback()}",
"VAT Mapping Update Error"
)
# Потом пытаемся создать/обновить E-Taxes Customers (если упадет - не страшно)
try:
# Проверяем существование по полям, а не по name (autoname может генерировать другой формат!)
existing = frappe.db.get_value('E-Taxes Customers',
{'etaxes_tax_id': tin},
'name')
if existing:
try:
etaxes_customer_doc = frappe.get_doc('E-Taxes Customers', existing)
if not etaxes_customer_doc.mapped_customer:
etaxes_customer_doc.mapped_customer = erp_customer
etaxes_customer_doc.save(ignore_permissions=True)
frappe.db.commit()
except Exception:
# Игнорируем ошибки при обновлении существующего
pass
else:
try:
etaxes_customer_doc = frappe.new_doc('E-Taxes Customers')
etaxes_customer_doc.etaxes_party_name = etaxes_name or f"Customer-{tin}"
etaxes_customer_doc.etaxes_tax_id = tin
etaxes_customer_doc.etaxes_party_type = 'Receiver'
etaxes_customer_doc.status = 'New'
etaxes_customer_doc.mapped_customer = erp_customer
etaxes_customer_doc.insert(ignore_permissions=True)
frappe.db.commit()
except frappe.exceptions.DuplicateEntryError:
# Игнорируем ошибку дубликата - кто-то уже создал
pass
except Exception:
# Игнорируем любые другие ошибки создания E-Taxes Customers
pass
except Exception:
# Игнорируем ошибки E-Taxes Customers полностью
pass
def find_account_by_number(account_number, company):
"""
Поиск счета по номеру счета
@ -349,7 +614,7 @@ def get_accounts_for_operation_type(operation_type, company, expense_income_type
return (None, None)
def create_journal_entry_from_vat_operation(operation_data, company, create_as_draft=0):
def create_journal_entry_from_vat_operation(operation_data, company, create_as_draft=0, auto_create_customers=0):
"""
Создание Journal Entry из VAT операции
@ -357,6 +622,7 @@ def create_journal_entry_from_vat_operation(operation_data, company, create_as_d
operation_data (dict): Данные операции из API
company (str): Название компании
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
auto_create_customers (int): 1 для автосоздания клиентов, 0 для отключения
Returns:
dict: Результат создания с именем JE или ошибкой
@ -434,17 +700,44 @@ def create_journal_entry_from_vat_operation(operation_data, company, create_as_d
debit_needs_party = debit_account_type in party_required_types
credit_needs_party = credit_account_type in party_required_types
# Получаем TIN и ищем клиента (если хотя бы один счет требует party)
# Получаем TIN и ищем/создаем клиента (если хотя бы один счет требует party)
tin = operation_data.get('tin', '').strip()
customer_name_from_operation = operation_data.get('name', '')
customer = None
if debit_needs_party or credit_needs_party:
customer = find_customer_by_tin(tin)
# Получаем настройки E-Taxes для маппингов
settings_doc = None
try:
settings_name = frappe.db.get_value('E-Taxes Settings',
{'is_default': 1, 'is_active': 1},
'name')
if not settings_name:
settings_name = frappe.db.get_value('E-Taxes Settings',
{'is_active': 1},
'name')
if settings_name:
settings_doc = frappe.get_doc('E-Taxes Settings', settings_name)
except Exception as e:
frappe.logger().warning(f"[VAT] Could not load E-Taxes Settings: {str(e)}")
# Используем новую функцию для поиска/создания клиента
customer = find_or_create_customer_from_vat_operation(
tin=tin,
name=customer_name_from_operation,
settings_doc=settings_doc,
auto_create=bool(auto_create_customers)
)
if not customer:
error_message = f'Customer not found for TIN: {tin}'
if auto_create_customers:
error_message = f'Could not find or create customer for TIN: {tin}'
return {
'success': False,
'message': f'Customer not found for TIN: {tin}',
'error_type': 'customer_not_found',
'message': error_message,
'error_type': 'customer_creation_failed' if auto_create_customers else 'customer_not_found',
'tin': tin
}
@ -588,7 +881,7 @@ def create_vat_operation_record(operation_data, journal_entry_name=None, status=
@frappe.whitelist()
def import_vat_operations(operations_data, company=None, create_as_draft=0):
def import_vat_operations(operations_data, company=None, create_as_draft=0, auto_create_customers=0):
"""
Импорт VAT операций и создание Journal Entries
@ -596,6 +889,7 @@ def import_vat_operations(operations_data, company=None, create_as_draft=0):
operations_data (str/list): JSON строка или список операций
company (str): Название компании (опционально)
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
auto_create_customers (int): 1 для автосоздания клиентов, 0 для отключения
Returns:
dict: Результат импорта с статистикой
@ -613,6 +907,12 @@ def import_vat_operations(operations_data, company=None, create_as_draft=0):
except (ValueError, TypeError):
create_as_draft = 0
# Обработка auto_create_customers параметра
try:
auto_create_customers = int(auto_create_customers)
except (ValueError, TypeError):
auto_create_customers = 0
# Получаем компанию
if not company:
company = frappe.defaults.get_user_default('Company')
@ -644,7 +944,7 @@ def import_vat_operations(operations_data, company=None, create_as_draft=0):
continue
# Создаем Journal Entry
je_result = create_journal_entry_from_vat_operation(operation, company, create_as_draft)
je_result = create_journal_entry_from_vat_operation(operation, company, create_as_draft, auto_create_customers)
if je_result.get('success'):
# Создаем запись отслеживания