from __future__ import annotations

import json
import math
import re
import secrets
import hashlib
import threading
import traceback
import tkinter as tk
from copy import deepcopy
from datetime import datetime
from pathlib import Path
from tkinter import filedialog, messagebox, ttk

from app_core.paths import DB_PATH, DEBUG_DIR
from app_core.filter_helpers import create_extra_filter_row, destroy_filter_row, regrid_filter_rows
from app_core.ui_helpers import configure_striped_treeview
from app_core.server_api import delete_bom_item as delete_bom_item_via_api
from app_core.server_api import fetch_app_setting, fetch_bom_items, save_app_setting, save_bom_item as save_bom_item_via_api
from database import (
    delete_bom_item,
    get_app_setting,
    list_bom_items,
    normalize_style_identifier,
    normalize_style_identifier_list,
    normalize_workflow_status,
    save_bom_item,
    set_app_setting,
    list_suppliers,
    summarize_purchase_order_requirements_by_style,
)
from fds_parser import FDSParseError, parse_fds_workbook

BOM_SORT_KEY_OPTIONS = [
    ("created_at", "create_time_column"),
    ("item_code", "bom_item_code_column"),
    ("item_name", "bom_item_name_column"),
    ("item_type", "bom_item_type_column"),
    ("section", "bom_section_column"),
    ("po_summary", "bom_linked_pos_column"),
    ("booking_required_qty", "bom_booking_req_column"),
    ("production_required_qty", "bom_production_req_column"),
    ("settlement_required_qty", "bom_settlement_req_column"),
    ("ordered_qty", "bom_ordered_qty_column"),
    ("received_qty", "bom_received_qty_column"),
    ("issued_qty", "bom_issued_qty_column"),
    ("on_hand_qty", "bom_on_hand_qty_column"),
    ("shortage_qty", "bom_shortage_column"),
    ("article_ref", "bom_article_column"),
    ("color", "samples_color_column"),
    ("spec_summary", "bom_spec_summary_column"),
    ("uom", "UOM"),
    ("default_supplier", "bom_supplier_column"),
    ("status", "status"),
    ("updated_at", "revised_time_column"),
    ("latest_user", "latest_user_column"),
]

BOM_FILTER_FIELD_OPTIONS = [
    ("__all__", "samples_filter_all_option"),
    ("created_at", "create_time_column"),
    ("item_code", "bom_item_code_column"),
    ("item_name", "bom_item_name_column"),
    ("item_type", "bom_item_type_column"),
    ("section", "bom_section_column"),
    ("po_summary", "bom_linked_pos_column"),
    ("booking_required_qty", "bom_booking_req_column"),
    ("production_required_qty", "bom_production_req_column"),
    ("settlement_required_qty", "bom_settlement_req_column"),
    ("ordered_qty", "bom_ordered_qty_column"),
    ("received_qty", "bom_received_qty_column"),
    ("issued_qty", "bom_issued_qty_column"),
    ("on_hand_qty", "bom_on_hand_qty_column"),
    ("shortage_qty", "bom_shortage_column"),
    ("article_ref", "bom_article_column"),
    ("color", "samples_color_column"),
    ("spec_summary", "bom_spec_summary_column"),
    ("uom", "UOM"),
    ("default_supplier", "bom_supplier_column"),
    ("status", "status"),
    ("updated_at", "revised_time_column"),
    ("latest_user", "latest_user_column"),
]

BOM_DATE_FILTER_FIELDS = {"created_at", "updated_at"}
BOM_COLUMNS = (
    "select_flag",
    "created_at",
    "item_code",
    "item_name",
    "item_type",
    "section",
    "po_summary",
    "booking_required_qty",
    "production_required_qty",
    "settlement_required_qty",
    "ordered_qty",
    "received_qty",
    "issued_qty",
    "on_hand_qty",
    "shortage_qty",
    "article_ref",
    "color",
    "spec_summary",
    "uom",
    "default_supplier",
    "status",
    "updated_at",
    "latest_user",
)
BOM_FILTER_DEBOUNCE_MS = 180

ITEM_TYPE_OPTIONS = ["Fabric", "Trims", "Label", "Packaging", "Artwork", "Other"]
ITEM_STATUS_OPTIONS = ["draft", "checking", "approved"]
MATERIAL_DOCUMENT_STATUS_OPTIONS = ["draft", "checking", "approved"]
MATERIAL_PO_STATUS_OPTIONS = ["draft", "checking", "approved"]
PIECE_UOM_OPTIONS = ["EA", "SET", "CONE"]
LENGTH_UOM_OPTIONS = ["MM", "CM", "M", "INCH", "YARD"]
WEIGHT_UOM_OPTIONS = ["G", "KG", "LB", "OZ"]
BOM_CURRENCY_OPTIONS = ["USD", "VND"]
BOM_CHARGE_UNIT_OPTIONS = ["%", "USD", "VND"]
LEAD_TIME_UNIT_OPTIONS = ["DAY", "WEEK", "MONTH"]
PURCHASE_UOM_OPTIONS = PIECE_UOM_OPTIONS + LENGTH_UOM_OPTIONS + WEIGHT_UOM_OPTIONS
BOM_UOM_OPTIONS_BY_ITEM_TYPE = {
    "fabric": LENGTH_UOM_OPTIONS + WEIGHT_UOM_OPTIONS,
    "trim": PIECE_UOM_OPTIONS + LENGTH_UOM_OPTIONS,
    "trims": PIECE_UOM_OPTIONS + LENGTH_UOM_OPTIONS,
    "label": PIECE_UOM_OPTIONS,
    "packaging": PIECE_UOM_OPTIONS,
    "artwork": PIECE_UOM_OPTIONS,
    "other": PIECE_UOM_OPTIONS + LENGTH_UOM_OPTIONS,
}
SECTION_TYPE_MAP = {
    "MATERIAL": "Fabric",
    "TRIM": "Trims",
    "LABELS": "Label",
    "PACKAGING": "Packaging",
    "ARTWORK": "Artwork",
}
SECTION_CODE_MAP = {
    "MATERIAL": "MAT",
    "TRIM": "TRM",
    "LABELS": "LBL",
    "PACKAGING": "PKG",
    "ARTWORK": "ART",
}
BOM_APP_SETTING_KEY = "bom_items"
MATERIAL_PO_APP_SETTING_KEY = "material_po_records"
MATERIAL_RECEIPT_APP_SETTING_KEY = "material_receipt_records"
MATERIAL_ISSUE_APP_SETTING_KEY = "material_issue_records"
WAREHOUSE_APP_SETTING_KEY = "warehouse_records"
BULK_FABRIC_BOM_VERSION = "BULK"
BULK_TRIMS_BOM_VERSION = "BULK-TRIMS"
_BOM_DEBUG_LOG_PATH = DEBUG_DIR / "bom_debug.log"
try:
    if _BOM_DEBUG_LOG_PATH.exists():
        _BOM_DEBUG_LOG_PATH.unlink()
except Exception:
    pass


def _append_bom_debug_log(message: str) -> None:
    try:
        DEBUG_DIR.mkdir(parents=True, exist_ok=True)
        with _BOM_DEBUG_LOG_PATH.open("a", encoding="utf-8") as handle:
            handle.write(f"[{_bom_now_text()}] {message}\n")
    except Exception:
        pass


def _bom_now_text() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def _safe_float_text(value: object) -> str:
    text = str(value or "").strip()
    if not text:
        return ""
    try:
        return str(float(text))
    except Exception:
        return text


def _safe_float_value(value: object) -> float:
    text = str(value or "").strip().replace(",", "")
    if not text:
        return 0.0
    match = re.search(r"-?\d+(?:\.\d+)?", text)
    if match:
        text = match.group(0)
    try:
        return float(text)
    except Exception:
        return 0.0


def _format_metric_text(value: float) -> str:
    normalized = round(float(value or 0.0), 4)
    if abs(normalized - round(normalized)) < 1e-9:
        return str(int(round(normalized)))
    return f"{normalized:.4f}".rstrip("0").rstrip(".")


def _format_metric_with_uom(value: float, uom: object) -> str:
    text = _format_metric_text(value)
    normalized_uom = str(uom or "").strip().upper()
    return f"{text} {normalized_uom}".strip() if normalized_uom else text


def _is_material_po_status_active(status: object) -> bool:
    normalized = str(status or "").strip().lower()
    return normalized not in {"cancelled", "canceled", "closed", "done"}


def _is_material_po_approved(status: object) -> bool:
    normalized = str(status or "").strip().lower()
    return normalized == "approved"


def _is_material_receipt_approved(status: object) -> bool:
    normalized = str(status or "").strip().lower()
    return normalized == "approved"


_LENGTH_UOM_FACTORS_TO_M = {
    "MM": 0.001,
    "CM": 0.01,
    "M": 1.0,
    "INCH": 0.0254,
    "YARD": 0.9144,
}
_WEIGHT_UOM_FACTORS_TO_KG = {
    "G": 0.001,
    "KG": 1.0,
    "LB": 0.45359237,
    "OZ": 0.028349523125,
}


def _uom_conversion_context(uom: object) -> tuple[str, str, float]:
    normalized_uom = str(uom or "").strip().upper()
    if normalized_uom in _LENGTH_UOM_FACTORS_TO_M:
        return "length", "M", _LENGTH_UOM_FACTORS_TO_M[normalized_uom]
    if normalized_uom in _WEIGHT_UOM_FACTORS_TO_KG:
        return "weight", "KG", _WEIGHT_UOM_FACTORS_TO_KG[normalized_uom]
    if normalized_uom == "EA":
        return "piece:EA", "EA", 1.0
    if normalized_uom == "SET":
        return "piece:SET", "SET", 1.0
    if normalized_uom:
        return f"raw:{normalized_uom}", normalized_uom, 1.0
    return "raw:", "", 1.0


def _convert_quantity_to_common_uom(value: float, from_uom: object, common_uom: object) -> float:
    normalized_value = float(value or 0.0)
    if abs(normalized_value) <= 1e-12:
        return 0.0
    from_family, from_common, from_factor = _uom_conversion_context(from_uom)
    target_family, target_common, target_factor = _uom_conversion_context(common_uom)
    if from_family == target_family and from_common == target_common:
        return normalized_value * (from_factor / max(target_factor, 1e-12))
    if not str(from_uom or "").strip() or not str(common_uom or "").strip():
        return normalized_value
    return normalized_value


def _normalize_material_identity_text(value: object) -> str:
    return " ".join(str(value or "").strip().upper().split())


def _bulk_fabric_clean_text(value: object) -> str:
    if isinstance(value, dict) and "value" in value:
        value = value.get("value")
    if value is None:
        return ""
    if isinstance(value, float) and value.is_integer():
        return str(int(value))
    text = str(value).replace("\r", "\n").strip()
    return re.sub(r"[ \t]+", " ", text).strip()


def _bulk_fabric_cell_is_struck(value: object) -> bool:
    return bool(isinstance(value, dict) and value.get("strike"))


def _bulk_fabric_openpyxl_cell_text(cell: object) -> str:
    value = getattr(cell, "value", None)
    if value is None:
        return ""
    if isinstance(value, (str, int, float)):
        return str(value)
    parts: list[str] = []
    try:
        for part in value:
            part_text = getattr(part, "text", None)
            parts.append(str(part_text if part_text is not None else part))
    except TypeError:
        return str(value)
    return "".join(parts)


def _bulk_fabric_openpyxl_cell_has_strike(cell: object) -> bool:
    if bool(getattr(getattr(cell, "font", None), "strike", False)):
        return True
    value = getattr(cell, "value", None)
    if value is None or isinstance(value, (str, int, float)):
        return False
    try:
        for part in value:
            if bool(getattr(getattr(part, "font", None), "strike", False)):
                return True
    except TypeError:
        return False
    return False


def _bulk_fabric_header_key(value: object) -> str:
    text = _bulk_fabric_clean_text(value).lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    return re.sub(r"\s+", " ", text).strip()


def _bulk_fabric_code_token(value: object) -> str:
    return re.sub(r"[^A-Za-z0-9]+", "-", str(value or "").strip().upper()).strip("-")


def _bulk_fabric_number_text(value: object) -> str:
    text = _bulk_fabric_clean_text(value)
    if not text:
        return ""
    match = re.search(r"-?\d+(?:\.\d+)?", text.replace(",", ""))
    if not match:
        return text
    number = match.group(0)
    if "." in number:
        number = number.rstrip("0").rstrip(".")
    return number


def _bulk_fabric_loss_percent_text(value: object) -> str:
    number_text = _bulk_fabric_number_text(value)
    if not number_text:
        return ""
    try:
        number_value = float(number_text)
    except ValueError:
        return number_text
    if number_value > 1:
        number_value = (number_value - 1) * 100
    if abs(number_value) < 0.0000001:
        number_value = 0
    text = f"{number_value:.6f}".rstrip("0").rstrip(".")
    return text


def _bulk_fabric_style_tokens(value: object) -> list[str]:
    text = str(value or "").strip()
    if not text:
        return []
    tokens: list[str] = []
    for raw_token in re.split(r"[,;/\n]+", text):
        normalized = normalize_style_identifier(raw_token)
        if normalized and normalized not in tokens:
            tokens.append(normalized)
    if not tokens:
        normalized = normalize_style_identifier(text)
        if normalized:
            tokens.append(normalized)
    return tokens


def _bulk_fabric_join_style_codes(values: object) -> str:
    tokens: list[str] = []
    source_values = values if isinstance(values, (list, tuple, set)) else [values]
    for value in source_values:
        for token in _bulk_fabric_style_tokens(value):
            if token not in tokens:
                tokens.append(token)
    return ", ".join(sorted(tokens))


def _bulk_fabric_material_signature(row: dict) -> tuple[str, ...]:
    return (
        str(row.get("item_code", "")).strip().upper(),
        str(row.get("item_name", "")).strip().upper(),
        str(row.get("section", "")).strip().upper(),
        str(row.get("composition", "")).strip().upper(),
        str(row.get("color", "")).strip().upper(),
        str(row.get("color_code", "")).strip().upper(),
        str(row.get("width_value", "")).strip().upper(),
        str(row.get("width_unit", "")).strip().upper(),
        str(row.get("gsm_value", "")).strip().upper(),
        str(row.get("gsm_unit", "")).strip().upper(),
        str(row.get("waste_percent", "")).strip().upper(),
    )


def _bulk_fabric_existing_material_signature(item: dict) -> tuple[str, ...]:
    return (
        str(item.get("article_ref") or item.get("item_code") or "").strip().upper(),
        str(item.get("item_name", "")).strip().upper(),
        str(item.get("section", "")).strip().upper(),
        str(item.get("composition", "")).strip().upper(),
        str(item.get("color", "")).strip().upper(),
        str(item.get("color_code", "")).strip().upper(),
        str(item.get("width_value", "")).strip().upper(),
        str(item.get("width_unit", "")).strip().upper(),
        str(item.get("gsm_value", "")).strip().upper(),
        str(item.get("gsm_unit", "")).strip().upper(),
        str(item.get("waste_percent", "")).strip().upper(),
    )


def _bulk_fabric_group_source_rows(rows: list[dict]) -> list[dict]:
    grouped: dict[tuple[str, ...], dict] = {}
    for row in rows:
        signature = _bulk_fabric_material_signature(row)
        if not signature[0] or not signature[1]:
            continue
        group = grouped.get(signature)
        if group is None:
            group = dict(row)
            group["style_codes"] = []
            group["source_rows"] = []
            grouped[signature] = group
        group["source_rows"].append(dict(row))
        for style_code in _bulk_fabric_style_tokens(row.get("style_code")):
            if style_code not in group["style_codes"]:
                group["style_codes"].append(style_code)
    return list(grouped.values())


def _bom_style_codes_for_row(row: dict) -> list[str]:
    return _bulk_fabric_style_tokens(row.get("style_code") if isinstance(row, dict) else "")


_BOM_SIZE_ALIASES = {
    "1XL": "1X",
    "2XL": "2X",
    "3XL": "3X",
    "XXL": "2XL",
    "XXXL": "3XL",
    "PETXS": "PXS",
    "PETSM": "PS",
    "PETS": "PS",
    "PETMD": "PM",
    "PETM": "PM",
    "PETLG": "PL",
    "PETL": "PL",
    "PETXL": "PXL",
}
_BOM_GARMENT_SIZE_CODES = {
    "XS",
    "S",
    "M",
    "L",
    "XL",
    "2XL",
    "3XL",
    "1X",
    "2X",
    "3X",
    "PXS",
    "PS",
    "PM",
    "PL",
    "PXL",
}


def _bom_normalize_size_code(value: object) -> str:
    text = str(value or "").strip().upper().replace(" ", "")
    return _BOM_SIZE_ALIASES.get(text, text)


def _bom_row_specific_size_code(row: dict) -> str:
    size_code = _bom_normalize_size_code(row.get("trim_size_um"))
    return size_code if size_code in _BOM_GARMENT_SIZE_CODES else ""


def _combine_po_summary_for_styles(po_summary_map: dict, style_codes: list[str], size_code: object = "") -> dict:
    combined = {"style_code": ", ".join(style_codes), "total_qty": 0.0, "po_numbers": []}
    if not isinstance(po_summary_map, dict):
        return combined
    normalized_size = _bom_normalize_size_code(size_code)
    for style_code in style_codes:
        item = po_summary_map.get(style_code, {})
        if not isinstance(item, dict):
            continue
        source_item = item
        if normalized_size:
            by_size = item.get("by_size", {})
            source_item = by_size.get(normalized_size, {}) if isinstance(by_size, dict) else {}
        combined["total_qty"] = _safe_float_value(combined.get("total_qty")) + _safe_float_value(source_item.get("total_qty"))
        for po_number in list(source_item.get("po_numbers", []) or []):
            po_text = str(po_number).strip()
            if po_text and po_text not in combined["po_numbers"]:
                combined["po_numbers"].append(po_text)
    combined["po_count"] = len(combined["po_numbers"])
    combined["po_summary"] = ", ".join(combined["po_numbers"][:3]) + (" ..." if len(combined["po_numbers"]) > 3 else "")
    return combined


def _build_bom_shared_material_key(row: dict) -> tuple[str, ...]:
    normalized_uom = str(row.get("uom") or row.get("purchase_uom") or "").strip().upper()
    identifier = (
        _normalize_material_identity_text(row.get("supplier_item_code"))
        or _normalize_material_identity_text(row.get("article_ref"))
        or "|".join(
            part
            for part in (
                _normalize_material_identity_text(row.get("item_name")),
                _normalize_material_identity_text(row.get("composition")),
            )
            if part
        )
        or _normalize_material_identity_text(row.get("item_code"))
    )
    uom_family, common_uom, _factor = _uom_conversion_context(normalized_uom)
    return (
        _normalize_material_identity_text(row.get("item_type")),
        identifier,
        _normalize_material_identity_text(row.get("color")),
        _normalize_material_identity_text(row.get("color_code")),
        _bom_row_specific_size_code(row),
        uom_family,
        common_uom,
    )


def _is_thread_bom_row(row: dict) -> bool:
    section = str(row.get("section", "") or "").strip().lower()
    item_name = str(row.get("item_name", "") or "").strip().lower()
    return section == "thread" or "thread" in item_name


def _thread_cone_length_meters(row: dict) -> float:
    if not _is_thread_bom_row(row):
        return 0.0
    length_value = _safe_float_value(row.get("length_value"))
    length_unit = str(row.get("length_unit", "") or "").strip().upper() or "M"
    if length_value <= 0:
        length_value = _safe_float_value(row.get("trim_size_um"))
        length_unit = str(row.get("trim_size_unit", "") or "").strip().upper() or "M"
    factor = _LENGTH_UOM_FACTORS_TO_M.get(length_unit)
    if factor is None:
        return 0.0
    return length_value * factor


def _thread_required_cones_from_meters(required_meters: float, row: dict) -> float:
    cone_length_meters = _thread_cone_length_meters(row)
    if cone_length_meters <= 0 or required_meters <= 0:
        return 0.0
    return float(math.ceil(required_meters / cone_length_meters))


def _parse_qty_and_uom(value: object) -> tuple[str, str]:
    text = str(value or "").strip()
    if not text:
        return "", ""
    parts = text.split()
    if not parts:
        return "", ""
    first = parts[0].replace(",", "")
    try:
        qty = str(float(first))
        if qty.endswith(".0"):
            qty = qty[:-2]
        return qty, " ".join(parts[1:]).strip()
    except Exception:
        return "", text


def _split_measurement_and_unit(value: object) -> tuple[str, str]:
    text = str(value or "").strip()
    if not text:
        return "", ""
    parts = text.split()
    if len(parts) >= 2:
        return " ".join(parts[:-1]).strip(), parts[-1].strip().upper()
    return text, ""


def _bom_uom_options_for_item_type(item_type: object, current_value: object = "") -> list[str]:
    item_type_key = str(item_type or "").strip().lower()
    values = list(BOM_UOM_OPTIONS_BY_ITEM_TYPE.get(item_type_key, PIECE_UOM_OPTIONS + LENGTH_UOM_OPTIONS))
    current_text = str(current_value or "").strip().upper()
    if current_text and current_text not in values:
        values.insert(0, current_text)
    return values


def _purchase_uom_options(current_value: object = "") -> list[str]:
    values = list(PURCHASE_UOM_OPTIONS)
    current_text = str(current_value or "").strip().upper()
    if current_text and current_text not in values:
        values.insert(0, current_text)
    return values


def _default_trim_size_options(current_value: object = "") -> list[str]:
    values = ["ALL", "XXS", "XS", "S", "M", "L", "XL", "XXL", "3XL", "4XL", "5XL"]
    current_text = _normalize_trim_size_option(current_value)
    if _is_valid_trim_size_option(current_text) and current_text not in values:
        values.insert(1, current_text)
    return values


def _normalize_trim_size_option(value: object) -> str:
    return " ".join(str(value or "").strip().upper().split())


def _is_valid_trim_size_option(value: object) -> bool:
    normalized = _normalize_trim_size_option(value)
    if not normalized:
        return False
    if normalized == "ALL":
        return True
    return re.fullmatch(r"(XXS|XS|S|M|L|XL|XXL|XXXL|XXXXL|XXXXXL|[2-9]XL|[0-9]{1,2}X|[0-9]{1,2})", normalized) is not None


def get_bom_trim_size_options(self, style_code: object, current_value: object = "") -> list[str]:
    normalized_style = normalize_style_identifier(style_code)
    collected: list[str] = []
    library_rows = list(getattr(self, "tech_pack_library", []) or [])
    for record in library_rows:
        if not isinstance(record, dict):
            continue
        record_style = normalize_style_identifier(record.get("style_code"))
        if normalized_style and record_style != normalized_style:
            continue
        for pom_entry in list(record.get("pom_data", [])):
            if not isinstance(pom_entry, dict):
                continue
            for header in list(pom_entry.get("size_headers", [])):
                normalized_header = _normalize_trim_size_option(header)
                if _is_valid_trim_size_option(normalized_header) and normalized_header not in collected:
                    collected.append(normalized_header)
    if not collected:
        return _default_trim_size_options(current_value)
    if "ALL" not in collected:
        collected.insert(0, "ALL")
    current_text = _normalize_trim_size_option(current_value)
    if _is_valid_trim_size_option(current_text) and current_text not in collected:
        collected.append(current_text)
    return collected


def _is_bom_missing_endpoint_error(exc: Exception) -> bool:
    message = str(exc or "").strip().lower()
    return "http 404" in message or "404: not found" in message or message.endswith("404 not found")


def _is_bom_recoverable_source_error(exc: Exception) -> bool:
    message = str(exc or "").strip().lower()
    if _is_bom_missing_endpoint_error(exc):
        return True
    return any(
        token in message
        for token in (
            "http 500",
            "http 502",
            "http 503",
            "http 504",
            "internal server error",
            "bad gateway",
            "service unavailable",
            "gateway timeout",
            "connection failed",
            "timed out",
        )
    )


def _normalize_bom_item_list(payload: object) -> list[dict]:
    if not isinstance(payload, list):
        return []
    normalized_rows: list[dict] = []
    for item in payload:
        normalized = normalize_bom_item_payload(item)
        if isinstance(normalized, dict):
            normalized_rows.append(normalized)
    return normalized_rows


BOM_CONSUMPTION_GROUP_SOURCE_MARKER = "bom-approved-consumption-group-v1"
BOM_LEGACY_CONSUMPTION_GROUP_SOURCE_MARKERS = {
    BOM_CONSUMPTION_GROUP_SOURCE_MARKER,
    "bulk-trims-group-v1",
}


def _bom_consumption_number(value: object) -> float:
    try:
        text = str(value or "").strip().replace(",", "")
        return float(text) if text else 0.0
    except (TypeError, ValueError):
        return 0.0


def _bom_consumption_number_text(value: object) -> str:
    number = _bom_consumption_number(value)
    if abs(number - round(number)) < 1e-9:
        return str(int(round(number)))
    return f"{number:.6f}".rstrip("0").rstrip(".")


def _bom_consumption_optional_number_text(value: object) -> str:
    if not str(value or "").strip():
        return ""
    return _bom_consumption_number_text(value)


def _bom_consumption_line_source_marker(line: dict) -> str:
    raw_payload = str(line.get("source_payload_json", "") or "").strip()
    if not raw_payload:
        return ""
    try:
        payload = json.loads(raw_payload)
    except (TypeError, ValueError, json.JSONDecodeError):
        return ""
    if not isinstance(payload, dict):
        return ""
    return str(payload.get("source", "") or "").strip()


def _is_bom_auto_consumption_group_line(line: dict, style_code: str) -> bool:
    marker = _bom_consumption_line_source_marker(line)
    if marker in BOM_LEGACY_CONSUMPTION_GROUP_SOURCE_MARKERS:
        return True
    costing_id = str(line.get("costing_id", "") or "").strip()
    return bool(costing_id and costing_id.startswith(f"{normalize_style_identifier(style_code)}-PRODUCTION-BOM-GROUP-"))


def _bom_consumption_group_key(item: dict) -> tuple[str, ...]:
    return (
        str(item.get("item_type", "") or "").strip().upper(),
        str(item.get("section", "") or "").strip().upper(),
        str(item.get("item_name", "") or "").strip().upper(),
        str(item.get("color", "") or "").strip().upper(),
        str(item.get("length_value", "") or "").strip().upper(),
        str(item.get("length_unit", "") or "").strip().upper(),
        str(item.get("width_value", "") or "").strip().upper(),
        str(item.get("gsm_value", "") or "").strip().upper(),
        str(item.get("purchase_uom") or item.get("uom") or "").strip().upper(),
        _bom_consumption_optional_number_text(item.get("qty_per_garment")),
        _bom_consumption_number_text(item.get("waste_percent")),
    )


def _build_bom_auto_consumption_groups(style_code: str, approved_items: list[dict]) -> list[dict]:
    normalized_style = normalize_style_identifier(style_code)
    grouped: dict[tuple[str, ...], dict] = {}
    for item in list(approved_items or []):
        can_link_thread_without_consumption = _is_thread_bom_row(item) and _thread_cone_length_meters(item) > 0
        if _bom_consumption_number(item.get("qty_per_garment")) <= 0 and not can_link_thread_without_consumption:
            continue
        key = _bom_consumption_group_key(item)
        if not key[2]:
            continue
        group = grouped.setdefault(
            key,
            {
                "rows": [],
                "item_type": str(item.get("item_type", "") or "").strip(),
                "section": str(item.get("section", "") or "").strip(),
                "item_name": str(item.get("item_name", "") or "").strip(),
                "color": str(item.get("color", "") or "").strip(),
                "length_value": str(item.get("length_value", "") or "").strip(),
                "length_unit": str(item.get("length_unit", "") or "").strip().upper(),
                "width_value": str(item.get("width_value", "") or "").strip(),
                "gsm_value": str(item.get("gsm_value", "") or "").strip(),
                "uom": str(item.get("purchase_uom") or item.get("uom") or "").strip().upper(),
                "consumption": _bom_consumption_optional_number_text(item.get("qty_per_garment")),
                "waste_percent": _bom_consumption_number_text(item.get("waste_percent")),
            },
        )
        group["rows"].append(dict(item))

    lines: list[dict] = []
    sort_order = {"LABEL": 10, "TRIMS": 20, "TRIM": 20, "PACKAGING": 30, "FABRIC": 40}
    sorted_groups = sorted(
        grouped.values(),
        key=lambda group: (
            sort_order.get(str(group.get("item_type", "")).upper(), 999),
            str(group.get("section", "")).lower(),
            str(group.get("item_name", "")).lower(),
            str(group.get("color", "")).lower(),
        ),
    )
    for line_index, group in enumerate(sorted_groups, start=1):
        rows = sorted(
            [dict(item) for item in group.get("rows", []) if isinstance(item, dict)],
            key=lambda item: (
                str(item.get("trim_size_um", "") or "").strip().upper(),
                str(item.get("item_code", "") or "").strip().upper(),
            ),
        )
        if not rows:
            continue
        representative = rows[0]
        source_sizes = [
            str(item.get("trim_size_um", "") or "").strip()
            for item in rows
            if str(item.get("trim_size_um", "") or "").strip()
        ]
        source_item_ids = [
            str(item.get("item_id", "") or "").strip()
            for item in rows
            if str(item.get("item_id", "") or "").strip()
        ]
        source_item_codes = [
            str(item.get("item_code", "") or "").strip()
            for item in rows
            if str(item.get("item_code", "") or "").strip()
        ]
        line_digest = hashlib.sha1(
            "|".join((normalized_style, *(_bom_consumption_group_key(representative)))).encode("utf-8")
        ).hexdigest()[:12]
        line_id = f"{normalized_style}-BOM-GROUP-{line_digest}"
        size_value = "ALL" if len(rows) > 1 else str(representative.get("trim_size_um", "") or "").strip()
        source_payload = {
            "source": BOM_CONSUMPTION_GROUP_SOURCE_MARKER,
            "style_code": normalized_style,
            "size_mode": "ALL" if len(rows) > 1 else "SINGLE",
            "source_sizes": source_sizes,
            "source_bom_item_ids": source_item_ids,
            "source_bom_item_codes": source_item_codes,
            "representative_bom_item_id": str(representative.get("item_id", "") or "").strip(),
            "representative_bom_item_code": str(representative.get("item_code", "") or "").strip(),
        }
        lines.append(
            {
                "line_id": line_id,
                "costing_id": f"{normalized_style}-PRODUCTION-BOM-GROUP-{line_index:03d}-{line_digest[:6].upper()}",
                "bom_item_id": str(representative.get("item_id", "") or "").strip(),
                "bom_item_code": str(representative.get("item_code", "") or "").strip(),
                "bom_item_name": str(group.get("item_name", "") or "").strip(),
                "fabric_type": str(group.get("item_type", "") or "").strip(),
                "section": str(group.get("section", "") or "").strip(),
                "color": str(group.get("color", "") or "").strip(),
                "trim_size_um": size_value,
                "length_value": str(group.get("length_value", "") or "").strip(),
                "length_unit": str(group.get("length_unit", "") or "").strip().upper(),
                "width_value": str(group.get("width_value", "") or "").strip(),
                "gsm_value": str(group.get("gsm_value", "") or "").strip(),
                "waste_percent": str(group.get("waste_percent", "") or "").strip(),
                "uom": str(group.get("uom", "") or "").strip(),
                "consumption": str(group.get("consumption", "") or "").strip(),
                "source_payload_json": json.dumps(source_payload, ensure_ascii=False, separators=(",", ":")),
            }
        )
    return lines


def sync_tech_pack_consumption_with_bom_status(self, bom_rows: list[dict] | None = None) -> int:
    library_rows = [dict(item) for item in list(getattr(self, "tech_pack_library", []) or []) if isinstance(item, dict)]
    if not library_rows and hasattr(self, "load_tech_pack_library_from_source"):
        try:
            loaded_rows = self.load_tech_pack_library_from_source()
            if isinstance(loaded_rows, list):
                library_rows = [dict(item) for item in loaded_rows if isinstance(item, dict)]
                self.tech_pack_library = list(library_rows)
        except Exception as exc:
            _append_bom_debug_log(f"sync-consumption load-library warning={exc}")
    if not library_rows:
        return 0
    normalized_bom_rows = [
        item
        for item in (normalize_bom_item_payload(item) for item in list(bom_rows or getattr(self, "bom_items", []) or []))
        if isinstance(item, dict)
    ]
    bom_by_id = {
        str(item.get("item_id", "")).strip(): dict(item)
        for item in normalized_bom_rows
        if str(item.get("item_id", "")).strip()
    }
    bom_by_code: dict[str, dict] = {}
    for item in normalized_bom_rows:
        item_code = str(item.get("item_code", "")).strip()
        if item_code and item_code not in bom_by_code:
            bom_by_code[item_code] = dict(item)
    approved_by_style: dict[str, list[dict]] = {}
    approved_item_ids: set[str] = set()
    approved_item_codes: set[str] = set()
    for item in normalized_bom_rows:
        if str(item.get("status", "")).strip().lower() != "approved":
            continue
        item_id = str(item.get("item_id", "")).strip()
        item_code = str(item.get("item_code", "")).strip()
        if item_id:
            approved_item_ids.add(item_id)
        if item_code:
            approved_item_codes.add(item_code)
        for style_code in _bulk_fabric_style_tokens(item.get("style_code")):
            if item_code:
                approved_by_style.setdefault(style_code, []).append(dict(item))

    updated_library_by_style = {
        normalize_style_identifier(item.get("style_code")): dict(item)
        for item in library_rows
        if normalize_style_identifier(item.get("style_code"))
    }
    sync_change_count = 0
    for record in library_rows:
        style_code = normalize_style_identifier(record.get("style_code"))
        if not style_code:
            continue
        versions = record.get("consumption_versions", [])
        version_by_type = {
            str(version.get("version_type", "")).strip().lower(): dict(version)
            for version in list(versions or [])
            if isinstance(version, dict) and str(version.get("version_type", "")).strip()
        }
        approved_items = approved_by_style.get(style_code, [])
        normalized_versions: list[dict] = []
        record_changed = False
        version_templates = [
            ("costing", "Costing"),
            ("sample", "Sample"),
            ("booking", "Booking"),
            ("production", "Production"),
            ("settlement", "Settlement"),
        ]
        for version_type, version_name in version_templates:
            version = dict(version_by_type.get(version_type, {}))
            source_lines = version.get("lines", [])
            if not isinstance(source_lines, list):
                source_lines = []
            kept_lines: list[dict] = []
            for line in source_lines:
                if not isinstance(line, dict):
                    continue
                working_line = dict(line)
                bom_item_id = str(working_line.get("bom_item_id", "")).strip()
                bom_item_code = str(working_line.get("bom_item_code", "")).strip()
                matched_bom = bom_by_id.get(bom_item_id) if bom_item_id else None
                if matched_bom is None and bom_item_code:
                    matched_bom = bom_by_code.get(bom_item_code)
                if isinstance(matched_bom, dict):
                    target_uom = str(matched_bom.get("purchase_uom") or matched_bom.get("uom") or "").strip().upper()
                    if target_uom and str(working_line.get("uom", "")).strip().upper() != target_uom:
                        working_line["uom"] = target_uom
                        sync_change_count += 1
                        record_changed = True
                is_approved = (bom_item_id and bom_item_id in approved_item_ids) or (bom_item_code and bom_item_code in approved_item_codes)
                if is_approved:
                    kept_lines.append(working_line)
                else:
                    sync_change_count += 1
                    record_changed = True
            if version_type == "production":
                manual_lines: list[dict] = []
                removed_auto_count = 0
                for line in kept_lines:
                    if _is_bom_auto_consumption_group_line(line, style_code):
                        removed_auto_count += 1
                    else:
                        manual_lines.append(line)
                auto_group_lines = _build_bom_auto_consumption_groups(style_code, approved_items)
                rebuilt_lines = manual_lines + auto_group_lines
                if removed_auto_count or auto_group_lines:
                    if rebuilt_lines != kept_lines:
                        sync_change_count += abs(len(rebuilt_lines) - len(kept_lines)) or 1
                        record_changed = True
                    kept_lines = rebuilt_lines
            normalized_version = {
                "version_id": str(version.get("version_id", "")).strip() or version_type,
                "version_type": version_type,
                "version_name": str(version.get("version_name", "")).strip() or version_name,
            }
            normalized_version["lines"] = kept_lines
            normalized_versions.append(normalized_version)
        if record_changed:
            saved_record = self.save_tech_pack_record_patch_to_source(style_code, {"consumption_versions": normalized_versions})
            updated_library_by_style[style_code] = dict(saved_record) if isinstance(saved_record, dict) and saved_record else dict(record)
            _append_bom_debug_log(
                f"sync-consumption patched style={style_code} approved_count={len(approved_items)} change_count={sync_change_count}"
            )
    if sync_change_count:
        self.tech_pack_library = list(updated_library_by_style.values())
    return sync_change_count


def default_bom_item_payload() -> dict:
    return {
        "item_id": "",
        "bom_id": "",
        "style_code": "",
        "bom_version": "",
        "bom_name": "",
        "season": "",
        "collection": "",
        "item_code": "",
        "item_name": "",
        "item_type": "Other",
        "section": "",
        "article_ref": "",
        "color": "",
        "color_code": "",
        "description": "",
        "composition": "",
        "position_of_use": "",
        "spec_summary": "",
        "uom": "",
        "qty_per_garment": "",
        "trim_size_um": "",
        "trim_size_unit": "",
        "length_value": "",
        "length_unit": "",
        "width_value": "",
        "width_unit": "",
        "gsm_value": "",
        "gsm_unit": "",
        "waste_percent": "",
        "tech_comment": "",
        "default_supplier": "",
        "supplier_item_code": "",
        "moq": "",
        "moq_unit": "",
        "moq_charge": "",
        "moq_charge_unit": "",
        "mcq": "",
        "mcq_unit": "",
        "mcq_charge": "",
        "mcq_charge_unit": "",
        "lead_time": "",
        "lead_time_unit": "",
        "sample_lead_time": "",
        "sample_lead_time_unit": "",
        "purchase_uom": "",
        "standard_price": "",
        "currency": "",
        "purchase_note": "",
        "fds_date": "",
        "image_path": "",
        "status": "draft",
        "source_style_code": "",
        "source_section": "",
        "source_section_index": "",
        "source_item_index": "",
        "source_payload_json": "",
        "created_at": "",
        "updated_at": "",
    }


def normalize_bom_item_payload(payload: object) -> dict | None:
    if not isinstance(payload, dict):
        return None
    normalized = default_bom_item_payload()
    for key in normalized:
        value = payload.get(key, normalized[key])
        normalized[key] = str(value or "").strip()
    normalized["item_id"] = normalized["item_id"] or secrets.token_hex(16)
    normalized["style_code"] = normalize_style_identifier_list(normalized.get("style_code"))
    normalized["source_style_code"] = normalize_style_identifier_list(normalized.get("source_style_code"))
    normalized["item_type"] = normalized["item_type"] or "Other"
    normalized["status"] = str(normalized["status"] or "draft").strip().lower() or "draft"
    normalized["trim_size_unit"] = str(normalized.get("trim_size_unit", "")).strip().upper()
    normalized["length_unit"] = str(normalized.get("length_unit", "")).strip().upper()
    normalized["width_unit"] = str(normalized.get("width_unit", "")).strip().upper()
    normalized["gsm_unit"] = str(normalized.get("gsm_unit", "")).strip().upper()
    normalized["uom"] = str(normalized.get("uom", "")).strip().upper()
    normalized["purchase_uom"] = str(normalized.get("purchase_uom", "")).strip().upper()
    normalized["currency"] = str(normalized.get("currency", "")).strip().upper()
    normalized["moq_unit"] = str(normalized.get("moq_unit", "")).strip().upper()
    normalized["moq_charge_unit"] = str(normalized.get("moq_charge_unit", "")).strip().upper()
    normalized["mcq_unit"] = str(normalized.get("mcq_unit", "")).strip().upper()
    normalized["mcq_charge_unit"] = str(normalized.get("mcq_charge_unit", "")).strip().upper()
    normalized["lead_time_unit"] = str(normalized.get("lead_time_unit", "")).strip().upper()
    normalized["sample_lead_time_unit"] = str(normalized.get("sample_lead_time_unit", "")).strip().upper()
    normalized["qty_per_garment"] = _safe_float_text(normalized.get("qty_per_garment"))
    normalized["waste_percent"] = _safe_float_text(normalized.get("waste_percent"))
    normalized["moq"] = _safe_float_text(normalized.get("moq"))
    normalized["moq_charge"] = _safe_float_text(normalized.get("moq_charge"))
    normalized["mcq"] = _safe_float_text(normalized.get("mcq"))
    normalized["mcq_charge"] = _safe_float_text(normalized.get("mcq_charge"))
    normalized["standard_price"] = _safe_float_text(normalized.get("standard_price"))
    for key in (
        "po_summary",
        "booking_required_qty",
        "production_required_qty",
        "settlement_required_qty",
        "ordered_qty",
        "received_qty",
        "issued_qty",
        "on_hand_qty",
        "shortage_qty",
    ):
        normalized[key] = str(payload.get(key, "") or "").strip()
    return normalized


def _load_store_rows(self, setting_key: str) -> list[dict]:
    if self.should_use_server_data_source():
        try:
            payload = fetch_app_setting(self.server_settings, setting_key, [])
        except Exception:
            payload = []
    else:
        payload = get_app_setting(DB_PATH, setting_key, [])
    return [dict(item) for item in list(payload) if isinstance(item, dict)]


def _save_store_rows(self, setting_key: str, rows: list[dict]) -> list[dict]:
    payload = [dict(item) for item in rows if isinstance(item, dict)]
    if self.should_use_server_data_source():
        saved = save_app_setting(self.server_settings, setting_key, payload)
    else:
        saved = set_app_setting(DB_PATH, setting_key, payload)
    return [dict(item) for item in list(saved) if isinstance(item, dict)]


def default_material_po_payload() -> dict:
    return {
        "record_id": "",
        "mpo_number": "",
        "order_date": "",
        "eta_date": "",
        "supplier_name": "",
        "style_code": "",
        "item_id": "",
        "item_code": "",
        "item_name": "",
        "linked_po_numbers": "",
        "ordered_qty": "",
        "purchase_uom": "",
        "unit_price": "",
        "currency": "",
        "status": "draft",
        "note": "",
        "created_at": "",
        "updated_at": "",
    }


def normalize_material_po_row(payload: object) -> dict | None:
    if not isinstance(payload, dict):
        return None
    normalized = default_material_po_payload()
    for key in normalized:
        normalized[key] = str(payload.get(key, normalized[key]) or "").strip()
    normalized["record_id"] = normalized["record_id"] or secrets.token_hex(16)
    normalized["style_code"] = normalize_style_identifier(normalized.get("style_code"))
    normalized["status"] = str(normalized.get("status") or "draft").strip().lower() or "draft"
    if normalized["status"] not in MATERIAL_DOCUMENT_STATUS_OPTIONS:
        normalized["status"] = "draft"
    normalized["ordered_qty"] = _safe_float_text(normalized.get("ordered_qty"))
    normalized["unit_price"] = _safe_float_text(normalized.get("unit_price"))
    if not normalized["item_code"] and not normalized["item_name"]:
        return None
    return normalized


def default_material_receipt_payload() -> dict:
    return {
        "receipt_id": "",
        "receipt_number": "",
        "receipt_date": "",
        "po_record_id": "",
        "mpo_number": "",
        "supplier_name": "",
        "warehouse_code": "",
        "style_code": "",
        "item_id": "",
        "item_code": "",
        "item_name": "",
        "received_qty": "",
        "uom": "",
        "lot_no": "",
        "status": "draft",
        "note": "",
        "created_at": "",
        "updated_at": "",
    }


def normalize_material_receipt_row(payload: object) -> dict | None:
    if not isinstance(payload, dict):
        return None
    normalized = default_material_receipt_payload()
    for key in normalized:
        normalized[key] = str(payload.get(key, normalized[key]) or "").strip()
    normalized["receipt_id"] = normalized["receipt_id"] or secrets.token_hex(16)
    normalized["style_code"] = normalize_style_identifier(normalized.get("style_code"))
    normalized["received_qty"] = _safe_float_text(normalized.get("received_qty"))
    normalized["status"] = str(normalized.get("status") or "draft").strip().lower() or "draft"
    normalized["warehouse_code"] = str(normalized.get("warehouse_code", "")).strip().upper()
    if not normalized["item_code"] and not normalized["item_name"]:
        return None
    return normalized


def default_material_issue_payload() -> dict:
    return {
        "issue_id": "",
        "issue_number": "",
        "issue_date": "",
        "po_number": "",
        "warehouse_code": "",
        "style_code": "",
        "item_id": "",
        "item_code": "",
        "item_name": "",
        "issued_qty": "",
        "uom": "",
        "reason": "",
        "note": "",
        "created_at": "",
        "updated_at": "",
    }


def normalize_material_issue_row(payload: object) -> dict | None:
    if not isinstance(payload, dict):
        return None
    normalized = default_material_issue_payload()
    for key in normalized:
        normalized[key] = str(payload.get(key, normalized[key]) or "").strip()
    normalized["issue_id"] = normalized["issue_id"] or secrets.token_hex(16)
    normalized["style_code"] = normalize_style_identifier(normalized.get("style_code"))
    normalized["issued_qty"] = _safe_float_text(normalized.get("issued_qty"))
    if not normalized["item_code"] and not normalized["item_name"]:
        return None
    return normalized


def default_warehouse_payload() -> dict:
    return {
        "warehouse_id": "",
        "warehouse_code": "",
        "warehouse_name": "",
        "status": "active",
        "note": "",
        "created_at": "",
        "updated_at": "",
    }


def normalize_warehouse_row(payload: object) -> dict | None:
    if not isinstance(payload, dict):
        return None
    normalized = default_warehouse_payload()
    for key in normalized:
        normalized[key] = str(payload.get(key, normalized[key]) or "").strip()
    normalized["warehouse_id"] = normalized["warehouse_id"] or secrets.token_hex(16)
    normalized["warehouse_code"] = normalized["warehouse_code"].upper()
    normalized["status"] = str(normalized.get("status") or "active").strip().lower() or "active"
    if not normalized["warehouse_code"] or not normalized["warehouse_name"]:
        return None
    return normalized


def _find_bom_consumption_detail(self, row: dict) -> dict[str, object]:
    style_codes = _bom_style_codes_for_row(row)
    item_id = str(row.get("item_id", "")).strip()
    item_code = str(row.get("item_code", "")).strip()
    values = {"booking": 0.0, "production": 0.0, "settlement": 0.0}
    matched_versions: set[str] = set()
    nonzero_versions: set[str] = set()
    matched_line_count = 0
    if str(row.get("status", "")).strip().lower() != "approved":
        return {
            "values": values,
            "matched_versions": matched_versions,
            "nonzero_versions": nonzero_versions,
            "matched_line_count": matched_line_count,
            "has_actual_consumption_match": False,
            "has_consumption_match": False,
            "fallback_used": False,
        }
    if style_codes and item_code:
        for record in list(getattr(self, "tech_pack_library", []) or []):
            if normalize_style_identifier(record.get("style_code")) not in style_codes:
                continue
            versions = record.get("consumption_versions", [])
            if not isinstance(versions, list):
                continue
            for version in [dict(item) for item in versions if isinstance(item, dict)]:
                version_type = str(version.get("version_type", "")).strip().lower()
                if version_type not in values:
                    continue
                total = 0.0
                for line in list(version.get("lines", [])):
                    if not isinstance(line, dict):
                        continue
                    line_item_id = str(line.get("bom_item_id", "")).strip()
                    line_item_code = str(line.get("bom_item_code", "")).strip()
                    is_match = (item_id and line_item_id == item_id) or (item_code and line_item_code == item_code)
                    if not is_match:
                        raw_payload = str(line.get("source_payload_json", "") or "").strip()
                        if raw_payload:
                            try:
                                payload = json.loads(raw_payload)
                            except (TypeError, ValueError, json.JSONDecodeError):
                                payload = {}
                            if isinstance(payload, dict):
                                source_item_ids = {
                                    str(value or "").strip()
                                    for value in list(payload.get("source_bom_item_ids", []) or [])
                                    if str(value or "").strip()
                                }
                                source_item_codes = {
                                    str(value or "").strip()
                                    for value in list(payload.get("source_bom_item_codes", []) or [])
                                    if str(value or "").strip()
                                }
                                is_match = (item_id and item_id in source_item_ids) or (item_code and item_code in source_item_codes)
                    if not is_match:
                        continue
                    consumption_value = _safe_float_value(line.get("consumption"))
                    total += consumption_value
                    matched_versions.add(version_type)
                    if consumption_value > 0:
                        nonzero_versions.add(version_type)
                    matched_line_count += 1
                values[version_type] += total
    fallback_qty = _safe_float_value(row.get("qty_per_garment"))
    fallback_used = False
    if fallback_qty <= 0:
        spec_summary = str(row.get("spec_summary", "")).strip().split()
        if spec_summary:
            try:
                fallback_qty = float(spec_summary[0].replace(",", ""))
            except Exception:
                fallback_qty = 0.0
    if fallback_qty > 0:
        for key in values:
            if values[key] <= 0 and key not in nonzero_versions:
                values[key] = fallback_qty
                fallback_used = True
    return {
        "values": values,
        "matched_versions": matched_versions,
        "nonzero_versions": nonzero_versions,
        "matched_line_count": matched_line_count,
        "has_actual_consumption_match": bool(matched_versions),
        "has_consumption_match": bool(matched_versions) or fallback_used,
        "fallback_used": fallback_used,
    }


def _find_bom_consumption_by_version(self, row: dict) -> dict[str, float]:
    detail = self._find_bom_consumption_detail(row)
    values = detail.get("values", {})
    return dict(values) if isinstance(values, dict) else {"booking": 0.0, "production": 0.0, "settlement": 0.0}


def enrich_bom_requirement_rows(self, rows: list[dict]) -> list[dict]:
    try:
        po_summary_map = self.summarize_bom_purchase_order_requirements()
    except Exception as exc:
        _append_bom_debug_log(f"po-summary build failed error={exc}")
        po_summary_map = {}
    material_po_rows = self.load_material_po_rows_from_source()
    receipt_rows = [row for row in self.load_material_receipt_rows_from_source() if _is_material_receipt_approved(row.get("status"))]
    issue_rows = self.load_material_issue_rows_from_source()
    prepared_rows: list[dict] = []
    shared_key_by_style_item: dict[tuple[str, str], tuple[str, ...]] = {}
    group_totals: dict[tuple[str, ...], dict[str, object]] = {}

    for source_row in rows:
        row = dict(source_row)
        is_approved_item = str(row.get("status", "")).strip().lower() == "approved"
        style_codes = _bom_style_codes_for_row(row)
        style_code = style_codes[0] if style_codes else ""
        item_code = str(row.get("item_code", "")).strip()
        size_code = _bom_row_specific_size_code(row)
        po_info = _combine_po_summary_for_styles(po_summary_map, style_codes, size_code) if is_approved_item else {"total_qty": 0.0, "po_numbers": []}
        demand_qty = _safe_float_value(po_info.get("total_qty")) if is_approved_item else 0.0
        consumption_detail = self._find_bom_consumption_detail(row)
        consumption_by_version = dict(consumption_detail.get("values", {}))
        has_consumption_match = bool(consumption_detail.get("has_consumption_match")) and is_approved_item
        has_actual_consumption_match = bool(consumption_detail.get("has_actual_consumption_match")) and is_approved_item
        waste_percent = _safe_float_value(row.get("waste_percent"))
        booking_required_qty = demand_qty * _safe_float_value(consumption_by_version.get("booking"))
        production_required_qty = demand_qty * _safe_float_value(consumption_by_version.get("production"))
        settlement_required_qty = demand_qty * _safe_float_value(consumption_by_version.get("settlement"))
        if waste_percent > 0:
            multiplier = 1 + (waste_percent / 100.0)
            if booking_required_qty > 0:
                booking_required_qty *= multiplier
            if production_required_qty > 0:
                production_required_qty *= multiplier
            if settlement_required_qty > 0:
                settlement_required_qty *= multiplier
        if _is_thread_bom_row(row) and str(row.get("purchase_uom") or row.get("uom") or "").strip().upper() == "CONE":
            booking_required_qty = _thread_required_cones_from_meters(booking_required_qty, row)
            production_required_qty = _thread_required_cones_from_meters(production_required_qty, row)
            settlement_required_qty = _thread_required_cones_from_meters(settlement_required_qty, row)
        shared_key = _build_bom_shared_material_key(row)
        req_uom = str(row.get("uom") or row.get("purchase_uom") or "").strip().upper()
        for row_style_code in style_codes:
            shared_key_by_style_item[(row_style_code, item_code)] = shared_key
        row["_shared_material_key"] = shared_key
        row["_requirement_uom"] = req_uom
        row["_booking_required_qty_value"] = booking_required_qty
        row["_production_required_qty_value"] = production_required_qty
        row["_settlement_required_qty_value"] = settlement_required_qty
        row["_has_consumption_match"] = has_consumption_match
        row["_has_actual_consumption_match"] = has_actual_consumption_match
        row["_po_numbers"] = list(po_info.get("po_numbers", [])) if isinstance(po_info, dict) else []
        if not is_approved_item:
            row["_inactive_bom_status"] = True
            self._trace_bom_requirement_debug(
                row,
                po_info=po_info,
                consumption_detail=consumption_detail,
                demand_qty=demand_qty,
            )
            prepared_rows.append(row)
            continue
        group = group_totals.setdefault(
            shared_key,
            {
                "common_uom": _uom_conversion_context(req_uom)[1],
                "booking_required_qty": 0.0,
                "production_required_qty": 0.0,
                "settlement_required_qty": 0.0,
                "ordered_qty": 0.0,
                "received_qty": 0.0,
                "issued_qty": 0.0,
                "po_numbers": set(),
            },
        )
        common_uom = str(group.get("common_uom", "")).strip().upper()
        group["booking_required_qty"] = _safe_float_value(group.get("booking_required_qty")) + _convert_quantity_to_common_uom(
            booking_required_qty,
            req_uom,
            common_uom,
        )
        group["production_required_qty"] = _safe_float_value(group.get("production_required_qty")) + _convert_quantity_to_common_uom(
            production_required_qty,
            req_uom,
            common_uom,
        )
        group["settlement_required_qty"] = _safe_float_value(group.get("settlement_required_qty")) + _convert_quantity_to_common_uom(
            settlement_required_qty,
            req_uom,
            common_uom,
        )
        for po_number in row["_po_numbers"]:
            if str(po_number).strip():
                group["po_numbers"].add(str(po_number).strip())
        self._trace_bom_requirement_debug(
            row,
            po_info=po_info,
            consumption_detail=consumption_detail,
            demand_qty=demand_qty,
        )
        prepared_rows.append(row)

    for record in material_po_rows:
        style_code = normalize_style_identifier(record.get("style_code"))
        item_code = str(record.get("item_code", "")).strip()
        shared_key = shared_key_by_style_item.get((style_code, item_code))
        if not shared_key:
            continue
        group = group_totals.get(shared_key)
        if not isinstance(group, dict):
            continue
        common_uom = str(group.get("common_uom", "")).strip().upper()
        ordered_qty = _convert_quantity_to_common_uom(
            _safe_float_value(record.get("ordered_qty")),
            record.get("purchase_uom"),
            common_uom,
        )
        group["ordered_qty"] = _safe_float_value(group.get("ordered_qty")) + ordered_qty

    for record in receipt_rows:
        style_code = normalize_style_identifier(record.get("style_code"))
        item_code = str(record.get("item_code", "")).strip()
        shared_key = shared_key_by_style_item.get((style_code, item_code))
        if not shared_key:
            continue
        group = group_totals.get(shared_key)
        if not isinstance(group, dict):
            continue
        common_uom = str(group.get("common_uom", "")).strip().upper()
        received_qty = _convert_quantity_to_common_uom(
            _safe_float_value(record.get("received_qty")),
            record.get("uom"),
            common_uom,
        )
        group["received_qty"] = _safe_float_value(group.get("received_qty")) + received_qty

    for record in issue_rows:
        style_code = normalize_style_identifier(record.get("style_code"))
        item_code = str(record.get("item_code", "")).strip()
        shared_key = shared_key_by_style_item.get((style_code, item_code))
        if not shared_key:
            continue
        group = group_totals.get(shared_key)
        if not isinstance(group, dict):
            continue
        common_uom = str(group.get("common_uom", "")).strip().upper()
        issued_qty = _convert_quantity_to_common_uom(
            _safe_float_value(record.get("issued_qty")),
            record.get("uom"),
            common_uom,
        )
        group["issued_qty"] = _safe_float_value(group.get("issued_qty")) + issued_qty

    enriched_rows: list[dict] = []
    for row in prepared_rows:
        if bool(row.get("_inactive_bom_status")):
            common_uom = str(row.get("_requirement_uom", "") or row.get("uom") or row.get("purchase_uom") or "").strip().upper()
            row["po_summary"] = "-"
            row["booking_required_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["production_required_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["settlement_required_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["ordered_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["received_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["issued_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["on_hand_qty"] = _format_metric_with_uom(0.0, common_uom)
            row["shortage_qty"] = _format_metric_with_uom(0.0, common_uom)
            enriched_rows.append(row)
            continue
        shared_key = row.get("_shared_material_key")
        group = group_totals.get(shared_key, {})
        common_uom = str(group.get("common_uom", row.get("_requirement_uom", "")) or "").strip().upper()
        has_consumption_match = bool(row.get("_has_consumption_match"))
        has_actual_consumption_match = bool(row.get("_has_actual_consumption_match"))
        booking_required_qty = _safe_float_value(group.get("booking_required_qty"))
        production_required_qty = _safe_float_value(group.get("production_required_qty"))
        settlement_required_qty = _safe_float_value(group.get("settlement_required_qty"))
        ordered_qty = _safe_float_value(group.get("ordered_qty"))
        received_qty = _safe_float_value(group.get("received_qty"))
        issued_qty = _safe_float_value(group.get("issued_qty"))
        on_hand_qty = received_qty - issued_qty
        shortage_qty = max(production_required_qty - (on_hand_qty + max(ordered_qty - received_qty, 0.0)), 0.0)
        po_numbers = sorted(str(item).strip() for item in group.get("po_numbers", set()) if str(item).strip())
        po_summary = ", ".join(po_numbers[:3]) + (" ..." if len(po_numbers) > 3 else "") if po_numbers else "-"
        row["po_summary"] = po_summary
        row["booking_required_qty"] = _format_metric_with_uom(booking_required_qty, common_uom) if has_consumption_match else _format_metric_with_uom(0.0, common_uom)
        row["production_required_qty"] = _format_metric_with_uom(production_required_qty, common_uom) if has_consumption_match else _format_metric_with_uom(0.0, common_uom)
        row["settlement_required_qty"] = _format_metric_with_uom(settlement_required_qty, common_uom) if has_consumption_match else _format_metric_with_uom(0.0, common_uom)
        row["ordered_qty"] = _format_metric_with_uom(ordered_qty, common_uom)
        row["received_qty"] = _format_metric_with_uom(received_qty, common_uom)
        row["issued_qty"] = _format_metric_with_uom(issued_qty, common_uom)
        row["on_hand_qty"] = _format_metric_with_uom(on_hand_qty, common_uom)
        row["shortage_qty"] = _format_metric_with_uom(shortage_qty, common_uom)
        enriched_rows.append(row)
    return enriched_rows


def sanitize_bom_requirement_row(row: dict) -> dict:
    sanitized = dict(row)
    if str(sanitized.get("status", "")).strip().lower() == "approved":
        return sanitized
    common_uom = str(sanitized.get("_requirement_uom", "") or sanitized.get("uom") or sanitized.get("purchase_uom") or "").strip().upper()
    sanitized["po_summary"] = "-"
    for key in (
        "booking_required_qty",
        "production_required_qty",
        "settlement_required_qty",
        "ordered_qty",
        "received_qty",
        "issued_qty",
        "on_hand_qty",
        "shortage_qty",
    ):
        sanitized[key] = _format_metric_with_uom(0.0, common_uom)
    sanitized["_booking_required_qty_value"] = 0.0
    sanitized["_production_required_qty_value"] = 0.0
    sanitized["_settlement_required_qty_value"] = 0.0
    sanitized["_has_consumption_match"] = False
    sanitized["_has_actual_consumption_match"] = False
    sanitized["_po_numbers"] = []
    sanitized["_inactive_bom_status"] = True
    return sanitized


def sanitize_bom_requirement_rows(self, rows: list[dict]) -> list[dict]:
    return [sanitize_bom_requirement_row(row) for row in list(rows or []) if isinstance(row, dict)]


def summarize_bom_purchase_order_requirements(self) -> dict[str, dict]:
    if self.should_use_server_data_source():
        return self.summarize_bom_purchase_order_requirements_from_source()
    return summarize_purchase_order_requirements_by_style(DB_PATH)


def summarize_bom_purchase_order_requirements_from_source(self) -> dict[str, dict]:
    summary: dict[str, dict] = {}
    source_rows = list(self.fetch_order_records_from_source() or [])
    if not source_rows:
        source_rows = list(getattr(self, "all_orders", []) or [])

    for raw_row in source_rows:
        row = dict(raw_row) if isinstance(raw_row, dict) else {}
        raw_payload = {}
        raw_json = str(row.get("raw_json") or "").strip()
        if raw_json:
            try:
                parsed_payload = json.loads(raw_json)
                if isinstance(parsed_payload, dict):
                    raw_payload = parsed_payload
            except Exception:
                raw_payload = {}
        order_status = normalize_workflow_status(row.get("status") or raw_payload.get("status"), "draft")
        if order_status != "approved":
            continue

        po_number = str(row.get("po_number") or raw_payload.get("po_number") or "").strip()
        line_items = raw_payload.get("items", [])
        if not isinstance(line_items, list):
            line_items = []
        for item in line_items:
            if not isinstance(item, dict):
                continue
            style_code = normalize_style_identifier(item.get("mfg_style") or item.get("style_code"))
            if not style_code:
                continue
            size_code = _bom_normalize_size_code(item.get("size_desc"))
            total_units = _safe_float_value(item.get("total_units"))
            if total_units <= 0:
                total_units = _safe_float_value(item.get("total_packs")) * _safe_float_value(item.get("pack_qty"))
            summary_item = summary.setdefault(
                style_code,
                {
                    "style_code": style_code,
                    "total_qty": 0.0,
                    "po_numbers": [],
                    "by_size": {},
                },
            )
            summary_item["total_qty"] = _safe_float_value(summary_item.get("total_qty")) + total_units
            if po_number and po_number not in summary_item["po_numbers"]:
                summary_item["po_numbers"].append(po_number)
            if size_code:
                by_size = summary_item.setdefault("by_size", {})
                if not isinstance(by_size, dict):
                    by_size = {}
                    summary_item["by_size"] = by_size
                size_item = by_size.setdefault(
                    size_code,
                    {
                        "style_code": style_code,
                        "size_code": size_code,
                        "total_qty": 0.0,
                        "po_numbers": [],
                    },
                )
                size_item["total_qty"] = _safe_float_value(size_item.get("total_qty")) + total_units
                if po_number and po_number not in size_item["po_numbers"]:
                    size_item["po_numbers"].append(po_number)

    for item in summary.values():
        po_numbers = list(item.get("po_numbers", []))
        item["po_count"] = len(po_numbers)
        item["po_summary"] = ", ".join(po_numbers[:3]) + (" ..." if len(po_numbers) > 3 else "")
        by_size = item.get("by_size", {})
        if isinstance(by_size, dict):
            for size_item in by_size.values():
                if not isinstance(size_item, dict):
                    continue
                size_po_numbers = list(size_item.get("po_numbers", []))
                size_item["po_count"] = len(size_po_numbers)
                size_item["po_summary"] = ", ".join(size_po_numbers[:3]) + (" ..." if len(size_po_numbers) > 3 else "")
    debug_focus = summary.get("EEL2008")
    if debug_focus:
        _append_bom_debug_log(
            "po-summary style=EEL2008 "
            + json.dumps(
                {
                    "total_qty": debug_focus.get("total_qty"),
                    "po_numbers": list(debug_focus.get("po_numbers", [])),
                    "po_count": debug_focus.get("po_count"),
                    "po_summary": debug_focus.get("po_summary"),
                },
                ensure_ascii=False,
            )
        )
    else:
        _append_bom_debug_log("po-summary style=EEL2008 missing")
    return summary


def _trace_bom_requirement_debug(
    self,
    row: dict,
    *,
    po_info: dict,
    consumption_detail: dict,
    demand_qty: float,
) -> None:
    style_code = normalize_style_identifier(row.get("style_code"))
    if style_code != "EEL2008":
        return
    try:
        payload = {
            "style_code": style_code,
            "item_code": str(row.get("item_code", "")).strip(),
            "item_name": str(row.get("item_name", "")).strip(),
            "po_summary": str(row.get("po_summary", "")).strip(),
            "demand_qty": demand_qty,
            "po_numbers": list(po_info.get("po_numbers", [])) if isinstance(po_info, dict) else [],
            "matched_versions": sorted(str(item) for item in set(consumption_detail.get("matched_versions", set()) or set())),
            "matched_line_count": int(consumption_detail.get("matched_line_count", 0) or 0),
            "has_actual_consumption_match": bool(consumption_detail.get("has_actual_consumption_match")),
            "has_consumption_match": bool(consumption_detail.get("has_consumption_match")),
            "consumption_values": dict(consumption_detail.get("values", {})) if isinstance(consumption_detail.get("values", {}), dict) else {},
            "source_style_code": str(row.get("source_style_code", "")).strip(),
            "source_section": str(row.get("source_section", "")).strip(),
            "spec_summary": str(row.get("spec_summary", "")).strip(),
            "qty_per_garment": str(row.get("qty_per_garment", "")).strip(),
        }
        _append_bom_debug_log("enrich-row " + json.dumps(payload, ensure_ascii=False))
    except Exception as exc:
        _append_bom_debug_log(f"enrich-row debug-error style=EEL2008 error={exc}")


def load_material_po_rows_from_source(self) -> list[dict]:
    return [row for row in (normalize_material_po_row(item) for item in self._load_store_rows(MATERIAL_PO_APP_SETTING_KEY)) if isinstance(row, dict)]


def save_material_po_row_to_source(self, row: dict) -> dict:
    normalized = normalize_material_po_row(row)
    if normalized is None:
        return {}
    rows = self.load_material_po_rows_from_source()
    record_id = str(normalized.get("record_id", "")).strip()
    merged: list[dict] = []
    replaced = False
    for existing in rows:
        if str(existing.get("record_id", "")).strip() == record_id:
            merged.append(normalized)
            replaced = True
        else:
            merged.append(existing)
    if not replaced:
        merged.append(normalized)
    saved_rows = [row for row in (normalize_material_po_row(item) for item in self._save_store_rows(MATERIAL_PO_APP_SETTING_KEY, merged)) if isinstance(row, dict)]
    return next((item for item in saved_rows if str(item.get("record_id", "")).strip() == record_id), normalized)


def delete_material_po_row_from_source(self, record_id: str) -> None:
    normalized_record_id = str(record_id or "").strip()
    if not normalized_record_id:
        return
    rows = [row for row in self.load_material_po_rows_from_source() if str(row.get("record_id", "")).strip() != normalized_record_id]
    self._save_store_rows(MATERIAL_PO_APP_SETTING_KEY, rows)


def _generate_auto_material_po_number(self, existing_rows: list[dict]) -> str:
    prefix = f"AUTO-MPO-{datetime.now().strftime('%Y%m%d')}"
    used_numbers = {
        str(row.get("mpo_number", "")).strip().upper()
        for row in existing_rows
        if isinstance(row, dict) and str(row.get("mpo_number", "")).strip()
    }
    sequence = 1
    while True:
        candidate = f"{prefix}-{sequence:03d}"
        if candidate.upper() not in used_numbers:
            return candidate
        sequence += 1


def _generate_auto_material_receipt_number(self, existing_rows: list[dict], receipt_date_text: str) -> str:
    normalized_date_text = str(receipt_date_text or "").strip()
    try:
        date_token = datetime.strptime(normalized_date_text, "%Y-%m-%d").strftime("%Y%m%d")
    except Exception:
        date_token = datetime.now().strftime("%Y%m%d")
    prefix = f"GR-{date_token}"
    used_numbers = {
        str(row.get("receipt_number", "")).strip().upper()
        for row in existing_rows
        if isinstance(row, dict) and str(row.get("receipt_number", "")).strip()
    }
    sequence = 1
    while True:
        candidate = f"{prefix}-{sequence:03d}"
        if candidate.upper() not in used_numbers:
            return candidate
        sequence += 1


def auto_generate_material_po_from_booking_shortage(self, rows: list[dict]) -> int:
    source_rows = [dict(item) for item in list(rows or []) if isinstance(item, dict)]
    if not source_rows:
        return 0

    existing_rows = self.load_material_po_rows_from_source()
    active_rows = [row for row in existing_rows if _is_material_po_status_active(row.get("status"))]
    existing_auto_markers: set[str] = set()
    for row in active_rows:
        note_text = str(row.get("note", "")).strip().lower()
        if "auto-generated from bom booking shortage" not in note_text:
            continue
        marker_match = re.search(r"(AUTO-BOM-SHARED-[^\s]+)", str(row.get("note", "")).strip())
        if marker_match:
            existing_auto_markers.add(marker_match.group(1))

    created_count = 0
    processed_shared_keys: set[tuple[str, ...]] = set()
    all_rows = list(existing_rows)
    for row in source_rows:
        status_value = str(row.get("status", "")).strip().lower()
        if status_value != "approved":
            continue
        style_code = normalize_style_identifier(row.get("style_code"))
        item_code = str(row.get("item_code", "")).strip()
        item_name = str(row.get("item_name", "")).strip()
        shared_key = _build_bom_shared_material_key(row)
        if not style_code or not item_code or not item_name or shared_key in processed_shared_keys:
            continue
        if not row.get("_has_consumption_match"):
            continue
        processed_shared_keys.add(shared_key)
        booking_required_qty = _safe_float_value(row.get("booking_required_qty"))
        on_hand_qty = _safe_float_value(row.get("on_hand_qty"))
        ordered_qty = _safe_float_value(row.get("ordered_qty"))
        received_qty = _safe_float_value(row.get("received_qty"))
        outstanding_qty = max(ordered_qty - received_qty, 0.0)
        shortage_qty = booking_required_qty - (on_hand_qty + outstanding_qty)
        if shortage_qty <= 1e-9:
            continue
        shared_marker = "AUTO-BOM-SHARED-" + "|".join(shared_key)
        if shared_marker in existing_auto_markers:
            continue

        purchase_uom = str(row.get("purchase_uom") or row.get("uom") or "").strip().upper()
        po_numbers = str(row.get("po_summary", "")).strip()
        payload = default_material_po_payload()
        payload.update(
            {
                "mpo_number": self._generate_auto_material_po_number(all_rows),
                "order_date": datetime.now().strftime("%Y-%m-%d"),
                "supplier_name": str(row.get("default_supplier", "")).strip(),
                "style_code": style_code,
                "item_id": str(row.get("item_id", "")).strip(),
                "item_code": item_code,
                "item_name": item_name,
                "linked_po_numbers": "" if po_numbers == "-" else po_numbers,
                "ordered_qty": _format_metric_text(shortage_qty),
                "purchase_uom": purchase_uom,
                "status": "draft",
                "note": (
                    f"Auto-generated from BOM booking shortage [{shared_marker}]"
                    + (f" | Linked PO(s): {po_numbers}" if po_numbers and po_numbers != "-" else "")
                ),
            }
        )
        saved = self.save_material_po_row_to_source(payload)
        if not saved:
            continue
        all_rows.append(dict(saved))
        existing_auto_markers.add(shared_marker)
        created_count += 1
        _append_bom_debug_log(
            f"auto-material-po created style={style_code} item_code={item_code} shortage={_format_metric_text(shortage_qty)} uom={purchase_uom}"
        )
    return created_count


def load_material_receipt_rows_from_source(self) -> list[dict]:
    return [row for row in (normalize_material_receipt_row(item) for item in self._load_store_rows(MATERIAL_RECEIPT_APP_SETTING_KEY)) if isinstance(row, dict)]


def save_material_receipt_row_to_source(self, row: dict) -> dict:
    normalized = normalize_material_receipt_row(row)
    if normalized is None:
        return {}
    rows = self.load_material_receipt_rows_from_source()
    receipt_id = str(normalized.get("receipt_id", "")).strip()
    merged: list[dict] = []
    replaced = False
    for existing in rows:
        if str(existing.get("receipt_id", "")).strip() == receipt_id:
            merged.append(normalized)
            replaced = True
        else:
            merged.append(existing)
    if not replaced:
        merged.append(normalized)
    saved_rows = [row for row in (normalize_material_receipt_row(item) for item in self._save_store_rows(MATERIAL_RECEIPT_APP_SETTING_KEY, merged)) if isinstance(row, dict)]
    return next((item for item in saved_rows if str(item.get("receipt_id", "")).strip() == receipt_id), normalized)


def delete_material_receipt_row_from_source(self, receipt_id: str) -> None:
    normalized_receipt_id = str(receipt_id or "").strip()
    if not normalized_receipt_id:
        return
    rows = [row for row in self.load_material_receipt_rows_from_source() if str(row.get("receipt_id", "")).strip() != normalized_receipt_id]
    self._save_store_rows(MATERIAL_RECEIPT_APP_SETTING_KEY, rows)


def summarize_material_receipts_by_po_item(self, *, approved_only: bool = True) -> dict[tuple[str, str], float]:
    summary: dict[tuple[str, str], float] = {}
    for receipt in self.load_material_receipt_rows_from_source():
        if approved_only and not _is_material_receipt_approved(receipt.get("status")):
            continue
        key = (str(receipt.get("mpo_number", "")).strip(), str(receipt.get("item_code", "")).strip())
        if not key[0] or not key[1]:
            continue
        summary[key] = summary.get(key, 0.0) + _safe_float_value(receipt.get("received_qty"))
    return summary


def get_material_receipt_history_for_po_item(self, mpo_number: str, item_code: str) -> list[dict]:
    normalized_mpo_number = str(mpo_number or "").strip()
    normalized_item_code = str(item_code or "").strip()
    if not normalized_mpo_number or not normalized_item_code:
        return []
    rows = [
        dict(row)
        for row in self.load_material_receipt_rows_from_source()
        if str(row.get("mpo_number", "")).strip() == normalized_mpo_number
        and str(row.get("item_code", "")).strip() == normalized_item_code
    ]
    rows.sort(
        key=lambda item: (
            str(item.get("receipt_date", "")).strip().lower(),
            str(item.get("receipt_number", "")).strip().lower(),
            str(item.get("updated_at", "")).strip().lower(),
        ),
        reverse=True,
    )
    return rows


def load_material_issue_rows_from_source(self) -> list[dict]:
    return [row for row in (normalize_material_issue_row(item) for item in self._load_store_rows(MATERIAL_ISSUE_APP_SETTING_KEY)) if isinstance(row, dict)]


def save_material_issue_row_to_source(self, row: dict) -> dict:
    normalized = normalize_material_issue_row(row)
    if normalized is None:
        return {}
    rows = self.load_material_issue_rows_from_source()
    issue_id = str(normalized.get("issue_id", "")).strip()
    merged: list[dict] = []
    replaced = False
    for existing in rows:
        if str(existing.get("issue_id", "")).strip() == issue_id:
            merged.append(normalized)
            replaced = True
        else:
            merged.append(existing)
    if not replaced:
        merged.append(normalized)
    saved_rows = [row for row in (normalize_material_issue_row(item) for item in self._save_store_rows(MATERIAL_ISSUE_APP_SETTING_KEY, merged)) if isinstance(row, dict)]
    return next((item for item in saved_rows if str(item.get("issue_id", "")).strip() == issue_id), normalized)


def delete_material_issue_row_from_source(self, issue_id: str) -> None:
    normalized_issue_id = str(issue_id or "").strip()
    if not normalized_issue_id:
        return
    rows = [row for row in self.load_material_issue_rows_from_source() if str(row.get("issue_id", "")).strip() != normalized_issue_id]
    self._save_store_rows(MATERIAL_ISSUE_APP_SETTING_KEY, rows)


def load_warehouse_rows_from_source(self) -> list[dict]:
    rows = [row for row in (normalize_warehouse_row(item) for item in self._load_store_rows(WAREHOUSE_APP_SETTING_KEY)) if isinstance(row, dict)]
    if rows:
        return rows
    defaults = [
        normalize_warehouse_row({"warehouse_code": "MAIN", "warehouse_name": "Main Warehouse", "status": "active"}),
        normalize_warehouse_row({"warehouse_code": "RM", "warehouse_name": "Raw Material Warehouse", "status": "active"}),
    ]
    rows = [dict(item) for item in defaults if isinstance(item, dict)]
    self._save_store_rows(WAREHOUSE_APP_SETTING_KEY, rows)
    return rows


def save_warehouse_row_to_source(self, row: dict) -> dict:
    normalized = normalize_warehouse_row(row)
    if normalized is None:
        return {}
    rows = self.load_warehouse_rows_from_source()
    warehouse_id = str(normalized.get("warehouse_id", "")).strip()
    merged: list[dict] = []
    replaced = False
    for existing in rows:
        if str(existing.get("warehouse_id", "")).strip() == warehouse_id:
            merged.append(normalized)
            replaced = True
        else:
            merged.append(existing)
    if not replaced:
        merged.append(normalized)
    saved_rows = [row for row in (normalize_warehouse_row(item) for item in self._save_store_rows(WAREHOUSE_APP_SETTING_KEY, merged)) if isinstance(row, dict)]
    return next((item for item in saved_rows if str(item.get("warehouse_id", "")).strip() == warehouse_id), normalized)


def delete_warehouse_row_from_source(self, warehouse_id: str) -> None:
    normalized_warehouse_id = str(warehouse_id or "").strip()
    if not normalized_warehouse_id:
        return
    rows = [row for row in self.load_warehouse_rows_from_source() if str(row.get("warehouse_id", "")).strip() != normalized_warehouse_id]
    self._save_store_rows(WAREHOUSE_APP_SETTING_KEY, rows)


def load_bom_items_from_source(self) -> list[dict]:
    if self.should_use_server_data_source():
        if self.is_local_data_mode():
            self.ensure_local_server_running()
        _append_bom_debug_log(f"load start mode=server local_mode={self.is_local_data_mode()} api_base={self.server_settings.get('api_base_url', '')}")
        try:
            items = [item for item in fetch_bom_items(self.server_settings) if isinstance(item, dict)]
            _append_bom_debug_log(f"load api success count={len(items)}")
            return items
        except Exception as exc:
            if _is_bom_recoverable_source_error(exc):
                if self.is_local_data_mode():
                    _append_bom_debug_log(f"load api recoverable restart local-server error={exc}")
                    restarted = self.restart_local_server("refreshing local BOM API endpoints") if hasattr(self, "restart_local_server") else False
                    _append_bom_debug_log(f"load restart result ready={restarted}")
                    if restarted:
                        try:
                            items = [item for item in fetch_bom_items(self.server_settings) if isinstance(item, dict)]
                            _append_bom_debug_log(f"load api retry success count={len(items)}")
                            return items
                        except Exception as retry_exc:
                            _append_bom_debug_log(
                                "load api retry error="
                                + str(retry_exc)
                                + " traceback="
                                + traceback.format_exc(limit=6).replace("\n", " | ")
                            )
                            if not _is_bom_recoverable_source_error(retry_exc):
                                raise
                payload = fetch_app_setting(self.server_settings, BOM_APP_SETTING_KEY, [])
                rows = _normalize_bom_item_list(payload)
                _append_bom_debug_log(f"load app-setting fallback count={len(rows)}")
                if rows:
                    return rows
                _append_bom_debug_log("load fallback db local")
                return list_bom_items(DB_PATH)
            _append_bom_debug_log(
                "load api error="
                + str(exc)
                + " traceback="
                + traceback.format_exc(limit=6).replace("\n", " | ")
            )
            raise
    return list_bom_items(DB_PATH)


def save_bom_item_to_source(self, row: dict) -> dict:
    normalized = normalize_bom_item_payload(row)
    if normalized is None:
        return {}
    defer_post_save_sync = bool(getattr(self, "_defer_bom_post_save_sync", False))
    existing_row = next(
        (
            dict(item)
            for item in list(getattr(self, "bom_items", []) or [])
            if str(item.get("item_id", "")).strip() == str(normalized.get("item_id", "")).strip()
        ),
        None,
    )
    if self.should_use_server_data_source():
        if self.is_local_data_mode():
            self.ensure_local_server_running()
        _append_bom_debug_log(
            f"save start mode=server local_mode={self.is_local_data_mode()} style={normalized.get('style_code','')} item_code={normalized.get('item_code','')}"
        )
        try:
            saved = save_bom_item_via_api(self.server_settings, normalized)
            _append_bom_debug_log(
                f"save api success item_id={saved.get('item_id','')} item_code={saved.get('item_code','')}"
            )
            if not defer_post_save_sync:
                try:
                    latest_bom_rows = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
                    self.sync_tech_pack_consumption_with_bom_status([item for item in latest_bom_rows if isinstance(item, dict)])
                except Exception as sync_exc:
                    _append_bom_debug_log(f"save api sync-consumption warning={sync_exc}")
                self.record_entity_history(
                    scope="bom",
                    entity_key=saved.get("item_id", ""),
                    entity_label=str(saved.get("item_code", "")).strip() or str(saved.get("item_name", "")).strip(),
                    action="update" if existing_row else "create",
                    before=existing_row,
                    after=dict(saved),
                    summary=f"Saved BOM item {saved.get('item_code', '')}",
                )
            return saved
        except Exception as exc:
            if _is_bom_recoverable_source_error(exc):
                if self.is_local_data_mode():
                    _append_bom_debug_log(f"save api recoverable restart local-server error={exc}")
                    restarted = self.restart_local_server("refreshing local BOM API endpoints") if hasattr(self, "restart_local_server") else False
                    _append_bom_debug_log(f"save restart result ready={restarted}")
                    if restarted:
                        try:
                            saved = save_bom_item_via_api(self.server_settings, normalized)
                            _append_bom_debug_log(
                                f"save api retry success item_id={saved.get('item_id','')} item_code={saved.get('item_code','')}"
                            )
                            if not defer_post_save_sync:
                                try:
                                    latest_bom_rows = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
                                    self.sync_tech_pack_consumption_with_bom_status([item for item in latest_bom_rows if isinstance(item, dict)])
                                except Exception as sync_exc:
                                    _append_bom_debug_log(f"save api retry sync-consumption warning={sync_exc}")
                            return saved
                        except Exception as retry_exc:
                            _append_bom_debug_log(
                                "save api retry error="
                                + str(retry_exc)
                                + " traceback="
                                + traceback.format_exc(limit=6).replace("\n", " | ")
                            )
                            if not _is_bom_recoverable_source_error(retry_exc):
                                raise
                current_rows = self.load_bom_items_from_source()
                item_id = str(normalized.get("item_id", "")).strip()
                merged_rows: list[dict] = []
                replaced = False
                for existing_row in current_rows:
                    existing_id = str(existing_row.get("item_id", "")).strip()
                    if item_id and existing_id == item_id:
                        merged_rows.append(normalized)
                        replaced = True
                    else:
                        merged_rows.append(existing_row)
                if not replaced:
                    merged_rows.append(normalized)
                payload = save_app_setting(self.server_settings, BOM_APP_SETTING_KEY, merged_rows)
                saved_rows = _normalize_bom_item_list(payload)
                matched = next((item for item in saved_rows if str(item.get("item_id", "")).strip() == item_id), normalized)
                _append_bom_debug_log(
                    f"save app-setting fallback item_id={matched.get('item_id','')} item_code={matched.get('item_code','')}"
                )
                if not defer_post_save_sync:
                    try:
                        self.sync_tech_pack_consumption_with_bom_status([item for item in saved_rows if isinstance(item, dict)])
                    except Exception as sync_exc:
                        _append_bom_debug_log(f"save app-setting sync-consumption warning={sync_exc}")
                    self.record_entity_history(
                        scope="bom",
                        entity_key=matched.get("item_id", ""),
                        entity_label=str(matched.get("item_code", "")).strip() or str(matched.get("item_name", "")).strip(),
                        action="update" if existing_row else "create",
                        before=existing_row,
                        after=dict(matched),
                        summary=f"Saved BOM item {matched.get('item_code', '')}",
                    )
                return matched
            _append_bom_debug_log(
                "save api error="
                + str(exc)
                + " traceback="
                + traceback.format_exc(limit=6).replace("\n", " | ")
            )
            raise
    saved = save_bom_item(DB_PATH, normalized)
    if not defer_post_save_sync:
        try:
            latest_bom_rows = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
            self.sync_tech_pack_consumption_with_bom_status([item for item in latest_bom_rows if isinstance(item, dict)])
        except Exception as sync_exc:
            _append_bom_debug_log(f"save local sync-consumption warning={sync_exc}")
    if isinstance(saved, dict) and not defer_post_save_sync:
        self.record_entity_history(
            scope="bom",
            entity_key=saved.get("item_id", ""),
            entity_label=str(saved.get("item_code", "")).strip() or str(saved.get("item_name", "")).strip(),
            action="update" if existing_row else "create",
            before=existing_row,
            after=dict(saved),
            summary=f"Saved BOM item {saved.get('item_code', '')}",
        )
    return dict(saved) if isinstance(saved, dict) else {}


def delete_bom_item_from_source(self, item_id: str) -> bool:
    normalized_item_id = str(item_id or "").strip()
    if not normalized_item_id:
        return False
    if self.should_use_server_data_source():
        if self.is_local_data_mode():
            self.ensure_local_server_running()
        _append_bom_debug_log(f"delete start mode=server local_mode={self.is_local_data_mode()} item_id={normalized_item_id}")
        try:
            result = delete_bom_item_via_api(self.server_settings, normalized_item_id)
            deleted_count = int(result.get("deleted_count", 0) or 0) if isinstance(result, dict) else 0
            _append_bom_debug_log(f"delete api success item_id={normalized_item_id} deleted_count={deleted_count}")
            if deleted_count <= 0:
                return False
            try:
                local_deleted_count = delete_bom_item(DB_PATH, normalized_item_id)
                if local_deleted_count:
                    _append_bom_debug_log(f"delete local mirror item_id={normalized_item_id} deleted_count={local_deleted_count}")
            except Exception as local_exc:
                _append_bom_debug_log(f"delete local mirror warning={local_exc}")
            try:
                payload = fetch_app_setting(self.server_settings, BOM_APP_SETTING_KEY, [])
                rows = _normalize_bom_item_list(payload)
                if rows:
                    remaining_rows = [
                        row for row in rows
                        if str(row.get("item_id", "")).strip() != normalized_item_id
                    ]
                    if len(remaining_rows) != len(rows):
                        save_app_setting(self.server_settings, BOM_APP_SETTING_KEY, remaining_rows)
                        _append_bom_debug_log(f"delete app-setting mirror item_id={normalized_item_id}")
            except Exception as mirror_exc:
                _append_bom_debug_log(f"delete app-setting mirror warning={mirror_exc}")
            return True
        except Exception as exc:
            if _is_bom_recoverable_source_error(exc):
                if self.is_local_data_mode():
                    _append_bom_debug_log(f"delete api recoverable restart local-server error={exc}")
                    restarted = self.restart_local_server("refreshing local BOM API endpoints") if hasattr(self, "restart_local_server") else False
                    _append_bom_debug_log(f"delete restart result ready={restarted}")
                    if restarted:
                        try:
                            result = delete_bom_item_via_api(self.server_settings, normalized_item_id)
                            deleted_count = int(result.get("deleted_count", 0) or 0) if isinstance(result, dict) else 0
                            _append_bom_debug_log(f"delete api retry success item_id={normalized_item_id} deleted_count={deleted_count}")
                            if deleted_count > 0:
                                try:
                                    local_deleted_count = delete_bom_item(DB_PATH, normalized_item_id)
                                    if local_deleted_count:
                                        _append_bom_debug_log(
                                            f"delete retry local mirror item_id={normalized_item_id} deleted_count={local_deleted_count}"
                                        )
                                except Exception as local_exc:
                                    _append_bom_debug_log(f"delete retry local mirror warning={local_exc}")
                            return deleted_count > 0
                        except Exception as retry_exc:
                            _append_bom_debug_log(
                                "delete api retry error="
                                + str(retry_exc)
                                + " traceback="
                                + traceback.format_exc(limit=6).replace("\n", " | ")
                            )
                            if not _is_bom_recoverable_source_error(retry_exc):
                                raise
                current_rows = self.load_bom_items_from_source()
                remaining_rows = [
                    row for row in current_rows
                    if str(row.get("item_id", "")).strip() != normalized_item_id
                ]
                save_app_setting(self.server_settings, BOM_APP_SETTING_KEY, remaining_rows)
                _append_bom_debug_log(f"delete app-setting fallback item_id={normalized_item_id}")
                return len(remaining_rows) != len(current_rows)
            else:
                _append_bom_debug_log(
                    "delete api error="
                    + str(exc)
                    + " traceback="
                    + traceback.format_exc(limit=6).replace("\n", " | ")
                )
                raise
    _append_bom_debug_log(f"delete fallback db item_id={normalized_item_id}")
    return delete_bom_item(DB_PATH, normalized_item_id) > 0


def _build_bom_module(self, parent: ttk.Frame) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(3, weight=1)

    toolbar = ttk.Frame(parent)
    toolbar.grid(row=0, column=0, sticky="ew")
    self.widget_text["bom_new_button"] = ttk.Button(toolbar, text=self.t("new_button"), command=self.open_new_bom_item_dialog)
    self.widget_text["bom_new_button"].pack(side="left")
    self.register_permission_widget("action.bom.new", self.widget_text["bom_new_button"])
    self.widget_text["bom_copy_button"] = ttk.Button(toolbar, text=self.t("copy_button"), command=self.copy_selected_bom_item)
    self.widget_text["bom_copy_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.copy", self.widget_text["bom_copy_button"])
    self.widget_text["bom_edit_button"] = ttk.Button(toolbar, text=self.t("edit"), command=self.edit_selected_bom_item)
    self.widget_text["bom_edit_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.edit", self.widget_text["bom_edit_button"])
    self.widget_text["bom_delete_button"] = ttk.Button(toolbar, text=self.t("delete_button"), command=self.delete_selected_bom_item)
    self.widget_text["bom_delete_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.delete", self.widget_text["bom_delete_button"])
    self.widget_text["quick_bom_status_button"] = ttk.Button(toolbar, text=self.t("quick_bom_status"), command=self.quick_update_bom_status)
    self.widget_text["quick_bom_status_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.edit", self.widget_text["quick_bom_status_button"])
    self.widget_text["bom_generate_button"] = ttk.Button(toolbar, text=self.t("bom_generate_button"), command=self.generate_bom_from_tech_pack)
    self.widget_text["bom_generate_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.generate_from_tech_pack", self.widget_text["bom_generate_button"])
    self.widget_text["bom_import_fds_button"] = ttk.Button(toolbar, text=self.t("bom_import_fds_button"), command=self.open_import_fds_menu)
    self.widget_text["bom_import_fds_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.import_fds", self.widget_text["bom_import_fds_button"])
    self.widget_text["bom_import_bulk_fabric_button"] = ttk.Button(toolbar, text=self.t("bom_import_bulk_fabric_button"), command=self.open_import_bulk_fabric_menu)
    self.widget_text["bom_import_bulk_fabric_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.import_bulk_fabric", self.widget_text["bom_import_bulk_fabric_button"])
    self.widget_text["bom_import_bulk_trims_button"] = ttk.Button(toolbar, text=self.t("bom_import_bulk_trims_button"), command=self.open_import_bulk_trims_menu)
    self.widget_text["bom_import_bulk_trims_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.import_bulk_trims", self.widget_text["bom_import_bulk_trims_button"])
    self.widget_text["bom_supplier_list_button"] = ttk.Button(toolbar, text=self.t("bom_supplier_list_button"), command=self.open_bom_supplier_list_window)
    self.widget_text["bom_supplier_list_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.supplier_list", self.widget_text["bom_supplier_list_button"])
    self.widget_text["bom_materials_po_button"] = ttk.Button(toolbar, text=self.t("bom_materials_po_button"), command=self.open_materials_po_window)
    self.widget_text["bom_materials_po_button"].pack(side="left", padx=(8, 0))
    self.widget_text["bom_warehouse_button"] = ttk.Button(toolbar, text=self.t("bom_warehouse_button"), command=self.open_material_warehouse_window)
    self.widget_text["bom_warehouse_button"].pack(side="left", padx=(8, 0))
    self.widget_text["bom_export_button"] = ttk.Button(
        toolbar,
        text="Export report",
        command=self.open_bom_report_builder,
    )
    self.widget_text["bom_export_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.bom.export_report", self.widget_text["bom_export_button"])

    meta = ttk.Frame(parent)
    meta.grid(row=1, column=0, sticky="ew", pady=(12, 14))
    meta.columnconfigure(14, weight=1)
    self.bom_filter_controls = meta
    self.bom_filter_rows = []
    self.widget_text["bom_filter_field_label"] = ttk.Label(meta, text=self.t("samples_filter_field_label"))
    self.widget_text["bom_filter_field_label"].grid(row=0, column=0, sticky="w", padx=(0, 6))
    self.bom_filter_field_combo = ttk.Combobox(meta, textvariable=self.bom_filter_field_var, state="readonly", width=22)
    self.bom_filter_field_combo.grid(row=0, column=1, sticky="w")
    self.bom_filter_field_combo.bind("<<ComboboxSelected>>", self.on_bom_filter_field_changed)
    self.widget_text["bom_filter_value_label"] = ttk.Label(meta, text=self.t("samples_filter_value_label"))
    self.widget_text["bom_filter_value_label"].grid(row=0, column=2, sticky="w", padx=(12, 6))
    self.bom_search_entry = ttk.Entry(meta, textvariable=self.bom_search_var, width=36)
    self.bom_search_entry.grid(row=0, column=3, sticky="w")
    self.bom_search_entry.bind("<Return>", self.on_bom_search_submit)
    self.bom_search_entry.bind("<KP_Enter>", self.on_bom_search_submit)
    self.widget_text["bom_date_from_label"] = ttk.Label(meta, text=self.t("date_from_label"))
    self.widget_text["bom_date_from_label"].grid(row=0, column=4, sticky="w", padx=(12, 6))
    self.bom_date_from_entry = ttk.Entry(meta, textvariable=self.bom_date_from_var, width=12)
    self.bom_date_from_entry.grid(row=0, column=5, sticky="w", padx=(0, 12))
    self.bind_date_entry_picker(self.bom_date_from_entry, self.bom_date_from_var)
    self.bom_date_from_entry.bind("<Return>", self.on_bom_search_submit)
    self.bom_date_from_entry.bind("<KP_Enter>", self.on_bom_search_submit)
    self.widget_text["bom_date_to_label"] = ttk.Label(meta, text=self.t("date_to_label"))
    self.widget_text["bom_date_to_label"].grid(row=0, column=6, sticky="w", padx=(0, 6))
    self.bom_date_to_entry = ttk.Entry(meta, textvariable=self.bom_date_to_var, width=12)
    self.bom_date_to_entry.grid(row=0, column=7, sticky="w")
    self.bind_date_entry_picker(self.bom_date_to_entry, self.bom_date_to_var)
    self.bom_date_to_entry.bind("<Return>", self.on_bom_search_submit)
    self.bom_date_to_entry.bind("<KP_Enter>", self.on_bom_search_submit)
    self.widget_text["bom_sort_by_label"] = ttk.Label(meta, text=self.t("sort_by_label"))
    self.widget_text["bom_sort_by_label"].grid(row=0, column=8, sticky="w", padx=(12, 6))
    self.bom_sort_key_combo = ttk.Combobox(meta, textvariable=self.bom_sort_key_var, state="readonly", width=20)
    self.bom_sort_key_combo.grid(row=0, column=9, sticky="w")
    self.bom_sort_key_combo.bind("<<ComboboxSelected>>", self.on_bom_sort_key_changed)
    self.widget_text["bom_sort_order_label"] = ttk.Label(meta, text=self.t("sort_order_label"))
    self.widget_text["bom_sort_order_label"].grid(row=0, column=10, sticky="w", padx=(12, 6))
    self.bom_sort_order_combo = ttk.Combobox(meta, textvariable=self.bom_sort_order_var, state="readonly", width=12)
    self.bom_sort_order_combo.grid(row=0, column=11, sticky="w")
    self.bom_sort_order_combo.bind("<<ComboboxSelected>>", self.on_bom_sort_order_changed)
    self.widget_text["bom_add_filter_button"] = ttk.Button(meta, text=self.t("po_add_filter"), command=self.add_bom_filter_row)
    self.widget_text["bom_add_filter_button"].grid(row=0, column=12, sticky="w", padx=(12, 0))
    self.widget_text["bom_clear_filters_button"] = ttk.Button(meta, text=self.t("po_clear_filters"), command=self.clear_bom_filters)
    self.widget_text["bom_clear_filters_button"].grid(row=0, column=13, sticky="w", padx=(8, 0))
    self.update_bom_filter_input_state()

    self.widget_text["bom_styles_section"] = ttk.Label(parent, style="Section.TLabel", text=self.t("bom_styles_section"))
    self.widget_text["bom_styles_section"].grid(row=2, column=0, sticky="nw", pady=(0, 8))

    table_frame = ttk.Frame(parent)
    table_frame.grid(row=3, column=0, sticky="nsew")
    table_frame.columnconfigure(0, weight=1)
    table_frame.rowconfigure(0, weight=1)

    self.bom_tree = ttk.Treeview(table_frame, columns=BOM_COLUMNS, show="headings", selectmode="browse", style="Costing.Treeview", height=12)
    heading_labels = {
        "select_flag": self.t("tree_export"),
        "created_at": self.t("create_time_column"),
        "item_code": self.t("bom_item_code_column"),
        "item_name": self.t("bom_item_name_column"),
        "item_type": self.t("bom_item_type_column"),
        "section": self.t("bom_section_column"),
        "po_summary": self.t("bom_linked_pos_column"),
        "booking_required_qty": self.t("bom_booking_req_column"),
        "production_required_qty": self.t("bom_production_req_column"),
        "settlement_required_qty": self.t("bom_settlement_req_column"),
        "ordered_qty": self.t("bom_ordered_qty_column"),
        "received_qty": self.t("bom_received_qty_column"),
        "issued_qty": self.t("bom_issued_qty_column"),
        "on_hand_qty": self.t("bom_on_hand_qty_column"),
        "shortage_qty": self.t("bom_shortage_column"),
        "article_ref": self.t("bom_article_column"),
        "color": self.t("samples_color_column"),
        "spec_summary": self.t("bom_spec_summary_column"),
        "uom": "UOM",
        "default_supplier": self.t("bom_supplier_column"),
        "status": self.t("status"),
        "updated_at": self.t("revised_time_column"),
        "latest_user": self.t("latest_user_column"),
    }
    for column_key, width in {
        "select_flag": 60,
        "created_at": 130,
        "item_code": 120,
        "item_name": 190,
        "item_type": 100,
        "section": 100,
        "po_summary": 180,
        "booking_required_qty": 110,
        "production_required_qty": 110,
        "settlement_required_qty": 110,
        "ordered_qty": 110,
        "received_qty": 110,
        "issued_qty": 110,
        "on_hand_qty": 110,
        "shortage_qty": 110,
        "article_ref": 120,
        "color": 90,
        "spec_summary": 220,
        "uom": 75,
        "default_supplier": 120,
        "status": 90,
        "updated_at": 130,
        "latest_user": 140,
    }.items():
        anchor = "center" if column_key == "select_flag" else "w"
        stretch = column_key == "spec_summary"
        self.bom_tree.column(column_key, width=width, anchor=anchor, stretch=stretch)
        self.bom_tree.heading(
            column_key,
            text=heading_labels.get(column_key, column_key),
            command=lambda key=column_key: self.on_bom_heading_click(key),
        )
    configure_striped_treeview(self.bom_tree)
    bom_scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=self.bom_tree.yview)
    bom_scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=self.bom_tree.xview)
    self.bom_tree.configure(yscrollcommand=bom_scroll_y.set, xscrollcommand=bom_scroll_x.set)
    self.bom_tree.grid(row=0, column=0, sticky="nsew")
    bom_scroll_y.grid(row=0, column=1, sticky="ns")
    bom_scroll_x.grid(row=1, column=0, sticky="ew")
    self.bom_tree.bind("<<TreeviewSelect>>", self.on_bom_item_selected)
    self.bom_tree.bind("<Button-1>", self.on_bom_tree_click)
    self.bom_tree.bind("<Double-1>", self.on_bom_item_double_click)

    self.bom_status_label = ttk.Label(
        parent,
        textvariable=self.status_var,
        relief="sunken",
        anchor="w",
        justify="left",
        padding=8,
        wraplength=1180,
    )
    self.bom_status_label.grid(row=4, column=0, sticky="ew", pady=(12, 0))

def _load_enriched_bom_rows_for_refresh(self) -> tuple[list[dict], int]:
    _append_bom_debug_log("refresh-bom start")
    try:
        latest_library = self.load_tech_pack_library_from_source()
        if isinstance(latest_library, list) and latest_library:
            self.tech_pack_library = latest_library
        _append_bom_debug_log(
            f"refresh-bom tech-pack-library count={len(list(getattr(self, 'tech_pack_library', []) or []))}"
        )
    except Exception:
        _append_bom_debug_log("refresh-bom tech-pack-library reload failed")
    try:
        new_rows = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
        eel2008_count = sum(
            1
            for item in new_rows
            if isinstance(item, dict) and normalize_style_identifier(item.get("style_code")) == "EEL2008"
        )
        _append_bom_debug_log(f"refresh-bom bom-row count={len(new_rows)} eel2008_count={eel2008_count}")
    except Exception as exc:
        raise RuntimeError(f"BOM load warning: {exc}") from exc
    try:
        removed_count = self.sync_tech_pack_consumption_with_bom_status([item for item in new_rows if isinstance(item, dict)])
        if removed_count:
            _append_bom_debug_log(f"refresh-bom sync-consumption removed_count={removed_count}")
    except Exception as sync_exc:
        _append_bom_debug_log(f"refresh-bom sync-consumption warning={sync_exc}")
    enriched_rows = self.enrich_bom_requirement_rows([item for item in new_rows if isinstance(item, dict)])
    auto_created_count = 0
    if not getattr(self, "_auto_generating_material_po", False):
        try:
            self._auto_generating_material_po = True
            auto_created_count = self.auto_generate_material_po_from_booking_shortage(enriched_rows)
        finally:
            self._auto_generating_material_po = False
    if auto_created_count:
        _append_bom_debug_log(f"refresh-bom auto-material-po created_count={auto_created_count}")
        enriched_rows = self.enrich_bom_requirement_rows([item for item in new_rows if isinstance(item, dict)])
    return enriched_rows, auto_created_count


def _apply_bom_refreshed_rows(self, enriched_rows: list[dict]) -> None:
    self.bom_items = self.sanitize_bom_requirement_rows(enriched_rows)
    for item in self.bom_items:
        item["latest_user_display"] = self.get_entity_latest_user_display("bom", item.get("item_id", ""))
        item["latest_user"] = item["latest_user_display"]
    self.apply_bom_filter()


def refresh_bom_module(self) -> None:
    try:
        enriched_rows, _auto_created_count = self._load_enriched_bom_rows_for_refresh()
    except Exception as exc:
        self.bom_items = []
        self.filtered_bom_items = []
        if hasattr(self, "bom_tree"):
            self.refresh_bom_tree()
        else:
            self.update_report_target_summary()
        self.set_status(str(exc))
        return
    self._apply_bom_refreshed_rows(enriched_rows)


def update_bom_filter_input_state(self, *, clear_irrelevant: bool = False) -> None:
    filter_field = getattr(self, "bom_filter_field_internal", "__all__")
    is_date_field = filter_field in BOM_DATE_FILTER_FIELDS
    if clear_irrelevant:
        if is_date_field:
            self.sync_bom_search_query("")
        else:
            self.bom_date_from_var.set("")
            self.bom_date_to_var.set("")
    search_state = "disabled" if is_date_field else "normal"
    date_state = "normal" if is_date_field else "disabled"
    if hasattr(self, "bom_search_entry"):
        self.bom_search_entry.configure(state=search_state)
    if hasattr(self, "bom_date_from_entry"):
        self.bom_date_from_entry.configure(state=date_state)
    if hasattr(self, "bom_date_to_entry"):
        self.bom_date_to_entry.configure(state=date_state)


def apply_bom_filter(self) -> None:
    if getattr(self, "_bom_filter_update_suspended", False):
        return
    self.bom_items = self.sanitize_bom_requirement_rows(list(getattr(self, "bom_items", []) or []))
    filtered = list(self.bom_items)
    active_filters = self.get_active_bom_filters()
    if active_filters:
        filtered = [
            row
            for row in filtered
            if all(self.bom_row_matches_filter(row, item) for item in active_filters)
        ]

    sort_key = self.bom_sort_key_internal
    reverse = self.bom_sort_order_internal == "desc"

    def sort_value(row: dict) -> object:
        value = row.get(sort_key, "")
        if sort_key in {"created_at", "updated_at"}:
            return self.parse_sort_value(sort_key, str(value or ""))
        if sort_key in {"booking_required_qty", "production_required_qty", "settlement_required_qty", "ordered_qty", "received_qty", "issued_qty", "on_hand_qty", "shortage_qty"}:
            return _safe_float_value(value)
        return str(value or "").lower()

    filtered.sort(key=sort_value, reverse=reverse)
    self.filtered_bom_items = filtered
    if hasattr(self, "bom_tree"):
        self.refresh_bom_tree()
    self.update_report_target_summary()


def commit_bom_search_query(self, value: object | None = None, *, apply_filter: bool = True) -> None:
    committed_value = self.bom_search_var.get() if value is None else value
    self.bom_filter_field_committed_internal = getattr(self, "bom_filter_field_internal", "__all__")
    self.bom_search_committed_value = str(committed_value or "").strip()
    self.bom_date_from_committed_value = self.bom_date_from_var.get().strip()
    self.bom_date_to_committed_value = self.bom_date_to_var.get().strip()
    for row_data in list(getattr(self, "bom_filter_rows", []) or []):
        row_data["committed_field_internal"] = str(row_data.get("field_internal", "__all__") or "__all__")
        value_var = row_data.get("value_var")
        from_var = row_data.get("from_var")
        to_var = row_data.get("to_var")
        row_data["committed_query"] = value_var.get().strip() if value_var is not None else ""
        row_data["committed_from"] = from_var.get().strip() if from_var is not None else ""
        row_data["committed_to"] = to_var.get().strip() if to_var is not None else ""
    if apply_filter:
        self.apply_bom_filter()


def bom_row_matches_filter(self, row: dict, filter_item: dict) -> bool:
    field = str(filter_item.get("field", "__all__") or "__all__")
    query = str(filter_item.get("query", "") or "").strip().lower()
    from_text = str(filter_item.get("from", "") or "").strip()
    to_text = str(filter_item.get("to", "") or "").strip()
    if field in BOM_DATE_FILTER_FIELDS and (from_text or to_text):
        return self.is_within_date_range(row.get(field), from_text, to_text)
    if not query:
        return True
    if field == "__all__":
        fields = [
            "item_code", "item_name", "item_type", "section", "po_summary", "article_ref", "color", "description",
            "composition", "booking_required_qty", "production_required_qty", "settlement_required_qty",
            "ordered_qty", "received_qty", "issued_qty", "on_hand_qty", "shortage_qty", "default_supplier",
            "position_of_use", "status",
        ]
        return query in " ".join(self.display_value(row.get(key)) for key in fields).lower()
    return query in self.display_value(row.get(field)).strip().lower()


def get_active_bom_filters(self) -> list[dict]:
    active: list[dict] = []
    field = str(getattr(self, "bom_filter_field_committed_internal", getattr(self, "bom_filter_field_internal", "__all__")) or "__all__")
    query = str(getattr(self, "bom_search_committed_value", "") or "").strip()
    from_text = str(getattr(self, "bom_date_from_committed_value", "") or "").strip()
    to_text = str(getattr(self, "bom_date_to_committed_value", "") or "").strip()
    if field in BOM_DATE_FILTER_FIELDS:
        if from_text or to_text:
            active.append({"field": field, "from": from_text, "to": to_text})
    elif query:
        active.append({"field": field, "query": query})
    for row_data in list(getattr(self, "bom_filter_rows", []) or []):
        row_field = str(row_data.get("committed_field_internal", row_data.get("field_internal", "__all__")) or "__all__")
        row_query = str(row_data.get("committed_query", "") or "").strip()
        row_from = str(row_data.get("committed_from", "") or "").strip()
        row_to = str(row_data.get("committed_to", "") or "").strip()
        if row_field in BOM_DATE_FILTER_FIELDS:
            if row_from or row_to:
                active.append({"field": row_field, "from": row_from, "to": row_to})
        elif row_query:
            active.append({"field": row_field, "query": row_query})
    return active


def add_bom_filter_row(self) -> None:
    rows = getattr(self, "bom_filter_rows", [])
    row_data = create_extra_filter_row(
        self,
        module_key="bom",
        parent=self.bom_filter_controls,
        options=BOM_FILTER_FIELD_OPTIONS,
        date_fields=BOM_DATE_FILTER_FIELDS,
        row_index=len(rows) + 1,
        remove_command=self.remove_bom_filter_row,
        submit_command=self.on_bom_search_submit,
        translate_filter_field=self.translate_bom_filter_field,
    )
    rows.append(row_data)
    self.bom_filter_rows = rows


def remove_bom_filter_row(self, row_data: dict) -> None:
    rows = [row for row in list(getattr(self, "bom_filter_rows", []) or []) if row is not row_data]
    destroy_filter_row(row_data)
    self.bom_filter_rows = rows
    regrid_filter_rows(rows)
    self.apply_bom_filter()


def clear_bom_filters(self) -> None:
    self._bom_filter_update_suspended = True
    try:
        self.bom_filter_field_internal = "__all__"
        self.bom_filter_field_committed_internal = "__all__"
        self.bom_filter_field_var.set(self.translate_bom_filter_field("__all__"))
        self.sync_bom_search_query("")
        self.bom_date_from_committed_value = ""
        self.bom_date_to_committed_value = ""
        self.bom_date_from_var.set("")
        self.bom_date_to_var.set("")
        for row_data in list(getattr(self, "bom_filter_rows", []) or []):
            destroy_filter_row(row_data)
        self.bom_filter_rows = []
    finally:
        self._bom_filter_update_suspended = False
    self.update_bom_filter_input_state()
    self.apply_bom_filter()


def on_bom_search_submit(self, _event: object | None = None) -> str:
    self.commit_bom_search_query()
    return "break"


def sync_bom_search_query(self, value: object) -> None:
    normalized_value = str(value or "").strip()
    self.bom_search_var.set(normalized_value)
    self.bom_search_committed_value = normalized_value


def schedule_bom_filter_apply(self) -> None:
    self.commit_bom_search_query()


def run_bom_filter_apply(self) -> None:
    self.apply_bom_filter()


def refresh_bom_tree(self) -> None:
    selected_item_id = self.get_selected_bom_item_id()
    self.bom_tree.delete(*self.bom_tree.get_children())
    self.bom_tree.tag_configure("bom_status_draft", background="#ffffff")
    self.bom_tree.tag_configure("bom_status_checking", background="#fff7cc")
    self.bom_tree.tag_configure("bom_status_approved", background="#dff5e3")
    valid_ids = {str(row.get("item_id", "")).strip() for row in self.bom_items if str(row.get("item_id", "")).strip()}
    self.checked_bom_item_ids.intersection_update(valid_ids)
    for index, row in enumerate(self.filtered_bom_items, start=1):
        row = sanitize_bom_requirement_row(row)
        item_id = str(row.get("item_id", "")).strip()
        if not item_id:
            continue
        values = []
        for key in BOM_COLUMNS:
            if key == "select_flag":
                values.append("[x]" if item_id in self.checked_bom_item_ids else "[ ]")
            else:
                values.append(self.display_value(row.get(key)))
        status_value = str(row.get("status", "")).strip().lower()
        status_tag = {
            "checking": "bom_status_checking",
            "approved": "bom_status_approved",
        }.get(status_value, "bom_status_draft")
        self.bom_tree.insert(
            "",
            "end",
            iid=item_id,
            values=tuple(values),
            tags=(status_tag,),
        )
    if selected_item_id and self.bom_tree.exists(selected_item_id):
        self.bom_tree.selection_set(selected_item_id)
        self.bom_tree.focus(selected_item_id)
    self.update_bom_module_status()


def update_bom_module_status(self) -> None:
    total = len(self.bom_items)
    shown = len(self.filtered_bom_items)
    checked = len(getattr(self, "checked_bom_item_ids", set()) or set())
    total_shortage = sum(_safe_float_value(row.get("shortage_qty")) for row in self.filtered_bom_items)
    self.set_status(f"BOM items: {shown}/{total} | Export checked: {checked} | Visible shortage: {_format_metric_text(total_shortage)}")


def get_selected_bom_item_id(self) -> str:
    if not hasattr(self, "bom_tree"):
        return ""
    selection = self.bom_tree.selection()
    return str(selection[0]).strip() if selection else ""


def get_selected_bom_item(self) -> dict | None:
    item_id = self.get_selected_bom_item_id()
    if not item_id:
        return None
    for row in self.bom_items:
        if str(row.get("item_id", "")).strip() == item_id:
            return dict(row)
    return None


def get_target_bom_status_rows(self) -> list[dict]:
    checked_rows = [
        dict(row)
        for row in self.filtered_bom_items
        if str(row.get("item_id", "")).strip() in self.checked_bom_item_ids
    ]
    if checked_rows:
        return checked_rows
    selected_row = self.get_selected_bom_item()
    return [selected_row] if selected_row is not None else []


def quick_update_bom_status(self) -> None:
    if not self.is_action_allowed("action.bom.edit"):
        return
    target_rows = self.get_target_bom_status_rows()
    if not target_rows:
        messagebox.showwarning(self.t("quick_bom_status_title"), self.t("quick_bom_status_need_selection"))
        return

    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("quick_bom_status_title"))
    dialog.resizable(False, False)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(1, weight=1)

    ttk.Label(frame, text=self.t("quick_bom_status_count", count=len(target_rows))).grid(
        row=0,
        column=0,
        columnspan=2,
        sticky="w",
        pady=(0, 12),
    )
    status_var = tk.StringVar(value="checking")
    ttk.Label(frame, text=self.t("status")).grid(row=1, column=0, sticky="w", padx=(0, 10), pady=4)
    ttk.Combobox(
        frame,
        textvariable=status_var,
        values=("draft", "checking", "approved"),
        state="readonly",
        width=18,
    ).grid(row=1, column=1, sticky="ew", pady=4)

    actions = ttk.Frame(frame)
    actions.grid(row=2, column=0, columnspan=2, sticky="e", pady=(14, 0))

    def apply_status() -> None:
        target_status = normalize_workflow_status(status_var.get(), "draft")
        if target_status not in {"draft", "checking", "approved"}:
            return
        if target_status == "approved" and not self.is_action_allowed("action.bom.approve"):
            self.deny_permission_action("action.bom.approve")
            return
        try:
            dialog.destroy()
        except tk.TclError:
            pass
        self.start_quick_bom_status_worker(target_rows, target_status)

    ttk.Button(actions, text=self.t("collection_list_save"), command=apply_status).pack(side="right")
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="right", padx=(0, 8))
    if hasattr(self, "center_dialog"):
        self.center_dialog(dialog, min_width=360, min_height=170)


def start_quick_bom_status_worker(self, target_rows: list[dict], target_status: str) -> None:
    rows_by_id: dict[str, dict] = {}
    for row in list(target_rows or []):
        if not isinstance(row, dict):
            continue
        item_id = str(row.get("item_id", "")).strip()
        if item_id and item_id not in rows_by_id:
            rows_by_id[item_id] = dict(row)
    if not rows_by_id:
        return
    if getattr(self, "quick_bom_status_loading", False):
        messagebox.showinfo(self.t("quick_bom_status_title"), self.t("quick_bom_status_running"))
        return

    self.quick_bom_status_loading = True
    quick_button = getattr(self, "widget_text", {}).get("quick_bom_status_button")
    if quick_button is not None:
        try:
            quick_button.configure(state="disabled")
        except tk.TclError:
            pass
    self.set_status(self.t("quick_bom_status_running_count", count=len(rows_by_id)))

    def worker() -> None:
        updated_count = 0
        skipped_count = 0
        updated_item_ids: list[str] = []
        failed: list[str] = []
        for item_id, row in rows_by_id.items():
            try:
                working_row = normalize_bom_item_payload(row)
                if not isinstance(working_row, dict):
                    failed.append(item_id)
                    continue
                if normalize_workflow_status(working_row.get("status"), "draft") == target_status:
                    skipped_count += 1
                    continue
                working_row["status"] = target_status
                saved = self.save_bom_item_to_source(working_row)
                saved_item_id = str((saved or {}).get("item_id", "")).strip() or item_id
                updated_item_ids.append(saved_item_id)
                updated_count += 1
            except Exception as exc:
                failed.append(f"{row.get('item_code') or item_id}: {exc}")
        payload = {
            "target_status": target_status,
            "updated_count": updated_count,
            "skipped_count": skipped_count,
            "updated_item_ids": updated_item_ids,
            "target_item_ids": list(rows_by_id.keys()),
            "failed": failed,
        }
        if hasattr(self, "schedule_root_after"):
            self.schedule_root_after(0, lambda result=payload: self.finish_quick_bom_status_worker(result))
        else:
            self.root.after(0, lambda result=payload: self.finish_quick_bom_status_worker(result))

    threading.Thread(target=worker, daemon=True).start()


def finish_quick_bom_status_worker(self, payload: dict) -> None:
    self.quick_bom_status_loading = False
    quick_button = getattr(self, "widget_text", {}).get("quick_bom_status_button")
    if quick_button is not None:
        try:
            quick_button.configure(state="normal")
        except tk.TclError:
            pass

    target_status = normalize_workflow_status(payload.get("target_status"), "draft") if isinstance(payload, dict) else "draft"
    updated_item_ids = {
        str(item_id).strip()
        for item_id in list(payload.get("updated_item_ids", []) or [])
        if str(item_id).strip()
    } if isinstance(payload, dict) else set()
    target_item_ids = {
        str(item_id).strip()
        for item_id in list(payload.get("target_item_ids", []) or [])
        if str(item_id).strip()
    } if isinstance(payload, dict) else set()
    failed = list(payload.get("failed", [])) if isinstance(payload, dict) and isinstance(payload.get("failed", []), list) else []
    try:
        self.refresh_bom_module()
    except Exception as exc:
        failed.append(f"Refresh BOM: {exc}")
    refreshed_by_id = {
        str(row.get("item_id", "")).strip(): dict(row)
        for row in list(getattr(self, "bom_items", []) or [])
        if isinstance(row, dict) and str(row.get("item_id", "")).strip()
    }
    not_updated = [
        item_id
        for item_id in sorted(updated_item_ids)
        if normalize_workflow_status(refreshed_by_id.get(item_id, {}).get("status"), "draft") != target_status
    ]
    if not_updated:
        failed.append(f"{len(not_updated)} BOM item(s) did not show the new status after refresh.")
    if failed:
        clear_item_ids = updated_item_ids
    else:
        clear_item_ids = target_item_ids or updated_item_ids
    for item_id in clear_item_ids:
        self.checked_bom_item_ids.discard(item_id)
    updated_count = int(payload.get("updated_count", 0) or 0) if isinstance(payload, dict) else 0
    skipped_count = int(payload.get("skipped_count", 0) or 0) if isinstance(payload, dict) else 0
    self.set_status(self.t("quick_bom_status_done", count=updated_count, status=target_status))
    if hasattr(self, "bom_tree"):
        self.update_bom_tree_check_cell()
        self.update_bom_module_status()
    if failed:
        messagebox.showwarning(
            self.t("quick_bom_status_title"),
            self.t("quick_bom_status_failed", count=len(failed)) + "\n\n" + "\n".join(failed[:12]),
        )
    else:
        messagebox.showinfo(
            self.t("quick_bom_status_title"),
            self.t("quick_bom_status_done", count=updated_count, status=target_status),
        )


def on_bom_heading_click(self, key: str) -> None:
    if key == "select_flag":
        visible_ids = [
            str(row.get("item_id", "")).strip()
            for row in self.filtered_bom_items
            if str(row.get("item_id", "")).strip()
        ]
        checked_visible_ids = {item_id for item_id in visible_ids if item_id in self.checked_bom_item_ids}
        if checked_visible_ids and len(checked_visible_ids) == len(visible_ids):
            self.checked_bom_item_ids.difference_update(visible_ids)
        else:
            self.checked_bom_item_ids.update(visible_ids)
        self.update_bom_tree_check_cell()
        selected_visible_ids = [item_id for item_id in visible_ids if item_id in self.checked_bom_item_ids]
        if selected_visible_ids:
            first_item_id = selected_visible_ids[0]
            if self.bom_tree.exists(first_item_id):
                self.bom_tree.selection_set(first_item_id)
                self.bom_tree.focus(first_item_id)
        else:
            self.bom_tree.selection_remove(self.bom_tree.selection())
        return
    if self.bom_sort_key_internal == key:
        self.bom_sort_order_internal = "asc" if self.bom_sort_order_internal == "desc" else "desc"
        self.bom_sort_order_var.set(self.translate_sort_order(self.bom_sort_order_internal))
    else:
        self.bom_sort_key_internal = key
        self.bom_sort_key_var.set(self.translate_bom_sort_key(key))
    self.apply_bom_filter()


def update_bom_tree_check_cell(self, row_id: str | None = None) -> None:
    if not hasattr(self, "bom_tree"):
        return
    target_ids = [row_id] if row_id else list(self.bom_tree.get_children())
    for item_id in target_ids:
        if not self.bom_tree.exists(item_id):
            continue
        self.bom_tree.set(item_id, "select_flag", "[x]" if item_id in self.checked_bom_item_ids else "[ ]")
    self.update_bom_module_status()


def on_bom_tree_click(self, event: tk.Event) -> str | None:
    region = self.bom_tree.identify_region(event.x, event.y)
    if region != "cell":
        return None
    column = self.bom_tree.identify_column(event.x)
    if column != "#1":
        return None
    row_id = self.bom_tree.identify_row(event.y)
    if not row_id:
        return None
    if row_id in self.checked_bom_item_ids:
        self.checked_bom_item_ids.remove(row_id)
    else:
        self.checked_bom_item_ids.add(row_id)
    self.update_bom_tree_check_cell(row_id)
    self.bom_tree.selection_set(row_id)
    self.bom_tree.focus(row_id)
    return "break"


def on_bom_item_selected(self, _event: object | None = None) -> None:
    self.update_report_target_summary()


def on_bom_item_double_click(self, _event: object | None = None) -> None:
    self.edit_selected_bom_item()


def translate_bom_sort_key(self, internal_value: str) -> str:
    for key, text_key in BOM_SORT_KEY_OPTIONS:
        if key == internal_value:
            return self.t(text_key)
    return internal_value


def translate_bom_filter_field(self, internal_value: str) -> str:
    for key, text_key in BOM_FILTER_FIELD_OPTIONS:
        if key == internal_value:
            return self.t(text_key)
    return internal_value


def on_bom_sort_key_changed(self, _event: object) -> None:
    selected = self.bom_sort_key_var.get()
    for internal, text_key in BOM_SORT_KEY_OPTIONS:
        if selected == self.t(text_key):
            self.bom_sort_key_internal = internal
            break
    self.apply_bom_filter()


def on_bom_filter_field_changed(self, _event: object) -> None:
    selected = self.bom_filter_field_var.get()
    for internal, text_key in BOM_FILTER_FIELD_OPTIONS:
        if selected == self.t(text_key):
            self.bom_filter_field_internal = internal
            break
    self.update_bom_filter_input_state(clear_irrelevant=True)


def on_bom_sort_order_changed(self, _event: object) -> None:
    selected = self.bom_sort_order_var.get()
    for internal, text_key in (("asc", "ascending"), ("desc", "descending")):
        if selected == self.t(text_key):
            self.bom_sort_order_internal = internal
            break
    self.apply_bom_filter()


def open_new_bom_item_dialog(self) -> None:
    if not self.is_action_allowed("action.bom.new"):
        return
    self.open_bom_item_detail_window(default_bom_item_payload(), is_new=True, is_copy=False)


def edit_selected_bom_item(self) -> None:
    if not self.is_action_allowed("action.bom.edit"):
        return
    row = self.get_selected_bom_item()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), "Please select one BOM item first.")
        return
    self.open_bom_item_detail_window(row, is_new=False, is_copy=False)


def copy_selected_bom_item(self) -> None:
    if not self.is_action_allowed("action.bom.copy"):
        return
    row = self.get_selected_bom_item()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), "Please select one BOM item first.")
        return
    copied = deepcopy(row)
    copied["item_id"] = ""
    copied["item_code"] = ""
    copied["created_at"] = ""
    copied["updated_at"] = ""
    self.open_bom_item_detail_window(copied, is_new=True, is_copy=True)


def delete_selected_bom_item(self) -> None:
    if not self.is_action_allowed("action.bom.delete"):
        return
    if getattr(self, "bom_delete_loading", False):
        messagebox.showinfo(self.t("warning_title"), "BOM delete is already running.")
        return
    checked_rows = [
        row
        for row in self.filtered_bom_items
        if str(row.get("item_id", "")).strip() in self.checked_bom_item_ids
    ]
    target_rows = checked_rows
    if not target_rows:
        selected_row = self.get_selected_bom_item()
        if selected_row is not None:
            target_rows = [selected_row]
    if not target_rows:
        messagebox.showwarning(self.t("warning_title"), "Please select one BOM item first.")
        return
    if len(target_rows) == 1:
        item_name = str(target_rows[0].get("item_name", "")).strip() or str(target_rows[0].get("item_code", "")).strip()
        confirm_message = f"Delete BOM item '{item_name}'?"
    else:
        item_name = f"{len(target_rows)} BOM items"
        confirm_message = f"Delete {len(target_rows)} BOM items?"
    confirmed = messagebox.askyesno(
        self.t("warning_title"),
        confirm_message,
    )
    if not confirmed:
        return
    self.start_delete_bom_items_worker([dict(row) for row in target_rows])


def start_delete_bom_items_worker(self, target_rows: list[dict]) -> None:
    rows = [dict(row) for row in list(target_rows or []) if isinstance(row, dict)]
    if not rows:
        return
    self.bom_delete_loading = True
    delete_button = self.widget_text.get("bom_delete_button") if hasattr(self, "widget_text") else None
    try:
        if delete_button is not None:
            delete_button.configure(state="disabled")
    except tk.TclError:
        pass
    self.set_status(f"Deleting {len(rows)} BOM item(s)...")

    def worker() -> None:
        payload = {
            "deleted_names": [],
            "deleted_item_ids": [],
            "failed": [],
            "enriched_rows": [],
            "refresh_error": "",
            "refresh_loaded": False,
        }
        for row in rows:
            item_id = str(row.get("item_id", "")).strip()
            if not item_id:
                continue
            try:
                deleted = bool(self.delete_bom_item_from_source(item_id))
            except Exception as exc:
                payload["failed"].append(f"{row.get('item_code') or item_id}: {exc}")
                continue
            if deleted:
                payload["deleted_item_ids"].append(item_id)
                payload["deleted_names"].append(
                    str(row.get("item_name", "")).strip()
                    or str(row.get("item_code", "")).strip()
                    or item_id
                )
        try:
            enriched_rows, _auto_created_count = self._load_enriched_bom_rows_for_refresh()
            payload["enriched_rows"] = [dict(row) for row in enriched_rows if isinstance(row, dict)]
            payload["refresh_loaded"] = True
        except Exception as exc:
            payload["refresh_error"] = str(exc)
        if hasattr(self, "schedule_root_after"):
            self.schedule_root_after(0, lambda result=payload: self.finish_delete_bom_items_worker(result))
        else:
            self.root.after(0, lambda result=payload: self.finish_delete_bom_items_worker(result))

    threading.Thread(target=worker, daemon=True).start()


def finish_delete_bom_items_worker(self, payload: dict) -> None:
    self.bom_delete_loading = False
    delete_button = self.widget_text.get("bom_delete_button") if hasattr(self, "widget_text") else None
    try:
        if delete_button is not None:
            delete_button.configure(state="normal")
    except tk.TclError:
        pass
    deleted_item_ids = [
        str(item_id).strip()
        for item_id in list(payload.get("deleted_item_ids", []) if isinstance(payload, dict) else [])
        if str(item_id).strip()
    ]
    for item_id in deleted_item_ids:
        self.checked_bom_item_ids.discard(item_id)
    enriched_rows = list(payload.get("enriched_rows", []) if isinstance(payload, dict) else [])
    if bool(payload.get("refresh_loaded", False) if isinstance(payload, dict) else False):
        self._apply_bom_refreshed_rows([dict(row) for row in enriched_rows if isinstance(row, dict)])
    elif payload.get("refresh_error") if isinstance(payload, dict) else "":
        self.refresh_bom_module()
    deleted_names = list(payload.get("deleted_names", []) if isinstance(payload, dict) else [])
    failed = list(payload.get("failed", []) if isinstance(payload, dict) else [])
    refresh_error = str(payload.get("refresh_error", "") if isinstance(payload, dict) else "").strip()
    if not deleted_names:
        messagebox.showwarning(self.t("warning_title"), "No BOM item was deleted. Please refresh and try again.")
        self.set_status("No BOM item was deleted.")
        return
    if len(deleted_names) == 1:
        self.set_status(f"Deleted BOM item {deleted_names[0]}.")
    else:
        self.set_status(f"Deleted {len(deleted_names)} BOM items.")
    if failed or refresh_error:
        issues = list(failed)
        if refresh_error:
            issues.append(f"Refresh BOM: {refresh_error}")
        messagebox.showwarning(self.t("warning_title"), "BOM delete finished with issues:\n" + "\n".join(issues[:12]))


def open_bom_report_builder(self) -> None:
    if not self.is_action_allowed("action.bom.export_report"):
        return
    checked_rows = [
        row
        for row in self.filtered_bom_items
        if str(row.get("item_id", "")).strip() in self.checked_bom_item_ids
    ]
    if not checked_rows:
        messagebox.showwarning(self.t("report_no_checked_title_bom"), self.t("report_no_checked_message_bom"))
        return
    self.export_report("bom")


def open_bom_item_detail_window(self, row: dict, *, is_new: bool, is_copy: bool) -> None:
    initial_row = normalize_bom_item_payload(row) or default_bom_item_payload()
    dialog = tk.Toplevel(self.root)
    dialog.title("New BOM Item" if is_new and not is_copy else ("Copy BOM Item" if is_copy else "Edit BOM Item"))
    dialog.resizable(True, True)
    dialog.geometry("1360x860")
    dialog.minsize(1180, 760)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(1, weight=1)

    header = ttk.Frame(frame)
    header.grid(row=0, column=0, sticky="ew")
    ttk.Label(header, text=dialog.title(), style="Section.TLabel").pack(side="left")

    notebook = ttk.Notebook(frame)
    notebook.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
    basic_tab = ttk.Frame(notebook, padding=14)
    spec_tab = ttk.Frame(notebook, padding=14)
    purchase_tab = ttk.Frame(notebook, padding=14)
    notebook.add(basic_tab, text="Basic Information")
    notebook.add(spec_tab, text="Spec")
    notebook.add(purchase_tab, text="Purchase")

    initial_payload = dict(initial_row or {})
    trim_size_value, trim_size_unit = _split_measurement_and_unit(initial_payload.get("trim_size_um", ""))
    length_value, length_unit = _split_measurement_and_unit(initial_payload.get("length_value", ""))
    width_value, width_unit = _split_measurement_and_unit(initial_payload.get("width_value", ""))
    gsm_value, gsm_unit = _split_measurement_and_unit(initial_payload.get("gsm_value", ""))
    initial_payload["trim_size_um"] = trim_size_value or str(initial_payload.get("trim_size_um", "")).strip()
    initial_payload["trim_size_unit"] = str(initial_payload.get("trim_size_unit", "")).strip() or trim_size_unit
    initial_payload["length_value"] = length_value or str(initial_payload.get("length_value", "")).strip()
    initial_payload["length_unit"] = str(initial_payload.get("length_unit", "")).strip() or length_unit
    initial_payload["width_value"] = width_value or str(initial_payload.get("width_value", "")).strip()
    initial_payload["width_unit"] = str(initial_payload.get("width_unit", "")).strip() or width_unit
    initial_payload["gsm_value"] = gsm_value or str(initial_payload.get("gsm_value", "")).strip()
    initial_payload["gsm_unit"] = str(initial_payload.get("gsm_unit", "")).strip() or gsm_unit

    vars_map = {key: tk.StringVar(value=str(initial_payload.get(key, "")).strip()) for key in default_bom_item_payload()}
    text_widgets: dict[str, tk.Text] = {}
    image_preview_state: dict[str, object] = {"photo": None}
    unit_combos: dict[str, ttk.Combobox] = {}
    numeric_vcmd = (dialog.register(lambda proposed: proposed == "" or re.fullmatch(r"\d*(?:\.\d*)?", proposed) is not None), "%P")

    def add_labeled_entry(
        parent_frame: ttk.Frame,
        row_index: int,
        column_index: int,
        label: str,
        key: str,
        *,
        columnspan: int = 1,
        readonly: bool = False,
        validatecommand: tuple[str, str] | None = None,
    ) -> ttk.Entry:
        field = ttk.Frame(parent_frame)
        field.grid(row=row_index, column=column_index, columnspan=columnspan, sticky="ew", padx=8, pady=(0, 12))
        field.columnconfigure(0, weight=1)
        ttk.Label(field, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        entry = ttk.Entry(field, textvariable=vars_map[key])
        if validatecommand is not None:
            entry.configure(validate="key", validatecommand=validatecommand)
        if readonly:
            entry.configure(state="readonly")
        entry.grid(row=1, column=0, sticky="ew")
        return entry

    def add_labeled_combo(
        parent_frame: ttk.Frame,
        row_index: int,
        column_index: int,
        label: str,
        key: str,
        values: list[str],
    ) -> ttk.Combobox:
        field = ttk.Frame(parent_frame)
        field.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        field.columnconfigure(0, weight=1)
        ttk.Label(field, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        combo = ttk.Combobox(field, textvariable=vars_map[key], values=values, state="readonly")
        combo.grid(row=1, column=0, sticky="ew")
        return combo

    def add_labeled_supplier_search_combo(
        parent_frame: ttk.Frame,
        row_index: int,
        column_index: int,
        label: str,
        key: str,
        values: list[str],
    ) -> ttk.Combobox:
        field = ttk.Frame(parent_frame)
        field.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        field.columnconfigure(0, weight=1)
        ttk.Label(field, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        combo = ttk.Combobox(field, textvariable=vars_map[key], values=values, state="normal")
        combo.grid(row=1, column=0, sticky="ew")

        def refresh_matches(_event: object | None = None) -> None:
            query = vars_map[key].get().strip().lower()
            if query:
                matched_values = [value for value in values if query in value.lower()]
            else:
                matched_values = list(values)
            combo.configure(values=matched_values)
            if matched_values:
                try:
                    combo.event_generate("<Down>")
                except tk.TclError:
                    pass

        combo.bind("<KeyRelease>", refresh_matches)
        combo.bind("<FocusIn>", lambda _event: combo.configure(values=list(values)))
        return combo

    def combo_values_with_current(values: list[str], current: object) -> list[str]:
        normalized_values = [str(value).strip().upper() for value in values if str(value).strip()]
        current_value = str(current or "").strip().upper()
        if current_value and current_value not in normalized_values:
            normalized_values.append(current_value)
        return normalized_values

    def get_active_supplier_names_for_purchase() -> list[str]:
        rows: list[dict] = []
        if hasattr(self, "load_supplier_rows_from_source"):
            try:
                rows = [row for row in self.load_supplier_rows_from_source() if isinstance(row, dict)]
            except Exception:
                rows = []
        if not rows:
            try:
                rows = [row for row in list_suppliers(DB_PATH) if isinstance(row, dict)]
            except Exception:
                rows = []
        names: list[str] = []
        seen: set[str] = set()
        for supplier_row in rows:
            status = str(supplier_row.get("status") or "").strip().lower()
            if status != "active":
                continue
            name = str(supplier_row.get("supplier_name") or supplier_row.get("short_name") or supplier_row.get("supplier_code") or "").strip()
            if not name:
                continue
            key_name = name.lower()
            if key_name in seen:
                continue
            seen.add(key_name)
            names.append(name)
        names.sort(key=str.lower)
        current_supplier = vars_map["default_supplier"].get().strip()
        if current_supplier and current_supplier.lower() not in seen:
            names.insert(0, current_supplier)
        return names

    def add_labeled_entry_with_unit(
        parent_frame: ttk.Frame,
        row_index: int,
        column_index: int,
        label: str,
        value_key: str,
        unit_key: str,
        unit_values: list[str],
        *,
        validatecommand: tuple[str, str] | None = None,
    ) -> tuple[ttk.Entry, ttk.Combobox]:
        field = ttk.Frame(parent_frame)
        field.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        field.columnconfigure(0, weight=1)
        field.columnconfigure(1, weight=0)
        ttk.Label(field, text=label).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 4))
        entry = ttk.Entry(field, textvariable=vars_map[value_key])
        if validatecommand is not None:
            entry.configure(validate="key", validatecommand=validatecommand)
        entry.grid(row=1, column=0, sticky="ew", padx=(0, 6))
        combo = ttk.Combobox(field, textvariable=vars_map[unit_key], values=unit_values, state="readonly", width=10)
        combo.grid(row=1, column=1, sticky="ew")
        return entry, combo

    def add_labeled_text(
        parent_frame: ttk.Frame,
        row_index: int,
        label: str,
        key: str,
        *,
        height: int = 5,
        columnspan: int = 1,
    ) -> tk.Text:
        host = ttk.LabelFrame(parent_frame, text=label)
        host.grid(row=row_index, column=0, columnspan=columnspan, sticky="nsew", padx=8, pady=(0, 12))
        host.columnconfigure(0, weight=1)
        host.rowconfigure(0, weight=1)
        text = tk.Text(host, height=height, wrap="word", relief="solid", bd=1, font=("Segoe UI", 10))
        text.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
        value = vars_map[key].get().strip()
        if value:
            text.insert("1.0", value)
        text_widgets[key] = text
        return text

    for tab in (basic_tab, spec_tab, purchase_tab):
        tab.columnconfigure(0, weight=1)
        tab.rowconfigure(0, weight=1)

    basic_host = ttk.Frame(basic_tab)
    basic_host.grid(row=0, column=0, sticky="nsew")
    basic_host.columnconfigure(0, weight=1)
    basic_host.columnconfigure(1, weight=0)
    basic_host.rowconfigure(1, weight=1)

    basic_form = ttk.Frame(basic_host)
    basic_form.grid(row=0, column=0, sticky="nsew", padx=(0, 12))
    basic_form.columnconfigure(0, weight=1)
    basic_form.columnconfigure(1, weight=1)

    add_labeled_entry(basic_form, 0, 0, "Style Code", "style_code")
    add_labeled_entry(basic_form, 0, 1, "BOM Version", "bom_version")
    add_labeled_entry(basic_form, 1, 0, "Item Code", "item_code")
    add_labeled_entry(basic_form, 1, 1, "Item Name", "item_name")
    item_type_combo = add_labeled_combo(basic_form, 2, 0, "Item Type", "item_type", list(ITEM_TYPE_OPTIONS))
    add_labeled_entry(basic_form, 2, 1, "Section", "section")
    add_labeled_entry(basic_form, 3, 0, "Article / Part No", "article_ref")
    add_labeled_combo(basic_form, 3, 1, "Status", "status", list(ITEM_STATUS_OPTIONS))
    add_labeled_entry(basic_form, 4, 0, "Main Color", "color")
    add_labeled_entry(basic_form, 4, 1, "Color Code", "color_code")

    preview_frame = ttk.LabelFrame(basic_host, text="Image Preview")
    preview_frame.grid(row=0, column=1, sticky="ne", padx=(8, 0))
    preview_frame.columnconfigure(0, weight=1)
    preview_label = ttk.Label(preview_frame, anchor="center", relief="solid")
    preview_label.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)

    description_host = ttk.Frame(basic_host)
    description_host.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(4, 0))
    description_host.columnconfigure(0, weight=1)
    description_host.rowconfigure(0, weight=1)
    add_labeled_text(description_host, 0, "Description", "description", height=10)

    spec_host = ttk.Frame(spec_tab)
    spec_host.grid(row=0, column=0, sticky="nsew")
    spec_host.columnconfigure(0, weight=1)
    spec_host.columnconfigure(1, weight=1)
    spec_host.rowconfigure(5, weight=1)
    add_labeled_entry(spec_host, 0, 0, "Composition", "composition")
    add_labeled_entry(spec_host, 0, 1, "Position of Use", "position_of_use")
    unit_combos["trim_size_um"] = add_labeled_combo(
        spec_host,
        1,
        0,
        "Trim Size",
        "trim_size_um",
        self.get_bom_trim_size_options(vars_map["style_code"].get(), vars_map["trim_size_um"].get()),
    )
    _length_entry, unit_combos["length_unit"] = add_labeled_entry_with_unit(
        spec_host,
        1,
        1,
        "Length",
        "length_value",
        "length_unit",
        list(LENGTH_UOM_OPTIONS),
        validatecommand=numeric_vcmd,
    )
    _width_entry, unit_combos["width_unit"] = add_labeled_entry_with_unit(
        spec_host,
        2,
        0,
        "Width",
        "width_value",
        "width_unit",
        list(LENGTH_UOM_OPTIONS),
        validatecommand=numeric_vcmd,
    )
    _gsm_entry, unit_combos["gsm_unit"] = add_labeled_entry_with_unit(
        spec_host,
        2,
        1,
        "GSM / Weight",
        "gsm_value",
        "gsm_unit",
        list(WEIGHT_UOM_OPTIONS),
        validatecommand=numeric_vcmd,
    )
    add_labeled_entry(spec_host, 3, 0, "Waste (%)", "waste_percent", validatecommand=numeric_vcmd)
    spec_notes = ttk.Frame(spec_host)
    spec_notes.grid(row=4, column=0, columnspan=2, sticky="nsew")
    spec_notes.columnconfigure(0, weight=1)
    spec_notes.columnconfigure(1, weight=1)
    add_labeled_text(spec_notes, 0, "Spec Summary", "spec_summary", height=6)
    add_labeled_text(spec_notes, 1, "Tech Comment", "tech_comment", height=6)

    purchase_host = ttk.Frame(purchase_tab)
    purchase_host.grid(row=0, column=0, sticky="nsew")
    purchase_host.columnconfigure(0, weight=1)
    purchase_host.columnconfigure(1, weight=1)
    purchase_host.rowconfigure(6, weight=1)
    active_supplier_names = get_active_supplier_names_for_purchase()
    add_labeled_supplier_search_combo(purchase_host, 0, 0, "Default Supplier", "default_supplier", active_supplier_names)
    add_labeled_entry(purchase_host, 0, 1, "Supplier Item Code", "supplier_item_code")
    _moq_entry, unit_combos["moq_unit"] = add_labeled_entry_with_unit(
        purchase_host,
        1,
        0,
        "MOQ",
        "moq",
        "moq_unit",
        _purchase_uom_options(vars_map["moq_unit"].get() or vars_map["purchase_uom"].get()),
        validatecommand=numeric_vcmd,
    )
    _moq_charge_entry, unit_combos["moq_charge_unit"] = add_labeled_entry_with_unit(
        purchase_host,
        1,
        1,
        "Charge MOQ",
        "moq_charge",
        "moq_charge_unit",
        combo_values_with_current(BOM_CHARGE_UNIT_OPTIONS, vars_map["moq_charge_unit"].get()),
        validatecommand=numeric_vcmd,
    )
    _mcq_entry, unit_combos["mcq_unit"] = add_labeled_entry_with_unit(
        purchase_host,
        2,
        0,
        "MCQ",
        "mcq",
        "mcq_unit",
        _purchase_uom_options(vars_map["mcq_unit"].get() or vars_map["purchase_uom"].get()),
        validatecommand=numeric_vcmd,
    )
    _mcq_charge_entry, unit_combos["mcq_charge_unit"] = add_labeled_entry_with_unit(
        purchase_host,
        2,
        1,
        "Charge MCQ",
        "mcq_charge",
        "mcq_charge_unit",
        combo_values_with_current(BOM_CHARGE_UNIT_OPTIONS, vars_map["mcq_charge_unit"].get()),
        validatecommand=numeric_vcmd,
    )
    unit_combos["purchase_uom"] = add_labeled_combo(
        purchase_host,
        3,
        0,
        "Purchase UOM",
        "purchase_uom",
        _purchase_uom_options(vars_map["purchase_uom"].get() or vars_map["uom"].get()),
    )
    _lead_time_entry, unit_combos["lead_time_unit"] = add_labeled_entry_with_unit(
        purchase_host,
        3,
        1,
        "Lead Time",
        "lead_time",
        "lead_time_unit",
        combo_values_with_current(LEAD_TIME_UNIT_OPTIONS, vars_map["lead_time_unit"].get()),
        validatecommand=numeric_vcmd,
    )
    _sample_lead_time_entry, unit_combos["sample_lead_time_unit"] = add_labeled_entry_with_unit(
        purchase_host,
        4,
        0,
        "Sample Lead Time",
        "sample_lead_time",
        "sample_lead_time_unit",
        combo_values_with_current(LEAD_TIME_UNIT_OPTIONS, vars_map["sample_lead_time_unit"].get()),
        validatecommand=numeric_vcmd,
    )
    _standard_price_entry, unit_combos["currency"] = add_labeled_entry_with_unit(
        purchase_host,
        4,
        1,
        "Standard Price",
        "standard_price",
        "currency",
        combo_values_with_current(BOM_CURRENCY_OPTIONS, vars_map["currency"].get()),
        validatecommand=numeric_vcmd,
    )
    add_labeled_entry(purchase_host, 5, 0, "FDS Date", "fds_date")
    purchase_note_host = ttk.Frame(purchase_host)
    purchase_note_host.grid(row=6, column=0, columnspan=2, sticky="nsew")
    purchase_note_host.columnconfigure(0, weight=1)
    purchase_note_host.rowconfigure(0, weight=1)
    add_labeled_text(purchase_note_host, 0, "Purchase Note", "purchase_note", height=8, columnspan=2)

    def refresh_image_preview(*_args: object) -> None:
        image_path = vars_map["image_path"].get().strip()
        photo = None
        if image_path:
            if hasattr(self, "create_tech_pack_bom_preview_photo"):
                photo = self.create_tech_pack_bom_preview_photo(image_path, target_width=220, target_height=220)
            elif hasattr(self, "create_tech_pack_thumbnail"):
                photo = self.create_tech_pack_thumbnail({"image_path": image_path}, target_width=220, target_height=220)
        if photo is not None:
            preview_label.configure(image=photo, text="")
            preview_label.image = photo
            image_preview_state["photo"] = photo
        else:
            preview_label.configure(image="", text="No preview image", anchor="center")
            preview_label.image = None
            image_preview_state["photo"] = None

    vars_map["image_path"].trace_add("write", refresh_image_preview)
    refresh_image_preview()

    def refresh_uom_combo_values(*_args: object) -> None:
        item_type_value = vars_map["item_type"].get().strip()
        if "purchase_uom" in unit_combos:
            purchase_current = vars_map["purchase_uom"].get().strip()
            unit_combos["purchase_uom"].configure(values=_purchase_uom_options(purchase_current))
        if "moq_unit" in unit_combos:
            unit_combos["moq_unit"].configure(values=_purchase_uom_options(vars_map["moq_unit"].get().strip()))
        if "mcq_unit" in unit_combos:
            unit_combos["mcq_unit"].configure(values=_purchase_uom_options(vars_map["mcq_unit"].get().strip()))

    def refresh_trim_size_combo_values(*_args: object) -> None:
        if "trim_size_um" not in unit_combos:
            return
        unit_combos["trim_size_um"].configure(
            values=self.get_bom_trim_size_options(vars_map["style_code"].get(), vars_map["trim_size_um"].get())
        )

    vars_map["item_type"].trace_add("write", refresh_uom_combo_values)
    vars_map["style_code"].trace_add("write", refresh_trim_size_combo_values)
    refresh_uom_combo_values()
    refresh_trim_size_combo_values()

    actions = ttk.Frame(frame)
    actions.grid(row=2, column=0, sticky="e", pady=(12, 0))

    def save_action() -> None:
        required_action = "action.bom.copy" if is_copy else ("action.bom.new" if is_new else "action.bom.edit")
        if not self.is_action_allowed(required_action):
            return
        payload = default_bom_item_payload()
        for key in payload:
            payload[key] = vars_map[key].get().strip()
        payload["currency"] = payload["currency"].upper()
        payload["moq_unit"] = payload["moq_unit"].upper()
        payload["moq_charge_unit"] = payload["moq_charge_unit"].upper()
        payload["mcq_unit"] = payload["mcq_unit"].upper()
        payload["mcq_charge_unit"] = payload["mcq_charge_unit"].upper()
        payload["lead_time_unit"] = payload["lead_time_unit"].upper()
        payload["sample_lead_time_unit"] = payload["sample_lead_time_unit"].upper()
        payload["purchase_uom"] = payload["purchase_uom"].upper()
        payload["trim_size_um"] = _normalize_trim_size_option(payload["trim_size_um"])
        payload["trim_size_unit"] = ""
        payload["length_value"] = " ".join(part for part in (payload["length_value"], payload["length_unit"]) if part).strip()
        payload["width_value"] = " ".join(part for part in (payload["width_value"], payload["width_unit"]) if part).strip()
        payload["gsm_value"] = " ".join(part for part in (payload["gsm_value"], payload["gsm_unit"]) if part).strip()
        for key, widget in text_widgets.items():
            payload[key] = widget.get("1.0", "end-1c").strip()
        payload["status"] = str(payload.get("status", "draft")).strip().lower() or "draft"
        payload["uom"] = payload["uom"] or payload["purchase_uom"]
        payload["qty_per_garment"] = ""
        if not payload["style_code"]:
            messagebox.showwarning(self.t("warning_title"), "Style Code is required.")
            notebook.select(basic_tab)
            return
        if not payload["bom_version"]:
            messagebox.showwarning(self.t("warning_title"), "BOM Version is required.")
            notebook.select(basic_tab)
            return
        if not payload["item_name"]:
            messagebox.showwarning(self.t("warning_title"), "Item Name is required.")
            notebook.select(basic_tab)
            return
        if not payload["item_code"]:
            payload["item_code"] = f"{payload['style_code']}-ITEM"
        if payload["status"] == "approved" and not self.is_action_allowed("action.bom.approve"):
            if hasattr(self, "deny_permission_action"):
                self.deny_permission_action("action.bom.approve")
            return
        existing = normalize_bom_item_payload(initial_row) or default_bom_item_payload()
        payload["item_id"] = payload["item_id"] or str(existing.get("item_id", "")).strip() or secrets.token_hex(16)
        payload["created_at"] = str(existing.get("created_at", "")).strip() or _bom_now_text()
        payload["updated_at"] = _bom_now_text()
        try:
            saved = self.save_bom_item_to_source(payload)
        except Exception as exc:
            messagebox.showerror(self.t("warning_title"), str(exc))
            return
        if not saved:
            messagebox.showerror(self.t("warning_title"), "Could not save BOM item.")
            return
        self.refresh_bom_module()
        saved_item_id = str(saved.get("item_id", "")).strip()
        if saved_item_id and hasattr(self, "bom_tree") and self.bom_tree.exists(saved_item_id):
            self.bom_tree.selection_set(saved_item_id)
            self.bom_tree.focus(saved_item_id)
        self.set_status(f"Saved BOM item {saved.get('item_code', '')}.")
        dialog.destroy()

    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="right")
    ttk.Button(actions, text="Save", command=save_action).pack(side="right", padx=(0, 8))


def _build_bom_item_code(style_code: str, section_name: str, item_index: int) -> str:
    section_upper = str(section_name or "").strip().upper()
    short_code = SECTION_CODE_MAP.get(section_upper, "OTH")
    return f"{style_code}-{short_code}-{item_index:03d}"


def _build_bom_spec_summary(item: dict, qty_um_text: str) -> str:
    parts = [
        str(item.get("description", "")).strip(),
        str(item.get("composition", "")).strip(),
        str(item.get("trim_size_um", "")).strip(),
        str(qty_um_text or "").strip(),
    ]
    return " | ".join(part for part in parts if part)


def _build_bom_variant_spec_summary(item: dict, qty_um_text: str, color_value: str, color_code: str) -> str:
    parts = [
        _build_bom_spec_summary(item, qty_um_text),
        str(color_value or "").strip(),
        str(color_code or "").strip(),
    ]
    return " | ".join(part for part in parts if part)


def _tech_pack_bom_item_needs_color_split(self, section_name: str, item: dict, colorways: list[str]) -> bool:
    normalized_colorways = [str(item).strip() for item in colorways if str(item).strip()]
    if len(normalized_colorways) <= 1:
        return False
    section_upper = str(section_name or "").strip().upper()
    if section_upper == "MATERIAL":
        return True
    item_text = " ".join(
        str(item.get(key, "")).upper()
        for key in ("component", "article_ref", "description", "composition", "position", "comments")
    )
    if "DTM" in item_text:
        return True
    if hasattr(self, "tech_pack_bom_item_has_colorway_data") and self.tech_pack_bom_item_has_colorway_data(item):
        return True
    for colorway in normalized_colorways:
        tokens = [token for token in colorway.upper().split() if token]
        if tokens and any(token in item_text for token in tokens):
            return True
    return False


def _build_tech_pack_bom_item_variants(
    self,
    tech_pack_row: dict,
    section_name: str,
    item: dict,
    display_values: dict[str, str],
    colorways: list[str],
) -> list[dict[str, str]]:
    normalized_colorways = [str(entry).strip() for entry in colorways if str(entry).strip()]
    if not self._tech_pack_bom_item_needs_color_split(section_name, item, normalized_colorways):
        return [{"color": "", "color_code": "", "variant_label": ""}]

    color_lookup = self.get_tech_pack_color_lookup(tech_pack_row) if hasattr(self, "get_tech_pack_color_lookup") else {}
    variants: list[dict[str, str]] = []
    for color_index, colorway in enumerate(normalized_colorways):
        color_meta = color_lookup.get(colorway.upper(), {}) if isinstance(color_lookup, dict) else {}
        default_color_name = str(color_meta.get("color_name", "")).strip()
        primary = default_color_name
        secondary = ""
        if hasattr(self, "get_tech_pack_bom_color_primary_line"):
            primary = self.get_tech_pack_bom_color_primary_line(section_name, item, default_color_name, color_index)
        if hasattr(self, "get_tech_pack_bom_color_secondary_line"):
            secondary = self.get_tech_pack_bom_color_secondary_line(item, color_index)
        if hasattr(self, "get_tech_pack_bom_color_override"):
            primary = self.get_tech_pack_bom_color_override(item, colorway, "primary", primary)
            secondary = self.get_tech_pack_bom_color_override(item, colorway, "secondary", secondary)
        color_value = primary or colorway
        color_code = secondary
        variant_label = " | ".join(part for part in (color_value, color_code) if part)
        variants.append(
            {
                "color": color_value,
                "color_code": color_code,
                "variant_label": variant_label,
            }
        )
    return variants or [{"color": "", "color_code": "", "variant_label": ""}]


def get_selected_or_prompt_bom_tech_pack_styles(self) -> list[str]:
    selected_styles = sorted(
        {
            normalize_style_identifier(style_code)
            for style_code in getattr(self, "checked_tech_pack_styles", set())
            if normalize_style_identifier(style_code)
        }
    )
    if selected_styles:
        return selected_styles
    selected_row = self.get_selected_tech_pack_row() if hasattr(self, "get_selected_tech_pack_row") else None
    if isinstance(selected_row, dict):
        style_code = normalize_style_identifier(selected_row.get("style_code"))
        if style_code:
            return [style_code]
    if not getattr(self, "tech_pack_rows", []):
        try:
            self.refresh_tech_pack_module(silent=True)
        except Exception:
            pass
    available_styles = sorted(
        {
            normalize_style_identifier(row.get("style_code"))
            for row in getattr(self, "tech_pack_rows", [])
            if isinstance(row, dict) and normalize_style_identifier(row.get("style_code"))
        }
    )
    if not available_styles:
        return []

    dialog = tk.Toplevel(self.root)
    dialog.title("Select Tech Pack Style")
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.resizable(False, False)
    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(2, weight=1)
    ttk.Label(frame, text="Search one Tech Pack style to generate BOM items from.").grid(row=0, column=0, sticky="w")
    selected_var = tk.StringVar(value="")
    search_var = tk.StringVar(value="")
    search_entry = ttk.Entry(frame, textvariable=search_var, width=36)
    search_entry.grid(row=1, column=0, sticky="ew", pady=(8, 0))
    result_frame = ttk.Frame(frame)
    result_frame.grid(row=2, column=0, sticky="nsew", pady=(8, 0))
    result_frame.columnconfigure(0, weight=1)
    result_frame.rowconfigure(0, weight=1)
    result_list = tk.Listbox(result_frame, height=8, exportselection=False, font=("Segoe UI", 10))
    result_list.grid(row=0, column=0, sticky="nsew")
    result_scroll = ttk.Scrollbar(result_frame, orient="vertical", command=result_list.yview)
    result_scroll.grid(row=0, column=1, sticky="ns")
    result_list.configure(yscrollcommand=result_scroll.set)
    result: dict[str, list[str]] = {"items": []}

    def get_filtered_candidates() -> list[str]:
        query = search_var.get().strip().lower()
        if not query:
            return list(available_styles)
        starts_with = [candidate for candidate in available_styles if candidate.lower().startswith(query)]
        contains = [candidate for candidate in available_styles if query in candidate.lower() and candidate not in starts_with]
        return starts_with + contains

    def refresh_candidate_list(*_args: object) -> None:
        filtered = get_filtered_candidates()
        result_list.delete(0, "end")
        for candidate in filtered:
            result_list.insert("end", candidate)
        if filtered:
            result_list.selection_clear(0, "end")
            result_list.selection_set(0)
            result_list.activate(0)
            selected_var.set(filtered[0])
        else:
            selected_var.set("")

    def sync_selected_from_list(_event: object | None = None) -> None:
        selection = result_list.curselection()
        if not selection:
            selected_var.set("")
            return
        selected_var.set(str(result_list.get(selection[0])).strip())

    def choose() -> None:
        style_code = normalize_style_identifier(selected_var.get())
        if style_code:
            result["items"] = [style_code]
        dialog.destroy()

    actions = ttk.Frame(frame)
    actions.grid(row=3, column=0, sticky="e", pady=(14, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="right")
    ttk.Button(actions, text="Use Style", command=choose).pack(side="right", padx=(0, 8))
    search_var.trace_add("write", refresh_candidate_list)
    result_list.bind("<<ListboxSelect>>", sync_selected_from_list)
    result_list.bind("<Double-Button-1>", lambda _event: choose())
    refresh_candidate_list()
    search_entry.focus_set()
    self.center_dialog(dialog, min_width=440, min_height=300)
    dialog.wait_window()
    return list(result.get("items", []))


def build_bom_items_from_tech_pack_style(self, style_code: str) -> tuple[list[dict], int]:
    _append_bom_debug_log(f"generate style start style={style_code}")
    tech_pack_row = self.get_tech_pack_row_by_style_code(style_code) if hasattr(self, "get_tech_pack_row_by_style_code") else None
    if not isinstance(tech_pack_row, dict):
        _append_bom_debug_log(f"generate style missing tech-pack row style={style_code}")
        return [], 0
    bom_sections = [section for section in list(tech_pack_row.get("bom_data", [])) if isinstance(section, dict)]
    if not bom_sections:
        _append_bom_debug_log(f"generate style no bom_data style={style_code}")
        return [], 0
    existing_items = {
        (
            normalize_style_identifier(item.get("style_code")),
            str(item.get("bom_version", "")).strip(),
            str(item.get("item_code", "")).strip(),
        ): dict(item)
        for item in self.bom_items
        if isinstance(item, dict)
    }
    overview_data = tech_pack_row.get("overview_data", {}) if isinstance(tech_pack_row.get("overview_data", {}), dict) else {}
    color_tokens = self.build_tech_pack_bom_color_token_set(tech_pack_row) if hasattr(self, "build_tech_pack_bom_color_token_set") else set()
    bom_version = "TP"
    saved_rows: list[dict] = []
    generated_count = 0
    section_counters: dict[str, int] = {}
    for section_index, section in enumerate(bom_sections):
        section_name = str(section.get("section", "")).strip()
        items = [item for item in list(section.get("items", [])) if isinstance(item, dict)]
        colorways = [str(entry).strip() for entry in list(section.get("colorways", [])) if str(entry).strip()]
        for item_index, item in enumerate(items, start=1):
            display_values = (
                self.get_tech_pack_bom_display_values(section_name, item, color_tokens)
                if hasattr(self, "get_tech_pack_bom_display_values")
                else {
                    "component": str(item.get("component", "")).strip(),
                    "description": str(item.get("description", "")).strip(),
                    "composition": str(item.get("composition", "")).strip(),
                    "comments": str(item.get("comments", "")).strip(),
                    "article": str(item.get("article_ref", "")).strip(),
                }
            )
            qty_um_text = str(item.get("qty_um", "")).strip()
            qty_per_garment, parsed_uom = _parse_qty_and_uom(qty_um_text)
            thumbnail_path = (
                self.resolve_tech_pack_bom_item_thumbnail_path(tech_pack_row, section, item, item_index)
                if hasattr(self, "resolve_tech_pack_bom_item_thumbnail_path")
                else str(item.get("thumbnail_path", "")).strip()
            )
            variants = self._build_tech_pack_bom_item_variants(
                tech_pack_row,
                section_name,
                item,
                display_values,
                colorways,
            )
            for variant in variants:
                section_counter = section_counters.get(section_name.upper(), 0) + 1
                section_counters[section_name.upper()] = section_counter
                item_code = _build_bom_item_code(style_code, section_name, section_counter)
                existing = existing_items.get((style_code, bom_version, item_code), {})
                item_name = str(display_values.get("component", "")).strip() or str(display_values.get("description", "")).strip()
                if str(variant.get("variant_label", "")).strip():
                    item_name = f"{item_name} {variant.get('variant_label', '')}".strip()
                payload = default_bom_item_payload()
                payload.update(
                    {
                        "item_id": str(existing.get("item_id", "")).strip(),
                        "bom_id": str(existing.get("bom_id", "")).strip(),
                        "style_code": style_code,
                        "bom_version": bom_version,
                        "bom_name": "Tech Pack Imported BOM",
                        "season": str(tech_pack_row.get("season", "")).strip() or str(overview_data.get("season", "")).strip(),
                        "collection": str(tech_pack_row.get("collection", "")).strip() or str(overview_data.get("brand", "")).strip(),
                        "item_code": item_code,
                        "item_name": item_name,
                        "item_type": SECTION_TYPE_MAP.get(section_name.upper(), "Other"),
                        "section": section_name,
                        "article_ref": str(item.get("article_ref", "")).strip(),
                        "color": str(variant.get("color", "")).strip(),
                        "color_code": str(variant.get("color_code", "")).strip(),
                        "description": str(display_values.get("description", "")).strip(),
                        "composition": str(display_values.get("composition", "")).strip(),
                        "position_of_use": str(item.get("position", "")).strip(),
                        "spec_summary": _build_bom_variant_spec_summary(
                            item,
                            qty_um_text,
                            str(variant.get("color", "")).strip(),
                            str(variant.get("color_code", "")).strip(),
                        ),
                        "uom": parsed_uom,
                        "qty_per_garment": qty_per_garment,
                        "trim_size_um": str(item.get("trim_size_um", "")).strip(),
                        "width_value": "",
                        "gsm_value": "",
                        "waste_percent": str(existing.get("waste_percent", "")).strip(),
                        "tech_comment": str(display_values.get("comments", "")).strip(),
                        "default_supplier": str(item.get("supplier", "")).strip(),
                        "supplier_item_code": str(existing.get("supplier_item_code", "")).strip(),
                        "moq": str(existing.get("moq", "")).strip(),
                        "moq_unit": str(existing.get("moq_unit", "")).strip(),
                        "moq_charge": str(existing.get("moq_charge", "")).strip(),
                        "moq_charge_unit": str(existing.get("moq_charge_unit", "")).strip(),
                        "mcq": str(existing.get("mcq", "")).strip(),
                        "mcq_unit": str(existing.get("mcq_unit", "")).strip(),
                        "mcq_charge": str(existing.get("mcq_charge", "")).strip(),
                        "mcq_charge_unit": str(existing.get("mcq_charge_unit", "")).strip(),
                        "lead_time": str(existing.get("lead_time", "")).strip(),
                        "lead_time_unit": str(existing.get("lead_time_unit", "")).strip(),
                        "purchase_uom": parsed_uom or str(existing.get("purchase_uom", "")).strip(),
                        "standard_price": str(existing.get("standard_price", "")).strip(),
                        "currency": str(existing.get("currency", "")).strip(),
                        "purchase_note": str(existing.get("purchase_note", "")).strip(),
                        "image_path": str(thumbnail_path or "").strip(),
                        "status": "draft",
                        "source_style_code": style_code,
                        "source_section": section_name,
                        "source_section_index": str(section_index),
                        "source_item_index": str(item_index - 1),
                        "source_payload_json": json.dumps(item, ensure_ascii=False),
                        "created_at": _bom_now_text(),
                    }
                )
                saved = self.save_bom_item_to_source(payload)
                if isinstance(saved, dict) and saved:
                    saved_rows.append(saved)
                    generated_count += 1
    _append_bom_debug_log(f"generate style done style={style_code} generated_count={generated_count}")
    return saved_rows, generated_count


def generate_bom_from_tech_pack(self) -> None:
    if not self.is_action_allowed("action.bom.generate_from_tech_pack"):
        return
    selected_styles = self.get_selected_or_prompt_bom_tech_pack_styles()
    if not selected_styles:
        messagebox.showwarning(self.t("warning_title"), "Please choose at least one Tech Pack style first.")
        return
    generated_total = 0
    failed: list[str] = []
    for style_code in selected_styles:
        try:
            _saved_rows, generated_count = self.build_bom_items_from_tech_pack_style(style_code)
            if generated_count <= 0:
                failed.append(f"{style_code}: no BOM item found in Tech Pack.")
            else:
                generated_total += generated_count
        except Exception as exc:
            _append_bom_debug_log(
                "generate style error="
                + style_code
                + " message="
                + str(exc)
                + " traceback="
                + traceback.format_exc(limit=8).replace("\n", " | ")
            )
            failed.append(f"{style_code}: {exc}")
    self.refresh_bom_module()
    if failed:
        messagebox.showwarning(self.t("warning_title"), "\n".join(failed[:10]))
    if generated_total > 0:
        messagebox.showinfo(self.t("import_done_title"), f"Generated {generated_total} BOM item(s) from Tech Pack.")
        self.set_status(f"Generated {generated_total} BOM item(s) from Tech Pack.")


def _stable_fds_bom_item_id(style_code: str, bom_version: str, item_code: str) -> str:
    source = "|".join(["FDS", str(bom_version or "").strip().upper(), str(item_code or "").strip().upper()])
    return hashlib.sha1(source.encode("utf-8")).hexdigest()[:32]


def _format_fds_spec_summary(fds_data: dict, variant: dict) -> str:
    parts = [
        str(fds_data.get("commodity", "")).strip(),
        str(fds_data.get("description", "")).strip(),
        str(fds_data.get("content", "")).strip(),
        str(fds_data.get("construction", "")).strip(),
    ]
    gsm_text = " ".join(
        token
        for token in [str(fds_data.get("gsm_value", "")).strip(), str(fds_data.get("gsm_unit", "")).strip()]
        if token
    )
    width_text = " ".join(
        token
        for token in [str(fds_data.get("width_value", "")).strip(), str(fds_data.get("width_unit", "")).strip()]
        if token
    )
    if gsm_text:
        parts.append(gsm_text)
    if width_text:
        parts.append(width_text)
    color = str(variant.get("color", "")).strip()
    if color:
        parts.append(f"Color: {color}")
    print_type = str(variant.get("print_type", "")).strip()
    if print_type:
        parts.append(print_type)
    return " | ".join(part for part in parts if part and part != "/")


def _merge_fds_purchase_note(existing_note: object, fds_data: dict) -> str:
    existing_text = str(existing_note or "").strip()
    surcharge_note = str(fds_data.get("surcharge_note", "") or "").strip()
    if not surcharge_note:
        return existing_text
    note_line = f"SURCHARGE FOR UNDER MOQ/MCQ: {surcharge_note}"
    if not existing_text:
        return note_line
    if note_line in existing_text or surcharge_note in existing_text:
        return existing_text
    return existing_text + "\n" + note_line


def build_bom_items_from_fds_data(self, fds_data: dict, *, style_code: str, bom_version: str, fds_date: str) -> tuple[list[dict], int]:
    normalized_style = normalize_style_identifier(style_code)
    normalized_version = str(bom_version or "").strip() or "FDS"
    normalized_date = str(fds_date or "").strip() or str(fds_data.get("fds_date", "")).strip()

    try:
        current_items = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
    except Exception:
        current_items = [normalize_bom_item_payload(item) for item in getattr(self, "bom_items", []) or []]
    existing_items = {
        (
            normalize_style_identifier(item.get("style_code")),
            str(item.get("bom_version", "")).strip(),
            str(item.get("item_code", "")).strip(),
        ): item
        for item in current_items
        if isinstance(item, dict) and str(item.get("item_code", "")).strip()
    }

    saved_rows: list[dict] = []
    for index, variant in enumerate(list(fds_data.get("variants") or []), start=1):
        if not isinstance(variant, dict):
            continue
        item_code = str(variant.get("item_code", "")).strip().upper()
        if not item_code:
            continue
        existing = existing_items.get((normalized_style, normalized_version, item_code), {})
        print_type = str(variant.get("print_type", "")).strip()
        color = str(variant.get("color", "")).strip()
        item_name = " ".join(
            token
            for token in [
                str(fds_data.get("description", "")).strip() or str(fds_data.get("article", "")).strip(),
                print_type,
                color,
            ]
            if token
        )
        payload = default_bom_item_payload()
        payload.update(
            {
                "item_id": str(existing.get("item_id", "")).strip() or _stable_fds_bom_item_id(normalized_style, normalized_version, item_code),
                "bom_id": str(existing.get("bom_id", "")).strip(),
                "style_code": normalized_style,
                "bom_version": normalized_version,
                "bom_name": "FDS Imported BOM",
                "item_code": item_code,
                "item_name": item_name,
                "item_type": "Fabric",
                "section": str(fds_data.get("section", "")).strip() or "Fabric",
                "article_ref": str(fds_data.get("article", "")).strip(),
                "color": color,
                "color_code": str(variant.get("color_code", "")).strip(),
                "description": str(fds_data.get("description", "")).strip(),
                "composition": str(fds_data.get("content", "")).strip(),
                "position_of_use": str(existing.get("position_of_use", "")).strip(),
                "spec_summary": _format_fds_spec_summary(fds_data, variant),
                "uom": str(variant.get("purchase_uom", "")).strip() or str(existing.get("uom", "")).strip(),
                "trim_size_um": str(existing.get("trim_size_um", "")).strip(),
                "width_value": str(fds_data.get("width_value", "")).strip(),
                "width_unit": str(fds_data.get("width_unit", "")).strip(),
                "gsm_value": str(fds_data.get("gsm_value", "")).strip(),
                "gsm_unit": str(fds_data.get("gsm_unit", "")).strip(),
                "waste_percent": str(existing.get("waste_percent", "")).strip(),
                "tech_comment": str(fds_data.get("construction", "")).strip(),
                "default_supplier": str(existing.get("default_supplier", "")).strip(),
                "supplier_item_code": str(existing.get("supplier_item_code", "")).strip(),
                "moq": str(fds_data.get("moq", "")).strip(),
                "moq_unit": str(fds_data.get("moq_unit", "")).strip(),
                "moq_charge": str(existing.get("moq_charge", "")).strip(),
                "moq_charge_unit": str(existing.get("moq_charge_unit", "")).strip(),
                "mcq": str(fds_data.get("mcq", "")).strip(),
                "mcq_unit": str(fds_data.get("mcq_unit", "")).strip(),
                "mcq_charge": str(existing.get("mcq_charge", "")).strip(),
                "mcq_charge_unit": str(existing.get("mcq_charge_unit", "")).strip(),
                "lead_time": str(fds_data.get("lead_time", "")).strip(),
                "lead_time_unit": str(fds_data.get("lead_time_unit", "")).strip(),
                "sample_lead_time": str(fds_data.get("sample_lead_time", "")).strip(),
                "sample_lead_time_unit": str(fds_data.get("sample_lead_time_unit", "")).strip(),
                "purchase_uom": str(variant.get("purchase_uom", "")).strip(),
                "standard_price": str(variant.get("price", "")).strip(),
                "currency": str(variant.get("currency", "")).strip(),
                "purchase_note": _merge_fds_purchase_note(existing.get("purchase_note", ""), fds_data),
                "fds_date": normalized_date,
                "status": str(existing.get("status", "")).strip() or "draft",
                "source_style_code": normalized_style,
                "source_section": "FDS",
                "source_section_index": "0",
                "source_item_index": str(index - 1),
                "source_payload_json": json.dumps({"fds": fds_data, "variant": variant}, ensure_ascii=False),
                "created_at": str(existing.get("created_at", "")).strip() or _bom_now_text(),
            }
        )
        saved = self.save_bom_item_to_source(payload)
        if isinstance(saved, dict) and saved:
            saved_rows.append(saved)
    return saved_rows, len(saved_rows)


def open_import_fds_menu(self) -> None:
    if not self.is_action_allowed("action.bom.import_fds"):
        return
    button = self.widget_text.get("bom_import_fds_button") if hasattr(self, "widget_text") else None
    menu = tk.Menu(self.root, tearoff=False)
    menu.add_command(label="Single file", command=self.import_fds_single_file)
    menu.add_command(label="Folder", command=self.import_fds_folder)
    try:
        if button is not None:
            x = button.winfo_rootx()
            y = button.winfo_rooty() + button.winfo_height()
        else:
            x = self.root.winfo_pointerx()
            y = self.root.winfo_pointery()
        menu.tk_popup(x, y)
    finally:
        menu.grab_release()


def import_fds_single_file(self) -> None:
    if not self.is_action_allowed("action.bom.import_fds"):
        return
    file_path = filedialog.askopenfilename(
        parent=self.root,
        title="Import FDS - Single file",
        filetypes=[
            ("Excel files", "*.xls *.xlsx *.xlsm"),
            ("All files", "*.*"),
        ],
    )
    if not file_path:
        return
    self.import_fds_files([file_path])


def import_fds_folder(self) -> None:
    if not self.is_action_allowed("action.bom.import_fds"):
        return
    folder_path = filedialog.askdirectory(parent=self.root, title="Import FDS - Folder")
    if not folder_path:
        return
    folder = Path(folder_path)
    file_paths = [
        str(path)
        for path in sorted(folder.iterdir(), key=lambda item: item.name.lower())
        if path.is_file()
        and path.suffix.lower() in {".xls", ".xlsx", ".xlsm"}
        and not path.name.startswith("~$")
    ]
    if not file_paths:
        messagebox.showwarning(self.t("warning_title"), "No Excel FDS file found in the selected folder.")
        return
    self.import_fds_files(file_paths)


def import_fds_files(self, file_paths: list[str]) -> None:
    saved_total = 0
    imported_files = 0
    failed: list[str] = []
    skipped_duplicates: list[str] = []
    try:
        existing_rows = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
    except Exception as exc:
        existing_rows = [normalize_bom_item_payload(item) for item in getattr(self, "bom_items", []) or []]
        _append_bom_debug_log(f"import-fds duplicate preload warning={exc}")
    imported_fds_keys = {
        (
            str(row.get("article_ref", "")).strip().upper(),
            str(row.get("fds_date", "")).strip(),
        )
        for row in existing_rows
        if isinstance(row, dict)
        and str(row.get("article_ref", "")).strip()
        and str(row.get("fds_date", "")).strip()
    }
    for file_path in file_paths:
        try:
            fds_data = parse_fds_workbook(file_path)
            fds_key = (
                str(fds_data.get("article", "")).strip().upper(),
                str(fds_data.get("fds_date", "")).strip(),
            )
            if fds_key[0] and fds_key[1] and fds_key in imported_fds_keys:
                skipped_duplicates.append(
                    f"{Path(file_path).name}: duplicate Article / Part No {fds_key[0]} and FDS Date {fds_key[1]}"
                )
                _append_bom_debug_log(
                    f"import-fds duplicate skipped file={file_path} article={fds_key[0]} fds_date={fds_key[1]}"
                )
                continue
            _saved_rows, saved_count = self.build_bom_items_from_fds_data(
                fds_data,
                style_code="",
                bom_version="FDS",
                fds_date=str(fds_data.get("fds_date", "")).strip(),
            )
            saved_total += saved_count
            imported_files += 1
            if fds_key[0] and fds_key[1] and saved_count > 0:
                imported_fds_keys.add(fds_key)
        except (FDSParseError, Exception) as exc:
            _append_bom_debug_log(
                "import-fds file error="
                + str(file_path)
                + " message="
                + str(exc)
                + " traceback="
                + traceback.format_exc(limit=8).replace("\n", " | ")
            )
            failed.append(f"{Path(file_path).name}: {exc}")
    try:
        self.refresh_bom_module()
    finally:
        self.set_status(f"Imported {saved_total} FDS BOM item(s) from {imported_files} file(s).")
    if failed:
        messagebox.showwarning(
            self.t("warning_title"),
            f"Imported {saved_total} FDS BOM item(s) from {imported_files} file(s).\n\nIssues:\n"
            + "\n".join((skipped_duplicates + failed)[:12]),
        )
    elif skipped_duplicates:
        messagebox.showwarning(
            self.t("warning_title"),
            f"Imported {saved_total} FDS BOM item(s) from {imported_files} file(s).\n\nSkipped duplicates:\n"
            + "\n".join(skipped_duplicates[:12]),
        )
    elif imported_files:
        messagebox.showinfo(self.t("import_done_title"), f"Imported {saved_total} FDS BOM item(s) from {imported_files} file(s).")


def _read_bulk_fabric_grid(path: Path) -> list[tuple[str, list[list[object]]]]:
    suffix = path.suffix.lower()
    if suffix in {".xlsx", ".xlsm"}:
        try:
            from openpyxl import load_workbook
        except ImportError as exc:
            raise RuntimeError("Missing openpyxl package. Please install openpyxl to import Bulk Fabric files.") from exc
        workbook = load_workbook(path, data_only=True, read_only=True, rich_text=True)
        return [
            (
                worksheet.title,
                [
                    [
                        {
                            "value": _bulk_fabric_openpyxl_cell_text(cell),
                            "strike": _bulk_fabric_openpyxl_cell_has_strike(cell),
                        }
                        for cell in row
                    ]
                    for row in worksheet.iter_rows()
                ],
            )
            for worksheet in workbook.worksheets
        ]
    if suffix == ".xls":
        try:
            import xlrd
        except ImportError as exc:
            raise RuntimeError("Missing xlrd package. Please install xlrd to import .xls Bulk Fabric files.") from exc
        workbook = xlrd.open_workbook(str(path))
        sheets: list[tuple[str, list[list[object]]]] = []
        for sheet in workbook.sheets():
            sheets.append((sheet.name, [[sheet.cell_value(r, c) for c in range(sheet.ncols)] for r in range(sheet.nrows)]))
        return sheets
    raise RuntimeError(f"Unsupported Bulk Fabric file type: {path.suffix}")


def _find_bulk_fabric_header_row(rows: list[list[object]]) -> tuple[int, dict[str, int]]:
    aliases = {
        "style": ("style",),
        "fabric": ("fabric",),
        "section": ("type of fabric",),
        "item_code": ("fabric code",),
        "composition": ("fabrication",),
        "color": ("color",),
        "color_code": ("color pantone",),
        "width_inch": ("cw",),
        "width_cm": ("cw cm",),
        "gsm": ("weight gsm", "weight"),
        "loss": ("loss",),
        "consumption": ("cons", "consumption"),
        "fabric_weight": ("fabric qty kg", "fabric quantity kg", "fabric qty"),
    }
    for row_index, row in enumerate(rows[:12]):
        normalized_cells = [_bulk_fabric_header_key(cell) for cell in row]
        field_map: dict[str, int] = {}
        for field_key, field_aliases in aliases.items():
            for column_index, header in enumerate(normalized_cells):
                if not header:
                    continue
                if field_key == "width_inch" and header == "cw":
                    field_map[field_key] = column_index
                    break
                if field_key == "color":
                    if header == "color":
                        field_map[field_key] = column_index
                        break
                    continue
                if field_key == "color_code":
                    if header == "color pantone":
                        field_map[field_key] = column_index
                        break
                    continue
                if any(alias == header or alias in header for alias in field_aliases):
                    field_map[field_key] = column_index
                    break
        required = {"fabric", "section", "item_code", "composition", "color", "color_code"}
        if required.issubset(field_map):
            return row_index, field_map
    return -1, {}


def parse_bulk_fabric_workbook(self, file_path: str | Path) -> list[dict]:
    path = Path(file_path)
    if not path.exists():
        raise RuntimeError(f"Bulk Fabric file not found: {path}")
    parsed_rows: list[dict] = []
    for sheet_name, rows in _read_bulk_fabric_grid(path):
        header_index, field_map = _find_bulk_fabric_header_row(rows)
        if header_index < 0:
            _append_bom_debug_log(f"bulk-fabric skip sheet={sheet_name} missing headers")
            continue
        for row_number, row in enumerate(rows[header_index + 1 :], start=header_index + 2):
            def cell(field_key: str) -> str:
                column_index = field_map.get(field_key)
                if column_index is None or column_index >= len(row):
                    return ""
                return _bulk_fabric_clean_text(row[column_index])

            def cell_struck(field_key: str) -> bool:
                column_index = field_map.get(field_key)
                if column_index is None or column_index >= len(row):
                    return False
                return _bulk_fabric_cell_is_struck(row[column_index])

            if cell_struck("color") or cell_struck("color_code"):
                _append_bom_debug_log(
                    f"bulk-fabric skip struck color sheet={sheet_name} row={row_number}"
                )
                continue

            item_name = cell("fabric")
            item_code = cell("item_code")
            if not item_name and not item_code:
                continue
            width_value = cell("width_cm")
            width_unit = "CM" if width_value else ""
            if not width_value:
                width_value = cell("width_inch")
                width_unit = "INCH" if width_value else ""
            parsed_rows.append(
                {
                    "source_sheet": sheet_name,
                    "source_row_number": str(row_number),
                    "style_code": normalize_style_identifier(cell("style")),
                    "item_name": item_name,
                    "section": cell("section"),
                    "item_code": item_code,
                    "composition": cell("composition"),
                    "color": cell("color"),
                    "color_code": cell("color_code"),
                    "width_value": _bulk_fabric_number_text(width_value),
                    "width_unit": width_unit,
                    "gsm_value": _bulk_fabric_number_text(cell("gsm")),
                    "gsm_unit": "GSM" if cell("gsm") else "",
                    "waste_percent": _bulk_fabric_loss_percent_text(cell("loss")),
                    "consumption": _bulk_fabric_number_text(cell("consumption")),
                    "fabric_weight": _bulk_fabric_number_text(cell("fabric_weight")),
                    "fabric_weight_uom": "KG" if cell("fabric_weight") else "",
                }
            )
    return parsed_rows


def _bulk_fabric_import_item_code(row: dict, used_identity_keys: dict[tuple[str, str, str], str], item_id: str) -> str:
    base_code = str(row.get("item_code", "")).strip().upper()
    style_code = normalize_style_identifier_list(row.get("style_code"))
    identity_key = (style_code, BULK_FABRIC_BOM_VERSION, base_code)
    existing_item_id = used_identity_keys.get(identity_key, "")
    if not existing_item_id or existing_item_id == item_id:
        return base_code
    color_token = _bulk_fabric_code_token(row.get("color")) or _bulk_fabric_code_token(row.get("color_code"))
    item_code = "-".join(token for token in (base_code, color_token) if token) or base_code
    identity_key = (style_code, BULK_FABRIC_BOM_VERSION, item_code)
    existing_item_id = used_identity_keys.get(identity_key, "")
    if not existing_item_id or existing_item_id == item_id:
        return item_code
    signature_token = hashlib.sha1("|".join(_bulk_fabric_material_signature(row)).encode("utf-8")).hexdigest()[:6].upper()
    return "-".join(token for token in (item_code, signature_token) if token)


def build_bom_items_from_bulk_fabric_rows(self, source_rows: list[dict], *, source_file: str) -> tuple[list[dict], int]:
    rows = [dict(row) for row in list(source_rows or []) if isinstance(row, dict)]
    if not rows:
        return [], 0
    grouped_rows = _bulk_fabric_group_source_rows(rows)
    try:
        current_items = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
    except Exception:
        current_items = [normalize_bom_item_payload(item) for item in getattr(self, "bom_items", []) or []]
    existing_items_by_identity = {
        (
            normalize_style_identifier_list(item.get("style_code")),
            str(item.get("bom_version", "")).strip(),
            str(item.get("item_code", "")).strip(),
        ): item
        for item in current_items
        if isinstance(item, dict) and str(item.get("item_code", "")).strip()
    }
    existing_items_by_signature = {
        _bulk_fabric_existing_material_signature(item): item
        for item in current_items
        if isinstance(item, dict)
        and str(item.get("bom_version", "")).strip() == BULK_FABRIC_BOM_VERSION
        and _bulk_fabric_existing_material_signature(item)[0]
    }
    used_identity_keys = {
        identity_key: str(item.get("item_id", "")).strip()
        for identity_key, item in existing_items_by_identity.items()
        if isinstance(item, dict)
    }
    saved_rows: list[dict] = []
    for row in grouped_rows:
        signature = _bulk_fabric_material_signature(row)
        item_name = str(row.get("item_name", "")).strip()
        if not signature[0] or not item_name:
            continue
        existing = existing_items_by_signature.get(signature, {})
        merged_style_code = _bulk_fabric_join_style_codes(
            list(_bulk_fabric_style_tokens(existing.get("style_code") if isinstance(existing, dict) else ""))
            + list(row.get("style_codes", []))
        )
        item_id = str(existing.get("item_id", "")).strip() if isinstance(existing, dict) else ""
        item_id = item_id or hashlib.sha1("|".join(("BULK-MATERIAL", *signature)).encode("utf-8")).hexdigest()[:32]
        row["style_code"] = merged_style_code
        item_code = str(existing.get("item_code", "")).strip() if isinstance(existing, dict) else ""
        if not item_code:
            item_code = _bulk_fabric_import_item_code(row, used_identity_keys, item_id)
        payload = default_bom_item_payload()
        if isinstance(existing, dict):
            payload.update(existing)
        payload.update(
            {
                "item_id": item_id,
                "bom_id": str(existing.get("bom_id", "")).strip(),
                "style_code": merged_style_code,
                "bom_version": BULK_FABRIC_BOM_VERSION,
                "bom_name": "Bulk Fabric Imported BOM",
                "item_code": item_code,
                "item_name": item_name,
                "item_type": "Fabric",
                "section": str(row.get("section", "")).strip(),
                "article_ref": str(row.get("item_code", "")).strip(),
                "color": str(row.get("color", "")).strip(),
                "color_code": str(row.get("color_code", "")).strip(),
                "composition": str(row.get("composition", "")).strip(),
                "spec_summary": " | ".join(
                    part
                    for part in (
                        str(row.get("section", "")).strip(),
                        str(row.get("composition", "")).strip(),
                        _format_metric_with_uom(_safe_float_value(row.get("width_value")), row.get("width_unit")) if str(row.get("width_value", "")).strip() else "",
                        _format_metric_with_uom(_safe_float_value(row.get("gsm_value")), row.get("gsm_unit")) if str(row.get("gsm_value", "")).strip() else "",
                    )
                    if part
                ),
                "uom": str(existing.get("uom", "")).strip() or "M",
                "width_value": str(row.get("width_value", "")).strip(),
                "width_unit": str(row.get("width_unit", "")).strip(),
                "gsm_value": str(row.get("gsm_value", "")).strip(),
                "gsm_unit": str(row.get("gsm_unit", "")).strip(),
                "waste_percent": str(row.get("waste_percent", "")).strip(),
                "purchase_uom": str(existing.get("purchase_uom", "")).strip() or "M",
                "status": "approved",
                "source_style_code": merged_style_code,
                "source_section": "Bulk Fabric",
                "source_section_index": str(row.get("source_sheet", "")).strip(),
                "source_item_index": str(row.get("source_row_number", "")).strip(),
                "source_payload_json": json.dumps(
                    {"source_file": source_file, "material_signature": list(signature), "rows": row.get("source_rows", [row])},
                    ensure_ascii=False,
                ),
                "created_at": str(existing.get("created_at", "")).strip() or _bom_now_text(),
            }
        )
        saved = self.save_bom_item_to_source(payload)
        if isinstance(saved, dict) and saved:
            saved_rows.append(saved)
            saved_identity = (
                normalize_style_identifier_list(saved.get("style_code")),
                str(saved.get("bom_version", "")).strip(),
                str(saved.get("item_code", "")).strip(),
            )
            used_identity_keys[saved_identity] = str(saved.get("item_id", "")).strip()
            existing_items_by_signature[signature] = saved
    return saved_rows, len(saved_rows)


def _bulk_fabric_saved_row_source_rows(saved_row: dict) -> list[dict]:
    raw_payload = str(saved_row.get("source_payload_json", "") or "").strip()
    if not raw_payload:
        return []
    try:
        payload = json.loads(raw_payload)
    except (TypeError, ValueError, json.JSONDecodeError):
        return []
    rows = payload.get("rows", []) if isinstance(payload, dict) else []
    return [dict(row) for row in rows if isinstance(row, dict)]


def _bulk_fabric_consumption_line_from_bom(
    saved_row: dict,
    *,
    style_code: str,
    consumption: object,
    fabric_weight: object = "",
    line_index: int,
    existing_line: dict | None = None,
) -> dict:
    existing = dict(existing_line or {})
    line_id = str(existing.get("line_id", "")).strip()
    if not line_id:
        line_id = hashlib.sha1("|".join(("BULK-PROD", style_code, str(saved_row.get("item_id", "")))).encode("utf-8")).hexdigest()[:32]
    costing_id = str(existing.get("costing_id", "")).strip()
    if not costing_id:
        token = re.sub(r"[^A-Z0-9]+", "", line_id.upper())[:6]
        costing_id = f"{normalize_style_identifier(style_code) or 'STYLE'}-PRODUCTION-{line_index:03d}-{token}"
    return {
        "line_id": line_id,
        "costing_id": costing_id,
        "bom_item_id": str(saved_row.get("item_id", "")).strip(),
        "bom_item_code": str(saved_row.get("item_code", "")).strip(),
        "bom_item_name": str(saved_row.get("item_name", "")).strip(),
        "fabric_type": str(saved_row.get("item_type", "")).strip() or str(saved_row.get("section", "")).strip(),
        "section": str(saved_row.get("section", "")).strip(),
        "color": str(saved_row.get("color", "")).strip(),
        "trim_size_um": str(saved_row.get("trim_size_um", "")).strip(),
        "length_value": str(saved_row.get("length_value", "")).strip(),
        "width_value": str(saved_row.get("width_value", "")).strip(),
        "gsm_value": str(saved_row.get("gsm_value", "")).strip(),
        "waste_percent": str(saved_row.get("waste_percent", "")).strip(),
        "uom": str(saved_row.get("purchase_uom") or saved_row.get("uom") or existing.get("uom") or "").strip(),
        "consumption": str(consumption or "").strip(),
        "fabric_weight": str(fabric_weight or existing.get("fabric_weight", "") or "").strip(),
        "fabric_weight_uom": "KG" if str(fabric_weight or existing.get("fabric_weight", "") or "").strip() else str(existing.get("fabric_weight_uom", "") or "").strip(),
    }


def sync_bulk_fabric_items_to_tech_pack_production(self, saved_rows: list[dict]) -> int:
    if not saved_rows or not hasattr(self, "save_tech_pack_record_patch_to_source"):
        return 0
    if not getattr(self, "tech_pack_library", []):
        try:
            self.tech_pack_library = self.load_tech_pack_library_from_source()
        except Exception as exc:
            _append_bom_debug_log(f"bulk-fabric production sync load-library warning={exc}")
            self.tech_pack_library = []
    library_by_style = {
        normalize_style_identifier(item.get("style_code")): dict(item)
        for item in list(getattr(self, "tech_pack_library", []) or [])
        if isinstance(item, dict) and normalize_style_identifier(item.get("style_code"))
    }
    additions_by_style: dict[str, list[tuple[dict, str, str]]] = {}
    for saved_row in list(saved_rows or []):
        if not isinstance(saved_row, dict):
            continue
        for source_row in _bulk_fabric_saved_row_source_rows(saved_row):
            style_code = normalize_style_identifier(source_row.get("style_code"))
            consumption_value = str(source_row.get("consumption", "")).strip()
            fabric_weight = _format_metric_text(_safe_float_value(source_row.get("fabric_weight"))) if _safe_float_value(source_row.get("fabric_weight")) > 0 else ""
            if not style_code or not consumption_value:
                continue
            additions_by_style.setdefault(style_code, []).append((dict(saved_row), consumption_value, fabric_weight))
    updated_styles = 0
    for style_code, additions in additions_by_style.items():
        record = dict(library_by_style.get(style_code, {}))
        if not record:
            record = self.default_tech_pack_record(style_code) if hasattr(self, "default_tech_pack_record") else {"style_code": style_code}
        versions = self.get_tech_pack_consumption_versions(record) if hasattr(self, "get_tech_pack_consumption_versions") else []
        versions_by_type = {
            str(version.get("version_type", "")).strip().lower(): dict(version)
            for version in list(versions or [])
            if isinstance(version, dict)
        }
        production = dict(
            versions_by_type.get(
                "production",
                {"version_id": "production", "version_type": "production", "version_name": "Production", "lines": []},
            )
        )
        existing_lines = [dict(line) for line in list(production.get("lines", []) or []) if isinstance(line, dict)]
        line_index_by_key: dict[str, int] = {}
        for index, line in enumerate(existing_lines):
            for key in (
                str(line.get("bom_item_id", "")).strip(),
                str(line.get("bom_item_code", "")).strip(),
            ):
                if key and key not in line_index_by_key:
                    line_index_by_key[key] = index
        changed = False
        for saved_row, consumption_value, fabric_weight in additions:
            match_key = str(saved_row.get("item_id", "")).strip() or str(saved_row.get("item_code", "")).strip()
            line_index = line_index_by_key.get(match_key)
            if line_index is None:
                code_key = str(saved_row.get("item_code", "")).strip()
                line_index = line_index_by_key.get(code_key) if code_key else None
            if line_index is None:
                new_line = _bulk_fabric_consumption_line_from_bom(
                    saved_row,
                    style_code=style_code,
                    consumption=consumption_value,
                    fabric_weight=fabric_weight,
                    line_index=len(existing_lines) + 1,
                )
                line_index_by_key[str(new_line.get("bom_item_id", "")).strip()] = len(existing_lines)
                line_index_by_key[str(new_line.get("bom_item_code", "")).strip()] = len(existing_lines)
                existing_lines.append(new_line)
                changed = True
                continue
            updated_line = _bulk_fabric_consumption_line_from_bom(
                saved_row,
                style_code=style_code,
                consumption=consumption_value,
                fabric_weight=fabric_weight,
                line_index=line_index + 1,
                existing_line=existing_lines[line_index],
            )
            if updated_line != existing_lines[line_index]:
                existing_lines[line_index] = updated_line
                changed = True
        if not changed:
            continue
        production["version_id"] = str(production.get("version_id", "")).strip() or "production"
        production["version_type"] = "production"
        production["version_name"] = str(production.get("version_name", "")).strip() or "Production"
        production["lines"] = existing_lines
        versions_by_type["production"] = production
        ordered_versions = []
        for version_type in ("costing", "sample", "booking", "production", "settlement"):
            version = versions_by_type.get(version_type)
            if isinstance(version, dict):
                ordered_versions.append(version)
        saved_record = self.save_tech_pack_record_patch_to_source(style_code, {"consumption_versions": ordered_versions})
        if isinstance(saved_record, dict) and saved_record:
            library_by_style[style_code] = dict(saved_record)
            updated_styles += 1
            if hasattr(self, "sync_costing_form_materials_from_consumption"):
                try:
                    self.sync_costing_form_materials_from_consumption(style_code, ordered_versions)
                except Exception as exc:
                    _append_bom_debug_log(f"bulk-fabric production sync costing warning style={style_code} error={exc}")
    if updated_styles:
        self.tech_pack_library = list(library_by_style.values())
    return updated_styles


def open_import_bulk_fabric_menu(self) -> None:
    if not self.is_action_allowed("action.bom.import_bulk_fabric"):
        return
    button = self.widget_text.get("bom_import_bulk_fabric_button") if hasattr(self, "widget_text") else None
    menu = tk.Menu(self.root, tearoff=False)
    menu.add_command(label="Single file", command=self.import_bulk_fabric_single_file)
    menu.add_command(label="Folder", command=self.import_bulk_fabric_folder)
    try:
        if button is not None:
            x = button.winfo_rootx()
            y = button.winfo_rooty() + button.winfo_height()
        else:
            x = self.root.winfo_pointerx()
            y = self.root.winfo_pointery()
        menu.tk_popup(x, y)
    finally:
        menu.grab_release()


def import_bulk_fabric_single_file(self) -> None:
    if not self.is_action_allowed("action.bom.import_bulk_fabric"):
        return
    file_path = filedialog.askopenfilename(
        parent=self.root,
        title="Import Bulk Fabric - Single file",
        filetypes=[
            ("Excel files", "*.xls *.xlsx *.xlsm"),
            ("All files", "*.*"),
        ],
    )
    if not file_path:
        return
    self.start_bulk_fabric_import_worker([file_path])


def import_bulk_fabric_folder(self) -> None:
    if not self.is_action_allowed("action.bom.import_bulk_fabric"):
        return
    folder_path = filedialog.askdirectory(parent=self.root, title="Import Bulk Fabric - Folder")
    if not folder_path:
        return
    folder = Path(folder_path)
    excel_paths = [
        path
        for path in sorted(folder.iterdir(), key=lambda item: item.name.lower())
        if path.is_file()
        and path.suffix.lower() in {".xls", ".xlsx", ".xlsm"}
        and not path.name.startswith("~$")
    ]
    if not excel_paths:
        messagebox.showwarning(self.t("warning_title"), "No Excel Bulk Fabric file found in the selected folder.")
        return
    extensions = sorted({path.suffix.lower() for path in excel_paths})
    if len(extensions) > 1:
        messagebox.showwarning(
            self.t("warning_title"),
            "The selected folder contains mixed Excel formats. Keep only one format (.xls, .xlsx, or .xlsm) in the folder before importing.",
        )
        return
    self.start_bulk_fabric_import_worker([str(path) for path in excel_paths])


def start_bulk_fabric_import_worker(self, file_paths: list[str]) -> None:
    normalized_paths = [
        str(Path(path))
        for path in list(file_paths or [])
        if str(path or "").strip()
    ]
    if not normalized_paths:
        return
    import_button = self.widget_text.get("bom_import_bulk_fabric_button") if hasattr(self, "widget_text") else None
    try:
        if import_button is not None:
            import_button.configure(state="disabled")
    except tk.TclError:
        pass
    self.set_status(f"Importing Bulk Fabric from {len(normalized_paths)} file(s)...")

    def worker() -> None:
        payload = {
            "saved_total": 0,
            "imported_files": 0,
            "synced_styles": 0,
            "failed": [],
        }
        all_saved_rows: list[dict] = []
        for file_path in normalized_paths:
            try:
                parsed_rows = self.parse_bulk_fabric_workbook(file_path)
                _saved_rows, saved_count = self.build_bom_items_from_bulk_fabric_rows(
                    parsed_rows,
                    source_file=str(file_path),
                )
                all_saved_rows.extend([dict(row) for row in list(_saved_rows or []) if isinstance(row, dict)])
                payload["saved_total"] += saved_count
                if saved_count > 0:
                    payload["imported_files"] += 1
            except Exception as exc:
                _append_bom_debug_log(
                    "bulk-fabric import error="
                    + str(file_path)
                    + " message="
                    + str(exc)
                    + " traceback="
                    + traceback.format_exc(limit=8).replace("\n", " | ")
                )
                payload["failed"].append(f"{Path(file_path).name}: {exc}")
        try:
            payload["synced_styles"] = self.sync_bulk_fabric_items_to_tech_pack_production(all_saved_rows)
        except Exception as exc:
            _append_bom_debug_log(
                "bulk-fabric production sync error="
                + str(exc)
                + " traceback="
                + traceback.format_exc(limit=8).replace("\n", " | ")
            )
            payload["failed"].append(f"Production consumption sync: {exc}")
        if hasattr(self, "schedule_root_after"):
            self.schedule_root_after(0, lambda result=payload: self.finish_bulk_fabric_import_worker(result))
        else:
            self.root.after(0, lambda result=payload: self.finish_bulk_fabric_import_worker(result))

    threading.Thread(target=worker, daemon=True).start()


def finish_bulk_fabric_import_worker(self, payload: dict) -> None:
    import_button = self.widget_text.get("bom_import_bulk_fabric_button") if hasattr(self, "widget_text") else None
    try:
        if import_button is not None:
            import_button.configure(state="normal")
    except tk.TclError:
        pass
    saved_total = int(payload.get("saved_total", 0) or 0) if isinstance(payload, dict) else 0
    imported_files = int(payload.get("imported_files", 0) or 0) if isinstance(payload, dict) else 0
    synced_styles = int(payload.get("synced_styles", 0) or 0) if isinstance(payload, dict) else 0
    failed = list(payload.get("failed", [])) if isinstance(payload, dict) and isinstance(payload.get("failed", []), list) else []
    if saved_total > 0:
        try:
            self.refresh_bom_module()
        except Exception as exc:
            failed.append(f"Refresh BOM: {exc}")
    self.set_status(f"Imported {saved_total} approved Bulk Fabric BOM item(s) from {imported_files} file(s). Synced Production consumption for {synced_styles} style(s).")
    if failed:
        messagebox.showwarning(
            self.t("warning_title"),
            f"Imported {saved_total} approved Bulk Fabric BOM item(s) from {imported_files} file(s).\nSynced Production consumption for {synced_styles} style(s).\n\nIssues:\n"
            + "\n".join(failed[:12]),
        )
    elif saved_total <= 0:
        messagebox.showwarning(self.t("warning_title"), "No valid Bulk Fabric rows were imported.")
    else:
        messagebox.showinfo(
            self.t("import_done_title"),
            f"Imported {saved_total} approved Bulk Fabric BOM item(s) from {imported_files} file(s).\nSynced Production consumption for {synced_styles} style(s).",
        )


def _bulk_trims_clean_text(value: object) -> str:
    return str(value or "").replace("\n", " / ").strip()


def _bulk_trims_code_token(value: object, *, limit: int = 28) -> str:
    return re.sub(r"[^A-Z0-9]+", "-", _bulk_trims_clean_text(value).upper()).strip("-")[:limit]


def _bulk_trims_number_text(value: object) -> str:
    text = _bulk_trims_clean_text(value).replace(",", "")
    if not text:
        return ""
    match = re.search(r"-?\d+(?:\.\d+)?", text)
    if not match:
        return text
    number_value = float(match.group(0))
    return _format_metric_text(number_value)


def _bulk_trims_loss_percent_text(value: object) -> str:
    text = _bulk_trims_clean_text(value).replace(",", "")
    if not text:
        return ""
    try:
        number_value = float(text)
    except Exception:
        return text
    if 0 < number_value < 2:
        number_value = (number_value - 1) * 100
    return _format_metric_text(number_value)


def _bulk_trims_uom_text(value: object) -> str:
    normalized = _bulk_trims_clean_text(value).upper()
    return {
        "METER": "M",
        "METERS": "M",
        "PCS": "EA",
        "PC": "EA",
        "PIECE": "EA",
        "PIECES": "EA",
    }.get(normalized, normalized)


def _bulk_trims_measurement_value_unit(value: object) -> tuple[str, str]:
    measurement_value, measurement_unit = _split_measurement_and_unit(value)
    if measurement_value and not measurement_unit:
        match = re.fullmatch(r"\s*(-?\d+(?:\.\d+)?)\s*([A-Za-z]+)\s*", str(measurement_value or ""))
        if match:
            measurement_value = match.group(1)
            measurement_unit = match.group(2).upper()
    return measurement_value, measurement_unit


def _bulk_trims_type_section(item_name: object) -> tuple[str, str]:
    normalized = _bulk_trims_clean_text(item_name).upper()
    if "ELASTIC" in normalized:
        return "Trims", "Elastic"
    if "PRICE TICKET" in normalized or "PRICE STICKER" in normalized or "PRICE STIKER" in normalized:
        return "Packaging", "Price Sticker"
    if "HANGTAG" in normalized or "HANG TAG" in normalized:
        return "Packaging", "Hangtag"
    if "HANGER" in normalized:
        return "Packaging", "Hanger"
    if "HEAT TRANSFER LABEL" in normalized:
        return "Label", "Heat Transfer Label"
    if "MAIN LABEL" in normalized:
        return "Label", "Main Label"
    if "CARE LABEL" in normalized:
        return "Label", "Care Label"
    if "CARTON BARCODE STICKER" in normalized:
        return "Packaging", "Carton Barcode Sticker"
    if "MOBILON TAPE" in normalized:
        return "Trims", "Mobilon Tape"
    if "LOGO NECK TAPE" in normalized:
        return "Trims", "Logo Neck Tape"
    if "THREAD" in normalized:
        return "Trims", "Thread"
    if "ZIPPER" in normalized:
        return "Trims", "Zipper"
    if "INTERLINING" in normalized:
        return "Trims", "Interlining"
    return "Trims", "Bulk Trims"


_BULK_TRIMS_COLORLESS_WHEN_BLANK_SECTIONS = {
    "Main Label",
    "Care Label",
    "Hangtag",
    "Price Sticker",
    "Carton Barcode Sticker",
}


def _bulk_trims_section_is_colorless_when_blank(section: object) -> bool:
    return str(section or "").strip() in _BULK_TRIMS_COLORLESS_WHEN_BLANK_SECTIONS


def _bulk_trims_item_is_fabric(item_name: object) -> bool:
    normalized = _bulk_trims_clean_text(item_name).upper()
    return "FABRIC" in normalized


def _bulk_trims_style_is_reference_code(value: object) -> bool:
    return re.fullmatch(r"RLM\d+", _bulk_trims_clean_text(value).upper()) is not None


def _bulk_trims_referenced_rows(raw_values: list[str]) -> list[int]:
    referenced: list[int] = []
    for value in raw_values:
        for match in re.finditer(r"\b[A-Z]{1,3}(\d+)\b", str(value or "").upper()):
            row_number = int(match.group(1))
            if row_number not in referenced:
                referenced.append(row_number)
    return referenced


def _bulk_trims_header_text(value: object) -> str:
    return re.sub(r"[^A-Z0-9#]+", " ", _bulk_trims_clean_text(value).upper()).strip()


def parse_bulk_trims_workbook(self, file_path: str | Path) -> list[dict]:
    path = Path(file_path)
    if not path.exists():
        raise RuntimeError(f"Bulk Trims file not found: {path}")
    if path.suffix.lower() not in {".xlsx", ".xlsm"}:
        raise RuntimeError("Bulk Trims import currently supports .xlsx and .xlsm files.")
    try:
        from openpyxl import load_workbook
    except ImportError as exc:
        raise RuntimeError("Missing openpyxl package. Please install openpyxl to import Bulk Trims files.") from exc

    formula_workbook = load_workbook(path, data_only=False)
    value_workbook = load_workbook(path, data_only=True)
    parsed_rows: list[dict] = []
    seen_item_keys: set[tuple[str, str]] = set()
    for worksheet in formula_workbook.worksheets:
        value_sheet = value_workbook[worksheet.title]
        if _bulk_trims_clean_text(worksheet["A1"].value).upper() != "THE BALANCE SHEET":
            continue
        merged_parent: dict[tuple[int, int], tuple[int, int]] = {}
        for merged_range in worksheet.merged_cells.ranges:
            min_col, min_row, max_col, max_row = merged_range.bounds
            for row_number in range(min_row, max_row + 1):
                for column_number in range(min_col, max_col + 1):
                    merged_parent[(row_number, column_number)] = (min_row, min_col)

        def read_cell(sheet, row_number: int, column_number: int) -> object:
            parent_row, parent_column = merged_parent.get((row_number, column_number), (row_number, column_number))
            return sheet.cell(parent_row, parent_column).value

        def display_cell(row_number: int, column_number: int) -> str:
            value = read_cell(value_sheet, row_number, column_number)
            if value is None:
                value = read_cell(worksheet, row_number, column_number)
            return _bulk_trims_clean_text(value)

        def formula_cell(row_number: int, column_number: int) -> str:
            return _bulk_trims_clean_text(read_cell(worksheet, row_number, column_number))

        header_row = -1
        header_columns: dict[str, int] = {}
        for row_number in range(1, min(40, worksheet.max_row) + 1):
            row_headers = {
                column_number: _bulk_trims_header_text(display_cell(row_number, column_number))
                for column_number in range(1, min(30, worksheet.max_column) + 1)
            }
            item_col = next((col for col, text in row_headers.items() if text == "ITEM"), 0)
            description_col = next((col for col, text in row_headers.items() if "DESCRIPTION" in text), 0)
            unit_col = next((col for col, text in row_headers.items() if text == "UNIT"), 0)
            if item_col and description_col and unit_col:
                header_row = row_number
                header_columns = {
                    "item": item_col,
                    "style": next((col for col, text in row_headers.items() if "STYLE" in text), 0),
                    "description": description_col,
                    "unit": unit_col,
                    "trim_color": next((col for col, text in row_headers.items() if "TRIM COLOR" in text or text == "COLOR"), 0),
                    "trim_size": next((col for col, text in row_headers.items() if text.startswith("SIZE")), 0),
                    "order_qty": next((col for col, text in row_headers.items() if "ORDER" in text and "Q" in text), 0),
                    "cons_buyer": next((col for col, text in row_headers.items() if "CONS" in text and "BUYER" in text), 0),
                    "cons_fty": next((col for col, text in row_headers.items() if "CONS" in text and "FTY" in text), 0),
                    "loss": next((col for col, text in row_headers.items() if "LOSS" in text), 0),
                    "need_qty": next((col for col, text in row_headers.items() if "NEED" in text and "Q" in text), 0),
                    "placed_order_qty": next((col for col, text in row_headers.items() if "ACTUAL" in text and ("IN PUT" in text or "INPUT" in text)), 0),
                    "balance_qty": next((col for col, text in row_headers.items() if text.startswith("BLC")), 0),
                }
                break
        if header_row < 0:
            _append_bom_debug_log(f"bulk-trims skip sheet={worksheet.title} missing balance-sheet header")
            continue

        sheet_style_code = normalize_style_identifier(worksheet.title)
        summary_styles_by_row: dict[int, str] = {}
        summary_colors_by_row: dict[int, str] = {}
        for row_number in range(1, header_row):
            style_code = normalize_style_identifier(display_cell(row_number, 2))
            color = display_cell(row_number, 5)
            if style_code and style_code.upper() != "STYLE":
                summary_styles_by_row[row_number] = style_code
                summary_colors_by_row[row_number] = color

        fill_values = {key: "" for key in ("item", "style", "description", "unit", "trim_color", "trim_size")}
        for row_number in range(header_row + 1, worksheet.max_row + 1):
            raw_values = [formula_cell(row_number, column_number) for column_number in range(1, min(30, worksheet.max_column) + 1)]
            display_values = [display_cell(row_number, column_number) for column_number in range(1, min(30, worksheet.max_column) + 1)]
            if not any(raw_values) and not any(display_values):
                continue

            def column_value(field_key: str, values: list[str]) -> str:
                column_number = int(header_columns.get(field_key, 0) or 0)
                if column_number <= 0 or column_number > len(values):
                    return ""
                return values[column_number - 1]

            field_values = {
                key: column_value(key, display_values)
                for key in ("item", "style", "description", "unit", "trim_color", "trim_size")
            }
            if _bulk_trims_header_text(field_values.get("item", "")) == "ITEM":
                continue
            owns_trim_color = bool(field_values.get("trim_color", ""))
            for key in ("item", "style", "description", "unit", "trim_color", "trim_size"):
                if field_values[key]:
                    fill_values[key] = field_values[key]
                else:
                    field_values[key] = fill_values[key]

            item_name = field_values["item"]
            source_style = field_values["style"]
            description = field_values["description"]
            unit = field_values["unit"]
            trim_color = field_values["trim_color"]
            trim_size = field_values["trim_size"]
            if not item_name:
                continue
            if _bulk_trims_item_is_fabric(item_name):
                continue
            style_code = ""
            include_reason = ""
            if source_style and not _bulk_trims_style_is_reference_code(source_style):
                style_code = normalize_style_identifier(source_style)
                include_reason = "direct-style"
            if not style_code:
                referenced_rows = _bulk_trims_referenced_rows(raw_values[4:7])
                for referenced_row in referenced_rows:
                    referenced_style = summary_styles_by_row.get(referenced_row, "")
                    if referenced_style:
                        style_code = referenced_style
                        include_reason = "summary-reference"
                        break
            if not style_code and sheet_style_code:
                style_code = sheet_style_code
                include_reason = "sheet-title"
            if not style_code:
                _append_bom_debug_log(f"bulk-trims skip sheet={worksheet.title} row={row_number} missing style")
                continue
            item_type, section = _bulk_trims_type_section(item_name)
            if _bulk_trims_section_is_colorless_when_blank(section) and not owns_trim_color:
                trim_color = ""
            elif (include_reason == "direct-style" and not owns_trim_color) or not trim_color:
                for summary_row, summary_style in summary_styles_by_row.items():
                    if summary_style == style_code and summary_colors_by_row.get(summary_row):
                        trim_color = summary_colors_by_row.get(summary_row, "")
                        break
            length_value = ""
            length_unit = ""
            normalized_trim_size = trim_size
            if section == "Thread":
                length_value, length_unit = _bulk_trims_measurement_value_unit(trim_size)
                normalized_trim_size = "ALL"
                if not length_unit and length_value:
                    length_unit = "M"
            source_ref = source_style if include_reason == "summary-reference" else ""
            item_code_parts = [
                style_code,
                _bulk_trims_code_token(item_name),
                _bulk_trims_code_token(source_ref),
                _bulk_trims_code_token(description),
                _bulk_trims_code_token(trim_color),
                _bulk_trims_code_token(trim_size),
            ]
            item_code = "-".join(part for part in item_code_parts if part)
            if len(item_code) > 96:
                digest = hashlib.sha1(item_code.encode("utf-8")).hexdigest()[:8].upper()
                item_code = "-".join(
                    part
                    for part in (
                        style_code,
                        _bulk_trims_code_token(item_name, limit=20),
                        _bulk_trims_code_token(source_ref, limit=12),
                        _bulk_trims_code_token(trim_color, limit=16),
                        _bulk_trims_code_token(trim_size, limit=10),
                        digest,
                    )
                    if part
                )
            item_key = (style_code, item_code)
            if item_key in seen_item_keys:
                _append_bom_debug_log(f"bulk-trims skip duplicate style={style_code} item_code={item_code}")
                continue
            seen_item_keys.add(item_key)
            parsed_rows.append(
                {
                    "source_sheet": worksheet.title,
                    "source_row_number": str(row_number),
                    "style_code": style_code,
                    "source_ref": source_ref,
                    "item_code": item_code,
                    "item_name": item_name,
                    "item_type": item_type,
                    "section": section,
                    "description": description,
                    "color": trim_color,
                    "trim_size_um": normalized_trim_size,
                    "length_value": length_value,
                    "length_unit": length_unit,
                    "uom": _bulk_trims_uom_text(unit),
                    "qty_per_garment": _bulk_trims_number_text(column_value("cons_fty", display_values) or column_value("cons_buyer", display_values)),
                    "waste_percent": _bulk_trims_loss_percent_text(column_value("loss", display_values)),
                    "order_qty": column_value("order_qty", display_values),
                    "need_qty": column_value("need_qty", display_values),
                    "placed_order_qty": column_value("placed_order_qty", display_values),
                    "actual_in_house": column_value("balance_qty", display_values),
                    "include_reason": include_reason,
                    "raw_values": raw_values,
                    "display_values": display_values,
                }
            )
    return parsed_rows


def build_bom_items_from_bulk_trims_rows(self, source_rows: list[dict], *, source_file: str) -> tuple[list[dict], int]:
    rows = [dict(row) for row in list(source_rows or []) if isinstance(row, dict)]
    if not rows:
        return [], 0
    saved_rows: list[dict] = []
    for row in rows:
        style_code = normalize_style_identifier(row.get("style_code"))
        item_code = str(row.get("item_code", "")).strip()
        item_name = str(row.get("item_name", "")).strip()
        if not style_code or not item_code or not item_name:
            continue
        item_id = hashlib.sha1("|".join((BULK_TRIMS_BOM_VERSION, style_code, item_code)).encode("utf-8")).hexdigest()[:32]
        uom = str(row.get("uom", "")).strip().upper()
        spec_summary = " | ".join(
            part
            for part in (
                str(row.get("description", "")).strip(),
                str(row.get("color", "")).strip(),
                str(row.get("trim_size_um", "")).strip(),
                f"Cons {row.get('qty_per_garment')} {uom}".strip() if str(row.get("qty_per_garment", "")).strip() else "",
            )
            if part
        )
        payload = default_bom_item_payload()
        payload.update(
            {
                "item_id": item_id,
                "style_code": style_code,
                "bom_version": BULK_TRIMS_BOM_VERSION,
                "bom_name": "Bulk Trims Imported BOM",
                "item_code": item_code,
                "item_name": item_name,
                "item_type": str(row.get("item_type", "")).strip() or "Trims",
                "section": str(row.get("section", "")).strip() or "Bulk Trims",
                "article_ref": str(row.get("source_ref", "")).strip(),
                "color": str(row.get("color", "")).strip(),
                "description": str(row.get("description", "")).strip(),
                "spec_summary": spec_summary,
                "uom": uom,
                "purchase_uom": uom,
                "qty_per_garment": str(row.get("qty_per_garment", "")).strip(),
                "trim_size_um": str(row.get("trim_size_um", "")).strip(),
                "length_value": str(row.get("length_value", "")).strip(),
                "length_unit": str(row.get("length_unit", "")).strip().upper(),
                "waste_percent": str(row.get("waste_percent", "")).strip(),
                "status": "checking",
                "source_style_code": style_code,
                "source_section": "Bulk Trims",
                "source_section_index": str(row.get("source_sheet", "")).strip(),
                "source_item_index": str(row.get("source_row_number", "")).strip(),
                "source_payload_json": json.dumps(
                    {
                        "source_file": source_file,
                        "source_sheet": row.get("source_sheet", ""),
                        "source_row_number": row.get("source_row_number", ""),
                        "include_reason": row.get("include_reason", ""),
                        "source_ref": row.get("source_ref", ""),
                        "length_value": row.get("length_value", ""),
                        "length_unit": row.get("length_unit", ""),
                        "raw_values": row.get("raw_values", []),
                        "display_values": row.get("display_values", []),
                        "order_qty": row.get("order_qty", ""),
                        "need_qty": row.get("need_qty", ""),
                        "placed_order_qty": row.get("placed_order_qty", ""),
                        "actual_in_house": row.get("actual_in_house", ""),
                    },
                    ensure_ascii=False,
                ),
            }
        )
        saved = self.save_bom_item_to_source(payload)
        if isinstance(saved, dict) and saved:
            saved_rows.append(saved)
    return saved_rows, len(saved_rows)


def open_import_bulk_trims_menu(self) -> None:
    if not self.is_action_allowed("action.bom.import_bulk_trims"):
        return
    button = self.widget_text.get("bom_import_bulk_trims_button") if hasattr(self, "widget_text") else None
    menu = tk.Menu(self.root, tearoff=False)
    menu.add_command(label="Single file", command=self.import_bulk_trims_single_file)
    menu.add_command(label="Folder", command=self.import_bulk_trims_folder)
    try:
        if button is not None:
            x = button.winfo_rootx()
            y = button.winfo_rooty() + button.winfo_height()
        else:
            x = self.root.winfo_pointerx()
            y = self.root.winfo_pointery()
        menu.tk_popup(x, y)
    finally:
        menu.grab_release()


def import_bulk_trims_single_file(self) -> None:
    if not self.is_action_allowed("action.bom.import_bulk_trims"):
        return
    file_path = filedialog.askopenfilename(
        parent=self.root,
        title="Import Bulk Trims - Single file",
        filetypes=[
            ("Excel files", "*.xlsx *.xlsm"),
            ("All files", "*.*"),
        ],
    )
    if not file_path:
        return
    self.start_bulk_trims_import_worker([file_path])


def import_bulk_trims_folder(self) -> None:
    if not self.is_action_allowed("action.bom.import_bulk_trims"):
        return
    folder_path = filedialog.askdirectory(parent=self.root, title="Import Bulk Trims - Folder")
    if not folder_path:
        return
    folder = Path(folder_path)
    excel_paths = [
        path
        for path in sorted(folder.iterdir(), key=lambda item: item.name.lower())
        if path.is_file()
        and path.suffix.lower() in {".xlsx", ".xlsm"}
        and not path.name.startswith("~$")
    ]
    if not excel_paths:
        messagebox.showwarning(self.t("warning_title"), "No Excel Bulk Trims file found in the selected folder.")
        return
    self.start_bulk_trims_import_worker([str(path) for path in excel_paths])


def start_bulk_trims_import_worker(self, file_paths: list[str]) -> None:
    normalized_paths = [str(Path(path)) for path in list(file_paths or []) if str(path or "").strip()]
    if not normalized_paths:
        return
    import_button = self.widget_text.get("bom_import_bulk_trims_button") if hasattr(self, "widget_text") else None
    try:
        if import_button is not None:
            import_button.configure(state="disabled")
    except tk.TclError:
        pass
    self.set_status(f"Importing Bulk Trims from {len(normalized_paths)} file(s)...")

    def worker() -> None:
        payload = {"saved_total": 0, "imported_files": 0, "failed": []}
        previous_defer = bool(getattr(self, "_defer_bom_post_save_sync", False))
        self._defer_bom_post_save_sync = True
        try:
            for file_path in normalized_paths:
                try:
                    parsed_rows = self.parse_bulk_trims_workbook(file_path)
                    _saved_rows, saved_count = self.build_bom_items_from_bulk_trims_rows(parsed_rows, source_file=str(file_path))
                    payload["saved_total"] += saved_count
                    if saved_count > 0:
                        payload["imported_files"] += 1
                except Exception as exc:
                    _append_bom_debug_log(
                        "bulk-trims import error="
                        + str(file_path)
                        + " message="
                        + str(exc)
                        + " traceback="
                        + traceback.format_exc(limit=8).replace("\n", " | ")
                    )
                    payload["failed"].append(f"{Path(file_path).name}: {exc}")
        finally:
            self._defer_bom_post_save_sync = previous_defer
        if payload["saved_total"]:
            try:
                latest_bom_rows = [normalize_bom_item_payload(item) for item in self.load_bom_items_from_source()]
                self.sync_tech_pack_consumption_with_bom_status([item for item in latest_bom_rows if isinstance(item, dict)])
            except Exception as sync_exc:
                payload["failed"].append(f"Bulk Trims post-import sync: {sync_exc}")
                _append_bom_debug_log(f"bulk-trims post-import sync warning={sync_exc}")
        if hasattr(self, "schedule_root_after"):
            self.schedule_root_after(0, lambda result=payload: self.finish_bulk_trims_import_worker(result))
        else:
            self.root.after(0, lambda result=payload: self.finish_bulk_trims_import_worker(result))

    threading.Thread(target=worker, daemon=True).start()


def finish_bulk_trims_import_worker(self, payload: dict) -> None:
    import_button = self.widget_text.get("bom_import_bulk_trims_button") if hasattr(self, "widget_text") else None
    try:
        if import_button is not None:
            import_button.configure(state="normal")
    except tk.TclError:
        pass
    saved_total = int(payload.get("saved_total", 0) or 0) if isinstance(payload, dict) else 0
    imported_files = int(payload.get("imported_files", 0) or 0) if isinstance(payload, dict) else 0
    failed = list(payload.get("failed", [])) if isinstance(payload, dict) and isinstance(payload.get("failed", []), list) else []
    if saved_total > 0:
        try:
            self.refresh_bom_module()
        except Exception as exc:
            failed.append(f"Refresh BOM: {exc}")
    self.set_status(f"Imported {saved_total} checking Bulk Trims BOM item(s) from {imported_files} file(s).")
    if failed:
        messagebox.showwarning(
            self.t("warning_title"),
            f"Imported {saved_total} checking Bulk Trims BOM item(s) from {imported_files} file(s).\n\nIssues:\n"
            + "\n".join(failed[:12]),
        )
    elif saved_total <= 0:
        messagebox.showwarning(self.t("warning_title"), "No valid Bulk Trims rows were imported.")
    else:
        messagebox.showinfo(
            self.t("import_done_title"),
            f"Imported {saved_total} checking Bulk Trims BOM item(s) from {imported_files} file(s).",
        )


def open_material_po_detail_window(self, row: dict | None = None) -> None:
    source_row = dict(row) if isinstance(row, dict) else default_material_po_payload()
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("bom_material_po_title"))
    dialog.geometry("760x560")
    dialog.minsize(680, 520)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(1, weight=1)

    ttk.Label(frame, text=dialog.title(), style="Section.TLabel").grid(row=0, column=0, sticky="w")
    form = ttk.Frame(frame)
    form.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
    form.columnconfigure(0, weight=1)
    form.columnconfigure(1, weight=1)

    vars_map = {key: tk.StringVar(value=str(source_row.get(key, "")).strip()) for key in default_material_po_payload()}
    text_note = tk.Text(form, height=6, wrap="word")
    text_note.insert("1.0", str(source_row.get("note", "")).strip())

    def add_entry(row_index: int, column_index: int, label: str, key: str) -> None:
        host = ttk.Frame(form)
        host.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        host.columnconfigure(0, weight=1)
        ttk.Label(host, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        ttk.Entry(host, textvariable=vars_map[key]).grid(row=1, column=0, sticky="ew")

    add_entry(0, 0, self.t("bom_mpo_no_column"), "mpo_number")
    add_entry(0, 1, self.t("order_date"), "order_date")
    add_entry(1, 0, self.t("bom_eta_date_column"), "eta_date")
    add_entry(1, 1, self.t("bom_supplier_column"), "supplier_name")
    add_entry(2, 0, self.t("samples_style_column"), "style_code")
    add_entry(2, 1, self.t("bom_item_code_column"), "item_code")
    add_entry(3, 0, self.t("bom_item_name_column"), "item_name")
    add_entry(3, 1, self.t("bom_linked_pos_column"), "linked_po_numbers")
    add_entry(4, 0, self.t("bom_ordered_qty_column"), "ordered_qty")
    add_entry(4, 1, self.t("bom_purchase_uom_column"), "purchase_uom")
    add_entry(5, 0, self.t("bom_unit_price_column"), "unit_price")
    add_entry(5, 1, self.t("bom_currency_column"), "currency")
    status_host = ttk.Frame(form)
    status_host.grid(row=6, column=0, sticky="ew", padx=8, pady=(0, 12))
    status_host.columnconfigure(0, weight=1)
    ttk.Label(status_host, text=self.t("status")).grid(row=0, column=0, sticky="w", pady=(0, 4))
    status_combo = ttk.Combobox(status_host, textvariable=vars_map["status"], values=MATERIAL_PO_STATUS_OPTIONS, state="readonly")
    status_combo.grid(row=1, column=0, sticky="ew")

    note_host = ttk.Frame(form)
    note_host.grid(row=6, column=1, sticky="nsew", padx=8, pady=(0, 12))
    note_host.columnconfigure(0, weight=1)
    ttk.Label(note_host, text=self.t("bom_note_label")).grid(row=0, column=0, sticky="w", pady=(0, 4))
    text_note.grid(in_=note_host, row=1, column=0, sticky="nsew")

    action_row = ttk.Frame(frame)
    action_row.grid(row=2, column=0, sticky="e", pady=(12, 0))

    def save_and_close() -> None:
        payload = {key: variable.get().strip() for key, variable in vars_map.items()}
        payload["note"] = text_note.get("1.0", "end").strip()
        saved = self.save_material_po_row_to_source(payload)
        if not saved:
            messagebox.showwarning(self.t("warning_title"), self.t("bom_item_code_or_name_required_warning"))
            return
        dialog.destroy()
        self.refresh_bom_module()
        self.refresh_materials_po_window()

    ttk.Button(action_row, text=self.t("save"), command=save_and_close).pack(side="left")
    ttk.Button(action_row, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(8, 0))


def refresh_materials_po_window(self) -> None:
    tree = getattr(self, "materials_po_tree", None)
    if tree is None:
        return
    try:
        if int(tree.winfo_exists()) != 1:
            self.materials_po_tree = None
            return
    except tk.TclError:
        self.materials_po_tree = None
        return
    rows = self.load_material_po_rows_from_source()
    search_query = str(getattr(self, "materials_po_search_var", tk.StringVar(value="")).get() or "").strip().lower()
    if search_query:
        rows = [
            row for row in rows
            if search_query in " ".join(
                [
                    self.display_value(row.get("mpo_number")),
                    self.display_value(row.get("supplier_name")),
                    self.display_value(row.get("style_code")),
                    self.display_value(row.get("item_code")),
                    self.display_value(row.get("item_name")),
                    self.display_value(row.get("linked_po_numbers")),
                    self.display_value(row.get("status")),
                ]
            ).lower()
        ]
    rows.sort(key=lambda item: (str(item.get("order_date", "")).lower(), str(item.get("mpo_number", "")).lower()), reverse=True)
    self.material_po_rows = rows
    try:
        existing_children = tree.get_children()
        if existing_children:
            tree.delete(*existing_children)
    except tk.TclError:
        self.materials_po_tree = None
        return
    receipt_by_mpo_item = self.summarize_material_receipts_by_po_item(approved_only=True)
    for index, row in enumerate(rows, start=1):
        record_id = str(row.get("record_id", "")).strip()
        received_qty = receipt_by_mpo_item.get((str(row.get("mpo_number", "")).strip(), str(row.get("item_code", "")).strip()), 0.0)
        tree.insert(
            "",
            "end",
            iid=record_id,
            values=(
                self.display_value(row.get("mpo_number")),
                self.display_value(row.get("order_date")),
                self.display_value(row.get("eta_date")),
                self.display_value(row.get("supplier_name")),
                self.display_value(row.get("style_code")),
                self.display_value(row.get("item_code")),
                self.display_value(row.get("item_name")),
                self.display_value(row.get("linked_po_numbers")),
                self.display_value(row.get("ordered_qty")),
                _format_metric_text(received_qty),
                self.display_value(row.get("purchase_uom")),
                self.display_value(row.get("status")),
            ),
            tags=("odd_row" if index % 2 == 1 else "even_row",),
        )
    status_var = getattr(self, "materials_po_status_var", None)
    if isinstance(status_var, tk.StringVar):
        status_var.set(f"Materials PO: {len(rows)}")


def open_materials_po_window(self) -> None:
    existing = getattr(self, "materials_po_dialog", None)
    if existing is not None and existing.winfo_exists():
        existing.lift()
        existing.focus_force()
        self.refresh_materials_po_window()
        return
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("bom_materials_po_button"))
    dialog.geometry("1320x700")
    dialog.minsize(1180, 620)
    self.materials_po_dialog = dialog

    def close_materials_po_window() -> None:
        self.materials_po_tree = None
        self.material_po_rows = []
        self.materials_po_dialog = None
        self.materials_po_status_var = None
        try:
            self.materials_po_search_var = None
        except Exception:
            pass
        if dialog.winfo_exists():
            dialog.destroy()

    dialog.protocol("WM_DELETE_WINDOW", close_materials_po_window)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(2, weight=1)

    toolbar = ttk.Frame(frame)
    toolbar.grid(row=0, column=0, sticky="ew")
    ttk.Button(toolbar, text=self.t("new_button"), command=lambda: self.open_material_po_detail_window()).pack(side="left")

    def edit_selected() -> None:
        selection = self.materials_po_tree.selection()
        if not selection:
            messagebox.showwarning(self.t("warning_title"), self.t("bom_select_materials_po_line_warning"))
            return
        record_id = str(selection[0]).strip()
        row = next((dict(item) for item in getattr(self, "material_po_rows", []) if str(item.get("record_id", "")).strip() == record_id), None)
        if row is None:
            return
        self.open_material_po_detail_window(row)

    def receive_selected() -> None:
        selection = self.materials_po_tree.selection()
        if not selection:
            messagebox.showwarning(self.t("warning_title"), self.t("bom_select_materials_po_line_warning"))
            return
        record_id = str(selection[0]).strip()
        row = next((dict(item) for item in getattr(self, "material_po_rows", []) if str(item.get("record_id", "")).strip() == record_id), None)
        if row is None:
            return
        if not _is_material_po_approved(row.get("status")):
            messagebox.showwarning(self.t("warning_title"), self.t("bom_materials_po_receive_approved_only"))
            return
        self.open_material_receipt_detail_window(material_po_row=row)

    def delete_selected() -> None:
        selection = self.materials_po_tree.selection()
        if not selection:
            messagebox.showwarning(self.t("warning_title"), self.t("bom_select_materials_po_line_warning"))
            return
        record_id = str(selection[0]).strip()
        if not messagebox.askyesno(self.t("warning_title"), self.t("bom_delete_materials_po_line_confirm")):
            return
        self.delete_material_po_row_from_source(record_id)
        self.refresh_materials_po_window()
        self.refresh_bom_module()

    ttk.Button(toolbar, text=self.t("edit"), command=edit_selected).pack(side="left", padx=(8, 0))
    ttk.Button(toolbar, text=self.t("bom_receive_button"), command=receive_selected).pack(side="left", padx=(8, 0))
    ttk.Button(toolbar, text=self.t("delete_button"), command=delete_selected).pack(side="left", padx=(8, 0))
    ttk.Button(toolbar, text=self.t("refresh"), command=self.refresh_materials_po_window).pack(side="left", padx=(8, 0))

    search_row = ttk.Frame(frame)
    search_row.grid(row=1, column=0, sticky="ew", pady=(12, 10))
    ttk.Label(search_row, text=self.t("search")).pack(side="left")
    self.materials_po_search_var = tk.StringVar()
    search_entry = ttk.Entry(search_row, textvariable=self.materials_po_search_var, width=42)
    search_entry.pack(side="left", padx=(8, 0))
    search_entry.bind("<Return>", lambda _event: self.refresh_materials_po_window())

    table_frame = ttk.Frame(frame)
    table_frame.grid(row=2, column=0, sticky="nsew")
    table_frame.columnconfigure(0, weight=1)
    table_frame.rowconfigure(0, weight=1)

    columns = ("mpo_number", "order_date", "eta_date", "supplier_name", "style_code", "item_code", "item_name", "linked_po_numbers", "ordered_qty", "received_qty", "purchase_uom", "status")
    tree = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse", style="Costing.Treeview")
    self.materials_po_tree = tree
    configure_striped_treeview(tree)
    for key, title, width in (
        ("mpo_number", self.t("bom_mpo_no_column"), 110),
        ("order_date", self.t("order_date"), 100),
        ("eta_date", self.t("bom_eta_column"), 100),
        ("supplier_name", self.t("bom_supplier_column"), 160),
        ("style_code", self.t("samples_style_column"), 90),
        ("item_code", self.t("bom_item_code_column"), 110),
        ("item_name", self.t("bom_item_name_column"), 180),
        ("linked_po_numbers", self.t("bom_linked_pos_column"), 180),
        ("ordered_qty", self.t("bom_ordered_column"), 90),
        ("received_qty", self.t("bom_received_column"), 90),
        ("purchase_uom", "UOM", 70),
        ("status", self.t("status"), 90),
    ):
        tree.heading(key, text=title)
        tree.column(key, width=width, anchor="w", stretch=key in {"supplier_name", "item_name", "linked_po_numbers"})
    tree.grid(row=0, column=0, sticky="nsew")
    scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
    scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
    scroll_y.grid(row=0, column=1, sticky="ns")
    scroll_x.grid(row=1, column=0, sticky="ew")
    tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
    self.materials_po_status_var = tk.StringVar(value="Materials PO: 0")
    ttk.Label(frame, textvariable=self.materials_po_status_var, relief="sunken", anchor="w", padding=8).grid(row=3, column=0, sticky="ew", pady=(12, 0))
    tree.bind("<Double-1>", lambda _event: edit_selected())
    self.refresh_materials_po_window()


def open_material_receipt_detail_window(self, row: dict | None = None, material_po_row: dict | None = None) -> None:
    source_row = dict(row) if isinstance(row, dict) else default_material_receipt_payload()
    selected_material_po = dict(material_po_row) if isinstance(material_po_row, dict) else None
    if selected_material_po is None and isinstance(row, dict) and "record_id" in row and "receipt_id" not in row:
        selected_material_po = dict(row)
        source_row = default_material_receipt_payload()
    if selected_material_po is not None:
        source_row.update(
            {
                "po_record_id": str(selected_material_po.get("record_id", "")).strip(),
                "mpo_number": str(selected_material_po.get("mpo_number", "")).strip(),
                "supplier_name": str(selected_material_po.get("supplier_name", "")).strip(),
                "style_code": normalize_style_identifier(selected_material_po.get("style_code")),
                "item_id": str(selected_material_po.get("item_id", "")).strip(),
                "item_code": str(selected_material_po.get("item_code", "")).strip(),
                "item_name": str(selected_material_po.get("item_name", "")).strip(),
                "uom": str(selected_material_po.get("purchase_uom", "")).strip(),
                "warehouse_code": str(source_row.get("warehouse_code", "")).strip() or "RM",
                "receipt_date": str(source_row.get("receipt_date", "")).strip() or datetime.now().strftime("%Y-%m-%d"),
            }
        )
    dialog = tk.Toplevel(self.root)
    dialog.title("Goods Receipt")
    dialog.geometry("980x760")
    dialog.minsize(860, 680)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(1, weight=1)
    ttk.Label(frame, text=dialog.title(), style="Section.TLabel").grid(row=0, column=0, sticky="w")

    form = ttk.Frame(frame)
    form.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
    form.columnconfigure(0, weight=1)
    form.columnconfigure(1, weight=1)
    vars_map = {key: tk.StringVar(value=str(source_row.get(key, "")).strip()) for key in default_material_receipt_payload()}
    note_text = tk.Text(form, height=6, wrap="word")
    note_text.insert("1.0", str(source_row.get("note", "")).strip())
    warehouse_codes = [str(item.get("warehouse_code", "")).strip() for item in self.load_warehouse_rows_from_source() if str(item.get("warehouse_code", "")).strip()]
    material_po_rows = [row for row in self.load_material_po_rows_from_source() if _is_material_po_approved(row.get("status"))]
    po_choices: list[tuple[str, dict]] = []
    for po_row in material_po_rows:
        choice_value = " | ".join(
            [
                str(po_row.get("mpo_number", "")).strip() or "-",
                normalize_style_identifier(po_row.get("style_code")) or "-",
                str(po_row.get("item_code", "")).strip() or "-",
                str(po_row.get("item_name", "")).strip() or "-",
            ]
        )
        po_choices.append((choice_value, dict(po_row)))
    po_choice_map = {label: payload for label, payload in po_choices}
    current_po_choice = next(
        (
            label
            for label, payload in po_choices
            if str(payload.get("record_id", "")).strip() == str(vars_map["po_record_id"].get()).strip()
        ),
        "",
    )
    selected_po_var = tk.StringVar(value=current_po_choice)
    receipt_summary_var = tk.StringVar(value="")
    if selected_material_po is not None and not _is_material_po_approved(selected_material_po.get("status")):
        messagebox.showwarning(self.t("warning_title"), "Only approved Materials PO can be received.")
        dialog.destroy()
        return

    def add_entry(row_index: int, column_index: int, label: str, key: str, *, state: str | None = None) -> None:
        host = ttk.Frame(form)
        host.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        host.columnconfigure(0, weight=1)
        ttk.Label(host, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        entry = ttk.Entry(host, textvariable=vars_map[key])
        if state:
            entry.configure(state=state)
        entry.grid(row=1, column=0, sticky="ew")
        return host, entry

    def refresh_receipt_number() -> None:
        current_receipt_id = str(vars_map["receipt_id"].get()).strip()
        if current_receipt_id and str(source_row.get("receipt_id", "")).strip() == current_receipt_id and str(source_row.get("receipt_number", "")).strip():
            vars_map["receipt_number"].set(str(source_row.get("receipt_number", "")).strip())
            return
        existing_rows = [
            dict(item)
            for item in self.load_material_receipt_rows_from_source()
            if str(item.get("receipt_id", "")).strip() != current_receipt_id
        ]
        vars_map["receipt_number"].set(
            self._generate_auto_material_receipt_number(existing_rows, vars_map["receipt_date"].get().strip())
        )

    _receipt_host, _receipt_entry = add_entry(0, 0, "Receipt No", "receipt_number", state="readonly")
    receipt_date_host = ttk.Frame(form)
    receipt_date_host.grid(row=0, column=1, sticky="ew", padx=8, pady=(0, 12))
    receipt_date_host.columnconfigure(0, weight=1)
    ttk.Label(receipt_date_host, text="Receipt Date").grid(row=0, column=0, sticky="w", pady=(0, 4))
    receipt_date_entry = ttk.Entry(receipt_date_host, textvariable=vars_map["receipt_date"], state="readonly")
    receipt_date_entry.grid(row=1, column=0, sticky="ew")
    self.bind_date_entry_picker(receipt_date_entry, vars_map["receipt_date"])
    mpo_host = ttk.Frame(form)
    mpo_host.grid(row=1, column=0, sticky="ew", padx=8, pady=(0, 12))
    mpo_host.columnconfigure(0, weight=1)
    ttk.Label(mpo_host, text="Materials PO Line").grid(row=0, column=0, sticky="w", pady=(0, 4))
    mpo_combo = ttk.Combobox(mpo_host, textvariable=selected_po_var, values=[label for label, _payload in po_choices], state="readonly")
    mpo_combo.grid(row=1, column=0, sticky="ew")
    warehouse_host = ttk.Frame(form)
    warehouse_host.grid(row=1, column=1, sticky="ew", padx=8, pady=(0, 12))
    warehouse_host.columnconfigure(0, weight=1)
    ttk.Label(warehouse_host, text="Warehouse").grid(row=0, column=0, sticky="w", pady=(0, 4))
    warehouse_combo = ttk.Combobox(warehouse_host, textvariable=vars_map["warehouse_code"], values=warehouse_codes, state="readonly")
    warehouse_combo.grid(row=1, column=0, sticky="ew")
    add_entry(2, 0, "MPO No", "mpo_number")
    add_entry(2, 1, "Supplier", "supplier_name")
    add_entry(3, 0, "Style Code", "style_code")
    add_entry(3, 1, "Item Code", "item_code")
    add_entry(4, 0, "Item Name", "item_name")
    add_entry(4, 1, "Received Qty", "received_qty")
    add_entry(5, 0, "UOM", "uom")
    add_entry(5, 1, "Lot No", "lot_no")
    status_host = ttk.Frame(form)
    status_host.grid(row=6, column=0, sticky="ew", padx=8, pady=(0, 12))
    status_host.columnconfigure(0, weight=1)
    ttk.Label(status_host, text="Status").grid(row=0, column=0, sticky="w", pady=(0, 4))
    status_combo = ttk.Combobox(status_host, textvariable=vars_map["status"], values=MATERIAL_DOCUMENT_STATUS_OPTIONS, state="readonly")
    status_combo.grid(row=1, column=0, sticky="ew")

    note_host = ttk.Frame(form)
    note_host.grid(row=7, column=0, columnspan=2, sticky="nsew", padx=8, pady=(0, 12))
    note_host.columnconfigure(0, weight=1)
    ttk.Label(note_host, text="Note").grid(row=0, column=0, sticky="w", pady=(0, 4))
    note_text.grid(in_=note_host, row=1, column=0, sticky="nsew")

    action_row = ttk.Frame(form)
    action_row.grid(row=8, column=0, columnspan=2, sticky="e", padx=8, pady=(0, 12))

    history_frame = ttk.LabelFrame(form, text="Receipt History For This PO Item")
    history_frame.grid(row=9, column=0, columnspan=2, sticky="nsew", padx=8, pady=(8, 0))
    history_frame.columnconfigure(0, weight=1)
    history_frame.rowconfigure(1, weight=1)
    ttk.Label(history_frame, textvariable=receipt_summary_var, foreground="#5f6b7a").grid(row=0, column=0, sticky="w", pady=(0, 8))
    history_table_frame = ttk.Frame(history_frame)
    history_table_frame.grid(row=1, column=0, sticky="nsew")
    history_table_frame.columnconfigure(0, weight=1)
    history_table_frame.rowconfigure(0, weight=1)
    history_columns = ("receipt_number", "receipt_date", "warehouse_code", "received_qty", "uom", "status")
    history_tree = ttk.Treeview(history_table_frame, columns=history_columns, show="headings", height=8, selectmode="browse", style="Costing.Treeview")
    configure_striped_treeview(history_tree)
    for key, title, width in (
        ("receipt_number", "Receipt No", 140),
        ("receipt_date", "Receipt Date", 120),
        ("warehouse_code", "Warehouse", 110),
        ("received_qty", "Qty", 90),
        ("uom", "UOM", 80),
        ("status", "Status", 90),
    ):
        history_tree.heading(key, text=title)
        history_tree.column(key, width=width, anchor="w", stretch=False)
    history_tree.grid(row=0, column=0, sticky="nsew")
    history_scroll_y = ttk.Scrollbar(history_table_frame, orient="vertical", command=history_tree.yview)
    history_scroll_y.grid(row=0, column=1, sticky="ns")
    history_tree.configure(yscrollcommand=history_scroll_y.set)

    def refresh_receipt_history() -> None:
        history_tree.delete(*history_tree.get_children())
        mpo_number = vars_map["mpo_number"].get().strip()
        item_code = vars_map["item_code"].get().strip()
        history_rows = self.get_material_receipt_history_for_po_item(mpo_number, item_code)
        approved_total = 0.0
        all_total = 0.0
        current_receipt_id = str(vars_map["receipt_id"].get()).strip()
        for index, history_row in enumerate(history_rows, start=1):
            qty_value = _safe_float_value(history_row.get("received_qty"))
            all_total += qty_value
            if _is_material_receipt_approved(history_row.get("status")):
                approved_total += qty_value
            row_values = (
                str(history_row.get("receipt_number", "")).strip(),
                str(history_row.get("receipt_date", "")).strip(),
                str(history_row.get("warehouse_code", "")).strip(),
                _format_metric_text(qty_value),
                str(history_row.get("uom", "")).strip(),
                str(history_row.get("status", "")).strip(),
            )
            history_tree.insert(
                "",
                "end",
                iid=str(history_row.get("receipt_id", "")).strip() or f"history-{index}",
                values=row_values,
                tags=("odd_row" if index % 2 == 1 else "even_row",),
            )
        selected_po = po_choice_map.get(selected_po_var.get().strip(), {})
        ordered_qty = _safe_float_value(selected_po.get("ordered_qty"))
        approved_before_current = approved_total
        if current_receipt_id:
            current_history_row = next((item for item in history_rows if str(item.get("receipt_id", "")).strip() == current_receipt_id), None)
            if current_history_row and _is_material_receipt_approved(current_history_row.get("status")):
                approved_before_current = max(approved_total - _safe_float_value(current_history_row.get("received_qty")), 0.0)
        remaining_qty = max(ordered_qty - approved_before_current, 0.0)
        receipt_summary_var.set(
            " | ".join(
                [
                    f"Ordered: {_format_metric_text(ordered_qty)} {str(selected_po.get('purchase_uom', vars_map['uom'].get())).strip()}".strip(),
                    f"Approved received before this receipt: {_format_metric_text(approved_before_current)}",
                    f"Remaining to receive: {_format_metric_text(remaining_qty)}",
                    f"History rows: {len(history_rows)}",
                ]
            )
        )

    def apply_material_po_selection(_event: object | None = None) -> None:
        selected_po = po_choice_map.get(selected_po_var.get().strip())
        if not isinstance(selected_po, dict):
            refresh_receipt_history()
            return
        vars_map["po_record_id"].set(str(selected_po.get("record_id", "")).strip())
        vars_map["mpo_number"].set(str(selected_po.get("mpo_number", "")).strip())
        vars_map["supplier_name"].set(str(selected_po.get("supplier_name", "")).strip())
        vars_map["style_code"].set(normalize_style_identifier(selected_po.get("style_code")))
        vars_map["item_id"].set(str(selected_po.get("item_id", "")).strip())
        vars_map["item_code"].set(str(selected_po.get("item_code", "")).strip())
        vars_map["item_name"].set(str(selected_po.get("item_name", "")).strip())
        vars_map["uom"].set(str(selected_po.get("purchase_uom", "")).strip())
        if not vars_map["warehouse_code"].get().strip():
            vars_map["warehouse_code"].set("RM")
        refresh_receipt_history()

    mpo_combo.bind("<<ComboboxSelected>>", apply_material_po_selection)
    vars_map["receipt_date"].trace_add("write", lambda *_args: refresh_receipt_number())
    if selected_po_var.get().strip():
        apply_material_po_selection()
    else:
        refresh_receipt_history()
    refresh_receipt_number()

    def save_and_close() -> None:
        payload = {key: variable.get().strip() for key, variable in vars_map.items()}
        payload["note"] = note_text.get("1.0", "end").strip()
        if not payload.get("receipt_date"):
            payload["receipt_date"] = datetime.now().strftime("%Y-%m-%d")
        selected_po = po_choice_map.get(selected_po_var.get().strip(), {})
        current_receipt_id = str(payload.get("receipt_id", "")).strip()
        existing_history = self.get_material_receipt_history_for_po_item(payload.get("mpo_number", ""), payload.get("item_code", ""))
        approved_before_current = 0.0
        for history_row in existing_history:
            if str(history_row.get("receipt_id", "")).strip() == current_receipt_id:
                continue
            if _is_material_receipt_approved(history_row.get("status")):
                approved_before_current += _safe_float_value(history_row.get("received_qty"))
        receipt_qty = _safe_float_value(payload.get("received_qty"))
        ordered_qty = _safe_float_value(selected_po.get("ordered_qty"))
        if ordered_qty > 0 and approved_before_current + receipt_qty > ordered_qty + 1e-9:
            should_continue = messagebox.askyesno(
                self.t("warning_title"),
                "Approved received quantity will exceed ordered quantity for this Materials PO line. Continue saving?",
            )
            if not should_continue:
                return
        saved = self.save_material_receipt_row_to_source(payload)
        if not saved:
            messagebox.showwarning(self.t("warning_title"), "Please fill at least item code or item name.")
            return
        dialog.destroy()
        self.refresh_material_warehouse_window()
        self.refresh_materials_po_window()
        self.refresh_bom_module()

    ttk.Button(action_row, text="Save", command=save_and_close).pack(side="left")
    ttk.Button(action_row, text="Close", command=dialog.destroy).pack(side="left", padx=(8, 0))


def open_material_issue_detail_window(self, row: dict | None = None) -> None:
    source_row = dict(row) if isinstance(row, dict) else default_material_issue_payload()
    dialog = tk.Toplevel(self.root)
    dialog.title("Material Issue")
    dialog.geometry("760x560")
    dialog.minsize(680, 520)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(1, weight=1)
    ttk.Label(frame, text=dialog.title(), style="Section.TLabel").grid(row=0, column=0, sticky="w")

    form = ttk.Frame(frame)
    form.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
    form.columnconfigure(0, weight=1)
    form.columnconfigure(1, weight=1)
    vars_map = {key: tk.StringVar(value=str(source_row.get(key, "")).strip()) for key in default_material_issue_payload()}
    note_text = tk.Text(form, height=6, wrap="word")
    note_text.insert("1.0", str(source_row.get("note", "")).strip())

    def add_entry(row_index: int, column_index: int, label: str, key: str) -> None:
        host = ttk.Frame(form)
        host.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        host.columnconfigure(0, weight=1)
        ttk.Label(host, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        ttk.Entry(host, textvariable=vars_map[key]).grid(row=1, column=0, sticky="ew")

    add_entry(0, 0, "Issue No", "issue_number")
    add_entry(0, 1, "Issue Date", "issue_date")
    add_entry(1, 0, "PO No", "po_number")
    add_entry(1, 1, "Warehouse", "warehouse_code")
    add_entry(2, 0, "Style Code", "style_code")
    add_entry(2, 1, "Item Code", "item_code")
    add_entry(3, 0, "Item Name", "item_name")
    add_entry(3, 1, "Issued Qty", "issued_qty")
    add_entry(4, 0, "UOM", "uom")
    add_entry(4, 1, "Reason", "reason")

    note_host = ttk.Frame(form)
    note_host.grid(row=5, column=0, columnspan=2, sticky="nsew", padx=8, pady=(0, 12))
    note_host.columnconfigure(0, weight=1)
    ttk.Label(note_host, text="Note").grid(row=0, column=0, sticky="w", pady=(0, 4))
    note_text.grid(in_=note_host, row=1, column=0, sticky="nsew")

    action_row = ttk.Frame(frame)
    action_row.grid(row=2, column=0, sticky="e", pady=(12, 0))

    def save_and_close() -> None:
        payload = {key: variable.get().strip() for key, variable in vars_map.items()}
        payload["note"] = note_text.get("1.0", "end").strip()
        saved = self.save_material_issue_row_to_source(payload)
        if not saved:
            messagebox.showwarning(self.t("warning_title"), "Please fill at least item code or item name.")
            return
        dialog.destroy()
        self.refresh_material_warehouse_window()
        self.refresh_bom_module()

    ttk.Button(action_row, text="Save", command=save_and_close).pack(side="left")
    ttk.Button(action_row, text="Close", command=dialog.destroy).pack(side="left", padx=(8, 0))


def open_warehouse_detail_window(self, row: dict | None = None) -> None:
    source_row = dict(row) if isinstance(row, dict) else default_warehouse_payload()
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("bom_warehouse_button"))
    dialog.geometry("620x360")
    dialog.minsize(560, 320)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.columnconfigure(1, weight=1)
    vars_map = {key: tk.StringVar(value=str(source_row.get(key, "")).strip()) for key in default_warehouse_payload()}

    def add_entry(row_index: int, column_index: int, label: str, key: str) -> None:
        host = ttk.Frame(frame)
        host.grid(row=row_index, column=column_index, sticky="ew", padx=8, pady=(0, 12))
        host.columnconfigure(0, weight=1)
        ttk.Label(host, text=label).grid(row=0, column=0, sticky="w", pady=(0, 4))
        ttk.Entry(host, textvariable=vars_map[key]).grid(row=1, column=0, sticky="ew")

    add_entry(0, 0, self.t("bom_warehouse_code_column"), "warehouse_code")
    add_entry(0, 1, self.t("bom_warehouse_name_column"), "warehouse_name")
    add_entry(1, 0, self.t("status"), "status")
    add_entry(1, 1, self.t("bom_note_label"), "note")

    action_row = ttk.Frame(frame)
    action_row.grid(row=2, column=0, columnspan=2, sticky="e", pady=(12, 0))

    def save_and_close() -> None:
        payload = {key: variable.get().strip() for key, variable in vars_map.items()}
        saved = self.save_warehouse_row_to_source(payload)
        if not saved:
            messagebox.showwarning(self.t("warning_title"), self.t("bom_warehouse_required_warning"))
            return
        dialog.destroy()
        self.refresh_material_warehouse_window()

    ttk.Button(action_row, text=self.t("save"), command=save_and_close).pack(side="left")
    ttk.Button(action_row, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(8, 0))


def refresh_material_warehouse_window(self) -> None:
    warehouse_tree = getattr(self, "warehouse_tree", None)
    receipt_tree = getattr(self, "receipt_tree", None)
    issue_tree = getattr(self, "issue_tree", None)
    inventory_tree = getattr(self, "inventory_tree", None)

    warehouse_rows = self.load_warehouse_rows_from_source()
    receipt_rows = self.load_material_receipt_rows_from_source()
    issue_rows = self.load_material_issue_rows_from_source()
    material_po_rows = self.load_material_po_rows_from_source()

    if warehouse_tree is not None:
        warehouse_tree.delete(*warehouse_tree.get_children())
        for index, row in enumerate(warehouse_rows, start=1):
            warehouse_tree.insert(
                "",
                "end",
                iid=str(row.get("warehouse_id", "")),
                values=(row.get("warehouse_code", ""), row.get("warehouse_name", ""), row.get("status", ""), row.get("updated_at", "")),
                tags=("odd_row" if index % 2 == 1 else "even_row",),
            )

    if receipt_tree is not None:
        receipt_tree.delete(*receipt_tree.get_children())
        for index, row in enumerate(sorted(receipt_rows, key=lambda item: (str(item.get("receipt_date", "")).lower(), str(item.get("receipt_number", "")).lower()), reverse=True), start=1):
            receipt_tree.insert(
                "",
                "end",
                iid=str(row.get("receipt_id", "")),
                values=(
                    row.get("receipt_number", ""),
                    row.get("receipt_date", ""),
                    row.get("mpo_number", ""),
                    row.get("warehouse_code", ""),
                    row.get("style_code", ""),
                    row.get("item_code", ""),
                    row.get("item_name", ""),
                    row.get("received_qty", ""),
                    row.get("uom", ""),
                    row.get("lot_no", ""),
                    row.get("status", ""),
                ),
                tags=("odd_row" if index % 2 == 1 else "even_row",),
            )

    if issue_tree is not None:
        issue_tree.delete(*issue_tree.get_children())
        for index, row in enumerate(sorted(issue_rows, key=lambda item: str(item.get("issue_date", "")).lower(), reverse=True), start=1):
            issue_tree.insert(
                "",
                "end",
                iid=str(row.get("issue_id", "")),
                values=(
                    row.get("issue_number", ""),
                    row.get("issue_date", ""),
                    row.get("po_number", ""),
                    row.get("warehouse_code", ""),
                    row.get("style_code", ""),
                    row.get("item_code", ""),
                    row.get("item_name", ""),
                    row.get("issued_qty", ""),
                    row.get("uom", ""),
                    row.get("reason", ""),
                ),
                tags=("odd_row" if index % 2 == 1 else "even_row",),
            )

    if inventory_tree is not None:
        inventory_tree.delete(*inventory_tree.get_children())
        ordered_by_item: dict[tuple[str, str], float] = {}
        received_by_item: dict[tuple[str, str, str], float] = {}
        issued_by_item: dict[tuple[str, str, str], float] = {}
        for row in material_po_rows:
            key = (normalize_style_identifier(row.get("style_code")), str(row.get("item_code", "")).strip())
            ordered_by_item[key] = ordered_by_item.get(key, 0.0) + _safe_float_value(row.get("ordered_qty"))
        approved_receipt_rows = [row for row in receipt_rows if _is_material_receipt_approved(row.get("status"))]
        for row in approved_receipt_rows:
            key = (str(row.get("warehouse_code", "")).strip(), normalize_style_identifier(row.get("style_code")), str(row.get("item_code", "")).strip())
            received_by_item[key] = received_by_item.get(key, 0.0) + _safe_float_value(row.get("received_qty"))
        for row in issue_rows:
            key = (str(row.get("warehouse_code", "")).strip(), normalize_style_identifier(row.get("style_code")), str(row.get("item_code", "")).strip())
            issued_by_item[key] = issued_by_item.get(key, 0.0) + _safe_float_value(row.get("issued_qty"))
        summary_rows: list[dict] = []
        all_keys = sorted(set(received_by_item) | set(issued_by_item))
        for warehouse_code, style_code, item_code in all_keys:
            received_qty = received_by_item.get((warehouse_code, style_code, item_code), 0.0)
            issued_qty = issued_by_item.get((warehouse_code, style_code, item_code), 0.0)
            on_hand_qty = received_qty - issued_qty
            ordered_qty = ordered_by_item.get((style_code, item_code), 0.0)
            summary_rows.append(
                {
                    "warehouse_code": warehouse_code,
                    "style_code": style_code,
                    "item_code": item_code,
                    "ordered_qty": ordered_qty,
                    "received_qty": received_qty,
                    "issued_qty": issued_qty,
                    "on_hand_qty": on_hand_qty,
                }
            )
        for index, row in enumerate(summary_rows, start=1):
            inventory_tree.insert(
                "",
                "end",
                values=(
                    row["warehouse_code"],
                    row["style_code"],
                    row["item_code"],
                    _format_metric_text(row["ordered_qty"]),
                    _format_metric_text(row["received_qty"]),
                    _format_metric_text(row["issued_qty"]),
                    _format_metric_text(row["on_hand_qty"]),
                ),
                tags=("odd_row" if index % 2 == 1 else "even_row",),
            )

    status_var = getattr(self, "material_warehouse_status_var", None)
    if isinstance(status_var, tk.StringVar):
        status_var.set(
            f"Warehouses: {len(warehouse_rows)} | Receipts: {len(receipt_rows)} | Issues: {len(issue_rows)}"
        )


def _build_simple_table(parent: ttk.Frame, columns: list[tuple[str, str, int]]) -> ttk.Treeview:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(0, weight=1)
    tree = ttk.Treeview(parent, columns=[key for key, _title, _width in columns], show="headings", selectmode="browse", style="Costing.Treeview")
    configure_striped_treeview(tree)
    for key, title, width in columns:
        tree.heading(key, text=title)
        tree.column(key, width=width, anchor="w", stretch=True)
    tree.grid(row=0, column=0, sticky="nsew")
    scroll_y = ttk.Scrollbar(parent, orient="vertical", command=tree.yview)
    scroll_x = ttk.Scrollbar(parent, orient="horizontal", command=tree.xview)
    tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
    scroll_y.grid(row=0, column=1, sticky="ns")
    scroll_x.grid(row=1, column=0, sticky="ew")
    return tree


def open_material_warehouse_window(self) -> None:
    existing = getattr(self, "material_warehouse_dialog", None)
    if existing is not None and existing.winfo_exists():
        existing.lift()
        existing.focus_force()
        self.refresh_material_warehouse_window()
        return
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("bom_warehouse_button"))
    dialog.geometry("1320x760")
    dialog.minsize(1180, 680)
    self.material_warehouse_dialog = dialog

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(1, weight=1)

    toolbar = ttk.Frame(frame)
    toolbar.grid(row=0, column=0, sticky="ew")
    ttk.Button(toolbar, text=self.t("refresh"), command=self.refresh_material_warehouse_window).pack(side="left")

    notebook = ttk.Notebook(frame)
    notebook.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
    warehouse_tab = ttk.Frame(notebook, padding=10)
    receipt_tab = ttk.Frame(notebook, padding=10)
    issue_tab = ttk.Frame(notebook, padding=10)
    inventory_tab = ttk.Frame(notebook, padding=10)
    notebook.add(warehouse_tab, text=self.t("bom_warehouses_tab"))
    notebook.add(receipt_tab, text=self.t("bom_goods_receipt_tab"))
    notebook.add(issue_tab, text=self.t("bom_material_issue_tab"))
    notebook.add(inventory_tab, text=self.t("bom_inventory_tab"))

    warehouse_actions = ttk.Frame(warehouse_tab)
    warehouse_actions.grid(row=0, column=0, sticky="ew", pady=(0, 8))
    ttk.Button(warehouse_actions, text=self.t("bom_new_warehouse_button"), command=lambda: self.open_warehouse_detail_window()).pack(side="left")
    ttk.Button(
        warehouse_actions,
        text=self.t("edit"),
        command=lambda: self.open_warehouse_detail_window(
            next(
                (
                    dict(item)
                    for item in self.load_warehouse_rows_from_source()
                    if str(item.get("warehouse_id", "")).strip() == str(self.warehouse_tree.selection()[0]).strip()
                ),
                None,
            )
            if self.warehouse_tree.selection()
            else None
        ),
    ).pack(side="left", padx=(8, 0))
    ttk.Button(
        warehouse_actions,
        text=self.t("delete_button"),
        command=lambda: (
            self.delete_warehouse_row_from_source(str(self.warehouse_tree.selection()[0]).strip()),
            self.refresh_material_warehouse_window(),
        )
        if self.warehouse_tree.selection() and messagebox.askyesno(self.t("warning_title"), self.t("bom_delete_warehouse_confirm"))
        else None,
    ).pack(side="left", padx=(8, 0))
    warehouse_table_frame = ttk.Frame(warehouse_tab)
    warehouse_table_frame.grid(row=1, column=0, sticky="nsew")
    warehouse_tab.columnconfigure(0, weight=1)
    warehouse_tab.rowconfigure(1, weight=1)
    self.warehouse_tree = _build_simple_table(
        warehouse_table_frame,
        [
            ("warehouse_code", self.t("bom_warehouse_code_column"), 120),
            ("warehouse_name", self.t("bom_warehouse_name_column"), 220),
            ("status", self.t("status"), 100),
            ("updated_at", self.t("revised_time_column"), 140),
        ],
    )

    receipt_actions = ttk.Frame(receipt_tab)
    receipt_actions.grid(row=0, column=0, sticky="ew", pady=(0, 8))
    ttk.Button(receipt_actions, text=self.t("bom_new_receipt_button"), command=lambda: self.open_material_receipt_detail_window()).pack(side="left")
    ttk.Button(
        receipt_actions,
        text=self.t("edit"),
        command=lambda: self.open_material_receipt_detail_window(
            next(
                (
                    dict(item)
                    for item in self.load_material_receipt_rows_from_source()
                    if str(item.get("receipt_id", "")).strip() == str(self.receipt_tree.selection()[0]).strip()
                ),
                None,
            )
            if self.receipt_tree.selection()
            else None
        ),
    ).pack(side="left", padx=(8, 0))
    ttk.Button(
        receipt_actions,
        text=self.t("delete_button"),
        command=lambda: (
            self.delete_material_receipt_row_from_source(str(self.receipt_tree.selection()[0]).strip()),
            self.refresh_material_warehouse_window(),
            self.refresh_materials_po_window(),
            self.refresh_bom_module(),
        )
        if self.receipt_tree.selection() and messagebox.askyesno(self.t("warning_title"), self.t("bom_delete_receipt_confirm"))
        else None,
    ).pack(side="left", padx=(8, 0))
    receipt_table_frame = ttk.Frame(receipt_tab)
    receipt_table_frame.grid(row=1, column=0, sticky="nsew")
    receipt_tab.columnconfigure(0, weight=1)
    receipt_tab.rowconfigure(1, weight=1)
    self.receipt_tree = _build_simple_table(
        receipt_table_frame,
        [("receipt_number", self.t("bom_receipt_no_column"), 120), ("receipt_date", self.t("bom_receipt_date_column"), 110), ("mpo_number", self.t("bom_mpo_no_column"), 110), ("warehouse_code", self.t("bom_warehouse_button"), 100), ("style_code", self.t("samples_style_column"), 90), ("item_code", self.t("bom_item_code_column"), 110), ("item_name", self.t("bom_item_name_column"), 180), ("received_qty", self.t("bom_received_qty_column"), 100), ("uom", "UOM", 70), ("lot_no", self.t("bom_lot_no_column"), 110), ("status", self.t("status"), 90)],
    )

    issue_actions = ttk.Frame(issue_tab)
    issue_actions.grid(row=0, column=0, sticky="ew", pady=(0, 8))
    ttk.Button(issue_actions, text=self.t("bom_new_issue_button"), command=lambda: self.open_material_issue_detail_window()).pack(side="left")
    ttk.Button(
        issue_actions,
        text=self.t("edit"),
        command=lambda: self.open_material_issue_detail_window(
            next(
                (
                    dict(item)
                    for item in self.load_material_issue_rows_from_source()
                    if str(item.get("issue_id", "")).strip() == str(self.issue_tree.selection()[0]).strip()
                ),
                None,
            )
            if self.issue_tree.selection()
            else None
        ),
    ).pack(side="left", padx=(8, 0))
    ttk.Button(
        issue_actions,
        text=self.t("delete_button"),
        command=lambda: (
            self.delete_material_issue_row_from_source(str(self.issue_tree.selection()[0]).strip()),
            self.refresh_material_warehouse_window(),
            self.refresh_bom_module(),
        )
        if self.issue_tree.selection() and messagebox.askyesno(self.t("warning_title"), self.t("bom_delete_issue_confirm"))
        else None,
    ).pack(side="left", padx=(8, 0))
    issue_table_frame = ttk.Frame(issue_tab)
    issue_table_frame.grid(row=1, column=0, sticky="nsew")
    issue_tab.columnconfigure(0, weight=1)
    issue_tab.rowconfigure(1, weight=1)
    self.issue_tree = _build_simple_table(
        issue_table_frame,
        [("issue_number", self.t("bom_issue_no_column"), 120), ("issue_date", self.t("bom_issue_date_column"), 110), ("po_number", self.t("bom_po_no_column"), 110), ("warehouse_code", self.t("bom_warehouse_button"), 100), ("style_code", self.t("samples_style_column"), 90), ("item_code", self.t("bom_item_code_column"), 110), ("item_name", self.t("bom_item_name_column"), 180), ("issued_qty", self.t("bom_issued_qty_column"), 100), ("uom", "UOM", 70), ("reason", self.t("bom_reason_column"), 140)],
    )

    inventory_table_frame = ttk.Frame(inventory_tab)
    inventory_table_frame.grid(row=0, column=0, sticky="nsew")
    inventory_tab.columnconfigure(0, weight=1)
    inventory_tab.rowconfigure(0, weight=1)
    self.inventory_tree = _build_simple_table(
        inventory_table_frame,
        [("warehouse_code", self.t("bom_warehouse_button"), 100), ("style_code", self.t("samples_style_column"), 90), ("item_code", self.t("bom_item_code_column"), 110), ("ordered_qty", self.t("bom_ordered_column"), 100), ("received_qty", self.t("bom_received_column"), 100), ("issued_qty", self.t("bom_issued_column"), 100), ("on_hand_qty", self.t("bom_on_hand_qty_column"), 100)],
    )

    self.material_warehouse_status_var = tk.StringVar(value="Warehouses: 0 | Receipts: 0 | Issues: 0")
    ttk.Label(frame, textvariable=self.material_warehouse_status_var, relief="sunken", anchor="w", padding=8).grid(row=2, column=0, sticky="ew", pady=(12, 0))
    self.refresh_material_warehouse_window()


def bind_bom_module(app_cls: type) -> None:
    method_map = {
        "default_bom_item_payload": staticmethod(default_bom_item_payload),
        "normalize_bom_item_payload": staticmethod(normalize_bom_item_payload),
        "sync_tech_pack_consumption_with_bom_status": sync_tech_pack_consumption_with_bom_status,
        "_load_store_rows": _load_store_rows,
        "_save_store_rows": _save_store_rows,
        "default_material_po_payload": staticmethod(default_material_po_payload),
        "normalize_material_po_row": staticmethod(normalize_material_po_row),
        "default_material_receipt_payload": staticmethod(default_material_receipt_payload),
        "normalize_material_receipt_row": staticmethod(normalize_material_receipt_row),
        "default_material_issue_payload": staticmethod(default_material_issue_payload),
        "normalize_material_issue_row": staticmethod(normalize_material_issue_row),
        "default_warehouse_payload": staticmethod(default_warehouse_payload),
        "normalize_warehouse_row": staticmethod(normalize_warehouse_row),
        "get_bom_trim_size_options": get_bom_trim_size_options,
        "_tech_pack_bom_item_needs_color_split": _tech_pack_bom_item_needs_color_split,
        "_build_tech_pack_bom_item_variants": _build_tech_pack_bom_item_variants,
        "_find_bom_consumption_detail": _find_bom_consumption_detail,
        "_find_bom_consumption_by_version": _find_bom_consumption_by_version,
        "summarize_bom_purchase_order_requirements": summarize_bom_purchase_order_requirements,
        "summarize_bom_purchase_order_requirements_from_source": summarize_bom_purchase_order_requirements_from_source,
        "_trace_bom_requirement_debug": _trace_bom_requirement_debug,
        "enrich_bom_requirement_rows": enrich_bom_requirement_rows,
        "sanitize_bom_requirement_rows": sanitize_bom_requirement_rows,
        "load_material_po_rows_from_source": load_material_po_rows_from_source,
        "save_material_po_row_to_source": save_material_po_row_to_source,
        "delete_material_po_row_from_source": delete_material_po_row_from_source,
        "_generate_auto_material_po_number": _generate_auto_material_po_number,
        "_generate_auto_material_receipt_number": _generate_auto_material_receipt_number,
        "auto_generate_material_po_from_booking_shortage": auto_generate_material_po_from_booking_shortage,
        "load_material_receipt_rows_from_source": load_material_receipt_rows_from_source,
        "save_material_receipt_row_to_source": save_material_receipt_row_to_source,
        "delete_material_receipt_row_from_source": delete_material_receipt_row_from_source,
        "summarize_material_receipts_by_po_item": summarize_material_receipts_by_po_item,
        "get_material_receipt_history_for_po_item": get_material_receipt_history_for_po_item,
        "load_material_issue_rows_from_source": load_material_issue_rows_from_source,
        "save_material_issue_row_to_source": save_material_issue_row_to_source,
        "delete_material_issue_row_from_source": delete_material_issue_row_from_source,
        "load_warehouse_rows_from_source": load_warehouse_rows_from_source,
        "save_warehouse_row_to_source": save_warehouse_row_to_source,
        "delete_warehouse_row_from_source": delete_warehouse_row_from_source,
        "load_bom_items_from_source": load_bom_items_from_source,
        "save_bom_item_to_source": save_bom_item_to_source,
        "delete_bom_item_from_source": delete_bom_item_from_source,
        "_build_bom_module": _build_bom_module,
        "_load_enriched_bom_rows_for_refresh": _load_enriched_bom_rows_for_refresh,
        "_apply_bom_refreshed_rows": _apply_bom_refreshed_rows,
        "refresh_bom_module": refresh_bom_module,
        "update_bom_filter_input_state": update_bom_filter_input_state,
        "apply_bom_filter": apply_bom_filter,
        "bom_row_matches_filter": bom_row_matches_filter,
        "get_active_bom_filters": get_active_bom_filters,
        "add_bom_filter_row": add_bom_filter_row,
        "remove_bom_filter_row": remove_bom_filter_row,
        "clear_bom_filters": clear_bom_filters,
        "commit_bom_search_query": commit_bom_search_query,
        "on_bom_search_submit": on_bom_search_submit,
        "schedule_bom_filter_apply": schedule_bom_filter_apply,
        "run_bom_filter_apply": run_bom_filter_apply,
        "sync_bom_search_query": sync_bom_search_query,
        "refresh_bom_tree": refresh_bom_tree,
        "update_bom_module_status": update_bom_module_status,
        "get_selected_bom_item_id": get_selected_bom_item_id,
        "get_selected_bom_item": get_selected_bom_item,
        "get_target_bom_status_rows": get_target_bom_status_rows,
        "quick_update_bom_status": quick_update_bom_status,
        "start_quick_bom_status_worker": start_quick_bom_status_worker,
        "finish_quick_bom_status_worker": finish_quick_bom_status_worker,
        "on_bom_heading_click": on_bom_heading_click,
        "update_bom_tree_check_cell": update_bom_tree_check_cell,
        "on_bom_tree_click": on_bom_tree_click,
        "on_bom_item_selected": on_bom_item_selected,
        "on_bom_item_double_click": on_bom_item_double_click,
        "translate_bom_sort_key": translate_bom_sort_key,
        "translate_bom_filter_field": translate_bom_filter_field,
        "on_bom_sort_key_changed": on_bom_sort_key_changed,
        "on_bom_filter_field_changed": on_bom_filter_field_changed,
        "on_bom_sort_order_changed": on_bom_sort_order_changed,
        "open_new_bom_item_dialog": open_new_bom_item_dialog,
        "edit_selected_bom_item": edit_selected_bom_item,
        "copy_selected_bom_item": copy_selected_bom_item,
        "delete_selected_bom_item": delete_selected_bom_item,
        "start_delete_bom_items_worker": start_delete_bom_items_worker,
        "finish_delete_bom_items_worker": finish_delete_bom_items_worker,
        "open_bom_report_builder": open_bom_report_builder,
        "open_bom_item_detail_window": open_bom_item_detail_window,
        "get_selected_or_prompt_bom_tech_pack_styles": get_selected_or_prompt_bom_tech_pack_styles,
        "build_bom_items_from_tech_pack_style": build_bom_items_from_tech_pack_style,
        "generate_bom_from_tech_pack": generate_bom_from_tech_pack,
        "build_bom_items_from_fds_data": build_bom_items_from_fds_data,
        "open_import_fds_menu": open_import_fds_menu,
        "import_fds_single_file": import_fds_single_file,
        "import_fds_folder": import_fds_folder,
        "import_fds_files": import_fds_files,
        "parse_bulk_fabric_workbook": parse_bulk_fabric_workbook,
        "build_bom_items_from_bulk_fabric_rows": build_bom_items_from_bulk_fabric_rows,
        "sync_bulk_fabric_items_to_tech_pack_production": sync_bulk_fabric_items_to_tech_pack_production,
        "open_import_bulk_fabric_menu": open_import_bulk_fabric_menu,
        "import_bulk_fabric_single_file": import_bulk_fabric_single_file,
        "import_bulk_fabric_folder": import_bulk_fabric_folder,
        "start_bulk_fabric_import_worker": start_bulk_fabric_import_worker,
        "finish_bulk_fabric_import_worker": finish_bulk_fabric_import_worker,
        "parse_bulk_trims_workbook": parse_bulk_trims_workbook,
        "build_bom_items_from_bulk_trims_rows": build_bom_items_from_bulk_trims_rows,
        "open_import_bulk_trims_menu": open_import_bulk_trims_menu,
        "import_bulk_trims_single_file": import_bulk_trims_single_file,
        "import_bulk_trims_folder": import_bulk_trims_folder,
        "start_bulk_trims_import_worker": start_bulk_trims_import_worker,
        "finish_bulk_trims_import_worker": finish_bulk_trims_import_worker,
        "open_material_po_detail_window": open_material_po_detail_window,
        "refresh_materials_po_window": refresh_materials_po_window,
        "open_materials_po_window": open_materials_po_window,
        "open_material_receipt_detail_window": open_material_receipt_detail_window,
        "open_material_issue_detail_window": open_material_issue_detail_window,
        "open_warehouse_detail_window": open_warehouse_detail_window,
        "refresh_material_warehouse_window": refresh_material_warehouse_window,
        "open_material_warehouse_window": open_material_warehouse_window,
    }
    for method_name, function in method_map.items():
        setattr(app_cls, method_name, function)
