diff --git a/docs/examples/inventory.md b/docs/examples/inventory.md new file mode 100644 index 00000000..af17fffb --- /dev/null +++ b/docs/examples/inventory.md @@ -0,0 +1,37 @@ +# Inventory + +Albert Inventory serves as a digital manifestation of your physical inventory. It enables you to sort, filter, trace, and manage all types of inventory. + +## Inventory function on CAS + +Inventory function is a business-controlled, multi-select list on the Inventory ↔ CAS relationship. +Use it by updating an existing inventory item. + +!!! example "Add inventory function values to a CAS entry" + ```python + from albert import Albert + from albert.resources.lists import ListItem, ListItemCategory + + client = Albert.from_client_credentials() + + inventory_id = "INV123" + cas_id = "CAS123" + + # Optional: create a new inventoryFunction list item first. + list_item = ListItem( + name="Primary Function", + category=ListItemCategory.INVENTORY, + list_type="inventoryFunction", + ) + list_item = client.lists.create(list_item=list_item) + + inventory_item = client.inventory.get_by_id(id=inventory_id) + if inventory_item.cas: + for cas_amount in inventory_item.cas: + if cas_amount.id == cas_id: + cas_amount.inventory_function = [list_item] + break + + updated_item = client.inventory.update(inventory_item=inventory_item) + print(updated_item.id) + ``` diff --git a/mkdocs.yml b/mkdocs.yml index 78ce5e02..fab0923e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -221,6 +221,7 @@ nav: - Worksheets: resources/worksheets.md - Sheets: resources/sheets.md - Examples: + - Inventory: examples/inventory.md - Property Data: examples/property_data.md - Tasks: examples/tasks.md - Notebooks: examples/notebook.md diff --git a/src/albert/__init__.py b/src/albert/__init__.py index 280296be..90c2d47f 100644 --- a/src/albert/__init__.py +++ b/src/albert/__init__.py @@ -4,4 +4,4 @@ __all__ = ["Albert", "AlbertClientCredentials", "AlbertSSOClient"] -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/src/albert/resources/inventory.py b/src/albert/resources/inventory.py index 33ea5003..5fa3290d 100644 --- a/src/albert/resources/inventory.py +++ b/src/albert/resources/inventory.py @@ -14,6 +14,7 @@ from albert.resources.acls import ACL from albert.resources.cas import Cas from albert.resources.companies import Company +from albert.resources.lists import ListItem from albert.resources.locations import Location from albert.resources.tagged_base import BaseTaggedResource from albert.resources.tags import Tag @@ -75,6 +76,8 @@ class CasAmount(BaseAlbertModel): The SMILES string of the CAS Number resource. Obtained from the Cas object when provided. number: str | None The CAS number. Obtained from the Cas object when provided. + inventory_function: list[ListItem | EntityLink | str] | None + Business-controlled functions associated with the CAS in this inventory context. !!! tip --- @@ -87,6 +90,9 @@ class CasAmount(BaseAlbertModel): target: float | None = Field(default=None, alias="inventoryValue") id: str | None = Field(default=None) cas_category: str | None = Field(default=None, alias="casCategory") + inventory_function: list[SerializeAsEntityLink[ListItem] | str] | None = Field( + default=None, alias="inventoryFunction" + ) type: str | None = Field(default=None) classification_type: str | None = Field(default=None, alias="classificationType") diff --git a/src/albert/resources/lists.py b/src/albert/resources/lists.py index d3ff6cf7..27c1054b 100644 --- a/src/albert/resources/lists.py +++ b/src/albert/resources/lists.py @@ -25,9 +25,11 @@ class ListItem(BaseResource): id : str | None The Albert ID of the list item. Set when the list item is retrieved from Albert. category : ListItemCategory | None - The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, and `extensions`. + The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, `extensions`, + and `inventory`. list_type : str | None - The type of the list item. Allowed values are `projectState` for `projects` and `extensions` for `extensions`. + The type of the list item. Allowed values are `projectState` for `projects`, `extensions` for `extensions`, + and `casCategory` or `inventoryFunction` for `inventory`. """ name: str @@ -37,14 +39,15 @@ class ListItem(BaseResource): @model_validator(mode="after") def validate_list_type(self) -> ListItem: + allowed_by_category = { + ListItemCategory.PROJECTS: {"projectState"}, + ListItemCategory.EXTENSIONS: {"extensions"}, + ListItemCategory.INVENTORY: {"casCategory", "inventoryFunction"}, + } if ( - self.category == ListItemCategory.PROJECTS - and self.list_type is not None - and self.list_type != "projectState" - ) or ( - self.category == ListItemCategory.EXTENSIONS - and self.list_type is not None - and self.list_type != "extensions" + self.list_type is not None + and self.category in allowed_by_category + and self.list_type not in allowed_by_category[self.category] ): raise ValueError( f"List type {self.list_type} is not allowed for category {self.category}" diff --git a/src/albert/utils/inventory.py b/src/albert/utils/inventory.py index 651c7d1e..af6e14ef 100644 --- a/src/albert/utils/inventory.py +++ b/src/albert/utils/inventory.py @@ -1,6 +1,7 @@ from collections.abc import Iterable from typing import Any +from albert.core.shared.models.base import BaseResource, EntityLink from albert.resources.inventory import CasAmount @@ -56,6 +57,61 @@ def _build_cas_delete_operation(identifier: str) -> dict[str, Any]: } +def _normalize_inventory_function_ids( + value: list[BaseResource | EntityLink | str] | None, +) -> list[str]: + if not value: + return [] + ids: list[str] = [] + for item in value: + if isinstance(item, str): + if item: + ids.append(item) + continue + if isinstance(item, BaseResource): + if item.id: + ids.append(item.id) + continue + if isinstance(item, EntityLink): + if item.id: + ids.append(item.id) + continue + return ids + + +def _build_inventory_function_operations( + *, + entity_id: str, + existing: list[BaseResource | EntityLink | str] | None, + updated: list[BaseResource | EntityLink | str] | None, +) -> list[dict[str, Any]]: + existing_ids = set(_normalize_inventory_function_ids(existing)) + updated_ids = set(_normalize_inventory_function_ids(updated)) + to_add = sorted(updated_ids - existing_ids) + to_delete = sorted(existing_ids - updated_ids) + + operations: list[dict[str, Any]] = [] + if to_add: + operations.append( + { + "attribute": "inventoryFunction", + "entityId": entity_id, + "operation": "add", + "newValue": to_add, + } + ) + if to_delete: + operations.append( + { + "attribute": "inventoryFunction", + "entityId": entity_id, + "operation": "delete", + "oldValue": to_delete, + } + ) + return operations + + def _build_cas_scalar_operation( *, attribute: str, @@ -117,6 +173,14 @@ def _build_cas_update_operations(existing: CasAmount, updated: CasAmount) -> lis if operation is not None: operations.append(operation) + operations.extend( + _build_inventory_function_operations( + entity_id=identifier, + existing=existing.inventory_function, + updated=updated.inventory_function, + ) + ) + return operations @@ -159,6 +223,13 @@ def _build_cas_patch_operations( ) if target_operation is not None: operations.append(target_operation) + operations.extend( + _build_inventory_function_operations( + entity_id=identifier, + existing=None, + updated=cas_amount.inventory_function, + ) + ) removals = [existing_lookup[key] for key in existing_lookup.keys() - updated_lookup.keys()] for cas_amount in removals: diff --git a/tests/resources/test_inventory.py b/tests/resources/test_inventory.py index a43b237b..03a4be09 100644 --- a/tests/resources/test_inventory.py +++ b/tests/resources/test_inventory.py @@ -29,6 +29,7 @@ def test_cas_amount_attributes(): "target", "id", "cas_category", + "inventory_function", "created", "updated", "classification_type",