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
668 lines
21 KiB
Python
668 lines
21 KiB
Python
"""UNO dialogs for the Silo Calc extension.
|
|
|
|
Provides login, settings, push summary, and PN conflict resolution dialogs.
|
|
All dialogs use the UNO dialog toolkit (``com.sun.star.awt``).
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
# UNO imports are only available inside LibreOffice
|
|
try:
|
|
import uno
|
|
|
|
_HAS_UNO = True
|
|
except ImportError:
|
|
_HAS_UNO = False
|
|
|
|
from . import settings as _settings
|
|
from .client import SiloClient
|
|
|
|
|
|
def _get_desktop():
|
|
"""Return the XSCRIPTCONTEXT desktop, or resolve via component context."""
|
|
ctx = uno.getComponentContext()
|
|
smgr = ctx.ServiceManager
|
|
return smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
|
|
|
|
|
|
def _msgbox(parent, title: str, message: str, box_type="infobox"):
|
|
"""Show a simple message box."""
|
|
if not _HAS_UNO:
|
|
return
|
|
ctx = uno.getComponentContext()
|
|
smgr = ctx.ServiceManager
|
|
toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
|
|
if parent is None:
|
|
parent = _get_desktop().getCurrentFrame().getContainerWindow()
|
|
mbt = uno.Enum(
|
|
"com.sun.star.awt.MessageBoxType",
|
|
"INFOBOX" if box_type == "infobox" else "ERRORBOX",
|
|
)
|
|
msg_box = toolkit.createMessageBox(parent, mbt, 1, title, message)
|
|
msg_box.execute()
|
|
|
|
|
|
def _input_box(
|
|
title: str, label: str, default: str = "", password: bool = False
|
|
) -> Optional[str]:
|
|
"""Show a simple single-field input dialog. Returns None on cancel."""
|
|
if not _HAS_UNO:
|
|
return None
|
|
ctx = uno.getComponentContext()
|
|
smgr = ctx.ServiceManager
|
|
dlg_provider = smgr.createInstanceWithContext(
|
|
"com.sun.star.awt.DialogProvider", ctx
|
|
)
|
|
|
|
# Build dialog model programmatically
|
|
dlg_model = smgr.createInstanceWithContext(
|
|
"com.sun.star.awt.UnoControlDialogModel", ctx
|
|
)
|
|
dlg_model.Width = 220
|
|
dlg_model.Height = 80
|
|
dlg_model.Title = title
|
|
|
|
# Label
|
|
lbl = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl.Name = "lbl"
|
|
lbl.PositionX = 10
|
|
lbl.PositionY = 10
|
|
lbl.Width = 200
|
|
lbl.Height = 12
|
|
lbl.Label = label
|
|
dlg_model.insertByName("lbl", lbl)
|
|
|
|
# Text field
|
|
tf = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf.Name = "tf"
|
|
tf.PositionX = 10
|
|
tf.PositionY = 24
|
|
tf.Width = 200
|
|
tf.Height = 14
|
|
tf.Text = default
|
|
if password:
|
|
tf.EchoChar = ord("*")
|
|
dlg_model.insertByName("tf", tf)
|
|
|
|
# OK button
|
|
btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_ok.Name = "btn_ok"
|
|
btn_ok.PositionX = 110
|
|
btn_ok.PositionY = 50
|
|
btn_ok.Width = 45
|
|
btn_ok.Height = 16
|
|
btn_ok.Label = "OK"
|
|
btn_ok.PushButtonType = 1 # OK
|
|
dlg_model.insertByName("btn_ok", btn_ok)
|
|
|
|
# Cancel button
|
|
btn_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_cancel.Name = "btn_cancel"
|
|
btn_cancel.PositionX = 160
|
|
btn_cancel.PositionY = 50
|
|
btn_cancel.Width = 45
|
|
btn_cancel.Height = 16
|
|
btn_cancel.Label = "Cancel"
|
|
btn_cancel.PushButtonType = 2 # CANCEL
|
|
dlg_model.insertByName("btn_cancel", btn_cancel)
|
|
|
|
# Create dialog control
|
|
dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx)
|
|
dlg.setModel(dlg_model)
|
|
dlg.setVisible(False)
|
|
toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
|
|
dlg.createPeer(toolkit, None)
|
|
|
|
result = dlg.execute()
|
|
if result == 1: # OK
|
|
text = dlg.getControl("tf").getText()
|
|
dlg.dispose()
|
|
return text
|
|
dlg.dispose()
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Login dialog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def show_login_dialog(parent=None) -> bool:
|
|
"""Two-step login: username then password. Returns True on success."""
|
|
username = _input_box("Silo Login", "Username:")
|
|
if not username:
|
|
return False
|
|
password = _input_box("Silo Login", f"Password for {username}:", password=True)
|
|
if not password:
|
|
return False
|
|
|
|
client = SiloClient()
|
|
try:
|
|
result = client.login(username, password)
|
|
_msgbox(
|
|
parent,
|
|
"Silo Login",
|
|
f"Logged in as {result['username']} ({result.get('role', 'viewer')})",
|
|
)
|
|
return True
|
|
except RuntimeError as e:
|
|
_msgbox(parent, "Silo Login Failed", str(e), box_type="errorbox")
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Settings dialog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def show_settings_dialog(parent=None) -> bool:
|
|
"""Show the settings dialog. Returns True if saved."""
|
|
if not _HAS_UNO:
|
|
return False
|
|
|
|
ctx = uno.getComponentContext()
|
|
smgr = ctx.ServiceManager
|
|
cfg = _settings.load()
|
|
|
|
dlg_model = smgr.createInstanceWithContext(
|
|
"com.sun.star.awt.UnoControlDialogModel", ctx
|
|
)
|
|
dlg_model.Width = 300
|
|
dlg_model.Height = 200
|
|
dlg_model.Title = "Silo Settings"
|
|
|
|
fields = [
|
|
("API URL", "api_url", cfg.get("api_url", "")),
|
|
("API Token", "api_token", cfg.get("api_token", "")),
|
|
("SSL Cert Path", "ssl_cert_path", cfg.get("ssl_cert_path", "")),
|
|
("Projects Dir", "projects_dir", cfg.get("projects_dir", "")),
|
|
("Default Schema", "default_schema", cfg.get("default_schema", "kindred-rd")),
|
|
]
|
|
|
|
y = 10
|
|
for label_text, name, default in fields:
|
|
lbl = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl.Name = f"lbl_{name}"
|
|
lbl.PositionX = 10
|
|
lbl.PositionY = y
|
|
lbl.Width = 80
|
|
lbl.Height = 12
|
|
lbl.Label = label_text
|
|
dlg_model.insertByName(f"lbl_{name}", lbl)
|
|
|
|
tf = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf.Name = f"tf_{name}"
|
|
tf.PositionX = 95
|
|
tf.PositionY = y
|
|
tf.Width = 195
|
|
tf.Height = 14
|
|
tf.Text = default
|
|
dlg_model.insertByName(f"tf_{name}", tf)
|
|
y += 22
|
|
|
|
# SSL verify checkbox
|
|
cb = dlg_model.createInstance("com.sun.star.awt.UnoControlCheckBoxModel")
|
|
cb.Name = "cb_ssl_verify"
|
|
cb.PositionX = 95
|
|
cb.PositionY = y
|
|
cb.Width = 120
|
|
cb.Height = 14
|
|
cb.Label = "Verify SSL"
|
|
cb.State = 1 if cfg.get("ssl_verify", True) else 0
|
|
dlg_model.insertByName("cb_ssl_verify", cb)
|
|
y += 22
|
|
|
|
# --- OpenRouter AI section ---
|
|
lbl_ai = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_ai.Name = "lbl_ai_section"
|
|
lbl_ai.PositionX = 10
|
|
lbl_ai.PositionY = y
|
|
lbl_ai.Width = 280
|
|
lbl_ai.Height = 12
|
|
lbl_ai.Label = "--- OpenRouter AI ---"
|
|
dlg_model.insertByName("lbl_ai_section", lbl_ai)
|
|
y += 16
|
|
|
|
# API Key (masked)
|
|
lbl_key = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_key.Name = "lbl_openrouter_api_key"
|
|
lbl_key.PositionX = 10
|
|
lbl_key.PositionY = y
|
|
lbl_key.Width = 80
|
|
lbl_key.Height = 12
|
|
lbl_key.Label = "API Key"
|
|
dlg_model.insertByName("lbl_openrouter_api_key", lbl_key)
|
|
|
|
tf_key = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf_key.Name = "tf_openrouter_api_key"
|
|
tf_key.PositionX = 95
|
|
tf_key.PositionY = y
|
|
tf_key.Width = 195
|
|
tf_key.Height = 14
|
|
tf_key.Text = cfg.get("openrouter_api_key", "")
|
|
tf_key.EchoChar = ord("*")
|
|
dlg_model.insertByName("tf_openrouter_api_key", tf_key)
|
|
y += 22
|
|
|
|
# AI Model
|
|
lbl_model = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_model.Name = "lbl_openrouter_model"
|
|
lbl_model.PositionX = 10
|
|
lbl_model.PositionY = y
|
|
lbl_model.Width = 80
|
|
lbl_model.Height = 12
|
|
lbl_model.Label = "AI Model"
|
|
dlg_model.insertByName("lbl_openrouter_model", lbl_model)
|
|
|
|
tf_model = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf_model.Name = "tf_openrouter_model"
|
|
tf_model.PositionX = 95
|
|
tf_model.PositionY = y
|
|
tf_model.Width = 195
|
|
tf_model.Height = 14
|
|
tf_model.Text = cfg.get("openrouter_model", "")
|
|
tf_model.HelpText = "openai/gpt-4.1-nano"
|
|
dlg_model.insertByName("tf_openrouter_model", tf_model)
|
|
y += 22
|
|
|
|
# AI Instructions (multi-line)
|
|
lbl_instr = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_instr.Name = "lbl_openrouter_instructions"
|
|
lbl_instr.PositionX = 10
|
|
lbl_instr.PositionY = y
|
|
lbl_instr.Width = 80
|
|
lbl_instr.Height = 12
|
|
lbl_instr.Label = "AI Instructions"
|
|
dlg_model.insertByName("lbl_openrouter_instructions", lbl_instr)
|
|
|
|
tf_instr = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf_instr.Name = "tf_openrouter_instructions"
|
|
tf_instr.PositionX = 95
|
|
tf_instr.PositionY = y
|
|
tf_instr.Width = 195
|
|
tf_instr.Height = 56
|
|
tf_instr.Text = cfg.get("openrouter_instructions", "")
|
|
tf_instr.MultiLine = True
|
|
tf_instr.VScroll = True
|
|
tf_instr.HelpText = "Custom system prompt (leave blank for default)"
|
|
dlg_model.insertByName("tf_openrouter_instructions", tf_instr)
|
|
y += 62
|
|
|
|
# Test connection button
|
|
btn_test = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_test.Name = "btn_test"
|
|
btn_test.PositionX = 10
|
|
btn_test.PositionY = y
|
|
btn_test.Width = 80
|
|
btn_test.Height = 16
|
|
btn_test.Label = "Test Connection"
|
|
dlg_model.insertByName("btn_test", btn_test)
|
|
|
|
# Status label
|
|
lbl_status = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_status.Name = "lbl_status"
|
|
lbl_status.PositionX = 95
|
|
lbl_status.PositionY = y + 2
|
|
lbl_status.Width = 195
|
|
lbl_status.Height = 12
|
|
lbl_status.Label = ""
|
|
dlg_model.insertByName("lbl_status", lbl_status)
|
|
y += 22
|
|
|
|
# OK / Cancel
|
|
btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_ok.Name = "btn_ok"
|
|
btn_ok.PositionX = 190
|
|
btn_ok.PositionY = y
|
|
btn_ok.Width = 45
|
|
btn_ok.Height = 16
|
|
btn_ok.Label = "Save"
|
|
btn_ok.PushButtonType = 1
|
|
dlg_model.insertByName("btn_ok", btn_ok)
|
|
|
|
btn_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_cancel.Name = "btn_cancel"
|
|
btn_cancel.PositionX = 240
|
|
btn_cancel.PositionY = y
|
|
btn_cancel.Width = 45
|
|
btn_cancel.Height = 16
|
|
btn_cancel.Label = "Cancel"
|
|
btn_cancel.PushButtonType = 2
|
|
dlg_model.insertByName("btn_cancel", btn_cancel)
|
|
|
|
dlg_model.Height = y + 26
|
|
|
|
dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx)
|
|
dlg.setModel(dlg_model)
|
|
dlg.setVisible(False)
|
|
toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
|
|
dlg.createPeer(toolkit, None)
|
|
|
|
result = dlg.execute()
|
|
if result == 1:
|
|
for _, name, _ in fields:
|
|
cfg[name] = dlg.getControl(f"tf_{name}").getText()
|
|
cfg["ssl_verify"] = bool(dlg.getControl("cb_ssl_verify").getModel().State)
|
|
cfg["openrouter_api_key"] = dlg.getControl("tf_openrouter_api_key").getText()
|
|
cfg["openrouter_model"] = dlg.getControl("tf_openrouter_model").getText()
|
|
cfg["openrouter_instructions"] = dlg.getControl(
|
|
"tf_openrouter_instructions"
|
|
).getText()
|
|
_settings.save(cfg)
|
|
dlg.dispose()
|
|
return True
|
|
|
|
dlg.dispose()
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Push summary dialog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def show_push_summary(
|
|
new_count: int,
|
|
modified_count: int,
|
|
conflict_count: int,
|
|
unchanged_count: int,
|
|
parent=None,
|
|
) -> bool:
|
|
"""Show push summary and return True if user confirms."""
|
|
lines = [
|
|
f"New items: {new_count}",
|
|
f"Modified items: {modified_count}",
|
|
f"Conflicts: {conflict_count}",
|
|
f"Unchanged: {unchanged_count}",
|
|
]
|
|
if conflict_count:
|
|
lines.append("\nConflicts must be resolved before pushing.")
|
|
|
|
msg = "\n".join(lines)
|
|
if conflict_count:
|
|
_msgbox(parent, "Silo Push -- Conflicts Found", msg, box_type="errorbox")
|
|
return False
|
|
|
|
if new_count == 0 and modified_count == 0:
|
|
_msgbox(parent, "Silo Push", "Nothing to push -- all rows are up to date.")
|
|
return False
|
|
|
|
# Confirmation -- for now use a simple info box (OK = proceed)
|
|
_msgbox(parent, "Silo Push", f"Ready to push:\n\n{msg}\n\nProceed?")
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PN Conflict Resolution dialog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Return values
|
|
PN_USE_EXISTING = "use_existing"
|
|
PN_CREATE_NEW = "create_new"
|
|
PN_CANCEL = "cancel"
|
|
|
|
|
|
def show_pn_conflict_dialog(
|
|
part_number: str,
|
|
existing_item: Dict[str, Any],
|
|
parent=None,
|
|
) -> str:
|
|
"""Show PN conflict dialog when a manually entered PN already exists.
|
|
|
|
Returns one of: PN_USE_EXISTING, PN_CREATE_NEW, PN_CANCEL.
|
|
"""
|
|
if not _HAS_UNO:
|
|
return PN_CANCEL
|
|
|
|
ctx = uno.getComponentContext()
|
|
smgr = ctx.ServiceManager
|
|
|
|
dlg_model = smgr.createInstanceWithContext(
|
|
"com.sun.star.awt.UnoControlDialogModel", ctx
|
|
)
|
|
dlg_model.Width = 320
|
|
dlg_model.Height = 220
|
|
dlg_model.Title = f"Part Number Conflict: {part_number}"
|
|
|
|
y = 10
|
|
info_lines = [
|
|
"This part number already exists in Silo:",
|
|
"",
|
|
f" Description: {existing_item.get('description', '')}",
|
|
f" Type: {existing_item.get('item_type', '')}",
|
|
f" Category: {existing_item.get('part_number', '')[:3]}",
|
|
f" Sourcing: {existing_item.get('sourcing_type', '')}",
|
|
f" Cost: ${existing_item.get('standard_cost', 0):.2f}",
|
|
]
|
|
|
|
for line in info_lines:
|
|
lbl = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl.Name = f"info_{y}"
|
|
lbl.PositionX = 10
|
|
lbl.PositionY = y
|
|
lbl.Width = 300
|
|
lbl.Height = 12
|
|
lbl.Label = line
|
|
dlg_model.insertByName(f"info_{y}", lbl)
|
|
y += 13
|
|
|
|
y += 5
|
|
|
|
# Radio buttons
|
|
rb_use = dlg_model.createInstance("com.sun.star.awt.UnoControlRadioButtonModel")
|
|
rb_use.Name = "rb_use"
|
|
rb_use.PositionX = 20
|
|
rb_use.PositionY = y
|
|
rb_use.Width = 280
|
|
rb_use.Height = 14
|
|
rb_use.Label = "Use existing item (add to BOM)"
|
|
rb_use.State = 1 # selected by default
|
|
dlg_model.insertByName("rb_use", rb_use)
|
|
y += 18
|
|
|
|
rb_new = dlg_model.createInstance("com.sun.star.awt.UnoControlRadioButtonModel")
|
|
rb_new.Name = "rb_new"
|
|
rb_new.PositionX = 20
|
|
rb_new.PositionY = y
|
|
rb_new.Width = 280
|
|
rb_new.Height = 14
|
|
rb_new.Label = "Create new item (auto-generate PN)"
|
|
rb_new.State = 0
|
|
dlg_model.insertByName("rb_new", rb_new)
|
|
y += 18
|
|
|
|
rb_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlRadioButtonModel")
|
|
rb_cancel.Name = "rb_cancel"
|
|
rb_cancel.PositionX = 20
|
|
rb_cancel.PositionY = y
|
|
rb_cancel.Width = 280
|
|
rb_cancel.Height = 14
|
|
rb_cancel.Label = "Cancel"
|
|
rb_cancel.State = 0
|
|
dlg_model.insertByName("rb_cancel", rb_cancel)
|
|
y += 25
|
|
|
|
# OK button
|
|
btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_ok.Name = "btn_ok"
|
|
btn_ok.PositionX = 210
|
|
btn_ok.PositionY = y
|
|
btn_ok.Width = 45
|
|
btn_ok.Height = 16
|
|
btn_ok.Label = "OK"
|
|
btn_ok.PushButtonType = 1
|
|
dlg_model.insertByName("btn_ok", btn_ok)
|
|
|
|
btn_cancel_btn = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_cancel_btn.Name = "btn_cancel_btn"
|
|
btn_cancel_btn.PositionX = 260
|
|
btn_cancel_btn.PositionY = y
|
|
btn_cancel_btn.Width = 45
|
|
btn_cancel_btn.Height = 16
|
|
btn_cancel_btn.Label = "Cancel"
|
|
btn_cancel_btn.PushButtonType = 2
|
|
dlg_model.insertByName("btn_cancel_btn", btn_cancel_btn)
|
|
|
|
dlg_model.Height = y + 26
|
|
|
|
dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx)
|
|
dlg.setModel(dlg_model)
|
|
dlg.setVisible(False)
|
|
toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
|
|
dlg.createPeer(toolkit, None)
|
|
|
|
result = dlg.execute()
|
|
if result != 1:
|
|
dlg.dispose()
|
|
return PN_CANCEL
|
|
|
|
if dlg.getControl("rb_use").getModel().State:
|
|
dlg.dispose()
|
|
return PN_USE_EXISTING
|
|
if dlg.getControl("rb_new").getModel().State:
|
|
dlg.dispose()
|
|
return PN_CREATE_NEW
|
|
|
|
dlg.dispose()
|
|
return PN_CANCEL
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AI Description review dialog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def show_ai_description_dialog(
|
|
seller_description: str, ai_description: str, parent=None
|
|
) -> Optional[str]:
|
|
"""Show AI-generated description for review/editing.
|
|
|
|
Side-by-side layout: seller description (read-only) on the left,
|
|
AI-generated description (editable) on the right.
|
|
|
|
Returns the accepted/edited description text, or None on cancel.
|
|
"""
|
|
if not _HAS_UNO:
|
|
return None
|
|
|
|
ctx = uno.getComponentContext()
|
|
smgr = ctx.ServiceManager
|
|
|
|
dlg_model = smgr.createInstanceWithContext(
|
|
"com.sun.star.awt.UnoControlDialogModel", ctx
|
|
)
|
|
dlg_model.Width = 400
|
|
dlg_model.Height = 210
|
|
dlg_model.Title = "AI Description Review"
|
|
|
|
# Left: Seller Description (read-only)
|
|
lbl_seller = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_seller.Name = "lbl_seller"
|
|
lbl_seller.PositionX = 10
|
|
lbl_seller.PositionY = 8
|
|
lbl_seller.Width = 185
|
|
lbl_seller.Height = 12
|
|
lbl_seller.Label = "Seller Description"
|
|
dlg_model.insertByName("lbl_seller", lbl_seller)
|
|
|
|
tf_seller = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf_seller.Name = "tf_seller"
|
|
tf_seller.PositionX = 10
|
|
tf_seller.PositionY = 22
|
|
tf_seller.Width = 185
|
|
tf_seller.Height = 140
|
|
tf_seller.Text = seller_description
|
|
tf_seller.MultiLine = True
|
|
tf_seller.VScroll = True
|
|
tf_seller.ReadOnly = True
|
|
dlg_model.insertByName("tf_seller", tf_seller)
|
|
|
|
# Right: Generated Description (editable)
|
|
lbl_gen = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel")
|
|
lbl_gen.Name = "lbl_gen"
|
|
lbl_gen.PositionX = 205
|
|
lbl_gen.PositionY = 8
|
|
lbl_gen.Width = 185
|
|
lbl_gen.Height = 12
|
|
lbl_gen.Label = "Generated Description (editable)"
|
|
dlg_model.insertByName("lbl_gen", lbl_gen)
|
|
|
|
tf_gen = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel")
|
|
tf_gen.Name = "tf_gen"
|
|
tf_gen.PositionX = 205
|
|
tf_gen.PositionY = 22
|
|
tf_gen.Width = 185
|
|
tf_gen.Height = 140
|
|
tf_gen.Text = ai_description
|
|
tf_gen.MultiLine = True
|
|
tf_gen.VScroll = True
|
|
dlg_model.insertByName("tf_gen", tf_gen)
|
|
|
|
# Accept button
|
|
btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_ok.Name = "btn_ok"
|
|
btn_ok.PositionX = 290
|
|
btn_ok.PositionY = 175
|
|
btn_ok.Width = 50
|
|
btn_ok.Height = 18
|
|
btn_ok.Label = "Accept"
|
|
btn_ok.PushButtonType = 1 # OK
|
|
dlg_model.insertByName("btn_ok", btn_ok)
|
|
|
|
# Cancel button
|
|
btn_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel")
|
|
btn_cancel.Name = "btn_cancel"
|
|
btn_cancel.PositionX = 345
|
|
btn_cancel.PositionY = 175
|
|
btn_cancel.Width = 45
|
|
btn_cancel.Height = 18
|
|
btn_cancel.Label = "Cancel"
|
|
btn_cancel.PushButtonType = 2 # CANCEL
|
|
dlg_model.insertByName("btn_cancel", btn_cancel)
|
|
|
|
dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx)
|
|
dlg.setModel(dlg_model)
|
|
dlg.setVisible(False)
|
|
toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
|
|
dlg.createPeer(toolkit, None)
|
|
|
|
result = dlg.execute()
|
|
if result == 1: # OK / Accept
|
|
text = dlg.getControl("tf_gen").getText()
|
|
dlg.dispose()
|
|
return text
|
|
|
|
dlg.dispose()
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Assembly / Project picker dialogs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def show_assembly_picker(client: SiloClient, parent=None) -> Optional[str]:
|
|
"""Show a dialog to pick an assembly by PN. Returns the PN or None."""
|
|
pn = _input_box("Pull BOM", "Assembly part number (e.g. A01-0003):")
|
|
return pn if pn and pn.strip() else None
|
|
|
|
|
|
def show_project_picker(client: SiloClient, parent=None) -> Optional[str]:
|
|
"""Show a dialog to pick a project code. Returns the code or None."""
|
|
try:
|
|
projects = client.get_projects()
|
|
except RuntimeError:
|
|
projects = []
|
|
|
|
if not projects:
|
|
code = _input_box("Pull Project", "Project code:")
|
|
return code if code and code.strip() else None
|
|
|
|
# Build a choice list
|
|
choices = [f"{p.get('code', '')} - {p.get('name', '')}" for p in projects]
|
|
# For simplicity, use an input box with hint. A proper list picker
|
|
# would use a ListBox control, but this is functional for now.
|
|
hint = "Available: " + ", ".join(p.get("code", "") for p in projects)
|
|
code = _input_box("Pull Project", f"Project code ({hint}):")
|
|
return code if code and code.strip() else None
|