feat: add accessory Pydantic schemas
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
135
app/schemas/accessory.py
Normal file
135
app/schemas/accessory.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# app/schemas/accessory.py
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
PAICHAN_PATTERN = re.compile(r"^[A-Z]{1,2}\d{5}(-J)?$")
|
||||
LOCATION_NORMAL_PATTERN = re.compile(r"^[A-Z]+\d+-\d+-\d+$")
|
||||
LOCATION_TRANS_PATTERN = re.compile(r"^TRANS-\d+")
|
||||
|
||||
|
||||
# ── Work order query ──
|
||||
|
||||
|
||||
class WorkOrderGroup(BaseModel):
|
||||
work_order_no: str
|
||||
zongpai_nos: list[str]
|
||||
|
||||
|
||||
class WorkOrderQueryResponse(BaseModel):
|
||||
paichan_no: str
|
||||
work_orders: list[WorkOrderGroup]
|
||||
|
||||
|
||||
# ── Accessory CRUD ──
|
||||
|
||||
|
||||
class AccessoryCreateRequest(BaseModel):
|
||||
paichan_no: str
|
||||
zongpai_no: str
|
||||
accessory_type: str
|
||||
quantity: int
|
||||
location_code: str | None = None
|
||||
|
||||
@field_validator("paichan_no")
|
||||
@classmethod
|
||||
def validate_paichan_no(cls, v: str) -> str:
|
||||
v = v.strip().upper()
|
||||
if not PAICHAN_PATTERN.match(v):
|
||||
raise ValueError("INVALID_PAICHAN")
|
||||
return v
|
||||
|
||||
@field_validator("zongpai_no")
|
||||
@classmethod
|
||||
def validate_zongpai_no(cls, v: str) -> str:
|
||||
v = v.strip().upper()
|
||||
if not re.match(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$", v):
|
||||
raise ValueError("INVALID_ZONGPAI")
|
||||
return v
|
||||
|
||||
@field_validator("quantity")
|
||||
@classmethod
|
||||
def validate_quantity(cls, v: int) -> int:
|
||||
if v <= 0:
|
||||
raise ValueError("INVALID_QUANTITY")
|
||||
return v
|
||||
|
||||
@field_validator("location_code")
|
||||
@classmethod
|
||||
def validate_location_code(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip().upper()
|
||||
if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v):
|
||||
raise ValueError("INVALID_LOCATION")
|
||||
return v
|
||||
|
||||
|
||||
class AccessoryResponse(BaseModel):
|
||||
id: int
|
||||
paichan_no: str
|
||||
zongpai_no: str
|
||||
accessory_type: str
|
||||
quantity: int
|
||||
location_code: str | None = None
|
||||
is_boxed: bool = False
|
||||
created_at: str
|
||||
|
||||
|
||||
class AccessoryListResponse(BaseModel):
|
||||
paichan_no: str
|
||||
items: list[AccessoryResponse]
|
||||
|
||||
|
||||
class AccessoryUpdateRequest(BaseModel):
|
||||
accessory_type: str | None = None
|
||||
quantity: int | None = None
|
||||
location_code: str | None = None
|
||||
|
||||
@field_validator("quantity")
|
||||
@classmethod
|
||||
def validate_quantity(cls, v: int | None) -> int | None:
|
||||
if v is not None and v <= 0:
|
||||
raise ValueError("INVALID_QUANTITY")
|
||||
return v
|
||||
|
||||
@field_validator("location_code")
|
||||
@classmethod
|
||||
def validate_location_code(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip().upper()
|
||||
if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v):
|
||||
raise ValueError("INVALID_LOCATION")
|
||||
return v
|
||||
|
||||
|
||||
# ── Accessory type management ──
|
||||
|
||||
|
||||
class AccessoryTypeItem(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
sort_order: int
|
||||
|
||||
|
||||
class AccessoryTypeListResponse(BaseModel):
|
||||
types: list[AccessoryTypeItem]
|
||||
|
||||
|
||||
class AccessoryTypeCreateRequest(BaseModel):
|
||||
name: str
|
||||
sort_order: int = 0
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("INVALID_TYPE_NAME")
|
||||
return v
|
||||
|
||||
|
||||
class AccessoryTypeUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
sort_order: int | None = None
|
||||
Reference in New Issue
Block a user