fix(silo): pull assembly dependencies on all open paths

Add _pull_dependencies() that fetches BOM children and downloads
missing files to canonical paths before an assembly document is
opened, so PropertyXLink references resolve correctly.

- Create _pull_dependencies(): iterates BOM, fetches canonical item
  description via get_item(), downloads latest file revision for each
  missing child
- Modify open_item(): detect assembly item_type and call
  _pull_dependencies() before opening
- Silo_Open: prefer open_item() when part_number is available so
  assemblies opened from search results also pull dependencies
- Silo_Pull: pull dependencies with progress dialog after main file
  download completes, before reopening the document

Closes #337
This commit is contained in:
2026-02-26 12:50:34 -06:00
parent 27e112e7da
commit cc6a79f1b1

View File

@@ -1048,6 +1048,59 @@ def get_tracked_object(doc):
return None
def _pull_dependencies(part_number: str) -> None:
"""Download BOM children so PropertyXLink references resolve on open.
For each child in the BOM that is not already present locally, fetch
the item metadata (for canonical description) and download the latest
file revision to the canonical path.
"""
try:
bom = _client.get_bom(part_number)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not fetch BOM for {part_number}: {e}\n")
return
if not bom:
return
for entry in bom:
child_pn = entry.get("child_part_number", "")
if not child_pn:
continue
# Check if we already have the file locally
existing = find_file_by_part_number(child_pn)
if existing and existing.exists():
continue
# Get canonical item description for the filename
try:
child_item = _client.get_item(child_pn)
child_desc = child_item.get("description", "")
except Exception:
child_desc = entry.get("child_description", "")
dest_path = get_cad_file_path(child_pn, child_desc)
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Find latest revision with a file
rev = _client.latest_file_revision(child_pn)
if not rev:
FreeCAD.Console.PrintWarning(f"No file revision for dependency {child_pn}, skipping.\n")
continue
rev_num = rev["revision_number"]
try:
ok = _client._download_file(child_pn, rev_num, str(dest_path))
if ok:
FreeCAD.Console.PrintMessage(f"Pulled dependency {child_pn} rev {rev_num}\n")
else:
FreeCAD.Console.PrintWarning(f"Failed to download dependency {child_pn}\n")
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error downloading dependency {child_pn}: {e}\n")
class SiloSync:
"""Handles synchronization between FreeCAD and Silo."""
@@ -1165,14 +1218,31 @@ class SiloSync:
return doc
def open_item(self, part_number: str):
"""Open or create item document."""
"""Open or create item document.
For assemblies, BOM dependencies are pulled first so that
PropertyXLink references resolve when the document loads.
"""
# Fetch item metadata to check item_type
try:
item = self.client.get_item(part_number)
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to fetch item: {e}\n")
item = {}
# Pull assembly dependencies before opening so links resolve
if item.get("item_type") == "assembly":
_pull_dependencies(part_number)
existing_path = find_file_by_part_number(part_number)
if existing_path and existing_path.exists():
return FreeCAD.openDocument(str(existing_path))
if not item:
return None
try:
item = self.client.get_item(part_number)
return self.create_document_for_item(item, save=True)
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to open: {e}\n")
@@ -1365,10 +1435,10 @@ class Silo_Open:
# inside the dialog's nested event loop, which can cause crashes.
data = _open_after_close[0]
if data is not None:
if data.get("path"):
FreeCAD.openDocument(data["path"])
else:
if data.get("part_number"):
_sync.open_item(data["part_number"])
elif data.get("path"):
FreeCAD.openDocument(data["path"])
def IsActive(self):
return True
@@ -1908,6 +1978,18 @@ class Silo_Pull:
FreeCAD.Console.PrintMessage(f"Pulled revision {rev_num} of {part_number}\n")
# Pull assembly dependencies before reopening
if item.get("item_type") == "assembly":
dep_progress = QtGui.QProgressDialog(
f"Pulling dependencies for {part_number}...", None, 0, 0
)
dep_progress.setWindowModality(2) # Qt.WindowModal
dep_progress.setMinimumDuration(0)
dep_progress.setValue(0)
QtGui.QApplication.processEvents()
_pull_dependencies(part_number)
dep_progress.close()
# Close existing document if open, then reopen
if doc and doc.FileName == str(dest_path):
FreeCAD.closeDocument(doc.Name)