swiftops-backend / src /app /models /inventory_transfer.py
kamau1's picture
feat: inventory management init
db7b74e
"""
Inventory Transfer Models - Agent-to-Agent Equipment Transfers
WORKFLOW:
1. Agent A initiates transfer to Agent B
2. Optional: PM/Dispatcher approves (if requires_approval=true)
3. Agent B accepts transfer
4. System creates new assignment for Agent B
5. Original assignment marked as transferred
6. Transfer marked as completed
USE CASES:
- Equipment shortage: Agent with extra equipment helps another agent
- Job reassignment: Transfer equipment when job is reassigned
- Emergency: Urgent need for equipment, faster than going to hub
"""
from sqlalchemy import Column, String, Boolean, Integer, Text, DateTime, Numeric, ForeignKey, CheckConstraint, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from datetime import datetime
from decimal import Decimal
from app.models.base import BaseModel
class InventoryTransfer(BaseModel):
"""
Agent-to-agent inventory transfers
Tracks peer-to-peer equipment transfers between field agents.
Supports approval workflow and acceptance by recipient.
"""
__tablename__ = "inventory_transfers"
# Source and destination
from_assignment_id = Column(UUID(as_uuid=True), ForeignKey("inventory_assignments.id", ondelete="RESTRICT"), nullable=False)
from_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False)
to_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False)
to_assignment_id = Column(UUID(as_uuid=True), ForeignKey("inventory_assignments.id", ondelete="SET NULL"))
# Transfer details
transfer_reason = Column(Text, nullable=False)
quantity = Column(Numeric(10, 2), nullable=False, default=1)
unit_identifier = Column(Text, nullable=False)
# Approval workflow
status = Column(String, nullable=False, default="pending") # TransferStatus enum
requires_approval = Column(Boolean, nullable=False, default=True)
approved_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"))
approved_at = Column(DateTime(timezone=True))
approval_notes = Column(Text)
# Rejection
rejected_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"))
rejected_at = Column(DateTime(timezone=True))
rejection_reason = Column(Text)
# Acceptance by recipient
accepted_by_to_user_at = Column(DateTime(timezone=True))
# Completion
completed_at = Column(DateTime(timezone=True))
# Location tracking
transfer_latitude = Column(Numeric(10, 7))
transfer_longitude = Column(Numeric(10, 7))
transfer_location_name = Column(Text)
location_verified = Column(Boolean, default=False)
# Metadata
notes = Column(Text)
additional_metadata = Column(JSONB, default={})
# Audit
initiated_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False)
# Relationships
from_assignment = relationship("InventoryAssignment", foreign_keys=[from_assignment_id],
backref="outgoing_transfers")
to_assignment = relationship("InventoryAssignment", foreign_keys=[to_assignment_id],
backref="incoming_transfers")
from_user = relationship("User", foreign_keys=[from_user_id])
to_user = relationship("User", foreign_keys=[to_user_id])
approved_by = relationship("User", foreign_keys=[approved_by_user_id])
rejected_by = relationship("User", foreign_keys=[rejected_by_user_id])
initiated_by = relationship("User", foreign_keys=[initiated_by_user_id])
# Table constraints
__table_args__ = (
CheckConstraint("quantity > 0", name="chk_positive_transfer_quantity"),
CheckConstraint("from_user_id != to_user_id", name="chk_different_users"),
Index("idx_inventory_transfers_from_assignment", "from_assignment_id",
postgresql_where=(Column("deleted_at").is_(None))),
Index("idx_inventory_transfers_to_assignment", "to_assignment_id",
postgresql_where=(Column("to_assignment_id").isnot(None)) & (Column("deleted_at").is_(None))),
Index("idx_inventory_transfers_from_user", "from_user_id", "status", "created_at",
postgresql_where=(Column("deleted_at").is_(None))),
Index("idx_inventory_transfers_to_user", "to_user_id", "status", "created_at",
postgresql_where=(Column("deleted_at").is_(None))),
Index("idx_inventory_transfers_status", "status", "created_at",
postgresql_where=(Column("deleted_at").is_(None))),
Index("idx_inventory_transfers_pending", "to_user_id", "status",
postgresql_where=(Column("status") == "pending") & (Column("deleted_at").is_(None))),
Index("idx_inventory_transfers_metadata_gin", "additional_metadata", postgresql_using="gin"),
)
@property
def is_pending(self) -> bool:
"""Check if transfer is pending approval or acceptance"""
return self.status == "pending"
@property
def is_completed(self) -> bool:
"""Check if transfer is completed"""
return self.status == "completed"
@property
def can_approve(self) -> bool:
"""Check if transfer can be approved"""
return self.status == "pending" and self.requires_approval and not self.approved_at
@property
def can_accept(self) -> bool:
"""Check if transfer can be accepted by recipient"""
if self.requires_approval:
return self.status == "approved" and not self.accepted_by_to_user_at
return self.status == "pending" and not self.accepted_by_to_user_at
def __repr__(self):
return f"<InventoryTransfer(id={self.id}, from={self.from_user_id}, to={self.to_user_id}, status={self.status})>"