LibreOffice Calc extension for Silo PLM integration. Uses shared silo-client package (submodule) for API communication. Changes from monorepo version: - SiloClient class removed from client.py, replaced with CalcSiloSettings adapter + factory function wrapping silo_client.SiloClient - silo_calc_component.py adds silo-client to sys.path - Makefile build-oxt copies silo_client into .oxt for self-contained packaging - All other modules unchanged
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
|