added expense/income to mappings

This commit is contained in:
Ali 2026-01-20 16:19:45 +04:00
parent b3acbfa6e9
commit 0ef93e4b36
5 changed files with 75 additions and 29 deletions

View File

@ -90,10 +90,10 @@ bench --site [site-name] console
- Import VAT Account operations from e-taxes
- Import all operations (both income and expense types)
- Create Journal Entries with configurable account mappings
- VAT Account Mappings by operation type (Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə, etc.)
- VAT Account Mappings by operation type and expense/income type (Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə, etc.)
- Classification Code support - allows specific account mappings per tax code with priority:
- Priority 1: Mapping with matching operation_type + classification_code
- Priority 2: Default mapping with matching operation_type (empty classification_code)
- Priority 1: Mapping with matching operation_type + expense_income + classification_code
- Priority 2: Default mapping with matching operation_type + expense_income (empty classification_code)
- Required mapping configuration - operation fails with error if mapping not found
- Smart party assignment: adds Customer/Supplier only for Receivable/Payable accounts
- Customer lookup by TIN
@ -148,7 +148,7 @@ bench --site [site-name] console
- **E-Taxes Supplier Mappings** - Links ERPNext Suppliers to e-taxes suppliers
- **E-Taxes Customer Mappings** - Links ERPNext Customers to e-taxes customers
- **E-Taxes Unit Mapping** - Links ERPNext UOM to e-taxes units
- **E-Taxes VAT Account Mapping** - Maps VAT operation types to Chart of Accounts (debit/credit), with optional classification code filtering
- **E-Taxes VAT Account Mapping** - Maps VAT operation types to Chart of Accounts (debit/credit), with expense/income type and optional classification code filtering
**Tracking Records:**
- **E-Taxes Purchase** - Tracks imported purchase documents
@ -170,7 +170,7 @@ bench --site [site-name] console
- **Two-step Signing**: Create draft invoice, then sign with ASAN Imza
- **Retry Signing**: Retry signing for failed/pending invoices
- **VAT Import**: Import VAT account operations as Journal Entries with configurable account mappings
- **VAT Account Mappings**: Configure debit/credit accounts by operation type (Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə, etc.)
- **VAT Account Mappings**: Configure debit/credit accounts by operation type and expense/income type (Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə, etc.)
- **Smart Party Assignment**: Automatically adds party (Customer/Supplier) only for Receivable/Payable accounts
### API Endpoints (E-Taxes)
@ -273,12 +273,16 @@ bench --site [site-name] console
**VAT Account Mapping Configuration:**
- Configure in E-Taxes Settings > VAT Account Mappings tab
- Each mapping specifies: operation type, classification code (optional), debit account, credit account
- Each mapping specifies: operation type, expense/income type (required), classification code (optional), debit account, credit account
- Expense/Income field: Select "Expense" or "Income" to distinguish between expense and income operations
- Classification Code field links to Classification code doctype
- Uniqueness: Each combination of operation_type + expense_income + classification_code must be unique
- Priority logic:
- System determines expense/income type from API data (income > 0 → Income, expense > 0 → Expense)
- If classification_code is set: mapping applies only to operations with matching taxCodeInfo.code
- If classification_code is empty: mapping is default for this operation type
- System searches with priority: specific classification_code first, then default (empty)
- All searches include expense_income filter
- Supported operation types:
- Sub uçot hesabı → Sub uçot hesabı (SUB_TO_SUB in API)
- Naməlum → digər Sub hesab (UNKNOWN_TO_OTHER_SUB in API)

View File

@ -659,7 +659,15 @@ VATETaxes.errors = {
}
if (error.operation_type) {
errorsHtml += '<div><strong>' + __('Operation Type:') + '</strong> ' + error.operation_type + '</div>';
let opTypeDisplay = error.operation_type;
if (error.expense_income_type) {
opTypeDisplay += ' (' + error.expense_income_type.toLowerCase() + ')';
}
errorsHtml += '<div><strong>' + __('Operation Type:') + '</strong> ' + opTypeDisplay + '</div>';
}
if (error.classification_code) {
errorsHtml += '<div><strong>' + __('Classification Code:') + '</strong> ' + error.classification_code + '</div>';
}
if (error.missing_tin) {

View File

@ -7,6 +7,7 @@
"field_order": [
"operation_type",
"classification_code",
"expense_income",
"column_break_1",
"debit_account",
"credit_account"
@ -28,6 +29,14 @@
"label": "Classification Code",
"options": "Classification code"
},
{
"fieldname": "expense_income",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Expense/Income",
"options": "Expense\nIncome",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
@ -53,7 +62,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-01-14 20:00:00",
"modified": "2026-01-20 12:00:00",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes VAT Account Mapping",

View File

@ -8,7 +8,7 @@ from frappe.model.document import Document
class ETaxesVATAccountMapping(Document):
def validate(self):
"""
Проверка уникальности комбинации operation_type + classification_code
Проверка уникальности комбинации operation_type + classification_code + expense_income
в рамках одного родительского документа (E-Taxes Settings)
"""
if not self.parent or not self.parenttype:
@ -22,10 +22,11 @@ class ETaxesVATAccountMapping(Document):
'parent': self.parent,
'parenttype': self.parenttype,
'operation_type': self.operation_type,
'expense_income': self.expense_income,
'name': ['!=', self.name] # Исключаем текущую строку
}
# Получаем все строки с таким же operation_type
# Получаем все строки с таким же operation_type и expense_income
existing_mappings = frappe.get_all(
'E-Taxes VAT Account Mapping',
filters=filters,
@ -40,13 +41,15 @@ class ETaxesVATAccountMapping(Document):
if current_code == existing_code:
if current_code:
frappe.throw(
f'Duplicate mapping found: Operation Type "{self.operation_type}" '
f'with Classification Code "{current_code}" already exists. '
f'Duplicate mapping found: Operation Type "{self.operation_type}", '
f'Expense/Income "{self.expense_income}", '
f'Classification Code "{current_code}" already exists. '
f'Each combination must be unique.'
)
else:
frappe.throw(
f'Duplicate mapping found: Operation Type "{self.operation_type}" '
f'Duplicate mapping found: Operation Type "{self.operation_type}", '
f'Expense/Income "{self.expense_income}" '
f'with empty Classification Code already exists. '
f'Each combination must be unique.'
)

View File

@ -267,19 +267,20 @@ def find_account_by_number(account_number, company):
return None
def get_accounts_for_operation_type(operation_type, company, tax_code=None):
def get_accounts_for_operation_type(operation_type, company, expense_income_type, tax_code=None):
"""
Получить счета дебета и кредита для типа операции из E-Taxes Settings.
Если маппинг не найден, вернуть (None, None).
Логика строгого соответствия (БЕЗ fallback):
- Если передан tax_code - ищем ТОЛЬКО маппинг с operation_type + этот tax_code
- Если НЕ передан tax_code - ищем ТОЛЬКО маппинг с operation_type + пустой classification_code
- Если передан tax_code - ищем ТОЛЬКО маппинг с operation_type + expense_income_type + этот tax_code
- Если НЕ передан tax_code - ищем ТОЛЬКО маппинг с operation_type + expense_income_type + пустой classification_code
- Не нашли точное совпадение - возвращаем (None, None)
Args:
operation_type (str): Тип операции (например, Sub uçot hesabı Sub uçot hesabı, Sub uçot hesabı Büdcə)
company (str): Название компании
expense_income_type (str): Тип операции - 'Expense' или 'Income'
tax_code (str, optional): Код классификации из taxCodeInfo.code
Returns:
@ -305,16 +306,17 @@ def get_accounts_for_operation_type(operation_type, company, tax_code=None):
{'parent': settings_name,
'parenttype': 'E-Taxes Settings',
'operation_type': operation_type,
'expense_income': expense_income_type,
'classification_code': tax_code},
['debit_account', 'credit_account'],
as_dict=True)
if mapping:
frappe.logger().info(f"Found mapping with classification_code: {tax_code}")
frappe.logger().info(f"Found mapping with classification_code: {tax_code}, expense/income: {expense_income_type}")
return (mapping.get('debit_account'), mapping.get('credit_account'))
else:
# Не нашли - возвращаем None (БЕЗ fallback на пустой classification_code)
frappe.logger().info(f"No mapping found for operation_type: {operation_type} with classification_code: {tax_code}")
frappe.logger().info(f"No mapping found for operation_type: {operation_type}, expense/income: {expense_income_type} with classification_code: {tax_code}")
return (None, None)
else:
# Случай Б: Нет tax_code - ищем ТОЛЬКО с пустым classification_code
@ -322,18 +324,19 @@ def get_accounts_for_operation_type(operation_type, company, tax_code=None):
filters={
'parent': settings_name,
'parenttype': 'E-Taxes Settings',
'operation_type': operation_type
'operation_type': operation_type,
'expense_income': expense_income_type
},
fields=['debit_account', 'credit_account', 'classification_code'])
# Ищем маппинг с пустым classification_code
for mapping in mappings:
if not mapping.get('classification_code'):
frappe.logger().info(f"Found default mapping (no classification_code)")
frappe.logger().info(f"Found default mapping (no classification_code) for expense/income: {expense_income_type}")
return (mapping.get('debit_account'), mapping.get('credit_account'))
# Не нашли маппинг с пустым classification_code
frappe.logger().info(f"No default mapping found for operation_type: {operation_type}")
frappe.logger().info(f"No default mapping found for operation_type: {operation_type}, expense/income: {expense_income_type}")
return (None, None)
# Маппинг не найден - возвращаем None, None
@ -372,10 +375,14 @@ def create_journal_entry_from_vat_operation(operation_data, company):
income = operation_data.get('income', 0)
expense = operation_data.get('expense', 0)
# Определяем сумму операции - используем то что заполнено
amount = income if income and income > 0 else expense
if not amount or amount <= 0:
# Определяем тип операции (Expense/Income) и сумму
if income and income > 0:
expense_income_type = 'Income'
amount = income
elif expense and expense > 0:
expense_income_type = 'Expense'
amount = expense
else:
return {
'success': False,
'message': 'Invalid amount: both income and expense are empty or zero'
@ -392,17 +399,32 @@ def create_journal_entry_from_vat_operation(operation_data, company):
tax_code = tax_code_info.get('code')
# Ищем счета по маппингу с учетом tax_code (если есть)
debit_account, credit_account = get_accounts_for_operation_type(operation_type, company, tax_code)
debit_account, credit_account = get_accounts_for_operation_type(
operation_type, company, expense_income_type, tax_code
)
# Проверяем что маппинг найден
if not debit_account or not credit_account:
return {
# Формируем понятное сообщение об ошибке
expense_income_label = expense_income_type.lower() # "Income" → "income", "Expense" → "expense"
error_message = f"VAT account mapping not found: operation type '{operation_type}', {expense_income_label}"
if tax_code:
error_message += f", classification code '{tax_code}'"
error_dict = {
'success': False,
'message': f'VAT account mapping not found for operation type: {operation_type}',
'message': error_message,
'error_type': 'mapping_not_found',
'operation_type': operation_type
'operation_type': operation_type,
'expense_income_type': expense_income_type
}
if tax_code:
error_dict['classification_code'] = tax_code
return error_dict
# Проверяем типы счетов - нужен ли party
debit_account_type = frappe.db.get_value('Account', debit_account, 'account_type')
credit_account_type = frappe.db.get_value('Account', credit_account, 'account_type')