feat(filament): add spool count and total remaining weight columns
Adds computed spool statistics to filament list: - spool_count: Number of non-archived spools of this filament - total_remaining_weight: Sum of remaining weight across all spools Backend changes: - Modified database/filament.py find() to compute stats via subquery - Added fields to Filament pydantic model in api/v1/models.py - Updated filament API endpoint to include stats in response Frontend changes: - Added fields to IFilament interface - Added columns to filament list table - Added translation keys Closes #15 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -260,7 +260,9 @@
|
|||||||
"longitudinal": "Longitudinal",
|
"longitudinal": "Longitudinal",
|
||||||
"external_id": "External ID",
|
"external_id": "External ID",
|
||||||
"spools": "Show Spools",
|
"spools": "Show Spools",
|
||||||
"not_set": "Not Set"
|
"not_set": "Not Set",
|
||||||
|
"spool_count": "Spools",
|
||||||
|
"total_remaining_weight": "Total Remaining"
|
||||||
},
|
},
|
||||||
"fields_help": {
|
"fields_help": {
|
||||||
"name": "Filament name, to distinguish this filament type among others from the same manufacturer. Should contain the color for example.",
|
"name": "Filament name, to distinguish this filament type among others from the same manufacturer. Should contain the color for example.",
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const allColumns: (keyof IFilamentCollapsed & string)[] = [
|
|||||||
"article_number",
|
"article_number",
|
||||||
"settings_extruder_temp",
|
"settings_extruder_temp",
|
||||||
"settings_bed_temp",
|
"settings_bed_temp",
|
||||||
|
"spool_count",
|
||||||
|
"total_remaining_weight",
|
||||||
"registered",
|
"registered",
|
||||||
"comment",
|
"comment",
|
||||||
];
|
];
|
||||||
@@ -324,6 +326,22 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
maxDecimals: 0,
|
maxDecimals: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
}),
|
}),
|
||||||
|
NumberColumn({
|
||||||
|
...commonProps,
|
||||||
|
id: "spool_count",
|
||||||
|
i18ncat: "filament",
|
||||||
|
unit: "",
|
||||||
|
maxDecimals: 0,
|
||||||
|
width: 80,
|
||||||
|
}),
|
||||||
|
NumberColumn({
|
||||||
|
...commonProps,
|
||||||
|
id: "total_remaining_weight",
|
||||||
|
i18ncat: "filament",
|
||||||
|
unit: "g",
|
||||||
|
maxDecimals: 0,
|
||||||
|
width: 120,
|
||||||
|
}),
|
||||||
DateColumn({
|
DateColumn({
|
||||||
...commonProps,
|
...commonProps,
|
||||||
id: "registered",
|
id: "registered",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface IFilament {
|
|||||||
multi_color_direction?: string;
|
multi_color_direction?: string;
|
||||||
external_id?: string;
|
external_id?: string;
|
||||||
extra: { [key: string]: string };
|
extra: { [key: string]: string };
|
||||||
|
spool_count?: number;
|
||||||
|
total_remaining_weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IFilamentParsedExtras is the same as IFilament, but with the extra field parsed into its real types
|
// IFilamentParsedExtras is the same as IFilament, but with the extra field parsed into its real types
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ async def find(
|
|||||||
else:
|
else:
|
||||||
filter_by_ids = None
|
filter_by_ids = None
|
||||||
|
|
||||||
db_items, total_count = await filament.find(
|
db_items, total_count, spool_stats = await filament.find(
|
||||||
db=db,
|
db=db,
|
||||||
ids=filter_by_ids,
|
ids=filter_by_ids,
|
||||||
vendor_name=vendor_name if vendor_name is not None else vendor_name_old,
|
vendor_name=vendor_name if vendor_name is not None else vendor_name_old,
|
||||||
@@ -383,7 +383,14 @@ async def find(
|
|||||||
# Set x-total-count header for pagination
|
# Set x-total-count header for pagination
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=jsonable_encoder(
|
content=jsonable_encoder(
|
||||||
(Filament.from_db(db_item) for db_item in db_items),
|
(
|
||||||
|
Filament.from_db(
|
||||||
|
db_item,
|
||||||
|
spool_count=spool_stats.get(db_item.id, (0, 0))[0],
|
||||||
|
total_remaining_weight=spool_stats.get(db_item.id, (0, 0))[1],
|
||||||
|
)
|
||||||
|
for db_item in db_items
|
||||||
|
),
|
||||||
exclude_none=True,
|
exclude_none=True,
|
||||||
),
|
),
|
||||||
headers={"x-total-count": str(total_count)},
|
headers={"x-total-count": str(total_count)},
|
||||||
|
|||||||
@@ -221,9 +221,25 @@ class Filament(BaseModel):
|
|||||||
"Query the /fields endpoint for more details about the fields."
|
"Query the /fields endpoint for more details about the fields."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
spool_count: Optional[int] = Field(
|
||||||
|
None,
|
||||||
|
ge=0,
|
||||||
|
description="Number of spools of this filament (excluding archived). Computed field.",
|
||||||
|
examples=[5],
|
||||||
|
)
|
||||||
|
total_remaining_weight: Optional[float] = Field(
|
||||||
|
None,
|
||||||
|
ge=0,
|
||||||
|
description="Total remaining weight across all spools of this filament (excluding archived), in grams. Computed field.",
|
||||||
|
examples=[2500.0],
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_db(item: models.Filament) -> "Filament":
|
def from_db(
|
||||||
|
item: models.Filament,
|
||||||
|
spool_count: Optional[int] = None,
|
||||||
|
total_remaining_weight: Optional[float] = None,
|
||||||
|
) -> "Filament":
|
||||||
"""Create a new Pydantic filament object from a database filament object."""
|
"""Create a new Pydantic filament object from a database filament object."""
|
||||||
return Filament(
|
return Filament(
|
||||||
id=item.id,
|
id=item.id,
|
||||||
@@ -251,6 +267,8 @@ class Filament(BaseModel):
|
|||||||
),
|
),
|
||||||
external_id=item.external_id,
|
external_id=item.external_id,
|
||||||
extra={field.key: field.value for field in item.extra},
|
extra={field.key: field.value for field in item.extra},
|
||||||
|
spool_count=spool_count,
|
||||||
|
total_remaining_weight=total_remaining_weight,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -114,13 +114,16 @@ async def find(
|
|||||||
sort_by: Optional[dict[str, SortOrder]] = None,
|
sort_by: Optional[dict[str, SortOrder]] = None,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> tuple[list[models.Filament], int]:
|
) -> tuple[list[models.Filament], int, dict[int, tuple[int, float]]]:
|
||||||
"""Find a list of filament objects by search criteria.
|
"""Find a list of filament objects by search criteria.
|
||||||
|
|
||||||
Sort by a field by passing a dict with the field name as key and the sort order as value.
|
Sort by a field by passing a dict with the field name as key and the sort order as value.
|
||||||
The field name can contain nested fields, e.g. vendor.name.
|
The field name can contain nested fields, e.g. vendor.name.
|
||||||
|
|
||||||
Returns a tuple containing the list of items and the total count of matching items.
|
Returns a tuple containing:
|
||||||
|
- list of filaments
|
||||||
|
- total count of matching items
|
||||||
|
- dict mapping filament_id to (spool_count, total_remaining_weight)
|
||||||
"""
|
"""
|
||||||
stmt = (
|
stmt = (
|
||||||
select(models.Filament)
|
select(models.Filament)
|
||||||
@@ -160,7 +163,25 @@ async def find(
|
|||||||
if total_count is None:
|
if total_count is None:
|
||||||
total_count = len(result)
|
total_count = len(result)
|
||||||
|
|
||||||
return result, total_count
|
# Fetch spool statistics for all filaments in the result
|
||||||
|
spool_stats: dict[int, tuple[int, float]] = {}
|
||||||
|
if result:
|
||||||
|
filament_ids = [f.id for f in result]
|
||||||
|
stats_stmt = (
|
||||||
|
select(
|
||||||
|
models.Spool.filament_id,
|
||||||
|
func.count(models.Spool.id).label("spool_count"),
|
||||||
|
func.coalesce(func.sum(models.Spool.initial_weight - models.Spool.used_weight), 0).label("total_remaining"),
|
||||||
|
)
|
||||||
|
.where(models.Spool.filament_id.in_(filament_ids))
|
||||||
|
.where((models.Spool.archived == False) | (models.Spool.archived == None)) # noqa: E712, E711
|
||||||
|
.group_by(models.Spool.filament_id)
|
||||||
|
)
|
||||||
|
stats_rows = await db.execute(stats_stmt)
|
||||||
|
for row in stats_rows.all():
|
||||||
|
spool_stats[row.filament_id] = (row.spool_count, float(row.total_remaining or 0))
|
||||||
|
|
||||||
|
return result, total_count, spool_stats
|
||||||
|
|
||||||
|
|
||||||
async def update(
|
async def update(
|
||||||
|
|||||||
Reference in New Issue
Block a user