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
14 changes: 8 additions & 6 deletions api/models/part.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import date
from datetime import datetime
from typing import List, Optional

from sqlalchemy import Boolean, Column, Date, Float, ForeignKey, Integer, String, Table
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Table
from sqlalchemy.orm import Mapped, mapped_column, relationship

from api.models.base import Base
Expand Down Expand Up @@ -50,14 +50,16 @@ class PartsUsed(Base):
__tablename__ = "PartsUsed"

id: Mapped[int] = mapped_column(Integer, primary_key=True)
meter_activity_id: Mapped[int] = mapped_column(
ForeignKey("MeterActivities.id"), nullable=False
meter_activity_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("MeterActivities.id"), nullable=True
)
part_id: Mapped[int] = mapped_column(ForeignKey("Parts.id"), nullable=False)
count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
note: Mapped[Optional[str]] = mapped_column(String, nullable=True)

part: Mapped["Parts"] = relationship(back_populates="parts_used_links")
meter_activity: Mapped["MeterActivities"] = relationship(
meter_activity: Mapped[Optional["MeterActivities"]] = relationship(
back_populates="parts_used_links"
)

Expand All @@ -68,7 +70,7 @@ class PartsAdded(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
part_id: Mapped[int] = mapped_column(ForeignKey("Parts.id"), nullable=False)
count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
date: Mapped[date] = mapped_column(Date, nullable=False)
date: Mapped[datetime] = mapped_column(DateTime, nullable=False)
note: Mapped[str | None] = mapped_column(String, nullable=True)

part: Mapped["Parts"] = relationship()
17 changes: 5 additions & 12 deletions api/routes/meters.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,7 @@ def sort_by_field_to_schema_field(name: MeterSortByField):
dependencies=[Depends(ScopedUser.Admin)],
tags=["Meters"],
)
def create_meter(
new_meter: meter.SubmitNewMeter, db: Session = Depends(get_db)
):
def create_meter(new_meter: meter.SubmitNewMeter, db: Session = Depends(get_db)):
"""
Create a new meter. This requires a SN and meter type.
Status is infered from based on if a well is provided.
Expand All @@ -131,6 +129,7 @@ def create_meter(
serial_number=new_meter.serial_number,
contact_name=new_meter.contact_name,
contact_phone=new_meter.contact_phone,
notes=new_meter.notes,
meter_type_id=new_meter.meter_type.id,
price=new_meter.price,
status_id=warehouse_status_id,
Expand Down Expand Up @@ -250,9 +249,7 @@ def get_meters_locations(
meter_pm_query,
{"mids": meter_ids, "pm_activity_type_id": pm_activity_type_id},
).fetchall()
meter_pm_dict = {
row.meter_id: row.last_pm_meter_activity for row in meter_pm_rows
}
meter_pm_dict = {row.meter_id: row.last_pm_meter_activity for row in meter_pm_rows}

location_only_dict = {}

Expand Down Expand Up @@ -404,9 +401,7 @@ def update_meter_type(
dependencies=[Depends(ScopedUser.Admin)],
tags=["Meters"],
)
def create_meter_type(
new_meter_type: meter.MeterTypeLU, db: Session = Depends(get_db)
):
def create_meter_type(new_meter_type: meter.MeterTypeLU, db: Session = Depends(get_db)):
new_type_model = MeterTypeLU(
brand=new_meter_type.brand,
series=new_meter_type.series,
Expand Down Expand Up @@ -441,9 +436,7 @@ def get_land_owners(
response_model=meter.Meter,
tags=["Meters"],
)
def patch_meter(
updated_meter: meter.SubmitMeterUpdate, db: Session = Depends(get_db)
):
def patch_meter(updated_meter: meter.SubmitMeterUpdate, db: Session = Depends(get_db)):
"""
Update the current state of a meter. This is only used by Meter Details on the frontend.

Expand Down
12 changes: 12 additions & 0 deletions api/routes/parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ def add_parts(payload: parts.PartsAddRequest, db: Session = Depends(get_db)):
return part_service.add_parts(db, payload)


@part_router.post(
"/parts/decrease",
response_model=parts.Part,
dependencies=[Depends(ScopedUser.Admin)],
tags=["Parts"],
)
def decrease_parts(
payload: parts.PartsDecreaseRequest, db: Session = Depends(get_db)
):
return part_service.decrease_parts(db, payload)


@part_router.get(
"/parts/{part_id}/history",
response_model=parts.PartHistoryResponse,
Expand Down
16 changes: 12 additions & 4 deletions api/schemas/parts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import date, datetime
from datetime import datetime
from typing import List, Literal, Optional

from api.schemas.base import ORMBase
Expand Down Expand Up @@ -55,17 +55,25 @@ class PartUsed(ORMBase):
class PartsAddRequest(ORMBase):
part_id: int
count: int
date: date
date: datetime
note: Optional[str] = None


class PartsDecreaseRequest(ORMBase):
part_id: int
count: int
date: datetime
note: Optional[str] = None


class PartHistoryRow(ORMBase):
row_id: str
part_id: int
event_date: datetime
event_type: Literal["initial", "added", "used"]
event_type: Literal["initial", "added", "used", "workorder"]
ref_id: int | None = None
work_order_id: int | None = None
meter_activity_type: str | None = None
note: str | None = None
delta: int
total_after: int
Expand All @@ -74,7 +82,7 @@ class PartHistoryRow(ORMBase):
class PartHistoryUpdateRow(ORMBase):
ref_id: int
event_date: datetime
event_type: Literal["added", "used"]
event_type: Literal["added", "used", "workorder"]
note: str | None = None
delta: int

Expand Down
101 changes: 81 additions & 20 deletions api/services/parts.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from datetime import date, datetime, time
from datetime import date, datetime
from io import BytesIO
from pathlib import Path
from typing import Optional

from fastapi import HTTPException
from jinja2 import Environment, FileSystemLoader, select_autoescape
from sqlalchemy import func, literal, select, union_all
from sqlalchemy import case, func, literal, select, union_all
from sqlalchemy.orm import Session, selectinload
from weasyprint import HTML

from api.models.meter import MeterActivities, meterRegisters
from api.models.meter import ActivityTypeLU, MeterActivities, meterRegisters
from api.models.part import Parts, PartsAdded, PartsUsed
from api.schemas import parts

Expand Down Expand Up @@ -46,9 +46,7 @@ def _part_count_subqueries():
return used_subq, added_subq, current_count


def build_part_history_response(
part_id: int, db: Session
) -> parts.PartHistoryResponse:
def build_part_history_response(part_id: int, db: Session) -> parts.PartHistoryResponse:
part = db.scalars(select(Parts).where(Parts.id == part_id)).first()
if not part:
raise HTTPException(status_code=404, detail="Part not found")
Expand All @@ -61,19 +59,32 @@ def build_part_history_response(
PartsAdded.note.label("note"),
PartsAdded.count.label("delta"),
literal(None).label("work_order_id"),
literal(None).label("meter_activity_type"),
).where(PartsAdded.part_id == part_id)

used_q = (
select(
PartsUsed.id.label("ref_id"),
PartsUsed.part_id.label("part_id"),
MeterActivities.timestamp_start.label("event_date"),
literal("used").label("event_type"),
func.nullif(func.trim(MeterActivities.description), "").label("note"),
func.coalesce(
PartsUsed.date,
MeterActivities.timestamp_start,
func.now(),
).label("event_date"),
case(
(MeterActivities.work_order_id.is_not(None), literal("workorder")),
else_=literal("used"),
).label("event_type"),
func.coalesce(
func.nullif(func.trim(PartsUsed.note), ""),
func.nullif(func.trim(MeterActivities.description), ""),
).label("note"),
(-PartsUsed.count).label("delta"),
MeterActivities.work_order_id.label("work_order_id"),
ActivityTypeLU.name.label("meter_activity_type"),
)
.join(MeterActivities, MeterActivities.id == PartsUsed.meter_activity_id)
.outerjoin(MeterActivities, MeterActivities.id == PartsUsed.meter_activity_id)
.outerjoin(ActivityTypeLU, ActivityTypeLU.id == MeterActivities.activity_type_id)
.where(PartsUsed.part_id == part_id)
)

Expand All @@ -87,6 +98,7 @@ def build_part_history_response(
events.c.note,
events.c.delta,
events.c.work_order_id,
events.c.meter_activity_type,
).order_by(events.c.event_date.asc(), events.c.ref_id.asc())
).all()

Expand All @@ -102,12 +114,20 @@ def build_part_history_response(
delta=0,
total_after=running,
work_order_id=None,
meter_activity_type=None,
)
]

for ref_id, pid, event_date, event_type, note, delta, work_order_id in rows:
if not isinstance(event_date, datetime):
event_date = datetime.combine(event_date, time.min)
for (
ref_id,
pid,
event_date,
event_type,
note,
delta,
work_order_id,
meter_activity_type,
) in rows:
running += int(delta)
history.append(
parts.PartHistoryRow(
Expand All @@ -120,6 +140,7 @@ def build_part_history_response(
delta=int(delta),
total_after=running,
work_order_id=work_order_id,
meter_activity_type=meter_activity_type,
)
)

Expand Down Expand Up @@ -150,7 +171,9 @@ def list_parts(db: Session, in_use: Optional[bool] = None):
return results


def get_parts_used_summary(db: Session, from_date: date, to_date: date, parts: list[int]):
def get_parts_used_summary(
db: Session, from_date: date, to_date: date, parts: list[int]
):
start_dt = datetime.combine(from_date, datetime.min.time())
end_dt = datetime.combine(to_date, datetime.max.time())
usage_subq = (
Expand Down Expand Up @@ -236,8 +259,8 @@ def get_part(db: Session, part_id: int):
).first()
register_details_obj = None
if register_details is not None:
register_details_obj = (
parts.Register.register_details.model_validate(register_details)
register_details_obj = parts.Register.register_details.model_validate(
register_details
)
returned_part = parts.Register(
**returned_part.model_dump(exclude_unset=True),
Expand Down Expand Up @@ -276,6 +299,37 @@ def add_parts(db: Session, payload: parts.PartsAddRequest):
return part_obj


def decrease_parts(db: Session, payload: parts.PartsDecreaseRequest):
part = db.scalars(select(Parts).where(Parts.id == payload.part_id)).first()
if not part:
raise HTTPException(status_code=404, detail="Part not found")

db.add(
PartsUsed(
part_id=payload.part_id,
count=payload.count,
date=payload.date,
note=payload.note,
meter_activity_id=None,
)
)
db.commit()

used_subq, added_subq, current_count = _part_count_subqueries()
row = db.execute(
select(Parts, current_count)
.outerjoin(used_subq, used_subq.c.part_id == Parts.id)
.outerjoin(added_subq, added_subq.c.part_id == Parts.id)
.where(Parts.id == payload.part_id)
.options(selectinload(Parts.part_type), selectinload(Parts.meter_types))
).first()
if not row:
raise HTTPException(status_code=404, detail="Part not found")
part_obj, curr = row
part_obj.current_count = curr
return part_obj


def patch_part_history(
db: Session, part_id: int, payload: parts.PartHistoryUpdateRequest
):
Expand All @@ -301,9 +355,11 @@ def patch_part_history(
)
).first()
if not added_row:
raise HTTPException(status_code=404, detail="Parts added row not found.")
raise HTTPException(
status_code=404, detail="Parts added row not found."
)
added_row.count = row.delta
added_row.date = row.event_date.date()
added_row.date = row.event_date
added_row.note = normalized_note
continue

Expand All @@ -320,6 +376,13 @@ def patch_part_history(
).first()
if not parts_used_row:
raise HTTPException(status_code=404, detail="Parts used row not found.")
parts_used_row.count = abs(row.delta)
parts_used_row.note = normalized_note

if parts_used_row.meter_activity_id is None:
parts_used_row.date = row.event_date
continue

activity = db.scalars(
select(MeterActivities).where(
MeterActivities.id == parts_used_row.meter_activity_id
Expand All @@ -335,9 +398,7 @@ def patch_part_history(
if activity.timestamp_end and activity.timestamp_start
else None
)
parts_used_row.count = abs(row.delta)
activity.timestamp_start = row.event_date
activity.description = normalized_note
activity.timestamp_end = (
row.event_date + duration if duration is not None else row.event_date
)
Expand Down
18 changes: 9 additions & 9 deletions frontend/package-lock.json

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

Loading
Loading