diff --git a/.env.example b/.env.example index a46415f..c2bd038 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ BAMBUDDY_API_KEY=replace-with-bambuddy-api-key # InvenTree URL. Use root URL or /api URL; the service normalizes it. INVENTREE_BASE_URL=http://host.docker.internal:1337 +# Optional public/browser URL for InvenTree. Defaults to INVENTREE_BASE_URL. +INVENTREE_WEB_URL= INVENTREE_TOKEN=replace-with-inventree-token # Existing InvenTree IDs where printed parts should be created/stored. @@ -22,6 +24,8 @@ WEBHOOK_SHARED_SECRET= SYNC_SUCCESS_ONLY=true SYNC_PART_IMAGES=true OVERWRITE_PART_IMAGES=false +SYNC_ARCHIVE_EXTERNAL_LINK=true +OVERWRITE_ARCHIVE_EXTERNAL_LINK=false DEFAULT_STOCK_QUANTITY=1 INVENTREE_STOCK_STATUS=10 PART_IPN_PREFIX=BMB diff --git a/README.md b/README.md index 2c5014c..1136b1a 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,22 @@ OVERWRITE_PART_IMAGES=false Set `OVERWRITE_PART_IMAGES=true` if Bambuddy thumbnails should replace existing part images. +## Bambuddy External Links + +When `SYNC_ARCHIVE_EXTERNAL_LINK=true`, the service writes the InvenTree part page URL into Bambuddy archive `external_url`. + +The default link format is: + +```text +/web/part// +``` + +Existing non-InvenTree external links are preserved unless: + +```env +OVERWRITE_ARCHIVE_EXTERNAL_LINK=true +``` + ## Useful Endpoints ```text diff --git a/src/bambuddy_inventree_sync/bambuddy.py b/src/bambuddy_inventree_sync/bambuddy.py index 0f9b248..3ba6f57 100644 --- a/src/bambuddy_inventree_sync/bambuddy.py +++ b/src/bambuddy_inventree_sync/bambuddy.py @@ -54,6 +54,10 @@ class BambuddyClient: filename=f"bambuddy-archive-{archive_id}-thumbnail.{extension}", ) + async def update_archive_external_url(self, archive_id: int, external_url: str) -> Archive: + data = await self._request("PATCH", f"/archives/{archive_id}", json={"external_url": external_url}) + return Archive.model_validate(data) + async def list_archives( self, *, diff --git a/src/bambuddy_inventree_sync/config.py b/src/bambuddy_inventree_sync/config.py index a2820ae..cd674b3 100644 --- a/src/bambuddy_inventree_sync/config.py +++ b/src/bambuddy_inventree_sync/config.py @@ -13,6 +13,7 @@ class Settings(BaseSettings): bambuddy_api_key: str inventree_base_url: str + inventree_web_url: str | None = None inventree_token: str inventree_part_category_id: int inventree_stock_location_id: int @@ -23,6 +24,8 @@ class Settings(BaseSettings): sync_success_only: bool = True sync_part_images: bool = True overwrite_part_images: bool = False + sync_archive_external_link: bool = True + overwrite_archive_external_link: bool = False default_stock_quantity: Annotated[float, Field(gt=0)] = 1 inventree_stock_status: int = 10 part_ipn_prefix: str = "BMB" @@ -33,9 +36,11 @@ class Settings(BaseSettings): http_timeout_seconds: Annotated[int, Field(ge=1)] = 30 data_dir: Path = Path("/data") - @field_validator("bambuddy_base_url", "inventree_base_url") + @field_validator("bambuddy_base_url", "inventree_base_url", "inventree_web_url") @classmethod - def strip_url(cls, value: str) -> str: + def strip_url(cls, value: str | None) -> str | None: + if value is None or not value.strip(): + return None return value.rstrip("/") @field_validator("part_ipn_prefix") @@ -53,6 +58,13 @@ class Settings(BaseSettings): fields = [field.strip() for field in self.part_key_fields.split(",") if field.strip()] return fields or ["filename", "name"] + @property + def inventree_browser_url(self) -> str: + base_url = (self.inventree_web_url or self.inventree_base_url).rstrip("/") + if base_url.endswith("/api"): + return base_url[:-4] + return base_url + @lru_cache def get_settings() -> Settings: diff --git a/src/bambuddy_inventree_sync/models.py b/src/bambuddy_inventree_sync/models.py index b497aad..800d827 100644 --- a/src/bambuddy_inventree_sync/models.py +++ b/src/bambuddy_inventree_sync/models.py @@ -27,6 +27,7 @@ class Archive(BaseModel): quantity: float | None = None object_count: int | None = None cost: float | None = None + external_url: str | None = None notes: str | None = None tags: list[str] | None = None diff --git a/src/bambuddy_inventree_sync/sync.py b/src/bambuddy_inventree_sync/sync.py index ee41e58..54d6a7c 100644 --- a/src/bambuddy_inventree_sync/sync.py +++ b/src/bambuddy_inventree_sync/sync.py @@ -133,6 +133,7 @@ class ArchiveSyncService: part_id = await self._get_or_create_part(part_key=part_key, ipn=ipn, name=name, description=description) await self._sync_part_parameters(part_id=part_id, archive=archive) await self._sync_part_image(part_id=part_id, archive=archive) + await self._sync_archive_external_link(part_id=part_id, archive=archive) batch = self.inventree.batch_for_archive(archive.id) existing_stock = await self.inventree.find_stock_by_batch(part_id=part_id, batch=batch) @@ -228,6 +229,23 @@ class ArchiveSyncService: ) logger.info("Synced Bambuddy archive %s thumbnail to InvenTree part %s", archive.id, part_id) + async def _sync_archive_external_link(self, *, part_id: int, archive: Archive) -> None: + if not self.settings.sync_archive_external_link: + return + + part_url = self.inventree_part_url(part_id) + if archive.external_url and not self.settings.overwrite_archive_external_link: + if archive.external_url == part_url: + return + if not archive.external_url.startswith(self.settings.inventree_browser_url): + return + + await self.bambuddy.update_archive_external_url(archive.id, part_url) + logger.info("Synced InvenTree part %s link to Bambuddy archive %s", part_id, archive.id) + + def inventree_part_url(self, part_id: int) -> str: + return f"{self.settings.inventree_browser_url}/web/part/{part_id}/" + def part_parameters_for_archive(self, archive: Archive) -> list[dict[str, str | float | None]]: parameters: list[dict[str, str | float | None]] = []