diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 7dfceed..23e3f97 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -134,6 +134,7 @@ "id": "ID", "filament_name": "Filament", "filament": "Filament", + "price": "Price", "material": "Material", "weight_to_use": "Weight", "used_weight": "Used Weight", @@ -150,6 +151,7 @@ "archived": "Archived" }, "fields_help": { + "price": "Price of a full spool in the system configured currency. If not set, the price of the filament will be assumed instead.", "weight_to_use": "Select what weight value to enter. Measured weight is only available if the spool weight is set for the selected filament.", "used_weight": "How much filament has been used from the spool. A new spool should have 0g used.", "remaining_weight": "How much filament is left on the spool. For a new spool this should match the spool weight.", diff --git a/client/src/pages/spools/create.tsx b/client/src/pages/spools/create.tsx index f9c41f7..d33d6ff 100644 --- a/client/src/pages/spools/create.tsx +++ b/client/src/pages/spools/create.tsx @@ -201,7 +201,20 @@ export const SpoolCreate: React.FC - + + + diff --git a/client/src/pages/spools/edit.tsx b/client/src/pages/spools/edit.tsx index 1bbce4b..86b84ce 100644 --- a/client/src/pages/spools/edit.tsx +++ b/client/src/pages/spools/edit.tsx @@ -181,6 +181,20 @@ export const SpoolEdit: React.FC = () => { }} /> + + + diff --git a/client/src/pages/spools/list.tsx b/client/src/pages/spools/list.tsx index cc17f38..714a6f8 100644 --- a/client/src/pages/spools/list.tsx +++ b/client/src/pages/spools/list.tsx @@ -52,6 +52,9 @@ function collapseSpool(element: ISpool): ISpoolCollapsed { } else { filament_name = element.filament.name ?? element.filament.id.toString(); } + if (!element.price){ + element.price = element.filament.price; + } return { ...element, combined_name: filament_name, @@ -73,6 +76,7 @@ const allColumns: (keyof ISpoolCollapsed & string)[] = [ "id", "combined_name", "filament.material", + "price", "used_weight", "remaining_weight", "used_length", @@ -312,6 +316,14 @@ export const SpoolList: React.FC = () => { filterValueQuery: useSpoolmanMaterials(), width: 120, }), + SortedColumn({ + id: "price", + i18ncat: "spool", + actions, + dataSource, + tableState, + width: 80, + }), NumberColumn({ id: "used_weight", i18ncat: "spool", diff --git a/client/src/pages/spools/model.tsx b/client/src/pages/spools/model.tsx index 7dd64d2..f365d03 100644 --- a/client/src/pages/spools/model.tsx +++ b/client/src/pages/spools/model.tsx @@ -6,6 +6,7 @@ export interface ISpool { first_used?: string; last_used?: string; filament: IFilament; + price?: number; remaining_weight?: number; used_weight: number; remaining_length?: number; diff --git a/client/src/pages/spools/show.tsx b/client/src/pages/spools/show.tsx index 2765310..f068b00 100644 --- a/client/src/pages/spools/show.tsx +++ b/client/src/pages/spools/show.tsx @@ -23,6 +23,15 @@ export const SpoolShow: React.FC = () => { const record = data?.data; + const spoolPrice = (item: ISpool) => { + let spoolPrice = ""; + if (!item.price){ + spoolPrice = `${item.filament.price}`; + return spoolPrice; + } + return item.price; + } + const formatFilament = (item: IFilament) => { let vendorPrefix = ""; if (item.vendor) { @@ -58,6 +67,8 @@ export const SpoolShow: React.FC = () => { {t("spool.fields.filament")} + {t("spool.fields.price")} + {t("spool.fields.registered")} None: + """Perform the upgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("spool", sa.Column("price", sa.Float(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Perform the downgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("spool", "price") + # ### end Alembic commands ### diff --git a/spoolman/api/v1/models.py b/spoolman/api/v1/models.py index b8a77f1..0be5a14 100644 --- a/spoolman/api/v1/models.py +++ b/spoolman/api/v1/models.py @@ -133,6 +133,11 @@ class Spool(BaseModel): first_used: Optional[datetime] = Field(description="First logged occurence of spool usage. UTC Timezone.") last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage. UTC Timezone.") filament: Filament = Field(description="The filament type of this spool.") + price: Optional[float] = Field( + ge=0, + description="The price of this spool in the system configured currency.", + example=20.0, + ) remaining_weight: Optional[float] = Field( default=None, ge=0, @@ -197,6 +202,7 @@ class Spool(BaseModel): first_used=item.first_used, last_used=item.last_used, filament=filament, + price=item.price, used_weight=item.used_weight, used_length=used_length, remaining_weight=remaining_weight, diff --git a/spoolman/api/v1/spool.py b/spoolman/api/v1/spool.py index f9d083c..35ec5ea 100644 --- a/spoolman/api/v1/spool.py +++ b/spoolman/api/v1/spool.py @@ -34,6 +34,11 @@ class SpoolParameters(BaseModel): first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.") last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.") filament_id: int = Field(description="The ID of the filament type of this spool.") + price: Optional[float] = Field( + ge=0, + description="The price of this filament in the system configured currency.", + example=20.0, + ) remaining_weight: Optional[float] = Field( ge=0, description=( @@ -336,6 +341,7 @@ async def create( # noqa: ANN201 db_item = await spool.create( db=db, filament_id=body.filament_id, + price=body.price, remaining_weight=body.remaining_weight, used_weight=body.used_weight, first_used=body.first_used, diff --git a/spoolman/database/models.py b/spoolman/database/models.py index 05e4f4c..45df367 100644 --- a/spoolman/database/models.py +++ b/spoolman/database/models.py @@ -50,6 +50,7 @@ class Spool(Base): registered: Mapped[datetime] = mapped_column() first_used: Mapped[Optional[datetime]] = mapped_column() last_used: Mapped[Optional[datetime]] = mapped_column() + price: Mapped[Optional[float]] = mapped_column() filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id")) filament: Mapped["Filament"] = relationship(back_populates="spools") used_weight: Mapped[float] = mapped_column() diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index 98336b7..dc29d32 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -38,6 +38,7 @@ async def create( used_weight: Optional[float] = None, first_used: Optional[datetime] = None, last_used: Optional[datetime] = None, + price: Optional[float] = None, location: Optional[str] = None, lot_nr: Optional[str] = None, comment: Optional[str] = None, @@ -63,6 +64,7 @@ async def create( filament=filament_item, registered=datetime.utcnow().replace(microsecond=0), used_weight=used_weight, + price=price, first_used=first_used, last_used=last_used, location=location, diff --git a/tests_integration/tests/spool/test_add.py b/tests_integration/tests/spool/test_add.py index f5ae7fd..fa64f48 100644 --- a/tests_integration/tests/spool/test_add.py +++ b/tests_integration/tests/spool/test_add.py @@ -19,6 +19,7 @@ def test_add_spool_remaining_weight(random_filament: dict[str, Any]): lot_nr = "123456789" comment = "abcdefghåäö" archived = True + price = 25 result = httpx.post( f"{URL}/api/v1/spool", json={ @@ -30,6 +31,7 @@ def test_add_spool_remaining_weight(random_filament: dict[str, Any]): "lot_nr": lot_nr, "comment": comment, "archived": archived, + "price": price, }, ) result.raise_for_status() @@ -62,6 +64,7 @@ def test_add_spool_remaining_weight(random_filament: dict[str, Any]): "lot_nr": lot_nr, "comment": comment, "archived": archived, + "price": price, } # Verify that registered happened almost now (within 1 minute) diff --git a/tests_integration/tests/spool/test_get.py b/tests_integration/tests/spool/test_get.py index acb8345..fa4e0f4 100644 --- a/tests_integration/tests/spool/test_get.py +++ b/tests_integration/tests/spool/test_get.py @@ -16,6 +16,7 @@ def test_get_spool(random_filament: dict[str, Any]): location = "The Pantry" lot_nr = "123456789" comment = "abcdefghåäö" + price = 25 archived = True result = httpx.post( f"{URL}/api/v1/spool", @@ -27,6 +28,7 @@ def test_get_spool(random_filament: dict[str, Any]): "location": location, "lot_nr": lot_nr, "comment": comment, + "price": price, "archived": archived, }, ) diff --git a/tests_integration/tests/spool/test_update.py b/tests_integration/tests/spool/test_update.py index c1d3be3..feaad44 100644 --- a/tests_integration/tests/spool/test_update.py +++ b/tests_integration/tests/spool/test_update.py @@ -33,6 +33,7 @@ def test_update_spool(random_filament: dict[str, Any]): lot_nr = "987654321" comment = "abcdefghåäö" archived = True + price = 25 result = httpx.patch( f"{URL}/api/v1/spool/{spool['id']}", json={ @@ -43,6 +44,7 @@ def test_update_spool(random_filament: dict[str, Any]): "lot_nr": lot_nr, "comment": comment, "archived": archived, + "price": price, }, ) result.raise_for_status() @@ -71,6 +73,7 @@ def test_update_spool(random_filament: dict[str, Any]): assert spool["lot_nr"] == lot_nr assert spool["comment"] == comment assert spool["archived"] == archived + assert spool["price"] == price # Clean up httpx.delete(f"{URL}/api/v1/spool/{spool['id']}").raise_for_status()