From 1d6830d769fa3f8547316fca1cc32aab50259497 Mon Sep 17 00:00:00 2001 From: Matt Gerega Date: Tue, 26 Mar 2024 13:55:44 -0400 Subject: [PATCH 01/15] Added initial_weight and empty_weight to spool --- client/src/pages/spools/create.tsx | 42 +++++++++++++++++ client/src/pages/spools/edit.tsx | 46 +++++++++++++++++++ ...4_03_26_0948-aafcd7fb0e84_spool_weights.py | 30 ++++++++++++ ...49-304a32906234_spool_weight_population.py | 46 +++++++++++++++++++ spoolman/api/v1/spool.py | 12 +++++ spoolman/database/models.py | 2 + spoolman/database/spool.py | 36 ++++++++++++--- 7 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py create mode 100644 migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py diff --git a/client/src/pages/spools/create.tsx b/client/src/pages/spools/create.tsx index 5381897..deeaf4e 100644 --- a/client/src/pages/spools/create.tsx +++ b/client/src/pages/spools/create.tsx @@ -107,6 +107,9 @@ export const SpoolCreate: React.FC a.label.localeCompare(b.label, undefined, { sensitivity: "base" })); + const [defaultEmptySpoolWeight, setDefaultEmptySpoolWeight] = useState(0); + const [defaultInitialTotalWeight, setDefaultInitialTotalWeight] = useState(0); + const [weightToEnter, setWeightToEnter] = useState(1); const [usedWeight, setUsedWeight] = useState(0); @@ -124,6 +127,9 @@ export const SpoolCreate: React.FC= 3) { if (!(newFilamentWeight && newSpoolWeight)) { setWeightToEnter(2); @@ -254,6 +260,42 @@ export const SpoolCreate: React.FC + + + + + + + + diff --git a/client/src/pages/spools/edit.tsx b/client/src/pages/spools/edit.tsx index a592dfc..35b335b 100644 --- a/client/src/pages/spools/edit.tsx +++ b/client/src/pages/spools/edit.tsx @@ -56,6 +56,9 @@ export const SpoolEdit: React.FC = () => { formProps.initialValues = ParsedExtras(formProps.initialValues); } + const [defaultEmptySpoolWeight, setDefaultEmptySpoolWeight] = useState(0); + const [defaultInitialTotalWeight, setDefaultInitialTotalWeight] = useState(0); + // Override the form's onFinish method to stringify the extra fields const originalOnFinish = formProps.onFinish; formProps.onFinish = (allValues: ISpoolParsedExtras) => { @@ -107,6 +110,13 @@ export const SpoolEdit: React.FC = () => { const newSelectedFilament = filamentOptions?.find((obj) => { return obj.value === newID; }); + + const newFilamentWeight = newSelectedFilament?.weight || 0; + const newSpoolWeight = newSelectedFilament?.spool_weight || 0; + + setDefaultEmptySpoolWeight(newSpoolWeight); + setDefaultInitialTotalWeight(newFilamentWeight + newSpoolWeight); + const filamentHasWeight = newSelectedFilament?.weight || 0; const filamentHasSpoolWeight = newSelectedFilament?.spool_weight || 0; @@ -238,6 +248,42 @@ export const SpoolEdit: React.FC = () => { parser={numberParser} /> + + + + + + + + diff --git a/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py b/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py new file mode 100644 index 0000000..0d834be --- /dev/null +++ b/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py @@ -0,0 +1,30 @@ +"""spool weights. + +Revision ID: aafcd7fb0e84 +Revises: b8881bdb716c +Create Date: 2024-03-26 09:48:09.930022 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'aafcd7fb0e84' +down_revision = 'b8881bdb716c' +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('initial_weight', sa.Float(), nullable=True, comment="The initial total weight of the spool (gross weight).")) + op.add_column('spool', sa.Column('empty_weight', sa.Float(), nullable=True, comment="The weight of the empty spool (tare weight).")) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Perform the downgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('spool', 'empty_weight') + op.drop_column('spool', 'initial_weight') + # ### end Alembic commands ### diff --git a/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py b/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py new file mode 100644 index 0000000..73861be --- /dev/null +++ b/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py @@ -0,0 +1,46 @@ +"""spool weight population. + +Revision ID: 304a32906234 +Revises: aafcd7fb0e84 +Create Date: 2024-03-26 13:49:26.594399 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '304a32906234' +down_revision = 'aafcd7fb0e84' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + filament = sa.Table('filament', + sa.MetaData(), + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('weight', sa.Float(), nullable=True), + sa.Column('spool_weight', sa.Float(), nullable=True)) + + spool = sa.Table( + 'spool', + sa.MetaData(), + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('filament_id', sa.Integer), + sa.Column('initial_weight', sa.Float(), nullable=False, comment="The initial total weight of the spool (gross weight)."), + sa.Column('empty_weight', sa.Float(), nullable=False, comment="The weight of the empty spool (tare weight).") + ) + + initial_weight = sa.select((filament.c.weight + filament.c.spool_weight).label("initial_weight")).where(filament.c.id == spool.c.filament_id).scalar_subquery() + empty_weight = sa.select(filament.c.spool_weight).where(filament.c.id == spool.c.filament_id).scalar_subquery() + + set_initial_weight = sa.update(spool).values(initial_weight=initial_weight) + op.execute(set_initial_weight) + + set_empty_weight = sa.update(spool).values(empty_weight=empty_weight) + op.execute(set_empty_weight) + pass + + +def downgrade() -> None: + """Perform the downgrade.""" + pass diff --git a/spoolman/api/v1/spool.py b/spoolman/api/v1/spool.py index 1391dfd..e701fca 100644 --- a/spoolman/api/v1/spool.py +++ b/spoolman/api/v1/spool.py @@ -40,6 +40,16 @@ class SpoolParameters(BaseModel): description="The price of this filament in the system configured currency.", example=20.0, ) + initial_weight: Optional[float] = Field( + ge=0, + description="The initial total weight of the spool.", + example=200, + ) + empty_weight: Optional[float] = Field( + ge=0, + description="The weight of an empty spool.", + example=200, + ) remaining_weight: Optional[float] = Field( ge=0, description=( @@ -354,6 +364,8 @@ async def create( # noqa: ANN201 db=db, filament_id=body.filament_id, price=body.price, + initial_weight=body.initial_weight, + empty_weight=body.empty_weight, 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 f06ac6f..b009bdf 100644 --- a/spoolman/database/models.py +++ b/spoolman/database/models.py @@ -64,6 +64,8 @@ class Spool(Base): price: Mapped[Optional[float]] = mapped_column() filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id")) filament: Mapped["Filament"] = relationship(back_populates="spools") + initial_weight: Mapped[float] = mapped_column() + empty_weight: Mapped[float] = mapped_column() used_weight: Mapped[float] = mapped_column() location: Mapped[Optional[str]] = mapped_column(String(64)) lot_nr: Mapped[Optional[str]] = mapped_column(String(64)) diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index dfbaad8..ca384d8 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -35,6 +35,8 @@ async def create( db: AsyncSession, filament_id: int, remaining_weight: Optional[float] = None, + initial_weight: Optional[float] = None, + empty_weight: Optional[float] = None, used_weight: Optional[float] = None, first_used: Optional[datetime] = None, last_used: Optional[datetime] = None, @@ -47,11 +49,19 @@ async def create( ) -> models.Spool: """Add a new spool to the database. Leave weight empty to assume full spool.""" filament_item = await filament.get_by_id(db, filament_id) + + if empty_weight is None: + empty_weight = filament_item.spool_weight if filament_item.spool_weight is not None else 0 + + # Calculate initial_weight if not provided + if initial_weight is None: + initial_weight = (filament_item.weight if filament_item.weight is not None else 0) + empty_weight + if used_weight is None: if remaining_weight is not None: - if filament_item.weight is None: - raise ItemCreateError("remaining_weight can only be used if the filament type has a weight set.") - used_weight = max(filament_item.weight - remaining_weight, 0) + if initial_weight is None or initial_weight == 0: + raise ItemCreateError("remaining_weight can only be used if the initial_weight is defined.") + used_weight = max(initial_weight - empty_weight - remaining_weight, 0) else: used_weight = 0 @@ -64,6 +74,8 @@ async def create( spool = models.Spool( filament=filament_item, registered=datetime.utcnow().replace(microsecond=0), + initial_weight=initial_weight, + empty_weight=empty_weight, used_weight=used_weight, price=price, first_used=first_used, @@ -150,7 +162,7 @@ async def find( for fieldstr, order in sort_by.items(): sorts = [] if fieldstr in {"remaining_weight", "remaining_length"}: - sorts.append(models.Filament.weight - models.Spool.used_weight) + sorts.append(models.Spool.initial_weight - models.Spool.empty_weight - models.Spool.used_weight) elif fieldstr == "filament.combined_name": sorts.append(models.Vendor.name) sorts.append(models.Filament.name) @@ -181,10 +193,20 @@ async def update( for k, v in data.items(): if k == "filament_id": spool.filament = await filament.get_by_id(db, v) + # If there is no initial_weight, calculate it from the filament weight + if ( + spool.initial_weight is None + and spool.empty_weight is None + and spool.filament.weight is not None + ): + spool_weight = (spool.empty_weight if spool.empty_weight is not None else 0) + spool.initial_weight = spool.filament.weight + spool_weight + spool.empty_weight = spool_weight + elif k == "remaining_weight": - if spool.filament.weight is None: - raise ItemCreateError("remaining_weight can only be used if the filament type has a weight set.") - spool.used_weight = max(spool.filament.weight - v, 0) + if spool.initial_weight is None: + raise ItemCreateError("remaining_weight can only be used if initial_weight is set.") + spool.used_weight = max((spool.initial_weight - spool.empty_weight) - v, 0) elif isinstance(v, datetime): setattr(spool, k, utc_timezone_naive(v)) elif k == "extra": From 83fd3073ec700b480974d68d15831a7bf905e753 Mon Sep 17 00:00:00 2001 From: Matt Gerega Date: Tue, 26 Mar 2024 14:02:17 -0400 Subject: [PATCH 02/15] Fixed formatting --- ...4_03_26_0948-aafcd7fb0e84_spool_weights.py | 23 ++++++--- ...49-304a32906234_spool_weight_population.py | 48 ++++++++++++------- spoolman/database/spool.py | 8 +--- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py b/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py index 0d834be..ec14929 100644 --- a/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py +++ b/migrations/versions/2024_03_26_0948-aafcd7fb0e84_spool_weights.py @@ -8,8 +8,8 @@ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = 'aafcd7fb0e84' -down_revision = 'b8881bdb716c' +revision = "aafcd7fb0e84" +down_revision = "b8881bdb716c" branch_labels = None depends_on = None @@ -17,14 +17,25 @@ depends_on = None def upgrade() -> None: """Perform the upgrade.""" # ### commands auto generated by Alembic - please adjust! ### - op.add_column('spool', sa.Column('initial_weight', sa.Float(), nullable=True, comment="The initial total weight of the spool (gross weight).")) - op.add_column('spool', sa.Column('empty_weight', sa.Float(), nullable=True, comment="The weight of the empty spool (tare weight).")) + op.add_column( + "spool", + sa.Column( + "initial_weight", + sa.Float(), + nullable=True, + comment="The initial total weight of the spool (gross weight).", + ), + ) + op.add_column( + "spool", + sa.Column("empty_weight", sa.Float(), nullable=True, comment="The weight of the empty spool (tare weight)."), + ) # ### end Alembic commands ### def downgrade() -> None: """Perform the downgrade.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('spool', 'empty_weight') - op.drop_column('spool', 'initial_weight') + op.drop_column("spool", "empty_weight") + op.drop_column("spool", "initial_weight") # ### end Alembic commands ### diff --git a/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py b/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py index 73861be..fc30c42 100644 --- a/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py +++ b/migrations/versions/2024_03_26_1349-304a32906234_spool_weight_population.py @@ -8,29 +8,46 @@ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = '304a32906234' -down_revision = 'aafcd7fb0e84' +revision = "304a32906234" +down_revision = "aafcd7fb0e84" branch_labels = None depends_on = None def upgrade() -> None: - filament = sa.Table('filament', + """Pre-populate the spool weights.""" + """This must be done in a separate migration because""" + """of cockroachdb's execution of alembic migrations""" + filament = sa.Table( + "filament", sa.MetaData(), - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('weight', sa.Float(), nullable=True), - sa.Column('spool_weight', sa.Float(), nullable=True)) - + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("weight", sa.Float(), nullable=True), + sa.Column("spool_weight", sa.Float(), nullable=True), + ) + spool = sa.Table( - 'spool', + "spool", sa.MetaData(), - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('filament_id', sa.Integer), - sa.Column('initial_weight', sa.Float(), nullable=False, comment="The initial total weight of the spool (gross weight)."), - sa.Column('empty_weight', sa.Float(), nullable=False, comment="The weight of the empty spool (tare weight).") - ) - - initial_weight = sa.select((filament.c.weight + filament.c.spool_weight).label("initial_weight")).where(filament.c.id == spool.c.filament_id).scalar_subquery() + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("filament_id", sa.Integer), + sa.Column( + "initial_weight", + sa.Float(), + nullable=False, + ), + sa.Column( + "empty_weight", + sa.Float(), + nullable=False, + ), + ) + + initial_weight = ( + sa.select((filament.c.weight + filament.c.spool_weight).label("initial_weight")) + .where(filament.c.id == spool.c.filament_id) + .scalar_subquery() + ) empty_weight = sa.select(filament.c.spool_weight).where(filament.c.id == spool.c.filament_id).scalar_subquery() set_initial_weight = sa.update(spool).values(initial_weight=initial_weight) @@ -43,4 +60,3 @@ def upgrade() -> None: def downgrade() -> None: """Perform the downgrade.""" - pass diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index ca384d8..7da98ec 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -194,12 +194,8 @@ async def update( if k == "filament_id": spool.filament = await filament.get_by_id(db, v) # If there is no initial_weight, calculate it from the filament weight - if ( - spool.initial_weight is None - and spool.empty_weight is None - and spool.filament.weight is not None - ): - spool_weight = (spool.empty_weight if spool.empty_weight is not None else 0) + if spool.initial_weight is None and spool.empty_weight is None and spool.filament.weight is not None: + spool_weight = spool.empty_weight if spool.empty_weight is not None else 0 spool.initial_weight = spool.filament.weight + spool_weight spool.empty_weight = spool_weight From e68f88da2ad59f408e95b06344f22e2e3b4328f9 Mon Sep 17 00:00:00 2001 From: Matt Gerega Date: Tue, 26 Mar 2024 20:35:50 -0400 Subject: [PATCH 03/15] Added empty spool weight as an option field for the vendor --- ...-5f069e51bd89_vendor_empty_spool_weight.py | 31 +++++++++++++++++++ spoolman/database/filament.py | 3 ++ spoolman/database/models.py | 5 +-- spoolman/database/spool.py | 3 ++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 migrations/versions/2024_03_26_1507-5f069e51bd89_vendor_empty_spool_weight.py diff --git a/migrations/versions/2024_03_26_1507-5f069e51bd89_vendor_empty_spool_weight.py b/migrations/versions/2024_03_26_1507-5f069e51bd89_vendor_empty_spool_weight.py new file mode 100644 index 0000000..7403eaf --- /dev/null +++ b/migrations/versions/2024_03_26_1507-5f069e51bd89_vendor_empty_spool_weight.py @@ -0,0 +1,31 @@ +"""vendor empty spool weight. + +Revision ID: 5f069e51bd89 +Revises: 304a32906234 +Create Date: 2024-03-26 15:07:18.366290 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "5f069e51bd89" +down_revision = "304a32906234" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Perform the upgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "vendor", + sa.Column("empty_spool_weight", sa.Float(), nullable=True, comment="The weight of an empty spool."), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Perform the downgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("vendor", "empty_spool_weight") + # ### end Alembic commands ### diff --git a/spoolman/database/filament.py b/spoolman/database/filament.py index a87f27a..a8f472a 100644 --- a/spoolman/database/filament.py +++ b/spoolman/database/filament.py @@ -48,6 +48,9 @@ async def create( vendor_item: Optional[models.Vendor] = None if vendor_id is not None: vendor_item = await vendor.get_by_id(db, vendor_id) + # default spool weight from vendor + if spool_weight is None and vendor_item.empty_spool_weight is not None: + spool_weight = vendor_item.empty_spool_weight filament = models.Filament( name=name, diff --git a/spoolman/database/models.py b/spoolman/database/models.py index b009bdf..1a2d5bd 100644 --- a/spoolman/database/models.py +++ b/spoolman/database/models.py @@ -18,6 +18,7 @@ class Vendor(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) registered: Mapped[datetime] = mapped_column() name: Mapped[str] = mapped_column(String(64)) + empty_spool_weight: Mapped[Optional[float]] = mapped_column(comment="The weight of an empty spool.") comment: Mapped[Optional[str]] = mapped_column(String(1024)) filaments: Mapped[list["Filament"]] = relationship(back_populates="vendor") extra: Mapped[list["VendorField"]] = relationship( @@ -64,8 +65,8 @@ class Spool(Base): price: Mapped[Optional[float]] = mapped_column() filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id")) filament: Mapped["Filament"] = relationship(back_populates="spools") - initial_weight: Mapped[float] = mapped_column() - empty_weight: Mapped[float] = mapped_column() + initial_weight: Mapped[Optional[float]] = mapped_column() + empty_weight: Mapped[Optional[float]] = mapped_column() used_weight: Mapped[float] = mapped_column() location: Mapped[Optional[str]] = mapped_column(String(64)) lot_nr: Mapped[Optional[str]] = mapped_column(String(64)) diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index 7da98ec..42d2e5e 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -235,6 +235,7 @@ async def use_weight_safe(db: AsyncSession, spool_id: int, weight: float) -> Non db (AsyncSession): Database session spool_id (int): Spool ID weight (float): Filament weight to consume, in grams + """ await db.execute( sqlalchemy.update(models.Spool) @@ -261,6 +262,7 @@ async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.S Returns: models.Spool: Updated spool object + """ await use_weight_safe(db, spool_id, weight) @@ -288,6 +290,7 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S Returns: models.Spool: Updated spool object + """ # Get filament diameter and density result = await db.execute( From 3aed735ceae4d6b5de153a4f92bece08987a8cf3 Mon Sep 17 00:00:00 2001 From: Matt Gerega Date: Tue, 26 Mar 2024 21:26:41 -0400 Subject: [PATCH 04/15] Updated vendor API to correct issues - Implement new UI --- client/public/locales/en/common.json | 4 ++++ client/src/pages/vendors/create.tsx | 18 +++++++++++++++++- client/src/pages/vendors/edit.tsx | 18 +++++++++++++++++- client/src/pages/vendors/list.tsx | 13 +++++++++++-- client/src/pages/vendors/model.tsx | 1 + client/src/pages/vendors/show.tsx | 2 ++ spoolman/api/v1/models.py | 2 ++ spoolman/api/v1/vendor.py | 11 ++++++----- spoolman/database/vendor.py | 2 ++ 9 files changed, 62 insertions(+), 9 deletions(-) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 53160f8..16d7800 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -227,9 +227,13 @@ "fields": { "id": "ID", "name": "Name", + "empty_spool_weight": "Empty Spool Weight", "registered": "Registered", "comment": "Comment" }, + "fields_help": { + "empty_spool_weight": "The weight of an empty spool from this vendor." + }, "titles": { "create": "Create Vendor", "clone": "Clone Vendor", diff --git a/client/src/pages/vendors/create.tsx b/client/src/pages/vendors/create.tsx index 9649ccd..69ac0ae 100644 --- a/client/src/pages/vendors/create.tsx +++ b/client/src/pages/vendors/create.tsx @@ -1,7 +1,7 @@ import React from "react"; import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core"; import { Create, useForm } from "@refinedev/antd"; -import { Button, Form, Input, Typography } from "antd"; +import { Button, Form, Input, Typography, InputNumber } from "antd"; import TextArea from "antd/es/input/TextArea"; import { IVendor, IVendorParsedExtras } from "./model"; import { EntityType, useGetFields } from "../../utils/queryFields"; @@ -85,6 +85,22 @@ export const VendorCreate: React.FC