This commit is contained in:
2026-03-25 23:59:29 +08:00
parent 7964ce1033
commit 23e5df0364
46 changed files with 4168 additions and 254 deletions
+2
View File
@@ -25,6 +25,7 @@ from .purchase_order_routes import router as purchase_order_router
from .sales_order_routes import router as sales_order_router
from .dashboard_routes import router as dashboard_router
from .finance_routes import router as finance_router
from .material_routes import router as material_router
inventory_router = APIRouter(prefix="/api", tags=["进销存"])
@@ -36,6 +37,7 @@ inventory_router.include_router(inventory_management_router)
inventory_router.include_router(stock_movement_router)
inventory_router.include_router(purchase_order_router)
inventory_router.include_router(sales_order_router)
inventory_router.include_router(material_router)
inventory_router.include_router(dashboard_router)
inventory_router.include_router(finance_router)
+327
View File
@@ -0,0 +1,327 @@
"""
物料管理路由模块
提供物料价格历史和供应商关联管理功能,包括:
- 物料价格历史记录
- 物料供应商关联管理
- 物料价格趋势分析
路由前缀: /api/materials
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from typing import Optional, List
from database.database import get_db_session
from services.auth_service import get_current_active_user
from models.database import User, Product, MaterialPriceHistory, MaterialSupplier, Supplier
from .schemas import (
MaterialPriceHistoryCreate,
MaterialPriceHistoryResponse,
MaterialSupplierCreate,
MaterialSupplierResponse,
MaterialPriceTrendResponse
)
router = APIRouter(prefix="/materials", tags=["物料管理"])
async def _get_product(db_session: AsyncSession, product_id: int) -> Product:
result = await db_session.execute(
select(Product).where(Product.id == product_id, Product.is_active == True)
)
product = result.scalar_one_or_none()
if not product:
raise HTTPException(status_code=404, detail="物料不存在")
if product.item_type != "material":
raise HTTPException(status_code=400, detail="仅物料类型支持价格历史管理")
return product
@router.post("/{product_id}/price-history", response_model=MaterialPriceHistoryResponse, status_code=201)
async def add_material_price_history(
product_id: int,
price_data: MaterialPriceHistoryCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
product = await _get_product(db_session, product_id)
# 检查供应商是否存在
if price_data.supplier_id:
supplier_result = await db_session.execute(
select(Supplier).where(Supplier.id == price_data.supplier_id, Supplier.is_active == True)
)
if not supplier_result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="供应商不存在")
price_history = MaterialPriceHistory(
product_id=product_id,
price=price_data.price,
supplier_id=price_data.supplier_id,
remark=price_data.remark
)
db_session.add(price_history)
await db_session.commit()
await db_session.refresh(price_history)
# 更新产品的成本价格为最新价格
product.cost_price = price_data.price
await db_session.commit()
return MaterialPriceHistoryResponse(
id=price_history.id,
product_id=price_history.product_id,
product_sku=product.sku,
product_name=product.name,
price=price_history.price,
effective_date=price_history.effective_date,
supplier_id=price_history.supplier_id,
supplier_name=price_history.supplier.name if price_history.supplier else None,
remark=price_history.remark,
created_at=price_history.created_at
)
@router.get("/{product_id}/price-history", response_model=List[MaterialPriceHistoryResponse])
async def get_material_price_history(
product_id: int,
limit: int = Query(20, ge=1, le=100),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
product = await _get_product(db_session, product_id)
result = await db_session.execute(
select(MaterialPriceHistory)
.where(MaterialPriceHistory.product_id == product_id)
.order_by(desc(MaterialPriceHistory.effective_date))
.limit(limit)
)
price_history_list = result.scalars().all()
return [
MaterialPriceHistoryResponse(
id=ph.id,
product_id=ph.product_id,
product_sku=product.sku,
product_name=product.name,
price=ph.price,
effective_date=ph.effective_date,
supplier_id=ph.supplier_id,
supplier_name=ph.supplier.name if ph.supplier else None,
remark=ph.remark,
created_at=ph.created_at
)
for ph in price_history_list
]
@router.get("/{product_id}/price-trend", response_model=MaterialPriceTrendResponse)
async def get_material_price_trend(
product_id: int,
months: int = Query(6, ge=1, le=24),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
product = await _get_product(db_session, product_id)
# 计算价格趋势
result = await db_session.execute(
select(MaterialPriceHistory)
.where(MaterialPriceHistory.product_id == product_id)
.order_by(desc(MaterialPriceHistory.effective_date))
.limit(months)
)
price_history_list = result.scalars().all()
if not price_history_list:
raise HTTPException(status_code=404, detail="无价格历史记录")
prices = [ph.price for ph in reversed(price_history_list)]
dates = [ph.effective_date for ph in reversed(price_history_list)]
# 计算价格变化
current_price = price_history_list[0].price
first_price = price_history_list[-1].price
price_change = current_price - first_price
price_change_percent = (price_change / first_price * 100) if first_price > 0 else 0
return MaterialPriceTrendResponse(
product_id=product_id,
product_sku=product.sku,
product_name=product.name,
current_price=current_price,
price_change=round(price_change, 2),
price_change_percent=round(price_change_percent, 2),
price_history=[
{
"date": ph.effective_date,
"price": ph.price,
"supplier_name": ph.supplier.name if ph.supplier else None
}
for ph in price_history_list
]
)
@router.post("/{product_id}/suppliers", response_model=MaterialSupplierResponse, status_code=201)
async def add_material_supplier(
product_id: int,
supplier_data: MaterialSupplierCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
product = await _get_product(db_session, product_id)
# 检查供应商是否存在
supplier_result = await db_session.execute(
select(Supplier).where(Supplier.id == supplier_data.supplier_id, Supplier.is_active == True)
)
supplier = supplier_result.scalar_one_or_none()
if not supplier:
raise HTTPException(status_code=400, detail="供应商不存在")
# 检查是否已存在关联
existing = await db_session.execute(
select(MaterialSupplier)
.where(
MaterialSupplier.product_id == product_id,
MaterialSupplier.supplier_id == supplier_data.supplier_id
)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="该供应商已关联到该物料")
# 如果设置为主要供应商,将其他供应商设置为非主要
if supplier_data.is_primary:
await db_session.execute(
MaterialSupplier.__table__.update()
.where(MaterialSupplier.product_id == product_id)
.values(is_primary=False)
)
material_supplier = MaterialSupplier(
product_id=product_id,
supplier_id=supplier_data.supplier_id,
is_primary=supplier_data.is_primary,
contact_person=supplier_data.contact_person,
contact_phone=supplier_data.contact_phone,
lead_time=supplier_data.lead_time,
min_order_quantity=supplier_data.min_order_quantity
)
db_session.add(material_supplier)
await db_session.commit()
await db_session.refresh(material_supplier)
return MaterialSupplierResponse(
id=material_supplier.id,
product_id=material_supplier.product_id,
product_sku=product.sku,
product_name=product.name,
supplier_id=material_supplier.supplier_id,
supplier_name=supplier.name,
is_primary=material_supplier.is_primary,
contact_person=material_supplier.contact_person,
contact_phone=material_supplier.contact_phone,
lead_time=material_supplier.lead_time,
min_order_quantity=material_supplier.min_order_quantity,
created_at=material_supplier.created_at,
updated_at=material_supplier.updated_at
)
@router.get("/{product_id}/suppliers", response_model=List[MaterialSupplierResponse])
async def get_material_suppliers(
product_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
product = await _get_product(db_session, product_id)
result = await db_session.execute(
select(MaterialSupplier)
.where(MaterialSupplier.product_id == product_id)
.order_by(MaterialSupplier.is_primary.desc(), MaterialSupplier.id.asc())
)
supplier_list = result.scalars().all()
return [
MaterialSupplierResponse(
id=ms.id,
product_id=ms.product_id,
product_sku=product.sku,
product_name=product.name,
supplier_id=ms.supplier_id,
supplier_name=ms.supplier.name if ms.supplier else None,
is_primary=ms.is_primary,
contact_person=ms.contact_person,
contact_phone=ms.contact_phone,
lead_time=ms.lead_time,
min_order_quantity=ms.min_order_quantity,
created_at=ms.created_at,
updated_at=ms.updated_at
)
for ms in supplier_list
]
@router.delete("/suppliers/{supplier_id}")
async def remove_material_supplier(
supplier_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
result = await db_session.execute(
select(MaterialSupplier).where(MaterialSupplier.id == supplier_id)
)
material_supplier = result.scalar_one_or_none()
if not material_supplier:
raise HTTPException(status_code=404, detail="物料供应商关联不存在")
await db_session.delete(material_supplier)
await db_session.commit()
return {"message": "物料供应商关联已删除"}
@router.get("/suppliers/{supplier_id}/materials", response_model=List[MaterialSupplierResponse])
async def get_supplier_materials(
supplier_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
# 检查供应商是否存在
supplier_result = await db_session.execute(
select(Supplier).where(Supplier.id == supplier_id, Supplier.is_active == True)
)
supplier = supplier_result.scalar_one_or_none()
if not supplier:
raise HTTPException(status_code=404, detail="供应商不存在")
result = await db_session.execute(
select(MaterialSupplier)
.where(MaterialSupplier.supplier_id == supplier_id)
.order_by(MaterialSupplier.is_primary.desc(), MaterialSupplier.id.asc())
)
material_list = result.scalars().all()
return [
MaterialSupplierResponse(
id=ms.id,
product_id=ms.product_id,
product_sku=ms.product.sku if ms.product else None,
product_name=ms.product.name if ms.product else None,
supplier_id=ms.supplier_id,
supplier_name=supplier.name,
is_primary=ms.is_primary,
contact_person=ms.contact_person,
contact_phone=ms.contact_phone,
lead_time=ms.lead_time,
min_order_quantity=ms.min_order_quantity,
created_at=ms.created_at,
updated_at=ms.updated_at
)
for ms in material_list
]
+19 -10
View File
@@ -144,16 +144,25 @@ async def _apply_order_items(
raise HTTPException(status_code=400, detail=f"物料不存在: {item_data.product_id}")
if product.item_type != "material":
raise HTTPException(status_code=400, detail=f"采购单仅允许物料: {product.name}")
item = PurchaseOrderItem(
order_id=order.id,
product_id=item_data.product_id,
quantity=item_data.quantity,
unit_price=item_data.unit_price,
amount=item_data.quantity * item_data.unit_price,
remark=item_data.remark
)
db_session.add(item)
total_amount += item.amount
# 使用物料的成本价格作为单价,忽略前端提交的单价
unit_price = product.cost_price or 0
if unit_price <= 0:
raise HTTPException(status_code=400, detail=f"物料 {product.name} 未设置成本价格,请在物料管理界面设置")
try:
item = PurchaseOrderItem(
order_id=order.id,
product_id=item_data.product_id,
quantity=int(item_data.quantity),
unit_price=float(unit_price),
amount=float(item_data.quantity) * float(unit_price),
remark=item_data.remark or None
)
db_session.add(item)
total_amount += item.amount
except Exception as e:
raise HTTPException(status_code=400, detail=f"创建订单明细失败: {str(e)}")
return total_amount
+66 -35
View File
@@ -10,7 +10,7 @@
"""
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, delete
from sqlalchemy import select, func, delete, update
from typing import Optional, List
from math import ceil
@@ -51,6 +51,9 @@ def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesO
customer_name=customer_name,
order_date=order.order_date,
delivery_date=order.delivery_date,
manufacturing_date=order.manufacturing_date,
actual_delivery_date=order.actual_delivery_date,
actual_payment_date=order.actual_payment_date,
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
@@ -79,6 +82,9 @@ async def _build_sales_order_detail_response(
customer_name=customer_name,
order_date=order.order_date,
delivery_date=order.delivery_date,
manufacturing_date=order.manufacturing_date,
actual_delivery_date=order.actual_delivery_date,
actual_payment_date=order.actual_payment_date,
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
@@ -125,27 +131,28 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
if not order_items:
return [], 0
finished_ids = list({int(i.product_id) for i in order_items})
bom_result = await db_session.execute(
select(ProductMaterial, Product)
.join(Product, ProductMaterial.material_product_id == Product.id)
.where(ProductMaterial.finished_product_id.in_(finished_ids))
.where(Product.is_active == True)
.where(Product.item_type == "material")
)
bom_rows = bom_result.all()
if not bom_rows:
return [], 0
bom_by_finished_id = {}
for bom, material in bom_rows:
bom_by_finished_id.setdefault(int(bom.finished_product_id), []).append((bom, material))
required_qty_map = {}
for order_item in order_items:
bom_result = await db_session.execute(
select(ProductMaterial, Product)
.join(Product, ProductMaterial.material_product_id == Product.id)
.where(ProductMaterial.finished_product_id == order_item.product_id)
.where(Product.is_active == True)
.where(Product.item_type == "material")
)
bom_items = bom_result.all()
if not bom_items:
continue
bom_items = bom_by_finished_id.get(int(order_item.product_id)) or []
for bom, material in bom_items:
qty = float(order_item.quantity) * float(bom.quantity or 0) * (1 + float(bom.loss_rate or 0))
entry = required_qty_map.setdefault(
material.id,
{
"material": material,
"required_qty": 0.0
}
)
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": 0.0})
entry["required_qty"] += qty
if not required_qty_map:
@@ -220,18 +227,19 @@ async def _issue_materials_for_order_creation(
movement_count = 0
for item in plan_items:
inv_result = await db_session.execute(
select(Inventory)
upd_result = await db_session.execute(
update(Inventory)
.where(Inventory.product_id == item.material_id)
.where(Inventory.warehouse_id == warehouse.id)
.where(Inventory.quantity >= item.required_quantity)
.values(quantity=Inventory.quantity - item.required_quantity)
.returning(Inventory.quantity)
)
inventory = inv_result.scalar_one_or_none()
if not inventory or inventory.quantity < item.required_quantity:
after_qty = upd_result.scalar_one_or_none()
if after_qty is None:
raise HTTPException(status_code=400, detail=f"{item.material_name} 在默认仓库库存不足")
before_qty = inventory.quantity
inventory.quantity -= item.required_quantity
after_qty = inventory.quantity
after_qty = int(after_qty)
before_qty = after_qty + int(item.required_quantity)
total_amount = item.required_quantity * item.unit_cost
actual_material_cost += total_amount
@@ -401,10 +409,15 @@ async def create_sales_order(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
from datetime import datetime
now = datetime.now()
order = SalesOrder(
order_no=generate_order_no("SO"),
customer_id=order_data.customer_id,
order_date=now,
delivery_date=order_data.delivery_date,
manufacturing_date=now.date(),
created_at=now,
remark=order_data.remark,
operator_id=current_user.id,
status="manufacturing"
@@ -441,6 +454,8 @@ async def update_sales_order(
current_user: User = Depends(get_current_active_user)
):
order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "delivered":
raise HTTPException(status_code=400, detail="已交付的销售订单禁止修改")
await _rollback_issued_materials(db_session, order, current_user)
await db_session.execute(delete(SalesOrderItem).where(SalesOrderItem.order_id == order.id))
@@ -475,6 +490,17 @@ async def update_sales_order_status(
raise HTTPException(status_code=400, detail="订单状态必须为 manufacturing、delivered、paid")
order, customer = await _get_sales_order_with_customer(db_session, order_id)
# 只禁止从已交付状态改为非已收款状态
if order.status == "delivered" and payload.status != "paid":
raise HTTPException(status_code=400, detail="已交付的销售订单只能修改为已收款状态")
# 根据状态更新相应的日期字段
from datetime import datetime
if payload.status == "delivered" and not order.actual_delivery_date:
order.actual_delivery_date = datetime.now()
elif payload.status == "paid" and not order.actual_payment_date:
order.actual_payment_date = datetime.now()
order.status = payload.status
await db_session.commit()
await db_session.refresh(order)
@@ -488,6 +514,8 @@ async def delete_sales_order(
current_user: User = Depends(get_current_active_user)
):
order, _ = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "delivered":
raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除")
await _rollback_issued_materials(db_session, order, current_user)
await db_session.delete(order)
await db_session.commit()
@@ -522,6 +550,8 @@ async def issue_sales_order_materials(
current_user: User = Depends(get_current_active_user)
):
order, _ = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "delivered":
raise HTTPException(status_code=400, detail="已交付的销售订单禁止领料")
if order.production_status == "completed":
raise HTTPException(status_code=400, detail="该销售单已完成生产")
if order.production_status == "material_issued":
@@ -548,18 +578,19 @@ async def issue_sales_order_materials(
actual_material_cost = 0.0
movement_count = 0
for item in plan_items:
inv_result = await db_session.execute(
select(Inventory)
upd_result = await db_session.execute(
update(Inventory)
.where(Inventory.product_id == item.material_id)
.where(Inventory.warehouse_id == warehouse.id)
.where(Inventory.quantity >= item.required_quantity)
.values(quantity=Inventory.quantity - item.required_quantity)
.returning(Inventory.quantity)
)
inventory = inv_result.scalar_one_or_none()
if not inventory or inventory.quantity < item.required_quantity:
after_qty = upd_result.scalar_one_or_none()
if after_qty is None:
raise HTTPException(status_code=400, detail=f"{item.material_name} 在所选仓库库存不足")
before_qty = inventory.quantity
inventory.quantity -= item.required_quantity
after_qty = inventory.quantity
after_qty = int(after_qty)
before_qty = after_qty + int(item.required_quantity)
total_amount = item.required_quantity * item.unit_cost
actual_material_cost += total_amount
@@ -586,7 +617,7 @@ async def issue_sales_order_materials(
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
if order.status == "draft":
order.status = "pending"
order.status = "manufacturing"
await db_session.commit()
+9
View File
@@ -47,6 +47,13 @@ from .finance_schemas import (
PartnerProductStatementItemResponse,
FinancePartnerProductStatementResponse
)
from .material_schemas import (
MaterialPriceHistoryCreate,
MaterialPriceHistoryResponse,
MaterialSupplierCreate,
MaterialSupplierResponse,
MaterialPriceTrendResponse
)
__all__ = [
"ProductCreate", "ProductResponse", "ProductMaterialItemUpdate", "ProductBOMUpdate",
@@ -67,4 +74,6 @@ __all__ = [
"FinanceSummaryResponse", "ReceivableItemResponse", "PayableItemResponse",
"PartnerStatementItemResponse", "FinancePartnerStatementResponse",
"PartnerProductStatementItemResponse", "FinancePartnerProductStatementResponse",
"MaterialPriceHistoryCreate", "MaterialPriceHistoryResponse",
"MaterialSupplierCreate", "MaterialSupplierResponse", "MaterialPriceTrendResponse",
]
@@ -0,0 +1,80 @@
"""
物料管理相关数据模型
定义物料价格历史和物料供应商关联的数据结构
"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional, List, Dict
class MaterialPriceHistoryCreate(BaseModel):
"""创建物料价格历史的请求模型"""
price: float = Field(..., gt=0, description="物料价格")
supplier_id: Optional[int] = Field(None, description="供应商ID")
remark: Optional[str] = Field(None, description="备注")
class MaterialPriceHistoryResponse(BaseModel):
"""物料价格历史的响应模型"""
id: int
product_id: int
product_sku: str
product_name: str
price: float
effective_date: datetime
supplier_id: Optional[int]
supplier_name: Optional[str]
remark: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class MaterialSupplierCreate(BaseModel):
"""创建物料供应商关联的请求模型"""
supplier_id: int = Field(..., description="供应商ID")
is_primary: bool = Field(False, description="是否为主要供应商")
contact_person: Optional[str] = Field(None, description="联系人")
contact_phone: Optional[str] = Field(None, description="联系电话")
lead_time: Optional[int] = Field(None, ge=1, description="交货周期(天)")
min_order_quantity: Optional[int] = Field(None, ge=1, description="最小订购量")
class MaterialSupplierResponse(BaseModel):
"""物料供应商关联的响应模型"""
id: int
product_id: int
product_sku: str
product_name: str
supplier_id: int
supplier_name: str
is_primary: bool
contact_person: Optional[str]
contact_phone: Optional[str]
lead_time: Optional[int]
min_order_quantity: Optional[int]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class PriceHistoryItem(BaseModel):
"""价格历史项"""
date: datetime
price: float
supplier_name: Optional[str]
class MaterialPriceTrendResponse(BaseModel):
"""物料价格趋势的响应模型"""
product_id: int
product_sku: str
product_name: str
current_price: float
price_change: float
price_change_percent: float
price_history: List[PriceHistoryItem]
@@ -1,20 +1,20 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
from datetime import datetime, date
class PurchaseOrderItemCreate(BaseModel):
product_id: int
quantity: int
unit_price: float
quantity: int = Field(..., gt=0, description="采购数量")
unit_price: Optional[float] = Field(None, description="单价(可选,后端自动使用物料成本价格)")
remark: Optional[str] = None
class PurchaseOrderCreate(BaseModel):
supplier_id: int
expected_date: Optional[datetime] = None
expected_date: Optional[date] = None
remark: Optional[str] = None
items: List[PurchaseOrderItemCreate]
items: List[PurchaseOrderItemCreate] = Field(min_length=1)
class PurchaseOrderResponse(BaseModel):
@@ -22,7 +22,7 @@ class PurchaseOrderResponse(BaseModel):
order_no: str
supplier_name: str
order_date: datetime
expected_date: Optional[datetime]
expected_date: Optional[date]
status: str
total_amount: float
paid_amount: float
@@ -52,10 +52,10 @@ class PurchaseOrderDetailResponse(PurchaseOrderResponse):
class PurchaseOrderReceiveItem(BaseModel):
item_id: int
receive_quantity: int
receive_quantity: int = Field(gt=0)
class PurchaseOrderReceiveRequest(BaseModel):
warehouse_id: Optional[int] = None
items: List[PurchaseOrderReceiveItem]
items: List[PurchaseOrderReceiveItem] = Field(min_length=1)
remark: Optional[str] = None
@@ -1,6 +1,6 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
from datetime import datetime, date
class SalesOrderItemCreate(BaseModel):
@@ -9,16 +9,16 @@ class SalesOrderItemCreate(BaseModel):
product_name: Optional[str] = None
product_category: Optional[str] = None
product_unit: Optional[str] = "件"
quantity: int
unit_price: float
quantity: int = Field(gt=0)
unit_price: float = Field(ge=0)
remark: Optional[str] = None
class SalesOrderCreate(BaseModel):
customer_id: int
delivery_date: Optional[datetime] = None
delivery_date: Optional[date] = None
remark: Optional[str] = None
items: List[SalesOrderItemCreate]
items: List[SalesOrderItemCreate] = Field(min_length=1)
class SalesOrderResponse(BaseModel):
@@ -26,7 +26,10 @@ class SalesOrderResponse(BaseModel):
order_no: str
customer_name: str
order_date: datetime
delivery_date: Optional[datetime]
delivery_date: Optional[date]
manufacturing_date: Optional[date]
actual_delivery_date: Optional[datetime]
actual_payment_date: Optional[datetime]
status: str
production_status: str
production_no: Optional[str]