Files
silo/pkg/calc/pythonpath/silo_calc/sheet_format.py
Zoe Forbes 36a8d9995d feat: LibreOffice Calc extension, ODS library, AI description, audit design
Calc extension (pkg/calc/):
- Python UNO ProtocolHandler with 8 toolbar commands
- SiloClient HTTP client adapted from FreeCAD workbench
- Pull BOM/Project: populates sheets with 28-col format, hidden property
  columns, row hash tracking, auto project tagging
- Push: row classification, create/update items, conflict detection
- Completion wizard: 3-step category/description/fields with PN conflict
  resolution dialog
- OpenRouter AI integration: generate standardized descriptions from seller
  text, configurable model/instructions, review dialog
- Settings: JSON persistence, env var fallbacks, OpenRouter fields
- 31 unit tests (no UNO/network required)

Go ODS library (internal/ods/):
- Pure Go ODS read/write (ZIP of XML, no headless LibreOffice)
- Writer, reader, 10 round-trip tests

Server ODS endpoints (internal/api/ods.go):
- GET /api/items/export.ods, template.ods, POST import.ods
- GET /api/items/{pn}/bom/export.ods
- GET /api/projects/{code}/sheet.ods
- POST /api/sheets/diff

Documentation:
- docs/CALC_EXTENSION.md: extension progress report
- docs/COMPONENT_AUDIT.md: web audit tool design with weighted scoring,
  assembly computed fields, batch AI assistance plan
2026-02-01 10:06:20 -06:00

179 lines
5.3 KiB
Python

"""BOM and Items sheet column layouts, constants, and detection helpers.
This module defines the column structure that matches the engineer's working
BOM format. Hidden property columns and sync-tracking columns are appended
to the right.
"""
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Column indices -- BOM sheet
# ---------------------------------------------------------------------------
# Visible core columns (always shown)
BOM_VISIBLE_HEADERS: List[str] = [
"Item", # A - assembly label / section header
"Level", # B - depth in expanded BOM
"Source", # C - sourcing_type (M/P)
"PN", # D - part_number
"Description", # E - item description
"Seller Description", # F - metadata.seller_description
"Unit Cost", # G - standard_cost / metadata.unit_cost
"QTY", # H - quantity on relationship
"Ext Cost", # I - formula =G*H
"Sourcing Link", # J - sourcing_link
"Schema", # K - schema name
]
# Hidden property columns (collapsed group, available when needed)
BOM_PROPERTY_HEADERS: List[str] = [
"Manufacturer", # L
"Manufacturer PN", # M
"Supplier", # N
"Supplier PN", # O
"Lead Time (days)", # P
"Min Order Qty", # Q
"Lifecycle Status", # R
"RoHS Compliant", # S
"Country of Origin", # T
"Material", # U
"Finish", # V
"Notes", # W
"Long Description", # X
]
# Hidden sync columns (never shown to user)
BOM_SYNC_HEADERS: List[str] = [
"_silo_row_hash", # Y - SHA256 of row data at pull time
"_silo_row_status", # Z - synced/modified/new/error
"_silo_updated_at", # AA - server timestamp
"_silo_parent_pn", # AB - parent assembly PN for this BOM entry
]
# All headers in order
BOM_ALL_HEADERS: List[str] = (
BOM_VISIBLE_HEADERS + BOM_PROPERTY_HEADERS + BOM_SYNC_HEADERS
)
# Index constants for quick access
COL_ITEM = 0
COL_LEVEL = 1
COL_SOURCE = 2
COL_PN = 3
COL_DESCRIPTION = 4
COL_SELLER_DESC = 5
COL_UNIT_COST = 6
COL_QTY = 7
COL_EXT_COST = 8
COL_SOURCING_LINK = 9
COL_SCHEMA = 10
# Property column range
COL_PROP_START = len(BOM_VISIBLE_HEADERS) # 11
COL_PROP_END = COL_PROP_START + len(BOM_PROPERTY_HEADERS) # 24
# Sync column range
COL_SYNC_START = COL_PROP_END # 24
COL_ROW_HASH = COL_SYNC_START # 24
COL_ROW_STATUS = COL_SYNC_START + 1 # 25
COL_UPDATED_AT = COL_SYNC_START + 2 # 26
COL_PARENT_PN = COL_SYNC_START + 3 # 27
# Total column count
BOM_TOTAL_COLS = len(BOM_ALL_HEADERS)
# ---------------------------------------------------------------------------
# Items sheet columns (flat list of all items for a project)
# ---------------------------------------------------------------------------
ITEMS_HEADERS: List[str] = [
"PN",
"Description",
"Type",
"Source",
"Schema",
"Standard Cost",
"Sourcing Link",
"Long Description",
"Manufacturer",
"Manufacturer PN",
"Supplier",
"Supplier PN",
"Lead Time (days)",
"Min Order Qty",
"Lifecycle Status",
"RoHS Compliant",
"Country of Origin",
"Material",
"Finish",
"Notes",
"Created",
"Updated",
]
# ---------------------------------------------------------------------------
# Property key mapping (header name -> DB field path)
# ---------------------------------------------------------------------------
PROPERTY_KEY_MAP: Dict[str, str] = {
"Manufacturer": "manufacturer",
"Manufacturer PN": "manufacturer_pn",
"Supplier": "supplier",
"Supplier PN": "supplier_pn",
"Lead Time (days)": "lead_time_days",
"Min Order Qty": "minimum_order_qty",
"Lifecycle Status": "lifecycle_status",
"RoHS Compliant": "rohs_compliant",
"Country of Origin": "country_of_origin",
"Material": "material",
"Finish": "finish",
"Notes": "notes",
}
# Reverse map
DB_FIELD_TO_HEADER: Dict[str, str] = {v: k for k, v in PROPERTY_KEY_MAP.items()}
# ---------------------------------------------------------------------------
# Row status colours (RGB tuples, 0-255)
# ---------------------------------------------------------------------------
STATUS_COLORS: Dict[str, Tuple[int, int, int]] = {
"synced": (198, 239, 206), # light green #C6EFCE
"modified": (255, 235, 156), # light yellow #FFEB9C
"new": (189, 215, 238), # light blue #BDD7EE
"error": (255, 199, 206), # light red #FFC7CE
"conflict": (244, 176, 132), # orange #F4B084
}
# ---------------------------------------------------------------------------
# Sheet type detection
# ---------------------------------------------------------------------------
def detect_sheet_type(headers: List[str]) -> Optional[str]:
"""Detect sheet type from the first row of headers.
Returns ``"bom"``, ``"items"``, or ``None`` if unrecognised.
"""
if not headers:
return None
# Normalise for comparison
norm = [h.strip().lower() for h in headers]
if "item" in norm and "level" in norm and "qty" in norm:
return "bom"
if "pn" in norm and "type" in norm:
return "items"
return None
def col_letter(index: int) -> str:
"""Convert 0-based column index to spreadsheet letter (A, B, ..., AA, AB)."""
result = ""
while True:
result = chr(65 + index % 26) + result
index = index // 26 - 1
if index < 0:
break
return result