@@ -134,6 +134,7 @@
|
|||||||
"id": "ID",
|
"id": "ID",
|
||||||
"filament_name": "Filament",
|
"filament_name": "Filament",
|
||||||
"filament": "Filament",
|
"filament": "Filament",
|
||||||
|
"price": "Price",
|
||||||
"material": "Material",
|
"material": "Material",
|
||||||
"weight_to_use": "Weight",
|
"weight_to_use": "Weight",
|
||||||
"used_weight": "Used Weight",
|
"used_weight": "Used Weight",
|
||||||
@@ -150,6 +151,7 @@
|
|||||||
"archived": "Archived"
|
"archived": "Archived"
|
||||||
},
|
},
|
||||||
"fields_help": {
|
"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.",
|
"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.",
|
"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.",
|
"remaining_weight": "How much filament is left on the spool. For a new spool this should match the spool weight.",
|
||||||
|
|||||||
@@ -201,7 +201,20 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.price")}
|
||||||
|
help={t("filament.fields_help.price")}
|
||||||
|
name={["price"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber precision={2} formatter={numberFormatter} parser={numberParser} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
|
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
|
||||||
<InputNumber value={usedWeight} />
|
<InputNumber value={usedWeight} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -181,6 +181,20 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("spool.fields.price")}
|
||||||
|
help={t("spool.fields_help.price")}
|
||||||
|
name={["price"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber precision={2} formatter={numberFormatter} parser={numberParser} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
|
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
|
||||||
<InputNumber value={usedWeight} />
|
<InputNumber value={usedWeight} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ function collapseSpool(element: ISpool): ISpoolCollapsed {
|
|||||||
} else {
|
} else {
|
||||||
filament_name = element.filament.name ?? element.filament.id.toString();
|
filament_name = element.filament.name ?? element.filament.id.toString();
|
||||||
}
|
}
|
||||||
|
if (!element.price){
|
||||||
|
element.price = element.filament.price;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
combined_name: filament_name,
|
combined_name: filament_name,
|
||||||
@@ -73,6 +76,7 @@ const allColumns: (keyof ISpoolCollapsed & string)[] = [
|
|||||||
"id",
|
"id",
|
||||||
"combined_name",
|
"combined_name",
|
||||||
"filament.material",
|
"filament.material",
|
||||||
|
"price",
|
||||||
"used_weight",
|
"used_weight",
|
||||||
"remaining_weight",
|
"remaining_weight",
|
||||||
"used_length",
|
"used_length",
|
||||||
@@ -312,6 +316,14 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
filterValueQuery: useSpoolmanMaterials(),
|
filterValueQuery: useSpoolmanMaterials(),
|
||||||
width: 120,
|
width: 120,
|
||||||
}),
|
}),
|
||||||
|
SortedColumn({
|
||||||
|
id: "price",
|
||||||
|
i18ncat: "spool",
|
||||||
|
actions,
|
||||||
|
dataSource,
|
||||||
|
tableState,
|
||||||
|
width: 80,
|
||||||
|
}),
|
||||||
NumberColumn({
|
NumberColumn({
|
||||||
id: "used_weight",
|
id: "used_weight",
|
||||||
i18ncat: "spool",
|
i18ncat: "spool",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface ISpool {
|
|||||||
first_used?: string;
|
first_used?: string;
|
||||||
last_used?: string;
|
last_used?: string;
|
||||||
filament: IFilament;
|
filament: IFilament;
|
||||||
|
price?: number;
|
||||||
remaining_weight?: number;
|
remaining_weight?: number;
|
||||||
used_weight: number;
|
used_weight: number;
|
||||||
remaining_length?: number;
|
remaining_length?: number;
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
|||||||
|
|
||||||
const record = data?.data;
|
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) => {
|
const formatFilament = (item: IFilament) => {
|
||||||
let vendorPrefix = "";
|
let vendorPrefix = "";
|
||||||
if (item.vendor) {
|
if (item.vendor) {
|
||||||
@@ -58,6 +67,8 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
|||||||
<NumberField value={record?.id ?? ""} />
|
<NumberField value={record?.id ?? ""} />
|
||||||
<Title level={5}>{t("spool.fields.filament")}</Title>
|
<Title level={5}>{t("spool.fields.filament")}</Title>
|
||||||
<TextField value={record ? filamentURL(record?.filament) : ""} />
|
<TextField value={record ? filamentURL(record?.filament) : ""} />
|
||||||
|
<Title level={5}>{t("spool.fields.price")}</Title>
|
||||||
|
<NumberField value={record ? spoolPrice(record) : ""} />
|
||||||
<Title level={5}>{t("spool.fields.registered")}</Title>
|
<Title level={5}>{t("spool.fields.registered")}</Title>
|
||||||
<DateField
|
<DateField
|
||||||
value={dayjs.utc(record?.registered).local()}
|
value={dayjs.utc(record?.registered).local()}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Added spool prices.
|
||||||
|
|
||||||
|
Revision ID: b82cd9e2aa6f
|
||||||
|
Revises: 92793c8a937c
|
||||||
|
Create Date: 2023-12-30 08:39:48.430846
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "b82cd9e2aa6f"
|
||||||
|
down_revision = "92793c8a937c"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> 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 ###
|
||||||
@@ -133,6 +133,11 @@ class Spool(BaseModel):
|
|||||||
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage. UTC Timezone.")
|
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.")
|
last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage. UTC Timezone.")
|
||||||
filament: Filament = Field(description="The filament type of this spool.")
|
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(
|
remaining_weight: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
ge=0,
|
ge=0,
|
||||||
@@ -197,6 +202,7 @@ class Spool(BaseModel):
|
|||||||
first_used=item.first_used,
|
first_used=item.first_used,
|
||||||
last_used=item.last_used,
|
last_used=item.last_used,
|
||||||
filament=filament,
|
filament=filament,
|
||||||
|
price=item.price,
|
||||||
used_weight=item.used_weight,
|
used_weight=item.used_weight,
|
||||||
used_length=used_length,
|
used_length=used_length,
|
||||||
remaining_weight=remaining_weight,
|
remaining_weight=remaining_weight,
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ class SpoolParameters(BaseModel):
|
|||||||
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.")
|
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.")
|
||||||
last_used: Optional[datetime] = Field(description="Last 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.")
|
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(
|
remaining_weight: Optional[float] = Field(
|
||||||
ge=0,
|
ge=0,
|
||||||
description=(
|
description=(
|
||||||
@@ -336,6 +341,7 @@ async def create( # noqa: ANN201
|
|||||||
db_item = await spool.create(
|
db_item = await spool.create(
|
||||||
db=db,
|
db=db,
|
||||||
filament_id=body.filament_id,
|
filament_id=body.filament_id,
|
||||||
|
price=body.price,
|
||||||
remaining_weight=body.remaining_weight,
|
remaining_weight=body.remaining_weight,
|
||||||
used_weight=body.used_weight,
|
used_weight=body.used_weight,
|
||||||
first_used=body.first_used,
|
first_used=body.first_used,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class Spool(Base):
|
|||||||
registered: Mapped[datetime] = mapped_column()
|
registered: Mapped[datetime] = mapped_column()
|
||||||
first_used: Mapped[Optional[datetime]] = mapped_column()
|
first_used: Mapped[Optional[datetime]] = mapped_column()
|
||||||
last_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_id: Mapped[int] = mapped_column(ForeignKey("filament.id"))
|
||||||
filament: Mapped["Filament"] = relationship(back_populates="spools")
|
filament: Mapped["Filament"] = relationship(back_populates="spools")
|
||||||
used_weight: Mapped[float] = mapped_column()
|
used_weight: Mapped[float] = mapped_column()
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ async def create(
|
|||||||
used_weight: Optional[float] = None,
|
used_weight: Optional[float] = None,
|
||||||
first_used: Optional[datetime] = None,
|
first_used: Optional[datetime] = None,
|
||||||
last_used: Optional[datetime] = None,
|
last_used: Optional[datetime] = None,
|
||||||
|
price: Optional[float] = None,
|
||||||
location: Optional[str] = None,
|
location: Optional[str] = None,
|
||||||
lot_nr: Optional[str] = None,
|
lot_nr: Optional[str] = None,
|
||||||
comment: Optional[str] = None,
|
comment: Optional[str] = None,
|
||||||
@@ -63,6 +64,7 @@ async def create(
|
|||||||
filament=filament_item,
|
filament=filament_item,
|
||||||
registered=datetime.utcnow().replace(microsecond=0),
|
registered=datetime.utcnow().replace(microsecond=0),
|
||||||
used_weight=used_weight,
|
used_weight=used_weight,
|
||||||
|
price=price,
|
||||||
first_used=first_used,
|
first_used=first_used,
|
||||||
last_used=last_used,
|
last_used=last_used,
|
||||||
location=location,
|
location=location,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ def test_add_spool_remaining_weight(random_filament: dict[str, Any]):
|
|||||||
lot_nr = "123456789"
|
lot_nr = "123456789"
|
||||||
comment = "abcdefghåäö"
|
comment = "abcdefghåäö"
|
||||||
archived = True
|
archived = True
|
||||||
|
price = 25
|
||||||
result = httpx.post(
|
result = httpx.post(
|
||||||
f"{URL}/api/v1/spool",
|
f"{URL}/api/v1/spool",
|
||||||
json={
|
json={
|
||||||
@@ -30,6 +31,7 @@ def test_add_spool_remaining_weight(random_filament: dict[str, Any]):
|
|||||||
"lot_nr": lot_nr,
|
"lot_nr": lot_nr,
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
"archived": archived,
|
"archived": archived,
|
||||||
|
"price": price,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
@@ -62,6 +64,7 @@ def test_add_spool_remaining_weight(random_filament: dict[str, Any]):
|
|||||||
"lot_nr": lot_nr,
|
"lot_nr": lot_nr,
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
"archived": archived,
|
"archived": archived,
|
||||||
|
"price": price,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify that registered happened almost now (within 1 minute)
|
# Verify that registered happened almost now (within 1 minute)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ def test_get_spool(random_filament: dict[str, Any]):
|
|||||||
location = "The Pantry"
|
location = "The Pantry"
|
||||||
lot_nr = "123456789"
|
lot_nr = "123456789"
|
||||||
comment = "abcdefghåäö"
|
comment = "abcdefghåäö"
|
||||||
|
price = 25
|
||||||
archived = True
|
archived = True
|
||||||
result = httpx.post(
|
result = httpx.post(
|
||||||
f"{URL}/api/v1/spool",
|
f"{URL}/api/v1/spool",
|
||||||
@@ -27,6 +28,7 @@ def test_get_spool(random_filament: dict[str, Any]):
|
|||||||
"location": location,
|
"location": location,
|
||||||
"lot_nr": lot_nr,
|
"lot_nr": lot_nr,
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
|
"price": price,
|
||||||
"archived": archived,
|
"archived": archived,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def test_update_spool(random_filament: dict[str, Any]):
|
|||||||
lot_nr = "987654321"
|
lot_nr = "987654321"
|
||||||
comment = "abcdefghåäö"
|
comment = "abcdefghåäö"
|
||||||
archived = True
|
archived = True
|
||||||
|
price = 25
|
||||||
result = httpx.patch(
|
result = httpx.patch(
|
||||||
f"{URL}/api/v1/spool/{spool['id']}",
|
f"{URL}/api/v1/spool/{spool['id']}",
|
||||||
json={
|
json={
|
||||||
@@ -43,6 +44,7 @@ def test_update_spool(random_filament: dict[str, Any]):
|
|||||||
"lot_nr": lot_nr,
|
"lot_nr": lot_nr,
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
"archived": archived,
|
"archived": archived,
|
||||||
|
"price": price,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
result.raise_for_status()
|
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["lot_nr"] == lot_nr
|
||||||
assert spool["comment"] == comment
|
assert spool["comment"] == comment
|
||||||
assert spool["archived"] == archived
|
assert spool["archived"] == archived
|
||||||
|
assert spool["price"] == price
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
httpx.delete(f"{URL}/api/v1/spool/{spool['id']}").raise_for_status()
|
httpx.delete(f"{URL}/api/v1/spool/{spool['id']}").raise_for_status()
|
||||||
|
|||||||
Reference in New Issue
Block a user