Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ofsc/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
CapacityAreaWorkZonesV1Response as CapacityAreaWorkZonesV1Response,
CapacityCategory as CapacityCategory,
CapacityCategoryListResponse as CapacityCategoryListResponse,
CapacityCategoryWorkSkill as CapacityCategoryWorkSkill,
CapacityCategoryWorkSkillGroup as CapacityCategoryWorkSkillGroup,
Condition as Condition,
EnumerationValue as EnumerationValue,
EnumerationValueList as EnumerationValueList,
Expand Down
24 changes: 21 additions & 3 deletions ofsc/models/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,16 +475,34 @@ def __getitem__(self, item):
return self.root[item]


class CapacityCategoryWorkSkill(BaseModel):
# extra="allow" preserves audit fields (created_by, creation_date,
# last_updated_by, last_update_date, last_update_login) and any future fields.
model_config = ConfigDict(extra="allow")
label: str
ratio: Optional[int] = None
startDate: Optional[str] = None
end_date: Optional[str] = None


class CapacityCategoryWorkSkillGroup(BaseModel):
model_config = ConfigDict(extra="allow")
label: str
startDate: Optional[str] = None
end_date: Optional[str] = None
links: Optional[list[Link]] = None


class CapacityCategory(BaseModel):
model_config = ConfigDict(extra="allow")
label: str
name: str
timeSlots: Optional[ItemList] = None
translations: Annotated[Optional[TranslationList], Field(alias="translations")] = None
workSkillGroups: Optional[ItemList] = None
workSkills: Optional[ItemList] = None
workSkillGroups: Optional[list[CapacityCategoryWorkSkillGroup]] = None
workSkills: Optional[list[CapacityCategoryWorkSkill]] = None
active: bool
links: Optional[list[Link]] = None
model_config = ConfigDict(extra="ignore")


class CapacityCategoryListResponse(OFSResponseList[CapacityCategory]):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ofsc"
version = "2.26.1"
version = "2.26.2"
description = "Python wrapper for Oracle Field Service API"
authors = [{ name = "Borja Toron", email = "borja.toron@gmail.com" }]
requires-python = ">=3.13"
Expand Down
104 changes: 104 additions & 0 deletions tests/metadata/test_capacity_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,107 @@ def test_get_capacity_category(instance, pp, demo_data):
)
assert metadata_response.label == category
assert metadata_response.name == capacity_categories[category].get("name")


def test_capacity_category_nested_fields_are_preserved():
"""Nested workSkills/workSkillGroups fields must not be silently dropped.

Mirrors the real OFS API shape (synthetic labels). Regression test for #175.
"""
from ofsc.models import (
CapacityCategory,
CapacityCategoryWorkSkill,
CapacityCategoryWorkSkillGroup,
)

payload = {
"label": "DEMO_CAT",
"name": "Demo Category",
"active": True,
"workSkills": [
{
"label": "S_DEMO",
"ratio": 100,
"startDate": "2023-01-06",
"end_date": "2025-01-04",
"created_by": "alice",
"creation_date": "2023-01-06 10:00:00",
"last_updated_by": "bob",
"last_update_date": "2024-01-01 12:00:00",
"last_update_login": "bob_login",
}
],
"workSkillGroups": [
{
"label": "Demo Group",
"startDate": "2025-01-05",
"end_date": "",
"created_by": "carol",
"creation_date": "2025-01-05 09:00:00",
"last_updated_by": "carol",
"last_update_date": "2025-01-05 09:00:00",
"last_update_login": "carol_login",
"links": [
{
"rel": "canonical",
"href": "https://x.example/rest/ofscMetadata/v1/workSkillGroups/Demo%20Group",
}
],
}
],
"timeSlots": [{"label": "AM"}, {"label": "PM"}],
"translations": [{"language": "en", "name": "Demo Category", "languageISO": "en-US"}],
"links": [{"rel": "canonical", "href": "https://x.example/cat/DEMO_CAT"}],
}

cat = CapacityCategory.model_validate(payload)

# workSkills: declared fields are typed and populated
skill = cat.workSkills[0]
assert isinstance(skill, CapacityCategoryWorkSkill)
assert skill.label == "S_DEMO"
assert skill.ratio == 100
assert skill.startDate == "2023-01-06"
assert skill.end_date == "2025-01-04"

# workSkills: undeclared audit fields preserved via extra="allow"
assert skill.model_extra["created_by"] == "alice"
assert skill.model_extra["last_update_login"] == "bob_login"

# workSkillGroups: declared fields + links typed and populated
group = cat.workSkillGroups[0]
assert isinstance(group, CapacityCategoryWorkSkillGroup)
assert group.label == "Demo Group"
assert group.startDate == "2025-01-05"
assert group.end_date == ""
assert group.links[0].rel == "canonical"
assert group.model_extra["created_by"] == "carol"

# timeSlots unchanged (label only)
assert cat.timeSlots[0].label == "AM"

# round-trip: extras survive model_dump()
dumped = cat.model_dump()
assert dumped["workSkills"][0]["created_by"] == "alice"
assert dumped["workSkills"][0]["ratio"] == 100


def test_capacity_category_with_empty_nested_lists():
"""A category with no skills/groups (the COPPER shape) parses cleanly."""
from ofsc.models import CapacityCategory

cat = CapacityCategory.model_validate(
{
"label": "COPPER",
"name": "Copper",
"active": True,
"workSkills": [],
"workSkillGroups": [],
"timeSlots": [{"label": "AM"}],
"translations": [{"language": "en", "name": "Copper"}],
"links": [],
}
)
assert cat.workSkills == []
assert cat.workSkillGroups == []
assert cat.label == "COPPER"
10 changes: 8 additions & 2 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,14 @@ def test_capacity_category_model_list():
assert item.translations == TranslationList.model_validate(capacityCategoryList["items"][idx]["translations"])
expected_links = [Link.model_validate(link) for link in capacityCategoryList["items"][idx]["links"]]
assert item.links == expected_links
# assert item.workSkills == capacityCategoryList["items"][idx]["workSkills"]
assert item.workSkillGroups == ItemList.model_validate(capacityCategoryList["items"][idx]["workSkillGroups"])
# workSkills now preserves ratio/startDate, previously dropped (see #175)
expected_ws = capacityCategoryList["items"][idx]["workSkills"]
assert len(item.workSkills) == len(expected_ws)
for ws_obj, ws_data in zip(item.workSkills, expected_ws):
assert ws_obj.label == ws_data["label"]
assert ws_obj.ratio == ws_data.get("ratio")
assert ws_obj.startDate == ws_data.get("startDate")
assert item.workSkillGroups == capacityCategoryList["items"][idx]["workSkillGroups"]


# endregion
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading