Sync InvenTree part links to Bambuddy archives
This commit is contained in:
@@ -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 URL. Use root URL or /api URL; the service normalizes it.
|
||||||
INVENTREE_BASE_URL=http://host.docker.internal:1337
|
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
|
INVENTREE_TOKEN=replace-with-inventree-token
|
||||||
|
|
||||||
# Existing InvenTree IDs where printed parts should be created/stored.
|
# Existing InvenTree IDs where printed parts should be created/stored.
|
||||||
@@ -22,6 +24,8 @@ WEBHOOK_SHARED_SECRET=
|
|||||||
SYNC_SUCCESS_ONLY=true
|
SYNC_SUCCESS_ONLY=true
|
||||||
SYNC_PART_IMAGES=true
|
SYNC_PART_IMAGES=true
|
||||||
OVERWRITE_PART_IMAGES=false
|
OVERWRITE_PART_IMAGES=false
|
||||||
|
SYNC_ARCHIVE_EXTERNAL_LINK=true
|
||||||
|
OVERWRITE_ARCHIVE_EXTERNAL_LINK=false
|
||||||
DEFAULT_STOCK_QUANTITY=1
|
DEFAULT_STOCK_QUANTITY=1
|
||||||
INVENTREE_STOCK_STATUS=10
|
INVENTREE_STOCK_STATUS=10
|
||||||
PART_IPN_PREFIX=BMB
|
PART_IPN_PREFIX=BMB
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -117,6 +117,22 @@ OVERWRITE_PART_IMAGES=false
|
|||||||
|
|
||||||
Set `OVERWRITE_PART_IMAGES=true` if Bambuddy thumbnails should replace existing part images.
|
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
|
||||||
|
<INVENTREE_WEB_URL or INVENTREE_BASE_URL>/web/part/<part_id>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing non-InvenTree external links are preserved unless:
|
||||||
|
|
||||||
|
```env
|
||||||
|
OVERWRITE_ARCHIVE_EXTERNAL_LINK=true
|
||||||
|
```
|
||||||
|
|
||||||
## Useful Endpoints
|
## Useful Endpoints
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ class BambuddyClient:
|
|||||||
filename=f"bambuddy-archive-{archive_id}-thumbnail.{extension}",
|
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(
|
async def list_archives(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Settings(BaseSettings):
|
|||||||
bambuddy_api_key: str
|
bambuddy_api_key: str
|
||||||
|
|
||||||
inventree_base_url: str
|
inventree_base_url: str
|
||||||
|
inventree_web_url: str | None = None
|
||||||
inventree_token: str
|
inventree_token: str
|
||||||
inventree_part_category_id: int
|
inventree_part_category_id: int
|
||||||
inventree_stock_location_id: int
|
inventree_stock_location_id: int
|
||||||
@@ -23,6 +24,8 @@ class Settings(BaseSettings):
|
|||||||
sync_success_only: bool = True
|
sync_success_only: bool = True
|
||||||
sync_part_images: bool = True
|
sync_part_images: bool = True
|
||||||
overwrite_part_images: bool = False
|
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
|
default_stock_quantity: Annotated[float, Field(gt=0)] = 1
|
||||||
inventree_stock_status: int = 10
|
inventree_stock_status: int = 10
|
||||||
part_ipn_prefix: str = "BMB"
|
part_ipn_prefix: str = "BMB"
|
||||||
@@ -33,9 +36,11 @@ class Settings(BaseSettings):
|
|||||||
http_timeout_seconds: Annotated[int, Field(ge=1)] = 30
|
http_timeout_seconds: Annotated[int, Field(ge=1)] = 30
|
||||||
data_dir: Path = Path("/data")
|
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
|
@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("/")
|
return value.rstrip("/")
|
||||||
|
|
||||||
@field_validator("part_ipn_prefix")
|
@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()]
|
fields = [field.strip() for field in self.part_key_fields.split(",") if field.strip()]
|
||||||
return fields or ["filename", "name"]
|
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
|
@lru_cache
|
||||||
def get_settings() -> Settings:
|
def get_settings() -> Settings:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Archive(BaseModel):
|
|||||||
quantity: float | None = None
|
quantity: float | None = None
|
||||||
object_count: int | None = None
|
object_count: int | None = None
|
||||||
cost: float | None = None
|
cost: float | None = None
|
||||||
|
external_url: str | None = None
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
tags: list[str] | None = None
|
tags: list[str] | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ class ArchiveSyncService:
|
|||||||
part_id = await self._get_or_create_part(part_key=part_key, ipn=ipn, name=name, description=description)
|
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_parameters(part_id=part_id, archive=archive)
|
||||||
await self._sync_part_image(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)
|
batch = self.inventree.batch_for_archive(archive.id)
|
||||||
existing_stock = await self.inventree.find_stock_by_batch(part_id=part_id, batch=batch)
|
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)
|
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]]:
|
def part_parameters_for_archive(self, archive: Archive) -> list[dict[str, str | float | None]]:
|
||||||
parameters: list[dict[str, str | float | None]] = []
|
parameters: list[dict[str, str | float | None]] = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user