feat(etaxes): classification code + explanation custom fields; quieter asset-category logs

Add read-only custom fields etaxes_classification_code (Təsnifat Kodu)
and etaxes_explanation on Journal Entry for E-Taxes VAT imports, with
matching translation markers.

Move asset-category creation logging from frappe.log_error (which spammed
the Error Log DocType) to a file logger; keep log_error only for real
creation failures. Drop the manual frappe.db.commit inside the
Company.on_update path so the framework keeps the save atomic.

Reuse the realtime progress-bar race fix in currency_exchange_list.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-12 16:19:22 +00:00
parent 4e8a151851
commit 4926d5c637
4 changed files with 99 additions and 42 deletions

View File

@ -1,6 +1,16 @@
import frappe import frappe
def _logger():
"""Файловый логгер для процесса создания категорий активов.
Используем frappe.logger() для info/debug-сообщений (пишутся в
logs/asset_categories.log), а frappe.log_error() оставляем только
для реальных ошибок (записи в Error Log DocType).
"""
return frappe.logger("asset_categories", allow_site=True, file_count=20)
def get_system_asset_categories(): def get_system_asset_categories():
""" """
Возвращает список системных категорий активов Возвращает список системных категорий активов
@ -79,25 +89,24 @@ def create_asset_categories_on_company_update(doc, method):
# Проверяем первую категорию из списка # Проверяем первую категорию из списка
if frappe.db.exists("Asset Category", {"asset_category_name": system_categories[0]}): if frappe.db.exists("Asset Category", {"asset_category_name": system_categories[0]}):
# Категории уже созданы (они создаются для всех компаний сразу) # Категории уже созданы (они создаются для всех компаний сразу)
frappe.log_error( _logger().info(
f"Asset categories already exist, skipping creation\nCompany: {company_name}\nFirst category found: {system_categories[0]}", f"Asset categories already exist, skipping creation. "
"AC Skip Existing" f"Company: {company_name}, first category found: {system_categories[0]}"
) )
return return
# Логируем начало создания # Логируем начало создания
frappe.log_error( _logger().info(
f"Creating asset categories automatically\nTriggered by: Company.on_update\nCompany: {company_name}", f"Creating asset categories automatically (Company.on_update). Company: {company_name}"
"AC Auto Create"
) )
try: try:
# Создаем категории # Создаем категории
_create_categories_for_company(company_name) _create_categories_for_company(company_name)
frappe.log_error( _logger().info(
f"Asset categories created successfully\nCompany: {company_name}\nCategories: {len(system_categories)}", f"Asset categories created successfully. "
"AC Created" f"Company: {company_name}, categories: {len(system_categories)}"
) )
except Exception as e: except Exception as e:
import traceback import traceback
@ -116,10 +125,7 @@ def create_asset_categories_for_company(company_name):
Args: Args:
company_name: Название компании company_name: Название компании
""" """
frappe.log_error( _logger().info(f"create_asset_categories_for_company started. Company: {company_name}")
f"Function started\nCompany: {company_name}",
"AC Create Start"
)
try: try:
# Проверяем, что компания существует # Проверяем, что компания существует
@ -132,10 +138,7 @@ def create_asset_categories_for_company(company_name):
) )
return return
frappe.log_error( _logger().debug(f"Company exists in database. Company: {company_name}")
f"Company exists in database\nCompany: {company_name}",
"AC Company OK"
)
# Проверка идемпотентности: если категории уже есть, не создаем # Проверка идемпотентности: если категории уже есть, не создаем
system_categories = get_system_asset_categories() system_categories = get_system_asset_categories()
@ -144,28 +147,27 @@ def create_asset_categories_for_company(company_name):
filters={"asset_category_name": ["in", system_categories]} filters={"asset_category_name": ["in", system_categories]}
) )
frappe.log_error( _logger().debug(
f"Idempotency check\nSystem categories: {len(system_categories)}\nExisting categories: {existing_categories}\nCategories list: {system_categories}", f"Idempotency check. System categories: {len(system_categories)}, "
"AC Check Existing" f"existing: {existing_categories}"
) )
if existing_categories > 0: if existing_categories > 0:
frappe.log_error( _logger().info(
f"Categories already exist, skipping creation\nFound: {existing_categories} of {len(system_categories)}", f"Categories already exist, skipping creation. "
"AC Already Exist" f"Found: {existing_categories} of {len(system_categories)}"
) )
return return
# Создаем категории # Создаем категории
frappe.log_error( _logger().info(
f"Starting category creation\nCompany: {company_name}\nCategories to create: {len(system_categories)}", f"Starting category creation. Company: {company_name}, "
"AC Creating" f"categories to create: {len(system_categories)}"
) )
_create_categories_for_company(company_name) _create_categories_for_company(company_name)
frappe.log_error( _logger().info(
f"Asset categories creation completed successfully\nCompany: {company_name}", f"Asset categories creation completed successfully. Company: {company_name}"
"AC Complete"
) )
except Exception as e: except Exception as e:
@ -185,10 +187,7 @@ def _create_categories_for_company(company_name):
asset_categories = get_system_asset_categories() asset_categories = get_system_asset_categories()
# Находим нужные счета # Находим нужные счета
frappe.log_error( _logger().debug(f"Searching for required accounts. Company: {company_name}, needed: 3")
f"Searching for required accounts\nCompany: {company_name}\nAccounts needed: 3",
"AC Search Accounts"
)
try: try:
accumulated_depreciation = get_account_by_name( accumulated_depreciation = get_account_by_name(
@ -204,9 +203,10 @@ def _create_categories_for_company(company_name):
company_name company_name
) )
frappe.log_error( _logger().debug(
f"All required accounts found\nCompany: {company_name}\n\nAccounts:\n1. Accumulated Depreciation: {accumulated_depreciation}\n2. Depreciation Expense: {depreciation_expense}\n3. Fixed Asset: {fixed_asset}", f"All required accounts found. Company: {company_name}, "
"AC Accounts OK" f"fixed_asset: {fixed_asset}, accumulated_depreciation: {accumulated_depreciation}, "
f"depreciation_expense: {depreciation_expense}"
) )
except Exception as e: except Exception as e:
@ -250,14 +250,22 @@ def _create_categories_for_company(company_name):
f"AC Cat ERROR {idx}" f"AC Cat ERROR {idx}"
) )
frappe.db.commit() # NB: commit намеренно НЕ вызываем. В активном пути (Company.on_update)
# транзакцию коммитит сам фреймворк; ручной commit внутри хука разрывает
# атомарность сохранения Company. В фоновой задаче (frappe.enqueue) и в
# патчах commit тоже выполняется обёрткой после успешного завершения.
# Финальный отчет # Финальный отчет
status = "SUCCESS" if created_count > 0 else ("SKIP" if skipped_count > 0 else "ERROR") status = "SUCCESS" if created_count > 0 else ("SKIP" if skipped_count > 0 else "ERROR")
frappe.log_error( summary = (
f"Category creation finished\nCompany: {company_name}\nCreated: {created_count}\nSkipped: {skipped_count}\nErrors: {len(errors)}\n\nError details:\n{chr(10).join(errors) if errors else 'No errors'}", f"Category creation finished [{status}]. Company: {company_name}, "
f"AC {status}" f"created: {created_count}, skipped: {skipped_count}, errors: {len(errors)}"
) )
if errors:
# Реальные ошибки создания категорий — в Error Log, чтобы были заметны.
frappe.log_error(summary + "\n\n" + "\n".join(errors), f"AC {status}")
else:
_logger().info(summary)
def prevent_delete_system_asset_categories(doc, method): def prevent_delete_system_asset_categories(doc, method):

View File

@ -1054,6 +1054,29 @@ def create_custom_fields():
hidden=1, hidden=1,
read_only=1, read_only=1,
no_copy=1 no_copy=1
),
dict(
fieldname='etaxes_classification_code',
label='Təsnifat Kodu',
fieldtype='Link',
options='Classification code',
insert_after='loaded_from_etaxes',
read_only=1,
no_copy=1,
translatable=0,
description='Təsnifat kodu (taxCodeInfo.code) imported from E-Taxes'
),
dict(
fieldname='etaxes_explanation',
label='Explanation',
fieldtype='Small Text',
insert_after='etaxes_classification_code',
in_list_view=1,
columns=3,
read_only=1,
no_copy=1,
translatable=0,
description='Explanation imported from E-Taxes VAT operation'
) )
], ],
"Bank": [ "Bank": [

View File

@ -50,10 +50,31 @@ function _show_import_dialog(available_currencies) {
d.hide(); d.hide();
// Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
// frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
// уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
let importFinished = false;
// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
const closeProgress = function () {
if (frappe.cur_progress) {
try {
frappe.cur_progress.$wrapper.modal("hide");
frappe.cur_progress.$wrapper.remove();
} catch (e) {}
frappe.cur_progress = null;
}
if ($(".modal:visible").length === 0) {
$(".modal-backdrop").remove();
$("body").removeClass("modal-open");
}
};
frappe.realtime.off("cbar_import_progress"); frappe.realtime.off("cbar_import_progress");
frappe.realtime.off("cbar_import_complete"); frappe.realtime.off("cbar_import_complete");
frappe.realtime.on("cbar_import_progress", function (data) { frappe.realtime.on("cbar_import_progress", function (data) {
if (importFinished) return;
frappe.show_progress( frappe.show_progress(
__("Importing CBAR Rates"), __("Importing CBAR Rates"),
data.current, data.current,
@ -63,9 +84,10 @@ function _show_import_dialog(available_currencies) {
}); });
frappe.realtime.on("cbar_import_complete", function (data) { frappe.realtime.on("cbar_import_complete", function (data) {
importFinished = true;
frappe.realtime.off("cbar_import_progress"); frappe.realtime.off("cbar_import_progress");
frappe.realtime.off("cbar_import_complete"); frappe.realtime.off("cbar_import_complete");
frappe.hide_progress(); closeProgress();
const { created, updated, skipped_dates } = data; const { created, updated, skipped_dates } = data;
let msg = `<b>${__("Import complete")}</b><br>${__("Created")}: ${created}<br>${__("Updated")}: ${updated}`; let msg = `<b>${__("Import complete")}</b><br>${__("Created")}: ${created}<br>${__("Updated")}: ${updated}`;
@ -89,7 +111,7 @@ function _show_import_dialog(available_currencies) {
error: function () { error: function () {
frappe.realtime.off("cbar_import_progress"); frappe.realtime.off("cbar_import_progress");
frappe.realtime.off("cbar_import_complete"); frappe.realtime.off("cbar_import_complete");
frappe.hide_progress(); closeProgress();
} }
}); });
} }

View File

@ -77,6 +77,8 @@ _lt("Employer Name")
_lt("Employer Position") _lt("Employer Position")
_lt("Enter the tax-exempt area value") _lt("Enter the tax-exempt area value")
_lt("Expense/Income") _lt("Expense/Income")
_lt("Explanation")
_lt("Explanation imported from E-Taxes VAT operation")
_lt("FIN") _lt("FIN")
_lt("First Name") _lt("First Name")
_lt("For agricultural: hectares. For industrial: square meters.") _lt("For agricultural: hectares. For industrial: square meters.")
@ -178,6 +180,8 @@ _lt("Taxable Assets Information")
_lt("Taxation system") _lt("Taxation system")
_lt("Taxpayer Activity Group") _lt("Taxpayer Activity Group")
_lt("Type of act for e-taxes") _lt("Type of act for e-taxes")
_lt("Təsnifat Kodu")
_lt("Təsnifat kodu (taxCodeInfo.code) imported from E-Taxes")
_lt("Used as the Expense Account when this service item is selected in a Landed Cost Voucher.") _lt("Used as the Expense Account when this service item is selected in a Landed Cost Voucher.")
_lt("Uçot metodu (Accounting Method)") _lt("Uçot metodu (Accounting Method)")
_lt("VAT 0% with amount") _lt("VAT 0% with amount")