Balloon quantities — BOM QTY check#

The effective quantity a balloon designates is not “one balloon = one part”: it is multiplicator × repetition × effective_leader_count (each None1), and the drawing total for a tag is the sum over its balloons. drawing_tools exposes all three attributes on every Balloon. This demo verifies that the drawing’s effective quantity per tag agrees with the BOM QTY column — BOM quantities drive procurement and kitting, so drift starves or inflates the assembly line.

Input drawing#

Sheet 1 of support_assembly.json.

How the check works#

Accumulate true_qty per tag over all balloons, then compare against the BOM QTY column.

src/drawing_tools/demo_scenarios/balloon_quantities/minimal_script.py#
"""Demo: BOM vs Balloon Quantity verification.

Computes each tag's effective drawing quantity
(``multiplicator * repetition * effective_leader_count`` summed over balloons)
and compares it against the BOM ``QTY`` column, then writes (into the current
working directory):
  - ``balloon_quantities_report.md`` — Markdown verification report
  - ``bom_vs_balloon_qty.html``      — annotated drawing (green=OK, red=MISMATCH)
"""

from pathlib import Path

from drawing_tools.sheet.featured_sheet import FeaturedSheet

from ..shared import read_bom_qty_by_tag, write_report
from ..viz_helpers import load_sheet, render_html
from . import build_visualization
from .build_visualization import BOM_XLSX, DRAWING_JSON, OUT_HTML

#: relative → written to the current working directory.
REPORT_MD = Path("balloon_quantities_report.md")


def compare(sheet: FeaturedSheet, expected_by_tag: dict[str, int]) -> list[tuple[str, int, int | None, str]]:
    """Return sorted (tag, bom_qty, drawing_qty, status) for every BOM tag.

    A BOM tag with no balloon on the sheet gets status ``MISSING`` (drawing qty
    ``None``) — conceptually distinct from a quantity ``MISMATCH``.
    """
    drawing_qty = build_visualization.drawing_qty_by_tag(sheet)
    rows = []
    for tag, expected in expected_by_tag.items():
        actual = drawing_qty.get(tag)
        if actual is None:
            status = "MISSING"
        elif actual == expected:
            status = "OK"
        else:
            status = "MISMATCH"
        rows.append((tag, expected, actual, status))
    rows.sort(key=lambda row: row[0])
    return rows


def build_report_md(rows: list[tuple[str, int, int | None, str]]) -> str:
    """Render the Markdown metrics + per-tag table for the quantity report."""
    total = len(rows)
    ok = sum(row[3] == "OK" for row in rows)
    mismatch = sum(row[3] == "MISMATCH" for row in rows)
    missing = sum(row[3] == "MISSING" for row in rows)
    rate = 100 * ok / total if total else 0.0
    metrics = (
        "| Metric | Value |\n|--------|-------|\n"
        f"| Total checked | {total} |\n| OK | {ok} |\n| MISMATCH | {mismatch} |\n| MISSING | {missing} |\n"
        f"| **Pass rate** | **{rate:.1f}%** |"
    )
    header = "| | TAG | BOM QTY | Drawing QTY | Status |\n|---|-----|---------|-------------|--------|"
    body = "\n".join(
        f"| {'🟢' if status == 'OK' else '🔴'} | {tag} | {expected} | {actual if actual is not None else '—'} | {status} |"
        for tag, expected, actual, status in rows
    )
    return "\n\n".join([metrics, header + "\n" + body])


def main() -> None:
    """Run the quantity check, write the report and the visualization."""
    sheet = load_sheet(DRAWING_JSON, sheet_index=1)
    expected_by_tag = read_bom_qty_by_tag(BOM_XLSX)
    rows = compare(sheet, expected_by_tag)

    for tag, expected, actual, status in rows:
        print(f"TAG {tag}: BOM={expected} drawing={actual if actual is not None else '—'} -> {status}")
    ok = sum(row[3] == "OK" for row in rows)
    mismatch = sum(row[3] == "MISMATCH" for row in rows)
    missing = sum(row[3] == "MISSING" for row in rows)
    rate = 100 * ok / len(rows) if rows else 0.0
    print(f"\n{len(rows)} checked | {ok} OK | {mismatch} MISMATCH | {missing} MISSING | pass rate {rate:.1f}%")

    write_report(REPORT_MD, "Quantity Verification Report", [build_report_md(rows)])

    render_html(sheet, build_visualization.build_extras(sheet, expected_by_tag), OUT_HTML)


if __name__ == "__main__":
    main()

Result#

Metric

Value

Total checked

9

OK

7

MISMATCH

2

Pass rate

77.8%

Mismatches: 102 (BOM 3, drawing 2) and 401 (BOM 1, drawing 2) — both exercise repetition=2 with a single leader. Tags 104/400/402 exercise effective_leader_count=2 × repetition=2 → 4 (OK). The visualization boxes each balloon green/red and labels the BOM-expected quantity under each mismatch.