feat: add partial shipment support (Phase 3)

- Add shipped_quantity field to OrderItem for tracking partial fulfillment
- Add partially_shipped order status for orders with partial shipments
- Add fulfill_item method for shipping individual items with quantities
- Add get_shipment_status method for detailed shipment tracking
- Add vendor API endpoints for partial shipment operations:
  - GET /orders/{id}/shipment-status - Get item-level shipment status
  - POST /orders/{id}/items/{item_id}/ship - Ship specific item quantity
- Automatic status updates: partially_shipped when some items shipped,
  shipped when all items fully shipped
- Migration to add shipped_quantity column with upgrade for existing data
- Update documentation with partial shipment usage examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 18:28:54 +01:00
parent 55c1a43f56
commit 5a3f2bce57
6 changed files with 597 additions and 5 deletions

View File

@@ -239,6 +239,37 @@ class Order(Base, TimestampMixin):
"""Check if this is a marketplace order."""
return self.channel != "direct"
@property
def is_fully_shipped(self) -> bool:
"""Check if all items are fully shipped."""
if not self.items:
return False
return all(item.is_fully_shipped for item in self.items)
@property
def is_partially_shipped(self) -> bool:
"""Check if some items are shipped but not all."""
if not self.items:
return False
has_shipped = any(item.shipped_quantity > 0 for item in self.items)
all_shipped = all(item.is_fully_shipped for item in self.items)
return has_shipped and not all_shipped
@property
def shipped_item_count(self) -> int:
"""Count of fully shipped items."""
return sum(1 for item in self.items if item.is_fully_shipped)
@property
def total_shipped_units(self) -> int:
"""Total quantity shipped across all items."""
return sum(item.shipped_quantity for item in self.items)
@property
def total_ordered_units(self) -> int:
"""Total quantity ordered across all items."""
return sum(item.quantity for item in self.items)
class OrderItem(Base, TimestampMixin):
"""
@@ -280,6 +311,9 @@ class OrderItem(Base, TimestampMixin):
inventory_reserved = Column(Boolean, default=False)
inventory_fulfilled = Column(Boolean, default=False)
# === Shipment Tracking ===
shipped_quantity = Column(Integer, default=0, nullable=False) # Units shipped so far
# === Exception Tracking ===
# True if product was not found by GTIN during import (linked to placeholder)
needs_product_match = Column(Boolean, default=False, index=True)
@@ -342,3 +376,20 @@ class OrderItem(Base, TimestampMixin):
if not self.exception:
return False
return self.exception.blocks_confirmation
# === SHIPMENT PROPERTIES ===
@property
def remaining_quantity(self) -> int:
"""Quantity not yet shipped."""
return max(0, self.quantity - self.shipped_quantity)
@property
def is_fully_shipped(self) -> bool:
"""Check if all units have been shipped."""
return self.shipped_quantity >= self.quantity
@property
def is_partially_shipped(self) -> bool:
"""Check if some but not all units have been shipped."""
return 0 < self.shipped_quantity < self.quantity