Skip to content

Ref manufacturing plan#5

Open
DHINESH00 wants to merge 5 commits intodevelop_aerelefrom
ref_manufacturing_plan
Open

Ref manufacturing plan#5
DHINESH00 wants to merge 5 commits intodevelop_aerelefrom
ref_manufacturing_plan

Conversation

@DHINESH00
Copy link

@DHINESH00 DHINESH00 commented Jan 9, 2026

PR Type

Enhancement, Bug fix


Description

  • Refactored on_submit to use bulk SQL update for manufacturing_order_qty instead of individual row updates

  • Implemented caching mechanism via get_manufacturing_plan_data() to reduce database queries during manufacturing order creation

  • Simplified BOM validation logic by removing complex BOM type checking and merging duplicate table handling

  • Removed unused helper functions (get_mwo, get_sales_order, get_repair_pending_ppo_sales_order) and consolidated into get_details_to_append

  • Refactored get_items_for_production() to use unified docs_to_append field and optimized MWO data fetching with GROUP BY

  • Added .pre-commit-config.yaml for code quality enforcement

  • Updated Manufacturing Plan schema to remove sales_order and manufacturing_work_order child tables


Diagram Walkthrough

flowchart LR
  A["on_submit"] -->|bulk SQL update| B["Sales Order Item<br/>manufacturing_order_qty"]
  A -->|cache data| C["get_manufacturing_plan_data"]
  C -->|fetch maps| D["SO/MWO/BOM/Customer<br/>Data Maps"]
  D -->|pass to loop| E["create_manufacturing_order"]
  E -->|use cached data| F["Reduce DB Queries"]
  G["get_items_for_production"] -->|unified field| H["docs_to_append"]
  H -->|GROUP BY query| I["MWO Data Aggregation"]
Loading

File Walkthrough

Relevant files
Enhancement, performance optimization
manufacturing_plan.py
Bulk update and caching optimization for manufacturing order creation

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.py

  • Replaced individual update_existing() calls with single bulk SQL
    UPDATE statement in on_submit()
  • Added get_manufacturing_plan_data() method to pre-fetch and cache all
    required document data (SO items, MWO, BOM, Customer, Item records)
  • Refactored create_manufacturing_order() to accept and use cached data
    maps instead of individual DB queries per row
  • Simplified validate_qty_with_bom_creation() by removing BOM type
    validation and auto-setting manufacturing_bom = bom
  • Consolidated get_items_for_production() to use single docs_to_append
    field and optimized MWO aggregation with GROUP BY SQL
  • Removed functions: get_mwo(), get_sales_order(),
    get_repair_pending_ppo_sales_order(), cancel_bom(), create_new_bom()
  • Added new helper functions: get_details_to_append(), map_docs(),
    fetch_doc_map()
+467/-440
Refactoring, code cleanup
utils.py
Cleanup utility functions and code formatting                       

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/doc_events/utils.py

  • Removed get_mwo() function (consolidated into main
    manufacturing_plan.py)
  • Refactored get_mwo_details() with improved code formatting and
    f-string to .format() conversion
  • Removed unused json import
+49/-54 
Refactoring
manufacturing_plan.js
Update method references for consolidated functions           

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.js

  • Updated get_sales_order button to call get_details_to_append instead
    of get_sales_order
  • Updated get_mwo button to call get_details_to_append instead of
    get_mwo
  • Changed map_current_doc to use custom map_docs method instead of
    frappe.model.mapper.map_docs
+3/-3     
Schema changes, configuration
manufacturing_plan.json
Remove child tables and update schema configuration           

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.json

  • Removed sales_order child table field and button
    get_items_for_production_order
  • Removed manufacturing_work_order child table field
  • Added grid_page_length: 50 and row_format: Dynamic configuration
  • Updated modified timestamp to 2025-12-17
+3/-23   
Schema changes
manufacturing_plan_table.json
Update field constraints and visibility in child table     

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan_table/manufacturing_plan_table.json

  • Changed qty_per_manufacturing_order field from reqd: 1 to
    non_negative: 1, read_only: 0
  • Changed mwo field from hidden: 1 to read_only: 1 (now visible but
    read-only)
  • Added grid_page_length: 50 and row_format: Dynamic configuration
  • Updated modified timestamp to 2025-12-18
+7/-4     
Configuration, dependencies
.pre-commit-config.yaml
Add pre-commit hooks for code quality                                       

.pre-commit-config.yaml

  • Added new pre-commit configuration file with hooks for trailing
    whitespace, YAML validation, merge conflicts, AST checks
  • Configured Prettier for JavaScript/Vue/SCSS formatting
  • Configured ESLint for JavaScript linting
  • Configured Ruff for Python import sorting, linting, and formatting
  • Set up CI auto-update schedule to weekly
+74/-0   


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

is_auto_command: True
is_new_pr: True
model_reasoning: vertex_ai/gemini-2.5-pro
model: gpt-5.2-2025-12-11
model_turbo: anthropic/claude-haiku-4-5-20251001
fallback_models: ['anthropic/claude-sonnet-4-5-20250929', 'bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0']
second_model_for_exhaustive_mode: o4-mini
git_provider: github
publish_output: True
publish_output_no_suggestions: True
publish_output_progress: True
verbosity_level: 0
publish_logs: False
debug_mode: False
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
use_global_wiki_settings_file: False
disable_auto_feedback: False
ai_timeout: 150
response_language: en-US
clone_repo_instead_of_fetch: True
always_clone: False
add_repo_metadata: True
clone_repo_time_limit: 300
publish_inline_comments_fallback_batch_size: 5
publish_inline_comments_fallback_sleep_time: 2
max_model_tokens: 32000
custom_model_max_tokens: -1
patch_extension_skip_types: ['.md', '.txt']
extra_allowed_extensions: []
allow_dynamic_context: True
allow_forward_dynamic_context: True
max_extra_lines_before_dynamic_context: 12
patch_extra_lines_before: 5
patch_extra_lines_after: 1
ai_handler: litellm
cli_mode: False
TRIAL_GIT_ORG_MAX_INVOKES_PER_MONTH: 75
TRIAL_RATIO_CLOSE_TO_LIMIT: 0.8
invite_only_mode: False
enable_request_access_msg_on_new_pr: False
check_also_invites_field: False
calculate_context: True
disable_checkboxes: False
output_relevant_configurations: True
large_patch_policy: clip
seed: -1
temperature: 0.2
allow_dynamic_context_ab_testing: False
choose_dynamic_context_ab_testing_ratio: 0.5
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^\\[Bump\\]']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: ['do-not-merge', 'skip-qodo']
ignore_ticket_labels: []
allow_only_specific_folders: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
enable_ai_metadata: True
present_reasoning: True
max_tickets: 10
max_tickets_chars: 8000
prevent_any_approval: False
enable_comment_approval: False
enable_auto_approval: False
auto_approve_for_low_review_effort: -1
auto_approve_for_no_suggestions: False
ensure_ticket_compliance: False
new_diff_format: True
new_diff_format_add_external_references: True
enable_custom_labels: False

[pr_description]

final_update_message: False
publish_labels: False
add_original_user_description: True
generate_ai_title: False
extra_instructions: Highlight any database schema changes (Doctype/child table modifications), new background jobs (frappe.enqueue / scheduler events),
and any changes that could increase DB queries or write amplification.

enable_pr_type: True
enable_help_text: False
enable_help_comment: False
bring_latest_tag: False
enable_pr_diagram: True
publish_description_as_comment: False
publish_description_as_comment_persistent: True
enable_semantic_files_types: True
collapsible_file_list: adaptive
collapsible_file_list_threshold: 8
inline_file_summary: False
use_description_markers: False
include_generated_by_header: True
enable_large_pr_handling: True
max_ai_calls: 4
auto_create_ticket: False

@qodo-free-for-open-source-projects

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🏅 Score: 62
🧪 No relevant tests
✅ No TODO sections
🔒 Security concerns

SQL injection:
Line 255 uses tuple(self.docs_to_append) directly in SQL query without validation. If docs_to_append contains user-controlled input, this could lead to SQL injection. Recommend validating entries or using parameterized queries with proper escaping for list values.

🔀 No multiple PR themes
⚡ Recommended focus areas for review

N+1 Query Pattern

Lines 395-397, 415-418, 437, 466-470, 479: Multiple fallback frappe.get_value/frappe.db.get_value calls inside the loop over self.manufacturing_plan_table (line 46). Even with caching, these fallbacks can trigger 1+N queries if cache misses occur. Recommend ensuring all required data is pre-fetched in get_manufacturing_plan_data() or adding explicit warnings if fallback queries are hit.

        for row in self.manufacturing_plan_table:
            if row.docname:
                create_manufacturing_order(self, row, cache_data)
                frappe.flags.is_manufactur_order_created = True
                if row.subcontracting:
                    is_subcontracting = True
                    # create_subcontracting_order(self, row)
                if row.manufacturing_bom is None:
                    frappe.throw(f"Row:{row.idx} Manufacturing Bom Missing")
        if frappe.flags.is_manufactur_order_created:
            frappe.msgprint(_("Manufacturing Orders Created Successfully"))

        if is_subcontracting:
            create_subcontracting_order(self)

    def validate(self):
        self.validate_qty_with_bom_creation()

    def validate_qty_with_bom_creation(self):
        total = 0
        for row in self.manufacturing_plan_table:
            # Validate Qty
            if not row.subcontracting:
                row.subcontracting_qty = 0
                row.supplier = None
            if (row.manufacturing_order_qty + row.subcontracting_qty) > row.pending_qty:
                error_message = _(
                    "Row #{0}: Total Order qty cannot be greater than {1}"
                ).format(row.idx, row.pending_qty)
                frappe.throw(error_message)
            total += cint(row.manufacturing_order_qty) + cint(row.subcontracting_qty)
            if row.qty_per_manufacturing_order == 0:
                frappe.throw(_("Qty per Manufacturing Order Can not  be 0"))

            # Set Manufacturing BOM if not set
            if not row.manufacturing_bom:
                row.manufacturing_bom = row.bom
        self.total_planned_qty = total

    def get_manufacturing_plan_data(self):
        so_items = set()
        mwo_items = set()
        bom_names = set()
        customer_names = set()
        item_codes = set()
        customer_diamond_keys = set()

        for row in self.manufacturing_plan_table:
            if row.docname:
                so_items.add(row.docname)
            if row.mwo:
                mwo_items.add(row.mwo)
            if row.manufacturing_bom:
                bom_names.add(row.manufacturing_bom)
            if row.serial_id_bom:
                bom_names.add(row.serial_id_bom)
            if row.customer:
                customer_names.add(row.customer)
            if row.item_code:
                item_codes.add(row.item_code)
            if row.customer and row.diamond_quality:
                customer_diamond_keys.add((row.customer, row.diamond_quality))

        so_data_map = fetch_doc_map(
            "Sales Order Item",
            so_items,
            ["name", "metal_type", "metal_touch", "metal_colour", "diamond_grade"],
        )

        mwo_data_map = fetch_doc_map(
            "Manufacturing Work Order",
            mwo_items,
            ["name", "metal_type", "metal_touch", "metal_colour", "master_bom"],
        )

        bom_data_map = fetch_doc_map(
            "BOM", bom_names, ["name", "metal_type_", "metal_colour", "metal_touch"]
        )

        customer_data_map = fetch_doc_map(
            "Customer", customer_names, ["name", "is_internal_customer"]
        )

        # Fetch Customer Diamond Grades
        customer_diamond_grade_map = {}
        if customer_diamond_keys:
            # We filter by 'parent' IN customer_names.
            cust_grades = frappe.get_all(
                "Customer Diamond Grade",
                filters={"parent": ["in", list(customer_names)]},
                fields=[
                    "parent",
                    "diamond_quality",
                    "diamond_grade_1",
                    "diamond_grade_2",
                    "diamond_grade_3",
                    "diamond_grade_4",
                ],
            )
            for cg in cust_grades:
                customer_diamond_grade_map[(cg.parent, cg.diamond_quality)] = cg

        item_data_map = fetch_doc_map("Item", item_codes, ["name", "has_batch_no"])

        # We can fetch all attribute values that are customer diamond qualities just in case.
        attr_values = frappe.get_all(
            "Attribute Value",
            filters={"is_customer_diamond_quality": 1},
            fields=["name"],
        )
        attribute_value_set = {d.name for d in attr_values}

        return {
            "so_data": so_data_map,
            "mwo_data": mwo_data_map,
            "bom_data": bom_data_map,
            "customer_data": customer_data_map,
            "item_data": item_data_map,
            "customer_diamond_grade": customer_diamond_grade_map,
            "attribute_value_set": attribute_value_set,
        }

    @frappe.whitelist()
    def get_items_for_production(self):
        if self.select_manufacture_order in ["Manufacturing", "Repair"]:
            SalesOrderItem = frappe.qb.DocType("Sales Order Item")
            Item = frappe.qb.DocType("Item")
            SalesOrder = frappe.qb.DocType("Sales Order")

            query = (
                frappe.qb.from_(SalesOrderItem)
                .join(Item)
                .on(SalesOrderItem.item_code == Item.name)
                .join(SalesOrder)
                .on(SalesOrderItem.parent == SalesOrder.name)
                .select(
                    SalesOrderItem.name.as_("docname"),
                    SalesOrderItem.parent.as_("sales_order"),
                    SalesOrderItem.item_code,
                    SalesOrderItem.bom,
                    SalesOrder.customer,
                    Item.mould.as_("mould_no"),
                    Item.master_bom.as_("master_bom"),
                    SalesOrderItem.diamond_quality,
                    SalesOrderItem.custom_customer_sample.as_("customer_sample"),
                    SalesOrderItem.custom_customer_voucher_no.as_(
                        "customer_voucher_no"
                    ),
                    SalesOrderItem.custom_customer_gold.as_("customer_gold"),
                    SalesOrderItem.custom_customer_diamond.as_("customer_diamond"),
                    SalesOrderItem.custom_customer_stone.as_("customer_stone"),
                    SalesOrderItem.custom_customer_good.as_("customer_good"),
                    SalesOrderItem.custom_customer_weight.as_("customer_weight"),
                    (SalesOrderItem.qty - SalesOrderItem.manufacturing_order_qty).as_(
                        "pending_qty"
                    ),
                    SalesOrderItem.order_form_type,
                    SalesOrderItem.custom_repair_type.as_("repair_type"),
                    SalesOrderItem.custom_product_type.as_("product_type"),
                    SalesOrderItem.serial_no,
                    SalesOrderItem.serial_id_bom,
                )
                .where(
                    (SalesOrderItem.parent.isin(self.docs_to_append))
                    & (SalesOrderItem.qty > SalesOrderItem.manufacturing_order_qty)
                )
            )

            if self.setting_type:
                query = query.where(SalesOrderItem.setting_type == self.setting_type)

            items = query.run(as_dict=True)

            self.manufacturing_plan_table = []
            for item_row in items:
                bom = item_row.get("bom") or item_row.get("master_bom")
                if bom:
                    item_row["manufacturing_order_qty"] = item_row.get("pending_qty")
                    if self.is_subcontracting:
                        item_row["subcontracting"] = self.is_subcontracting
                        item_row["subcontracting_qty"] = item_row.get("pending_qty")
                        item_row["supplier"] = self.supplier
                        item_row["estimated_delivery_date"] = self.estimated_date
                        item_row["purchase_type"] = self.purchase_type
                        item_row["manufacturing_order_qty"] = 0

                    item_row["qty_per_manufacturing_order"] = 1
                    item_row["bom"] = bom
                    item_row["order_form_type"] = item_row.get("order_form_type")
                    self.append("manufacturing_plan_table", item_row)
                else:
                    frappe.throw(
                        _(
                            f"Sales Order BOM Not Found.</br>Please Set Master BOM for <b>{item_row.get("item_code")}</b> into Item Master"
                        )
                    )
        else:
            self.manufacturing_plan_table = []

        mwo_data = frappe.db.sql(
            """
			SELECT
				item_code,
				SUM(qty) AS total_qty,
				MIN(name) AS mwo
			FROM `tabManufacturing Work Order`
			WHERE name IN %(docs)s
			GROUP BY item_code
		""",
            {"docs": tuple(self.docs_to_append)},
            as_dict=True,
        )

        for row in mwo_data:
            qty = row["total_qty"]
            self.append(
                "manufacturing_plan_table",
                {
                    "item_code": row["item_code"],
                    "pending_qty": qty,
                    "manufacturing_order_qty": qty,
                    "qty_per_manufacturing_order": qty,
                    "mwo": row["mwo"],
                },
            )


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_pending_ppo_sales_order(doctype, txt, searchfield, start, page_len, filters):
    SalesOrder = frappe.qb.DocType("Sales Order")
    SalesOrderItem = frappe.qb.DocType("Sales Order Item")

    conditions = (
        (SalesOrderItem.qty > SalesOrderItem.manufacturing_order_qty)
        & (SalesOrderItem.order_form_type != "Repair Order")
        & (SalesOrder.custom_repair_order_form.isnull())
    )

    if txt:
        conditions &= SalesOrder.name.like(f"%{txt}%")

    if customer := filters.get("customer"):
        conditions &= SalesOrder.customer == customer

    if company := filters.get("company"):
        conditions &= SalesOrder.company == company

    if branch := filters.get("branch"):
        conditions &= SalesOrder.branch == branch

    if txn_date := filters.get("transaction_date"):
        conditions &= SalesOrder.transaction_date == txn_date

    query = (
        frappe.qb.from_(SalesOrder)
        .distinct()
        .from_(SalesOrderItem)
        .select(
            SalesOrder.name,
            SalesOrder.transaction_date,
            SalesOrder.company,
            SalesOrder.customer,
        )
        .where(
            (SalesOrder.name == SalesOrderItem.parent)
            & (SalesOrder.docstatus == 1)
            & conditions
        )
        .orderby(SalesOrder.transaction_date, order=frappe.qb.desc)
        .limit(page_len)
        .offset(start)
    )
    so_data = query.run(as_dict=True)

    return so_data


@frappe.whitelist()
def get_details_to_append(source_names, target_doc=None):
    if not target_doc:
        target_doc = frappe.new_doc("Manufacturing Plan")
    elif isinstance(target_doc, str):
        target_doc = frappe.get_doc(json.loads(target_doc))
    target_doc.docs_to_append = json.loads(source_names)
    target_doc.get_items_for_production()
    return target_doc


@frappe.whitelist()
def map_docs(method, source_names, target_doc, args=None):
    method = frappe.get_attr(frappe.override_whitelisted_method(method))
    if method not in frappe.whitelisted:
        raise frappe.PermissionError
    _args = (
        (source_names, target_doc, json.loads(args))
        if args
        else (source_names, target_doc)
    )
    target_doc = method(*_args)
    return target_doc


def fetch_doc_map(doctype, names, fields, key_field="name"):
    if not names:
        return {}

    data = frappe.get_all(
        doctype,
        filters={"name": ["in", list(names)]},
        fields=fields,
    )

    return {d[key_field]: d for d in data}


def create_manufacturing_order(doc, row, cache_data=None):
    if cache_data is None:
        cache_data = {}

    cnt = int(row.manufacturing_order_qty / row.qty_per_manufacturing_order)

    if not cnt:
        return

    so_data_map = cache_data.get("so_data", {})
    mwo_data_map = cache_data.get("mwo_data", {})
    bom_data_map = cache_data.get("bom_data", {})
    customer_data_map = cache_data.get("customer_data", {})
    item_data_map = cache_data.get("item_data", {})
    attribute_value_set = cache_data.get("attribute_value_set", set())
    customer_diamond_grade_map = cache_data.get("customer_diamond_grade", {})

    so_det = {}
    # Use plain dict copy instead of frappe._dict for memory/speed
    if row.sales_order and row.docname in so_data_map:
        so_det = so_data_map[row.docname].copy()
    elif row.mwo and row.mwo in mwo_data_map:
        so_det = mwo_data_map[row.mwo].copy()
    else:
        # Fallback
        doc_type, docname = (
            ("Sales Order Item", row.docname)
            if row.sales_order
            else ("Manufacturing Work Order", row.mwo)
        )
        fields = ["metal_type", "metal_touch", "metal_colour"]
        if row.mwo:
            fields.append("master_bom")

        fetched_val = frappe.get_value(doc_type, docname, fields, as_dict=1)
        if fetched_val:
            so_det = fetched_val

    master_bom = None
    if doc.select_manufacture_order == "Manufacturing":
        master_bom = row.manufacturing_bom
    elif (
        doc.select_manufacture_order == "Repair"
        and row.order_form_type == "Repair Order"
    ):
        master_bom = row.serial_id_bom

    if master_bom:
        # caching check
        bom_details = bom_data_map.get(master_bom)
        if not bom_details:
            bom_details = frappe.db.get_value(
                "BOM",
                master_bom,
                ["metal_type_", "metal_colour", "metal_touch"],
                as_dict=1,
            )

        if bom_details:
            # Update dictionary values using subscript notation
            so_det["metal_type"] = bom_details.get("metal_type_") or so_det.get(
                "metal_type_"
            )
            so_det["metal_colour"] = bom_details.get("metal_colour") or so_det.get(
                "metal_colour"
            )
            so_det["metal_touch"] = bom_details.get("metal_touch") or so_det.get(
                "metal_touch"
            )

    # Check for internal customer
    customer_info = customer_data_map.get(row.customer)
    is_internal_customer = (
        customer_info.get("is_internal_customer")
        if customer_info
        else frappe.db.get_value("Customer", row.customer, "is_internal_customer")
    )

    if row.diamond_quality and not is_internal_customer:
        key = (row.customer, row.diamond_quality)
        diamond_grade = None

        if row.customer_diamond == "Yes":
            # Use cached customer_diamond_grade_map
            diamond_grade_data = customer_diamond_grade_map.get(key)
            if diamond_grade_data:
                grades_to_check = [
                    diamond_grade_data.get("diamond_grade_1"),
                    diamond_grade_data.get("diamond_grade_2"),
                    diamond_grade_data.get("diamond_grade_3"),
                    diamond_grade_data.get("diamond_grade_4"),
                ]
                for grade in grades_to_check:
                    if grade and grade in attribute_value_set:
                        diamond_grade = grade
                        break
                    # We trust attribute_value_set contains all relevant values, no fallback needed

        else:
            diamond_grade_data = customer_diamond_grade_map.get(key)
            if diamond_grade_data:
                diamond_grade = diamond_grade_data.get("diamond_grade_1")
            else:
                # Minimal fallback
                diamond_grade = frappe.db.get_value(
                    "Customer Diamond Grade",
                    {"parent": row.customer, "diamond_quality": row.diamond_quality},
                    "diamond_grade_1",
                )

        so_det["diamond_grade"] = diamond_grade

        has_batch_no = False
        item_info = item_data_map.get(row.item_code)
        if item_info:
            has_batch_no = item_info.get("has_batch_no")
        else:
            has_batch_no = frappe.db.get_value("Item", row.item_code, "has_batch_no")

        if not so_det.get("diamond_grade") and not has_batch_no:
            frappe.throw(
                _("Diamond Grade is not mentioned in customer {0}").format(row.customer)
            )

    for i in range(0, cnt):
        make_manufacturing_order(doc, row, master_bom=master_bom, so_det=so_det)
SQL Injection Risk

Lines 22-41: Raw SQL UPDATE with parameter binding is used correctly. However, line 255 uses tuple(self.docs_to_append) directly in SQL without validation. If docs_to_append contains untrusted input, this could be exploited. Recommend validating docs_to_append entries or using parameterized queries with proper escaping.

        mwo_data = frappe.db.sql(
            """
			SELECT
				item_code,
				SUM(qty) AS total_qty,
				MIN(name) AS mwo
			FROM `tabManufacturing Work Order`
			WHERE name IN %(docs)s
			GROUP BY item_code
		""",
            {"docs": tuple(self.docs_to_append)},
            as_dict=True,
        )
Heavy Loop Work

Lines 486-487: make_manufacturing_order() is called cnt times inside a loop (line 46 iterates manufacturing_plan_table, line 486 iterates cnt times). This nested loop can create a large number of Manufacturing Orders synchronously. Recommend batching or using frappe.enqueue for background processing to avoid blocking the request.

        for row in self.manufacturing_plan_table:
            if row.docname:
                create_manufacturing_order(self, row, cache_data)
                frappe.flags.is_manufactur_order_created = True
                if row.subcontracting:
                    is_subcontracting = True
                    # create_subcontracting_order(self, row)
                if row.manufacturing_bom is None:
                    frappe.throw(f"Row:{row.idx} Manufacturing Bom Missing")
        if frappe.flags.is_manufactur_order_created:
            frappe.msgprint(_("Manufacturing Orders Created Successfully"))

        if is_subcontracting:
            create_subcontracting_order(self)

    def validate(self):
        self.validate_qty_with_bom_creation()

    def validate_qty_with_bom_creation(self):
        total = 0
        for row in self.manufacturing_plan_table:
            # Validate Qty
            if not row.subcontracting:
                row.subcontracting_qty = 0
                row.supplier = None
            if (row.manufacturing_order_qty + row.subcontracting_qty) > row.pending_qty:
                error_message = _(
                    "Row #{0}: Total Order qty cannot be greater than {1}"
                ).format(row.idx, row.pending_qty)
                frappe.throw(error_message)
            total += cint(row.manufacturing_order_qty) + cint(row.subcontracting_qty)
            if row.qty_per_manufacturing_order == 0:
                frappe.throw(_("Qty per Manufacturing Order Can not  be 0"))

            # Set Manufacturing BOM if not set
            if not row.manufacturing_bom:
                row.manufacturing_bom = row.bom
        self.total_planned_qty = total

    def get_manufacturing_plan_data(self):
        so_items = set()
        mwo_items = set()
        bom_names = set()
        customer_names = set()
        item_codes = set()
        customer_diamond_keys = set()

        for row in self.manufacturing_plan_table:
            if row.docname:
                so_items.add(row.docname)
            if row.mwo:
                mwo_items.add(row.mwo)
            if row.manufacturing_bom:
                bom_names.add(row.manufacturing_bom)
            if row.serial_id_bom:
                bom_names.add(row.serial_id_bom)
            if row.customer:
                customer_names.add(row.customer)
            if row.item_code:
                item_codes.add(row.item_code)
            if row.customer and row.diamond_quality:
                customer_diamond_keys.add((row.customer, row.diamond_quality))

        so_data_map = fetch_doc_map(
            "Sales Order Item",
            so_items,
            ["name", "metal_type", "metal_touch", "metal_colour", "diamond_grade"],
        )

        mwo_data_map = fetch_doc_map(
            "Manufacturing Work Order",
            mwo_items,
            ["name", "metal_type", "metal_touch", "metal_colour", "master_bom"],
        )

        bom_data_map = fetch_doc_map(
            "BOM", bom_names, ["name", "metal_type_", "metal_colour", "metal_touch"]
        )

        customer_data_map = fetch_doc_map(
            "Customer", customer_names, ["name", "is_internal_customer"]
        )

        # Fetch Customer Diamond Grades
        customer_diamond_grade_map = {}
        if customer_diamond_keys:
            # We filter by 'parent' IN customer_names.
            cust_grades = frappe.get_all(
                "Customer Diamond Grade",
                filters={"parent": ["in", list(customer_names)]},
                fields=[
                    "parent",
                    "diamond_quality",
                    "diamond_grade_1",
                    "diamond_grade_2",
                    "diamond_grade_3",
                    "diamond_grade_4",
                ],
            )
            for cg in cust_grades:
                customer_diamond_grade_map[(cg.parent, cg.diamond_quality)] = cg

        item_data_map = fetch_doc_map("Item", item_codes, ["name", "has_batch_no"])

        # We can fetch all attribute values that are customer diamond qualities just in case.
        attr_values = frappe.get_all(
            "Attribute Value",
            filters={"is_customer_diamond_quality": 1},
            fields=["name"],
        )
        attribute_value_set = {d.name for d in attr_values}

        return {
            "so_data": so_data_map,
            "mwo_data": mwo_data_map,
            "bom_data": bom_data_map,
            "customer_data": customer_data_map,
            "item_data": item_data_map,
            "customer_diamond_grade": customer_diamond_grade_map,
            "attribute_value_set": attribute_value_set,
        }

    @frappe.whitelist()
    def get_items_for_production(self):
        if self.select_manufacture_order in ["Manufacturing", "Repair"]:
            SalesOrderItem = frappe.qb.DocType("Sales Order Item")
            Item = frappe.qb.DocType("Item")
            SalesOrder = frappe.qb.DocType("Sales Order")

            query = (
                frappe.qb.from_(SalesOrderItem)
                .join(Item)
                .on(SalesOrderItem.item_code == Item.name)
                .join(SalesOrder)
                .on(SalesOrderItem.parent == SalesOrder.name)
                .select(
                    SalesOrderItem.name.as_("docname"),
                    SalesOrderItem.parent.as_("sales_order"),
                    SalesOrderItem.item_code,
                    SalesOrderItem.bom,
                    SalesOrder.customer,
                    Item.mould.as_("mould_no"),
                    Item.master_bom.as_("master_bom"),
                    SalesOrderItem.diamond_quality,
                    SalesOrderItem.custom_customer_sample.as_("customer_sample"),
                    SalesOrderItem.custom_customer_voucher_no.as_(
                        "customer_voucher_no"
                    ),
                    SalesOrderItem.custom_customer_gold.as_("customer_gold"),
                    SalesOrderItem.custom_customer_diamond.as_("customer_diamond"),
                    SalesOrderItem.custom_customer_stone.as_("customer_stone"),
                    SalesOrderItem.custom_customer_good.as_("customer_good"),
                    SalesOrderItem.custom_customer_weight.as_("customer_weight"),
                    (SalesOrderItem.qty - SalesOrderItem.manufacturing_order_qty).as_(
                        "pending_qty"
                    ),
                    SalesOrderItem.order_form_type,
                    SalesOrderItem.custom_repair_type.as_("repair_type"),
                    SalesOrderItem.custom_product_type.as_("product_type"),
                    SalesOrderItem.serial_no,
                    SalesOrderItem.serial_id_bom,
                )
                .where(
                    (SalesOrderItem.parent.isin(self.docs_to_append))
                    & (SalesOrderItem.qty > SalesOrderItem.manufacturing_order_qty)
                )
            )

            if self.setting_type:
                query = query.where(SalesOrderItem.setting_type == self.setting_type)

            items = query.run(as_dict=True)

            self.manufacturing_plan_table = []
            for item_row in items:
                bom = item_row.get("bom") or item_row.get("master_bom")
                if bom:
                    item_row["manufacturing_order_qty"] = item_row.get("pending_qty")
                    if self.is_subcontracting:
                        item_row["subcontracting"] = self.is_subcontracting
                        item_row["subcontracting_qty"] = item_row.get("pending_qty")
                        item_row["supplier"] = self.supplier
                        item_row["estimated_delivery_date"] = self.estimated_date
                        item_row["purchase_type"] = self.purchase_type
                        item_row["manufacturing_order_qty"] = 0

                    item_row["qty_per_manufacturing_order"] = 1
                    item_row["bom"] = bom
                    item_row["order_form_type"] = item_row.get("order_form_type")
                    self.append("manufacturing_plan_table", item_row)
                else:
                    frappe.throw(
                        _(
                            f"Sales Order BOM Not Found.</br>Please Set Master BOM for <b>{item_row.get("item_code")}</b> into Item Master"
                        )
                    )
        else:
            self.manufacturing_plan_table = []

        mwo_data = frappe.db.sql(
            """
			SELECT
				item_code,
				SUM(qty) AS total_qty,
				MIN(name) AS mwo
			FROM `tabManufacturing Work Order`
			WHERE name IN %(docs)s
			GROUP BY item_code
		""",
            {"docs": tuple(self.docs_to_append)},
            as_dict=True,
        )

        for row in mwo_data:
            qty = row["total_qty"]
            self.append(
                "manufacturing_plan_table",
                {
                    "item_code": row["item_code"],
                    "pending_qty": qty,
                    "manufacturing_order_qty": qty,
                    "qty_per_manufacturing_order": qty,
                    "mwo": row["mwo"],
                },
            )


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_pending_ppo_sales_order(doctype, txt, searchfield, start, page_len, filters):
    SalesOrder = frappe.qb.DocType("Sales Order")
    SalesOrderItem = frappe.qb.DocType("Sales Order Item")

    conditions = (
        (SalesOrderItem.qty > SalesOrderItem.manufacturing_order_qty)
        & (SalesOrderItem.order_form_type != "Repair Order")
        & (SalesOrder.custom_repair_order_form.isnull())
    )

    if txt:
        conditions &= SalesOrder.name.like(f"%{txt}%")

    if customer := filters.get("customer"):
        conditions &= SalesOrder.customer == customer

    if company := filters.get("company"):
        conditions &= SalesOrder.company == company

    if branch := filters.get("branch"):
        conditions &= SalesOrder.branch == branch

    if txn_date := filters.get("transaction_date"):
        conditions &= SalesOrder.transaction_date == txn_date

    query = (
        frappe.qb.from_(SalesOrder)
        .distinct()
        .from_(SalesOrderItem)
        .select(
            SalesOrder.name,
            SalesOrder.transaction_date,
            SalesOrder.company,
            SalesOrder.customer,
        )
        .where(
            (SalesOrder.name == SalesOrderItem.parent)
            & (SalesOrder.docstatus == 1)
            & conditions
        )
        .orderby(SalesOrder.transaction_date, order=frappe.qb.desc)
        .limit(page_len)
        .offset(start)
    )
    so_data = query.run(as_dict=True)

    return so_data


@frappe.whitelist()
def get_details_to_append(source_names, target_doc=None):
    if not target_doc:
        target_doc = frappe.new_doc("Manufacturing Plan")
    elif isinstance(target_doc, str):
        target_doc = frappe.get_doc(json.loads(target_doc))
    target_doc.docs_to_append = json.loads(source_names)
    target_doc.get_items_for_production()
    return target_doc


@frappe.whitelist()
def map_docs(method, source_names, target_doc, args=None):
    method = frappe.get_attr(frappe.override_whitelisted_method(method))
    if method not in frappe.whitelisted:
        raise frappe.PermissionError
    _args = (
        (source_names, target_doc, json.loads(args))
        if args
        else (source_names, target_doc)
    )
    target_doc = method(*_args)
    return target_doc


def fetch_doc_map(doctype, names, fields, key_field="name"):
    if not names:
        return {}

    data = frappe.get_all(
        doctype,
        filters={"name": ["in", list(names)]},
        fields=fields,
    )

    return {d[key_field]: d for d in data}


def create_manufacturing_order(doc, row, cache_data=None):
    if cache_data is None:
        cache_data = {}

    cnt = int(row.manufacturing_order_qty / row.qty_per_manufacturing_order)

    if not cnt:
        return

    so_data_map = cache_data.get("so_data", {})
    mwo_data_map = cache_data.get("mwo_data", {})
    bom_data_map = cache_data.get("bom_data", {})
    customer_data_map = cache_data.get("customer_data", {})
    item_data_map = cache_data.get("item_data", {})
    attribute_value_set = cache_data.get("attribute_value_set", set())
    customer_diamond_grade_map = cache_data.get("customer_diamond_grade", {})

    so_det = {}
    # Use plain dict copy instead of frappe._dict for memory/speed
    if row.sales_order and row.docname in so_data_map:
        so_det = so_data_map[row.docname].copy()
    elif row.mwo and row.mwo in mwo_data_map:
        so_det = mwo_data_map[row.mwo].copy()
    else:
        # Fallback
        doc_type, docname = (
            ("Sales Order Item", row.docname)
            if row.sales_order
            else ("Manufacturing Work Order", row.mwo)
        )
        fields = ["metal_type", "metal_touch", "metal_colour"]
        if row.mwo:
            fields.append("master_bom")

        fetched_val = frappe.get_value(doc_type, docname, fields, as_dict=1)
        if fetched_val:
            so_det = fetched_val

    master_bom = None
    if doc.select_manufacture_order == "Manufacturing":
        master_bom = row.manufacturing_bom
    elif (
        doc.select_manufacture_order == "Repair"
        and row.order_form_type == "Repair Order"
    ):
        master_bom = row.serial_id_bom

    if master_bom:
        # caching check
        bom_details = bom_data_map.get(master_bom)
        if not bom_details:
            bom_details = frappe.db.get_value(
                "BOM",
                master_bom,
                ["metal_type_", "metal_colour", "metal_touch"],
                as_dict=1,
            )

        if bom_details:
            # Update dictionary values using subscript notation
            so_det["metal_type"] = bom_details.get("metal_type_") or so_det.get(
                "metal_type_"
            )
            so_det["metal_colour"] = bom_details.get("metal_colour") or so_det.get(
                "metal_colour"
            )
            so_det["metal_touch"] = bom_details.get("metal_touch") or so_det.get(
                "metal_touch"
            )

    # Check for internal customer
    customer_info = customer_data_map.get(row.customer)
    is_internal_customer = (
        customer_info.get("is_internal_customer")
        if customer_info
        else frappe.db.get_value("Customer", row.customer, "is_internal_customer")
    )

    if row.diamond_quality and not is_internal_customer:
        key = (row.customer, row.diamond_quality)
        diamond_grade = None

        if row.customer_diamond == "Yes":
            # Use cached customer_diamond_grade_map
            diamond_grade_data = customer_diamond_grade_map.get(key)
            if diamond_grade_data:
                grades_to_check = [
                    diamond_grade_data.get("diamond_grade_1"),
                    diamond_grade_data.get("diamond_grade_2"),
                    diamond_grade_data.get("diamond_grade_3"),
                    diamond_grade_data.get("diamond_grade_4"),
                ]
                for grade in grades_to_check:
                    if grade and grade in attribute_value_set:
                        diamond_grade = grade
                        break
                    # We trust attribute_value_set contains all relevant values, no fallback needed

        else:
            diamond_grade_data = customer_diamond_grade_map.get(key)
            if diamond_grade_data:
                diamond_grade = diamond_grade_data.get("diamond_grade_1")
            else:
                # Minimal fallback
                diamond_grade = frappe.db.get_value(
                    "Customer Diamond Grade",
                    {"parent": row.customer, "diamond_quality": row.diamond_quality},
                    "diamond_grade_1",
                )

        so_det["diamond_grade"] = diamond_grade

        has_batch_no = False
        item_info = item_data_map.get(row.item_code)
        if item_info:
            has_batch_no = item_info.get("has_batch_no")
        else:
            has_batch_no = frappe.db.get_value("Item", row.item_code, "has_batch_no")

        if not so_det.get("diamond_grade") and not has_batch_no:
            frappe.throw(
                _("Diamond Grade is not mentioned in customer {0}").format(row.customer)
            )

    for i in range(0, cnt):
        make_manufacturing_order(doc, row, master_bom=master_bom, so_det=so_det)

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

is_auto_command: True
is_new_pr: True
model_reasoning: vertex_ai/gemini-2.5-pro
model: gpt-5.2-2025-12-11
model_turbo: anthropic/claude-haiku-4-5-20251001
fallback_models: ['anthropic/claude-sonnet-4-5-20250929', 'bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0']
second_model_for_exhaustive_mode: o4-mini
git_provider: github
publish_output: True
publish_output_no_suggestions: True
publish_output_progress: True
verbosity_level: 0
publish_logs: False
debug_mode: False
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
use_global_wiki_settings_file: False
disable_auto_feedback: False
ai_timeout: 150
response_language: en-US
clone_repo_instead_of_fetch: True
always_clone: False
add_repo_metadata: True
clone_repo_time_limit: 300
publish_inline_comments_fallback_batch_size: 5
publish_inline_comments_fallback_sleep_time: 2
max_model_tokens: 32000
custom_model_max_tokens: -1
patch_extension_skip_types: ['.md', '.txt']
extra_allowed_extensions: []
allow_dynamic_context: True
allow_forward_dynamic_context: True
max_extra_lines_before_dynamic_context: 12
patch_extra_lines_before: 5
patch_extra_lines_after: 1
ai_handler: litellm
cli_mode: False
TRIAL_GIT_ORG_MAX_INVOKES_PER_MONTH: 75
TRIAL_RATIO_CLOSE_TO_LIMIT: 0.8
invite_only_mode: False
enable_request_access_msg_on_new_pr: False
check_also_invites_field: False
calculate_context: True
disable_checkboxes: False
output_relevant_configurations: True
large_patch_policy: clip
seed: -1
temperature: 0.2
allow_dynamic_context_ab_testing: False
choose_dynamic_context_ab_testing_ratio: 0.5
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^\\[Bump\\]']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: ['do-not-merge', 'skip-qodo']
ignore_ticket_labels: []
allow_only_specific_folders: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
enable_ai_metadata: True
present_reasoning: True
max_tickets: 10
max_tickets_chars: 8000
prevent_any_approval: False
enable_comment_approval: False
enable_auto_approval: False
auto_approve_for_low_review_effort: -1
auto_approve_for_no_suggestions: False
ensure_ticket_compliance: False
new_diff_format: True
new_diff_format_add_external_references: True
enable_custom_labels: False

[pr_reviewer]

require_score_review: True
require_tests_review: True
require_estimate_effort_to_review: True
require_can_be_split_review: True
require_security_review: True
require_todo_scan: True
require_ticket_analysis_review: True
require_ticket_labels: False
require_no_ticket_labels: False
check_pr_additional_content: False
persistent_comment: True
extra_instructions: You are a Senior Technical Architect specializing in the Frappe Framework, Python, and MariaDB optimization.
Be concise but strict about performance and security. Provide code snippets (or pseudocode) for fixes.

========================
1) 🚨 FRAPPE PERFORMANCE KILLERS (High Priority)
========================
A) N+1 Query Detection (must flag):
- Flag ANY loop that contains `frappe.get_doc`, `frappe.db.get_value`, `frappe.db.get_all/get_list`, or `frappe.db.sql`.
- Explain the query multiplier (e.g., “1 + N queries”) and propose a bulk-fetch refactor:
  - pre-fetch with `frappe.get_all/get_list` (with fields + filters) or one SQL query with IN,
  - build a dict/map keyed by name, then iterate without DB calls.

B) Heavy Writes / Controller chain (must flag):
- If `doc.save()` is used to update just 1–2 fields, aggressively suggest replacing with:
  - `frappe.db.set_value(doctype, name, field, value)`
  - or `frappe.db.set_value(doctype, name, {field1: v1, field2: v2})`
- Explain it avoids triggering heavy validations / hooks / controller logic unnecessarily.
- Exception: if controller validations are required for correctness, require justification.

C) ORM abuse / wrong API choice:
- Warn against `frappe.db.sql()` if the same can be achieved by permission-safe ORM (`frappe.get_list/get_all`)
  unless it is a complex join/aggregation needing SQL.
- Also warn when `frappe.db.*` is used in user-facing endpoints where permission checks matter.

D) Redundant commits (must flag):
- `frappe.db.commit()` inside loops is forbidden unless there is a carefully justified transactional reason.
- Suggest moving commit outside loop, batching, or using background jobs.

E) Unbounded reads:
- Flag `get_all/get_list` without strong filters or without pagination on potentially large tables.
- Recommend adding filters, indexed ordering, `limit/page_length`, or background processing.

F) Hot hooks:
- Flag heavy work (bulk queries, file IO, network calls) inside validate/on_update/after_insert hooks.
- Recommend `frappe.enqueue` and async jobs.

========================
2) 🛡️ SECURITY & SQL SAFETY (must be strict)
========================
A) SQL injection:
- Strictly forbid f-strings / concatenation inside `frappe.db.sql()`.
- REQUIRE parameter passing: `frappe.db.sql("... %s", (val,))`.

B) Whitelisting:
- If functions are intended to be called from client (JS / REST), ensure `@frappe.whitelist()` exists.
- Confirm sensitive functions are NOT accidentally exposed.

C) Permission checks:
- For new `get_list/get_all`/SQL calls, ensure strict permissions are not bypassed.
- If `ignore_permissions=True` or `frappe.db.*` is used, REQUIRE explicit justification + risk assessment.

D) Unsafe patterns:
- Flag `eval/exec`, insecure deserialization, and unsafe file path usage.

========================
3) 🐍 PYTHONIC & LOGIC OPTIMIZATIONS
========================
A) Mutable defaults (must flag):
- `def f(x=[])` / `def f(x={})` etc. Require `None` + initialization.

B) Data structure choices:
- If code does `if item in big_list`, suggest `set(big_list)` for O(1) lookups.

C) Inefficient loops:
- Reduce nested loops; pull invariants out; memoize expensive computations.

D) List building:
- Suggest list comprehensions ONLY when readability remains good.
- Avoid creating large intermediate lists when generators/chunking is better.

E) String + datetime churn:
- Flag repeated parsing/formatting/conversions inside loops.

========================
4) 📉 MARIADB / DATABASE OPTIMIZATION CHECKS
========================
A) Wildcards / over-fetching:
- Warn against `SELECT *`. Require selecting specific columns.

B) Indexing:
- If a field is used in WHERE / JOIN / ORDER BY patterns frequently, ask if it needs an index.
- If appropriate, suggest composite index order aligned with query filters (e.g., company + docstatus + posting_date).

C) Sorting + limiting:
- Flag ORDER BY on large sets without indexed order; recommend index or reduce rows first.

D) Aggregations:
- If Python loops compute SUM/COUNT over many rows, suggest SQL aggregation where safe.

========================
5) OUTPUT REQUIREMENTS
========================
- Provide concrete refactor steps and code snippets.
- Quantify impact when possible: “reduces from N queries to 1”.
- If table sizes/indexes are unknown, recommend checking EXPLAIN and adding indexes if confirmed.

final_update_message: False
enable_review_labels_security: True
enable_review_labels_effort: True
enable_help_text: False
num_max_findings: 8

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Jan 9, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Critical logic for cancellation is missing

The on_cancel method, which reverts quantity updates on Sales Order Item, was
removed. It must be reinstated to prevent data inconsistency when a
Manufacturing Plan is cancelled.

Examples:

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.py [16-60]
class ManufacturingPlan(Document):
    def on_submit(self):
        is_subcontracting = False
        # customer_diamond_data removed for memory efficiency
        frappe.db.sql(
            """
					UPDATE `tabSales Order Item` soi
					JOIN `tabManufacturing Plan Table` mpt
						ON soi.name = mpt.docname
					JOIN `tabManufacturing Plan` mp

 ... (clipped 35 lines)

Solution Walkthrough:

Before:

class ManufacturingPlan(Document):
    def on_submit(self):
        # Bulk update Sales Order Item quantities
        frappe.db.sql("""
            UPDATE `tabSales Order Item` soi
            ...
            SET
                soi.manufacturing_order_qty =
                    COALESCE(soi.manufacturing_order_qty, 0)
                    + COALESCE(mpt.manufacturing_order_qty, 0)
                    + COALESCE(mpt.subcontracting_qty, 0)
            ...
        """)
        # ... logic to create manufacturing orders

    # The on_cancel method was removed, leaving no rollback mechanism.

After:

class ManufacturingPlan(Document):
    def on_submit(self):
        # Bulk update Sales Order Item quantities
        frappe.db.sql("""
            UPDATE `tabSales Order Item` ...
            SET soi.manufacturing_order_qty = ...
        """)
        # ... logic to create manufacturing orders

    def on_cancel(self):
        # Reinstate bulk update to revert quantities
        frappe.db.sql("""
            UPDATE `tabSales Order Item` soi
            JOIN `tabManufacturing Plan Table` mpt ON soi.name = mpt.docname
            WHERE mpt.parent = %(mp_name)s
            SET
                soi.manufacturing_order_qty = GREATEST(0,
                    COALESCE(soi.manufacturing_order_qty, 0)
                    - COALESCE(mpt.manufacturing_order_qty, 0)
                    - COALESCE(mpt.subcontracting_qty, 0)
                )
        """, {"mp_name": self.name})
Suggestion importance[1-10]: 10

__

Why: This suggestion correctly identifies a critical bug where removing the on_cancel method breaks data integrity, as there is no longer a way to reverse the quantity updates made on on_submit.

High
Security
Use an explicit allow-list for methods

Enhance security in the map_docs function by replacing the generic
frappe.whitelisted check with an explicit allow-list of callable methods to
prevent unauthorized function execution.

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.py [335-346]

 @frappe.whitelist()
 def map_docs(method, source_names, target_doc, args=None):
-    method = frappe.get_attr(frappe.override_whitelisted_method(method))
-    if method not in frappe.whitelisted:
+    # Explicitly define which methods are allowed to be called via this function
+    ALLOWED_METHODS = [
+        "jewellery_erpnext.jewellery_erpnext.doctype.manufacturing_plan.manufacturing_plan.get_details_to_append",
+        # Add other allowed methods here
+    ]
+
+    if method not in ALLOWED_METHODS:
+        frappe.throw(_("Method not allowed."), frappe.PermissionError)
+
+    method_obj = frappe.get_attr(frappe.override_whitelisted_method(method))
+    if method_obj not in frappe.whitelisted:
         raise frappe.PermissionError
     _args = (
         (source_names, target_doc, json.loads(args))
         if args
         else (source_names, target_doc)
     )
-    target_doc = method(*_args)
+    target_doc = method_obj(*_args)
     return target_doc
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant security vulnerability where any whitelisted function could be called, and proposes a robust solution by implementing an explicit allow-list.

High
General
Remove fallback DB queries in loop

Remove fallback database queries from the create_manufacturing_order function to
prevent potential N+1 performance issues and rely solely on the pre-fetched
cache.

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.py [362-487]

 def create_manufacturing_order(doc, row, cache_data=None):
     if cache_data is None:
         cache_data = {}
 
     cnt = int(row.manufacturing_order_qty / row.qty_per_manufacturing_order)
 
     if not cnt:
         return
 
     so_data_map = cache_data.get("so_data", {})
     mwo_data_map = cache_data.get("mwo_data", {})
     bom_data_map = cache_data.get("bom_data", {})
     customer_data_map = cache_data.get("customer_data", {})
     item_data_map = cache_data.get("item_data", {})
     attribute_value_set = cache_data.get("attribute_value_set", set())
     customer_diamond_grade_map = cache_data.get("customer_diamond_grade", {})
 
     so_det = {}
     # Use plain dict copy instead of frappe._dict for memory/speed
     if row.sales_order and row.docname in so_data_map:
         so_det = so_data_map[row.docname].copy()
     elif row.mwo and row.mwo in mwo_data_map:
         so_det = mwo_data_map[row.mwo].copy()
-    else:
-        # Fallback
-        doc_type, docname = (
-            ("Sales Order Item", row.docname)
-            if row.sales_order
-            else ("Manufacturing Work Order", row.mwo)
-        )
-        fields = ["metal_type", "metal_touch", "metal_colour"]
-        if row.mwo:
-            fields.append("master_bom")
-
-        fetched_val = frappe.get_value(doc_type, docname, fields, as_dict=1)
-        if fetched_val:
-            so_det = fetched_val
 
     master_bom = None
     if doc.select_manufacture_order == "Manufacturing":
         master_bom = row.manufacturing_bom
     elif (
         doc.select_manufacture_order == "Repair"
         and row.order_form_type == "Repair Order"
     ):
         master_bom = row.serial_id_bom
 
     if master_bom:
-        # caching check
         bom_details = bom_data_map.get(master_bom)
-        if not bom_details:
-            bom_details = frappe.db.get_value(
-                "BOM",
-                master_bom,
-                ["metal_type_", "metal_colour", "metal_touch"],
-                as_dict=1,
-            )
-
         if bom_details:
             # Update dictionary values using subscript notation
             so_det["metal_type"] = bom_details.get("metal_type_") or so_det.get(
-                "metal_type_"
+                "metal_type"
             )
             so_det["metal_colour"] = bom_details.get("metal_colour") or so_det.get(
                 "metal_colour"
             )
             so_det["metal_touch"] = bom_details.get("metal_touch") or so_det.get(
                 "metal_touch"
             )
 
     # Check for internal customer
     customer_info = customer_data_map.get(row.customer)
-    is_internal_customer = (
-        customer_info.get("is_internal_customer")
-        if customer_info
-        else frappe.db.get_value("Customer", row.customer, "is_internal_customer")
-    )
+    is_internal_customer = customer_info.get("is_internal_customer") if customer_info else False
 
     if row.diamond_quality and not is_internal_customer:
         key = (row.customer, row.diamond_quality)
         diamond_grade = None
+        diamond_grade_data = customer_diamond_grade_map.get(key)
 
-        if row.customer_diamond == "Yes":
-            # Use cached customer_diamond_grade_map
-            diamond_grade_data = customer_diamond_grade_map.get(key)
-            if diamond_grade_data:
+        if diamond_grade_data:
+            if row.customer_diamond == "Yes":
                 grades_to_check = [
                     diamond_grade_data.get("diamond_grade_1"),
                     diamond_grade_data.get("diamond_grade_2"),
                     diamond_grade_data.get("diamond_grade_3"),
                     diamond_grade_data.get("diamond_grade_4"),
                 ]
                 for grade in grades_to_check:
                     if grade and grade in attribute_value_set:
                         diamond_grade = grade
                         break
-                    # We trust attribute_value_set contains all relevant values, no fallback needed
-
-        else:
-            diamond_grade_data = customer_diamond_grade_map.get(key)
-            if diamond_grade_data:
+            else:
                 diamond_grade = diamond_grade_data.get("diamond_grade_1")
-            else:
-                # Minimal fallback
-                diamond_grade = frappe.db.get_value(
-                    "Customer Diamond Grade",
-                    {"parent": row.customer, "diamond_quality": row.diamond_quality},
-                    "diamond_grade_1",
-                )
 
         so_det["diamond_grade"] = diamond_grade
 
         has_batch_no = False
         item_info = item_data_map.get(row.item_code)
         if item_info:
             has_batch_no = item_info.get("has_batch_no")
-        else:
-            has_batch_no = frappe.db.get_value("Item", row.item_code, "has_batch_no")
 
         if not so_det.get("diamond_grade") and not has_batch_no:
             frappe.throw(
                 _("Diamond Grade is not mentioned in customer {0}").format(row.customer)
             )
 
     for i in range(0, cnt):
         make_manufacturing_order(doc, row, master_bom=master_bom, so_det=so_det)
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that fallback database queries within a loop undermine the performance improvements from the new caching mechanism, and removing them enforces a better design.

Medium
Security
Validate permissions before raw SQL update

Add a permission check for Sales Order write access before executing the raw SQL
update to prevent unauthorized modifications and ensure data integrity.

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.py [20-41]

+# Validate write permissions on Sales Order before bulk update
+if not frappe.has_permission("Sales Order", "write"):
+    frappe.throw(_("Insufficient permissions to update Sales Orders"), frappe.PermissionError)
+
 frappe.db.sql(
     """
 				UPDATE `tabSales Order Item` soi
 				JOIN `tabManufacturing Plan Table` mpt
 					ON soi.name = mpt.docname
 				JOIN `tabManufacturing Plan` mp
 					ON (mpt.parent = mp.name AND mp.name = %(mp_name)s)
 				JOIN `tabSales Order` so
 					ON (soi.parent = so.name AND so.docstatus = 1)
 				SET
 					soi.manufacturing_order_qty =
 						COALESCE(soi.manufacturing_order_qty, 0)
 						+ COALESCE(mpt.manufacturing_order_qty, 0)
 						+ COALESCE(mpt.subcontracting_qty, 0),
 					so.modified = NOW(),
 					so.modified_by = %(modified_by)s
 			""",
     {
         "mp_name": self.name,
         "modified_by": frappe.session.user,
     },
 )
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the raw SQL query bypasses Frappe's permission model, and the proposed explicit permission check is a valid and important security improvement.

Medium
Possible issue
Prevent SQL error on empty list

Add a check to ensure self.docs_to_append is not empty before executing the SQL
query to prevent a syntax error with an empty IN clause.

jewellery_erpnext/jewellery_erpnext/doctype/manufacturing_plan/manufacturing_plan.py [245-257]

-mwo_data = frappe.db.sql(
-    """
+mwo_data = []
+if self.docs_to_append:
+    mwo_data = frappe.db.sql(
+        """
 		SELECT
 			item_code,
 			SUM(qty) AS total_qty,
 			MIN(name) AS mwo
 		FROM `tabManufacturing Work Order`
 		WHERE name IN %(docs)s
 		GROUP BY item_code
 	""",
-    {"docs": tuple(self.docs_to_append)},
-    as_dict=True,
-)
+        {"docs": tuple(self.docs_to_append)},
+        as_dict=True,
+    )
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential SQL syntax error if self.docs_to_append is empty and provides a simple guard condition to prevent a runtime crash.

Medium
  • More
  • Author self-review: I have reviewed the PR code suggestions, and addressed the relevant ones.

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

is_auto_command: True
is_new_pr: True
model_reasoning: vertex_ai/gemini-2.5-pro
model: gpt-5.2-2025-12-11
model_turbo: anthropic/claude-haiku-4-5-20251001
fallback_models: ['anthropic/claude-sonnet-4-5-20250929', 'bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0']
second_model_for_exhaustive_mode: o4-mini
git_provider: github
publish_output: True
publish_output_no_suggestions: True
publish_output_progress: True
verbosity_level: 0
publish_logs: False
debug_mode: False
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
use_global_wiki_settings_file: False
disable_auto_feedback: False
ai_timeout: 150
response_language: en-US
clone_repo_instead_of_fetch: True
always_clone: False
add_repo_metadata: True
clone_repo_time_limit: 300
publish_inline_comments_fallback_batch_size: 5
publish_inline_comments_fallback_sleep_time: 2
max_model_tokens: 32000
custom_model_max_tokens: -1
patch_extension_skip_types: ['.md', '.txt']
extra_allowed_extensions: []
allow_dynamic_context: True
allow_forward_dynamic_context: True
max_extra_lines_before_dynamic_context: 12
patch_extra_lines_before: 5
patch_extra_lines_after: 1
ai_handler: litellm
cli_mode: False
TRIAL_GIT_ORG_MAX_INVOKES_PER_MONTH: 75
TRIAL_RATIO_CLOSE_TO_LIMIT: 0.8
invite_only_mode: False
enable_request_access_msg_on_new_pr: False
check_also_invites_field: False
calculate_context: True
disable_checkboxes: False
output_relevant_configurations: True
large_patch_policy: clip
seed: -1
temperature: 0.2
allow_dynamic_context_ab_testing: False
choose_dynamic_context_ab_testing_ratio: 0.5
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^\\[Bump\\]']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: ['do-not-merge', 'skip-qodo']
ignore_ticket_labels: []
allow_only_specific_folders: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
enable_ai_metadata: True
present_reasoning: True
max_tickets: 10
max_tickets_chars: 8000
prevent_any_approval: False
enable_comment_approval: False
enable_auto_approval: False
auto_approve_for_low_review_effort: -1
auto_approve_for_no_suggestions: False
ensure_ticket_compliance: False
new_diff_format: True
new_diff_format_add_external_references: True
enable_custom_labels: False
extra_statistics: {'suggestion_statistics': [{'score': 7, 'label': 'general'}, {'score': 9, 'label': 'security'}]}

[pr_code_suggestions]

suggestions_depth: exhaustive
commitable_code_suggestions: False
decouple_hunks: False
dual_publishing_score_threshold: -1
focus_only_on_problems: True
allow_thumbs_up_down: False
enable_suggestion_type_reuse: False
enable_more_suggestions_checkbox: True
high_level_suggestions_enabled: True
extra_instructions: Focus suggestions on PERFORMANCE + SECURITY:
- Reduce DB roundtrips (bulk fetch, IN queries, joins/aggregates).
- Prefer set_value over save for small field updates unless validations are required.
- Ensure parameterized SQL only.
- Ensure endpoints are whitelisted intentionally and respect permissions.
- Prefer chunking/generators for big datasets.
Return copy-paste ready snippets when feasible.

enable_help_text: False
show_extra_context: False
persistent_comment: True
max_history_len: 5
apply_suggestions_checkbox: True
enable_chat_in_code_suggestions: True
apply_limit_scope: True
suggestions_score_threshold: 0
new_score_mechanism: True
new_score_mechanism_th_high: 9
new_score_mechanism_th_medium: 7
discard_unappliable_suggestions: False
num_code_suggestions_per_chunk: 6
num_best_practice_suggestions: 2
max_number_of_calls: 3
final_clip_factor: 0.8
demand_code_suggestions_self_review: True
code_suggestions_self_review_text: **Author self-review**: I have reviewed the PR code suggestions, and addressed the relevant ones.
approve_pr_on_self_review: False
fold_suggestions_on_self_review: True
publish_post_process_suggestion_impact: True
wiki_page_accepted_suggestions: True
enable_local_self_reflect_in_large_prs: False
simplify_response: True

@qodo-free-for-open-source-projects

Persistent suggestions updated to latest commit 8d3e5e7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant