added create if not mapped
This commit is contained in:
parent
c037dfe45c
commit
f1ae840d88
|
|
@ -82,6 +82,7 @@ VATETaxes.utils = {
|
||||||
humanizeErrorType: function(errorType) {
|
humanizeErrorType: function(errorType) {
|
||||||
const errorTypeMap = {
|
const errorTypeMap = {
|
||||||
'customer_not_found': __('Customer Not Found'),
|
'customer_not_found': __('Customer Not Found'),
|
||||||
|
'customer_creation_failed': __('Customer Creation Failed'),
|
||||||
'account_not_found': __('Account Not Found'),
|
'account_not_found': __('Account Not Found'),
|
||||||
'mapping_not_found': __('VAT Account Mapping Not Found'),
|
'mapping_not_found': __('VAT Account Mapping Not Found'),
|
||||||
'import_error': __('Import Error'),
|
'import_error': __('Import Error'),
|
||||||
|
|
@ -892,13 +893,6 @@ VATETaxes.import = {
|
||||||
options: 'Company',
|
options: 'Company',
|
||||||
reqd: true,
|
reqd: true,
|
||||||
default: frappe.defaults.get_user_default('Company')
|
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'),
|
primary_action_label: __('Search'),
|
||||||
|
|
@ -934,7 +928,7 @@ VATETaxes.import = {
|
||||||
const createAsDraft = values.create_as_draft || 0;
|
const createAsDraft = values.create_as_draft || 0;
|
||||||
|
|
||||||
d.hide();
|
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;
|
VATETaxes.cancelLoading = false;
|
||||||
$(document).off('progress-cancel.vat_import');
|
$(document).off('progress-cancel.vat_import');
|
||||||
|
|
||||||
|
|
@ -990,7 +984,7 @@ VATETaxes.import = {
|
||||||
|
|
||||||
if (hasMore && currentOperations.length > 0) {
|
if (hasMore && currentOperations.length > 0) {
|
||||||
const newOffset = offset + currentOperations.length;
|
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 {
|
} else {
|
||||||
if (allOperations.length > 0) {
|
if (allOperations.length > 0) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
|
|
@ -998,7 +992,7 @@ VATETaxes.import = {
|
||||||
indicator: 'blue'
|
indicator: 'blue'
|
||||||
}, 3);
|
}, 3);
|
||||||
|
|
||||||
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft);
|
VATETaxes.import.filterDuplicates(allOperations, mainToken, company, createAsDraft, autoCreateCustomers);
|
||||||
} else {
|
} else {
|
||||||
frappe.msgprint({
|
frappe.msgprint({
|
||||||
title: __('Information'),
|
title: __('Information'),
|
||||||
|
|
@ -1012,7 +1006,7 @@ VATETaxes.import = {
|
||||||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||||||
function() {
|
function() {
|
||||||
VATETaxes.auth.startProcess(function() {
|
VATETaxes.auth.startProcess(function() {
|
||||||
VATETaxes.import.loadOperations(fromDate, toDate, company, [], 0, createAsDraft);
|
VATETaxes.import.loadOperations(fromDate, toDate, company, [], 0, createAsDraft, autoCreateCustomers);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function() {
|
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({
|
frappe.call({
|
||||||
method: 'frappe.client.get_list',
|
method: 'frappe.client.get_list',
|
||||||
args: {
|
args: {
|
||||||
|
|
@ -1085,7 +1079,7 @@ VATETaxes.import = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredOperations.length > 0) {
|
if (filteredOperations.length > 0) {
|
||||||
VATETaxes.import.showOperationSelection(filteredOperations, token, company, createAsDraft);
|
VATETaxes.import.showOperationSelection(filteredOperations, token, company, createAsDraft, autoCreateCustomers);
|
||||||
} else {
|
} else {
|
||||||
frappe.msgprint({
|
frappe.msgprint({
|
||||||
title: __('Information'),
|
title: __('Information'),
|
||||||
|
|
@ -1105,7 +1099,7 @@ VATETaxes.import = {
|
||||||
},
|
},
|
||||||
error: function(err) {
|
error: function(err) {
|
||||||
console.error("Error fetching VAT operations:", 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({
|
frappe.show_alert({
|
||||||
message: __('Failed to check for duplicates. Showing all operations.'),
|
message: __('Failed to check for duplicates. Showing all operations.'),
|
||||||
indicator: 'orange'
|
indicator: 'orange'
|
||||||
|
|
@ -1170,6 +1164,30 @@ VATETaxes.import = {
|
||||||
fieldtype: 'HTML',
|
fieldtype: 'HTML',
|
||||||
options: infoMessage
|
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',
|
fieldname: 'operations_html',
|
||||||
fieldtype: 'HTML',
|
fieldtype: 'HTML',
|
||||||
|
|
@ -1198,6 +1216,11 @@ VATETaxes.import = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получить значения настроек из диалога
|
||||||
|
const values = d.get_values();
|
||||||
|
const createAsDraftValue = values.create_as_draft || 0;
|
||||||
|
const autoCreateCustomers = values.auto_create_customers || 0;
|
||||||
|
|
||||||
// Найти полные данные операций
|
// Найти полные данные операций
|
||||||
const selectedOperations = [];
|
const selectedOperations = [];
|
||||||
selectedOperationIds.forEach(function(id) {
|
selectedOperationIds.forEach(function(id) {
|
||||||
|
|
@ -1209,7 +1232,7 @@ VATETaxes.import = {
|
||||||
|
|
||||||
d.hide();
|
d.hide();
|
||||||
VATETaxes.cancelLoading = false;
|
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_label: __('Cancel'),
|
||||||
secondary_action: function() {
|
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) {
|
if (processedCount === 0 && currentIndex === 0) {
|
||||||
VATETaxes.loadingErrors = [];
|
VATETaxes.loadingErrors = [];
|
||||||
window.totalOperationsToProcess = operations.length;
|
window.totalOperationsToProcess = operations.length;
|
||||||
|
|
@ -1271,7 +1294,7 @@ VATETaxes.import = {
|
||||||
if (VATETaxes.cancelLoading) {
|
if (VATETaxes.cancelLoading) {
|
||||||
operations = [];
|
operations = [];
|
||||||
frappe.hide_progress();
|
frappe.hide_progress();
|
||||||
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft);
|
VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex, createAsDraft, autoCreateCustomers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1299,11 +1322,12 @@ VATETaxes.import = {
|
||||||
args: {
|
args: {
|
||||||
'operations_data': JSON.stringify([operation]),
|
'operations_data': JSON.stringify([operation]),
|
||||||
'company': company,
|
'company': company,
|
||||||
'create_as_draft': createAsDraft
|
'create_as_draft': createAsDraft,
|
||||||
|
'auto_create_customers': autoCreateCustomers
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
VATETaxes.import._handleImportResult(r, operation,
|
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) {
|
error: function(xhr, status, error) {
|
||||||
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
|
const errorMessage = 'Network error during import: ' + (error || 'Unknown network error');
|
||||||
|
|
@ -1312,13 +1336,13 @@ VATETaxes.import = {
|
||||||
xhr_response: xhr.responseText
|
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 && importR.message.success) {
|
||||||
if (importR.message.imported > 0) {
|
if (importR.message.imported > 0) {
|
||||||
// НЕ показываем индивидуальные алерты во время массового импорта
|
// НЕ показываем индивидуальные алерты во время массового импорта
|
||||||
|
|
@ -1330,19 +1354,19 @@ VATETaxes.import = {
|
||||||
VATETaxes.errors.add(operation, error.error_type || 'Import Error', error.message, error);
|
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 {
|
} else {
|
||||||
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
|
const errorMessage = importR.message ? importR.message.message : 'Unknown import error';
|
||||||
VATETaxes.errors.add(operation, 'Import Error', errorMessage, {
|
VATETaxes.errors.add(operation, 'Import Error', errorMessage, {
|
||||||
server_response: importR.message
|
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) {
|
if (isLastOperation) {
|
||||||
$(document).off('progress-cancel.loading_operations');
|
$(document).off('progress-cancel.loading_operations');
|
||||||
|
|
||||||
|
|
@ -1372,7 +1396,7 @@ VATETaxes.import = {
|
||||||
frappe.hide_progress();
|
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);
|
}, VATETaxes.PROGRESS_UPDATE_DELAY);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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."}
|
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):
|
def find_customer_by_tin(tin):
|
||||||
"""
|
"""
|
||||||
Поиск клиента по TIN (напрямую в Customer.tax_id)
|
Поиск клиента по TIN (напрямую в Customer.tax_id)
|
||||||
|
|
@ -224,6 +247,248 @@ def find_customer_by_tin(tin):
|
||||||
return None
|
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):
|
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)
|
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 операции
|
Создание Journal Entry из VAT операции
|
||||||
|
|
||||||
|
|
@ -357,6 +622,7 @@ def create_journal_entry_from_vat_operation(operation_data, company, create_as_d
|
||||||
operation_data (dict): Данные операции из API
|
operation_data (dict): Данные операции из API
|
||||||
company (str): Название компании
|
company (str): Название компании
|
||||||
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
|
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
|
||||||
|
auto_create_customers (int): 1 для автосоздания клиентов, 0 для отключения
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Результат создания с именем JE или ошибкой
|
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
|
debit_needs_party = debit_account_type in party_required_types
|
||||||
credit_needs_party = credit_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()
|
tin = operation_data.get('tin', '').strip()
|
||||||
|
customer_name_from_operation = operation_data.get('name', '')
|
||||||
customer = None
|
customer = None
|
||||||
|
|
||||||
if debit_needs_party or credit_needs_party:
|
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:
|
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 {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'Customer not found for TIN: {tin}',
|
'message': error_message,
|
||||||
'error_type': 'customer_not_found',
|
'error_type': 'customer_creation_failed' if auto_create_customers else 'customer_not_found',
|
||||||
'tin': tin
|
'tin': tin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,7 +881,7 @@ def create_vat_operation_record(operation_data, journal_entry_name=None, status=
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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
|
Импорт VAT операций и создание Journal Entries
|
||||||
|
|
||||||
|
|
@ -596,6 +889,7 @@ def import_vat_operations(operations_data, company=None, create_as_draft=0):
|
||||||
operations_data (str/list): JSON строка или список операций
|
operations_data (str/list): JSON строка или список операций
|
||||||
company (str): Название компании (опционально)
|
company (str): Название компании (опционально)
|
||||||
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
|
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
|
||||||
|
auto_create_customers (int): 1 для автосоздания клиентов, 0 для отключения
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Результат импорта с статистикой
|
dict: Результат импорта с статистикой
|
||||||
|
|
@ -613,6 +907,12 @@ def import_vat_operations(operations_data, company=None, create_as_draft=0):
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
create_as_draft = 0
|
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:
|
if not company:
|
||||||
company = frappe.defaults.get_user_default('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
|
continue
|
||||||
|
|
||||||
# Создаем Journal Entry
|
# Создаем 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'):
|
if je_result.get('success'):
|
||||||
# Создаем запись отслеживания
|
# Создаем запись отслеживания
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue