From 64096d7c5ac3fb3db7705c0f8f7a48a3f7e9b13a Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 13 Aug 2025 15:26:23 +0400 Subject: [PATCH] added item group rename --- jey_erp/hooks.py | 3 + jey_erp/overrides/custom_install.py | 172 ++++++++++ jey_erp/patches/replace_install_function.py | 351 -------------------- 3 files changed, 175 insertions(+), 351 deletions(-) create mode 100644 jey_erp/overrides/custom_install.py delete mode 100644 jey_erp/patches/replace_install_function.py diff --git a/jey_erp/hooks.py b/jey_erp/hooks.py index 12d230a..27bed16 100644 --- a/jey_erp/hooks.py +++ b/jey_erp/hooks.py @@ -39,6 +39,9 @@ def after_migrate_combined(): from jey_erp.custom_fields import create_custom_fields create_custom_fields() + from jey_erp.overrides.custom_install import rename_item_groups + rename_item_groups() + fixtures = [ { diff --git a/jey_erp/overrides/custom_install.py b/jey_erp/overrides/custom_install.py new file mode 100644 index 0000000..9fc0e0e --- /dev/null +++ b/jey_erp/overrides/custom_install.py @@ -0,0 +1,172 @@ +import frappe +import urllib.parse + +def rename_item_groups(): + """ + Переименовывает группы номенклатур: и названия, и ID на азербайджанский + """ + + # Маппинг: старый_ID -> {"new_name": "название", "new_id": "тоже_название"} + translation_map = { + "All Item Groups": { + "new_name": "Bütün Məhsul Qrupları", + }, + "Products": { + "new_name": "Məhsullar", + }, + "Raw Material": { + "new_name": "Xammal", + }, + "Services": { + "new_name": "Xidmətlər", + }, + "Sub Assemblies": { + "new_name": "Alt Montajlar", + }, + "Consumable": { + "new_name": "İstehlak Malları", + } + } + + # ID будет точно таким же, как и название (на азербайджанском) + for key, value in translation_map.items(): + new_name = value["new_name"] + # ID = название (без URL-encoding) + value["new_id"] = new_name + + try: + updated_count = 0 + renamed_count = 0 + + frappe.flags.ignore_permissions = True + frappe.flags.ignore_links = True + frappe.flags.ignore_validate_update_after_submit = True + + # Получаем все группы, отсортированные по иерархии + all_groups = frappe.get_all( + "Item Group", + fields=["name", "item_group_name", "parent_item_group", "lft", "rgt"], + order_by="lft" # От корневых к дочерним + ) + + # Создаем маппинг для обновления родительских ссылок + id_mapping = {} + groups_to_process = [] + + # Собираем информацию о группах для переименования + for old_id, translation in translation_map.items(): + new_name = translation["new_name"] + new_id = translation["new_id"] + + # Находим группу по текущему ID + existing_group = None + for group in all_groups: + if group.name == old_id: + existing_group = group + break + + if existing_group: + id_mapping[old_id] = new_id + groups_to_process.append({ + "old_id": old_id, + "new_id": new_id, + "new_name": new_name, + "lft": existing_group.lft, + "current_parent": existing_group.parent_item_group + }) + + # Обрабатываем группы в правильном порядке (от корневых к дочерним) + for group_info in sorted(groups_to_process, key=lambda x: x["lft"]): + old_id = group_info["old_id"] + new_id = group_info["new_id"] + new_name = group_info["new_name"] + current_parent = group_info["current_parent"] + + # Проверяем, не существует ли уже группа с новым ID + if frappe.db.exists("Item Group", new_id): + continue + + try: + # Обновляем родительскую ссылку если родитель тоже переименовывается + updated_parent = current_parent + if current_parent and current_parent in id_mapping: + updated_parent = id_mapping[current_parent] + + # Сначала обновляем название + frappe.db.sql(""" + UPDATE `tabItem Group` + SET item_group_name = %s + WHERE name = %s + """, (new_name, old_id)) + + updated_count += 1 + + # Теперь переименовываем ID + if old_id != new_id: + # Используем низкоуровневые SQL запросы для избежания проверок + + # 1. Обновляем основную запись + frappe.db.sql(""" + UPDATE `tabItem Group` + SET name = %s + WHERE name = %s + """, (new_id, old_id)) + + # 2. Обновляем parent_item_group ссылки + frappe.db.sql(""" + UPDATE `tabItem Group` + SET parent_item_group = %s + WHERE parent_item_group = %s + """, (new_id, old_id)) + + # 3. Обновляем ссылки в товарах + frappe.db.sql(""" + UPDATE `tabItem` + SET item_group = %s + WHERE item_group = %s + """, (new_id, old_id)) + + # 4. Обновляем другие возможные ссылки + other_tables = [ + ("tabItem Default", "parent_item_group"), + ("tabPurchase Invoice Item", "item_group"), + ("tabSales Invoice Item", "item_group"), + ("tabDelivery Note Item", "item_group"), + ("tabPurchase Receipt Item", "item_group") + ] + + for table, field in other_tables: + if frappe.db.has_column(table, field): + frappe.db.sql(f""" + UPDATE `{table}` + SET `{field}` = %s + WHERE `{field}` = %s + """, (new_id, old_id)) + + renamed_count += 1 + + except Exception as process_error: + continue + + # Сохраняем изменения + frappe.db.commit() + + # Очищаем кэш + frappe.clear_cache() + + # Перестраиваем дерево + try: + from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Item Group", "parent_item_group") + except Exception as tree_error: + pass + + except Exception as e: + frappe.log_error(f"URL-encoded Item Groups Error: {str(e)}") + frappe.db.rollback() + raise + + finally: + frappe.flags.ignore_permissions = False + frappe.flags.ignore_links = False + frappe.flags.ignore_validate_update_after_submit = False \ No newline at end of file diff --git a/jey_erp/patches/replace_install_function.py b/jey_erp/patches/replace_install_function.py deleted file mode 100644 index ebe72b8..0000000 --- a/jey_erp/patches/replace_install_function.py +++ /dev/null @@ -1,351 +0,0 @@ -# your_app/patches/replace_install_function.py - -import json -import os -from pathlib import Path - -import frappe -from frappe.desk.doctype.global_search_settings.global_search_settings import ( - update_global_search_doctypes, -) -from frappe.desk.page.setup_wizard.setup_wizard import make_records -from frappe.utils import cstr, getdate - -from erpnext.accounts.doctype.account.account import RootNotEditable -from erpnext.regional.address_template.setup import set_up_address_templates - - -def _(x, *args, **kwargs): - """Redefine the translation function to return the string as is. - - We want to create english records but still mark the strings as translatable. - The respective DocTypes have 'Translate Link Fields' enabled.""" - return x - - -def read_lines(filename: str) -> list[str]: - """Return a list of lines from a file in the data directory.""" - return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines() - - -def custom_install(country=None): - """Ваша замененная функция install - вставьте сюда ваш код""" - records = [ - # ensure at least an empty Address Template exists for this Country - {"doctype": "Address Template", "country": country}, - # item group - { - "doctype": "Item Group", - "item_group_name": _("Bütün Element Qrupları"), - "is_group": 1, - "parent_item_group": "", - }, - { - "doctype": "Item Group", - "item_group_name": _("Məhsullar"), - "is_group": 0, - "parent_item_group": _("Bütün Element Qrupları"), - "show_in_website": 1, - }, - { - "doctype": "Item Group", - "item_group_name": _("Xammal"), - "is_group": 0, - "parent_item_group": _("Bütün Element Qrupları"), - }, - { - "doctype": "Item Group", - "item_group_name": _("Xidmətlər"), - "is_group": 0, - "parent_item_group": _("Bütün Element Qrupları"), - }, - { - "doctype": "Item Group", - "item_group_name": _("Alt məclislər"), - "is_group": 0, - "parent_item_group": _("Bütün Element Qrupları"), - }, - { - "doctype": "Item Group", - "item_group_name": _("İstehlak materialı"), - "is_group": 0, - "parent_item_group": _("Bütün Element Qrupları"), - }, - # Stock Entry Type - { - "doctype": "Stock Entry Type", - "name": _("Material Issue"), - "purpose": "Material Issue", - "is_standard": 1, - }, - { - "doctype": "Stock Entry Type", - "name": _("Material Receipt"), - "purpose": "Material Receipt", - "is_standard": 1, - }, - { - "doctype": "Stock Entry Type", - "name": _("Material Transfer"), - "purpose": "Material Transfer", - "is_standard": 1, - }, - { - "doctype": "Stock Entry Type", - "name": _("Manufacture"), - "purpose": "Manufacture", - "is_standard": 1, - }, - { - "doctype": "Stock Entry Type", - "name": _("Repack"), - "purpose": "Repack", - "is_standard": 1, - }, - {"doctype": "Stock Entry Type", "name": "Disassemble", "purpose": "Disassemble", "is_standard": 1}, - { - "doctype": "Stock Entry Type", - "name": _("Send to Subcontractor"), - "purpose": "Send to Subcontractor", - "is_standard": 1, - }, - { - "doctype": "Stock Entry Type", - "name": _("Material Transfer for Manufacture"), - "purpose": "Material Transfer for Manufacture", - "is_standard": 1, - }, - { - "doctype": "Stock Entry Type", - "name": _("Material Consumption for Manufacture"), - "purpose": "Material Consumption for Manufacture", - "is_standard": 1, - }, - # territory: with two default territories, one for home country and one named Rest of the World - { - "doctype": "Territory", - "territory_name": _("All Territories"), - "is_group": 1, - "name": _("All Territories"), - "parent_territory": "", - }, - { - "doctype": "Territory", - "territory_name": country.replace("'", ""), - "is_group": 0, - "parent_territory": _("All Territories"), - }, - { - "doctype": "Territory", - "territory_name": _("Rest Of The World"), - "is_group": 0, - "parent_territory": _("All Territories"), - }, - # customer group - { - "doctype": "Customer Group", - "customer_group_name": _("All Customer Groups"), - "is_group": 1, - "name": _("All Customer Groups"), - "parent_customer_group": "", - }, - { - "doctype": "Customer Group", - "customer_group_name": _("Individual"), - "is_group": 0, - "parent_customer_group": _("All Customer Groups"), - }, - { - "doctype": "Customer Group", - "customer_group_name": _("Commercial"), - "is_group": 0, - "parent_customer_group": _("All Customer Groups"), - }, - { - "doctype": "Customer Group", - "customer_group_name": _("Non Profit"), - "is_group": 0, - "parent_customer_group": _("All Customer Groups"), - }, - { - "doctype": "Customer Group", - "customer_group_name": _("Government"), - "is_group": 0, - "parent_customer_group": _("All Customer Groups"), - }, - # supplier group - { - "doctype": "Supplier Group", - "supplier_group_name": _("All Supplier Groups"), - "is_group": 1, - "name": _("All Supplier Groups"), - "parent_supplier_group": "", - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Xidmətlər"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Local"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Xammal"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Electrical"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Hardware"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Pharmaceutical"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - { - "doctype": "Supplier Group", - "supplier_group_name": _("Distributor"), - "is_group": 0, - "parent_supplier_group": _("All Supplier Groups"), - }, - # Sales Person - { - "doctype": "Sales Person", - "sales_person_name": _("Sales Team"), - "is_group": 1, - "parent_sales_person": "", - }, - # Mode of Payment - { - "doctype": "Mode of Payment", - "mode_of_payment": "Check" if country == "United States" else _("Cheque"), - "type": "Bank", - }, - {"doctype": "Mode of Payment", "mode_of_payment": _("Cash"), "type": "Cash"}, - {"doctype": "Mode of Payment", "mode_of_payment": _("Credit Card"), "type": "Bank"}, - {"doctype": "Mode of Payment", "mode_of_payment": _("Wire Transfer"), "type": "Bank"}, - {"doctype": "Mode of Payment", "mode_of_payment": _("Bank Draft"), "type": "Bank"}, - # Activity Type - {"doctype": "Activity Type", "activity_type": _("Planning")}, - {"doctype": "Activity Type", "activity_type": _("Research")}, - {"doctype": "Activity Type", "activity_type": _("Proposal Writing")}, - {"doctype": "Activity Type", "activity_type": _("Execution")}, - {"doctype": "Activity Type", "activity_type": _("Communication")}, - { - "doctype": "Item Attribute", - "attribute_name": _("Size"), - "item_attribute_values": [ - {"attribute_value": _("Extra Small"), "abbr": "XS"}, - {"attribute_value": _("Small"), "abbr": "S"}, - {"attribute_value": _("Medium"), "abbr": "M"}, - {"attribute_value": _("Large"), "abbr": "L"}, - {"attribute_value": _("Extra Large"), "abbr": "XL"}, - ], - }, - { - "doctype": "Item Attribute", - "attribute_name": _("Colour"), - "item_attribute_values": [ - {"attribute_value": _("Red"), "abbr": "RED"}, - {"attribute_value": _("Green"), "abbr": "GRE"}, - {"attribute_value": _("Blue"), "abbr": "BLU"}, - {"attribute_value": _("Black"), "abbr": "BLA"}, - {"attribute_value": _("White"), "abbr": "WHI"}, - ], - }, - # Issue Priority - {"doctype": "Issue Priority", "name": _("Low")}, - {"doctype": "Issue Priority", "name": _("Medium")}, - {"doctype": "Issue Priority", "name": _("High")}, - {"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, - {"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, - {"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"}, - {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, - {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, - {"doctype": "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, - {"doctype": "Opportunity Type", "name": _("Sales")}, - {"doctype": "Opportunity Type", "name": _("Support")}, - {"doctype": "Opportunity Type", "name": _("Maintenance")}, - {"doctype": "Project Type", "project_type": _("Internal")}, - {"doctype": "Project Type", "project_type": _("External")}, - {"doctype": "Project Type", "project_type": _("Other")}, - {"doctype": "Print Heading", "print_heading": _("Credit Note")}, - {"doctype": "Print Heading", "print_heading": _("Debit Note")}, - # Share Management - {"doctype": "Share Type", "title": _("Equity")}, - {"doctype": "Share Type", "title": _("Preference")}, - # Market Segments - {"doctype": "Market Segment", "market_segment": _("Lower Income")}, - {"doctype": "Market Segment", "market_segment": _("Middle Income")}, - {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Warehouse Type - {"doctype": "Warehouse Type", "name": "Transit"}, - ] - - for doctype, title_field, filename in ( - ("Designation", "designation_name", "designation.txt"), - ("Sales Stage", "stage_name", "sales_stage.txt"), - ("Industry Type", "industry", "industry_type.txt"), - ("UTM Source", "name", "marketing_source.txt"), - ("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"), - ): - records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)] - - base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) - - records += [ - { - "doctype": "Email Template", - "name": _("Dispatch Notification"), - "response": response, - "subject": _("Your order is out for delivery!"), - "owner": frappe.session.user, - } - ] - - # Records for the Supplier Scorecard - from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records - - make_default_records() - make_records(records) - set_up_address_templates(default_country=country) - - # Импортируем функции из оригинального модуля - from erpnext.setup.setup_wizard.operations.install_fixtures import ( - update_selling_defaults, - update_buying_defaults, - add_uom_data, - update_item_variant_settings - ) - - update_selling_defaults() - update_buying_defaults() - add_uom_data() - update_item_variant_settings() - update_global_search_doctypes() - - -def execute(): - """Функция для применения monkey patch""" - import erpnext.setup.setup_wizard.operations.install_fixtures as install_fixtures - - # Заменяем функцию install на вашу кастомную версию - install_fixtures.install = custom_install - - frappe.logger().info("Custom install function has been applied via monkey patching") \ No newline at end of file