This commit is contained in:
2026-05-11 09:34:50 +08:00
parent 79ca3613ae
commit 5e1d475a22
14 changed files with 336 additions and 264 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ async def get_dashboard(
] ]
return { return {
"product_count": finished_product_count, "finished_product_count": finished_product_count,
"material_count": material_count, "material_count": material_count,
"supplier_count": supplier_count, "supplier_count": supplier_count,
"customer_count": customer_count, "customer_count": customer_count,
+22 -7
View File
@@ -31,8 +31,12 @@ from .schemas import (
PartnerStatementItemResponse, PartnerStatementItemResponse,
FinancePartnerProductStatementResponse, FinancePartnerProductStatementResponse,
PartnerProductStatementItemResponse, PartnerProductStatementItemResponse,
PaginatedResponse,
) )
from .utils import generate_order_no from .utils import generate_order_no
from utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/finance", tags=["财务管理"]) router = APIRouter(prefix="/finance", tags=["财务管理"])
@@ -227,7 +231,7 @@ async def create_payment(
return _build_transaction_response(created) return _build_transaction_response(created)
@router.get("/transactions", response_model=List[FinanceTransactionResponse]) @router.get("/transactions", response_model=PaginatedResponse[FinanceTransactionResponse])
async def list_transactions( async def list_transactions(
txn_type: Optional[str] = None, txn_type: Optional[str] = None,
status: Optional[str] = "confirmed", status: Optional[str] = "confirmed",
@@ -238,22 +242,29 @@ async def list_transactions(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user), current_user: User = Depends(get_current_active_user),
): ):
query = ( base_query = (
select(FinanceTransaction) select(FinanceTransaction)
.options(selectinload(FinanceTransaction.allocations)) .options(selectinload(FinanceTransaction.allocations))
.order_by(FinanceTransaction.created_at.desc()) .order_by(FinanceTransaction.created_at.desc())
) )
if txn_type: if txn_type:
query = query.where(FinanceTransaction.txn_type == txn_type) base_query = base_query.where(FinanceTransaction.txn_type == txn_type)
if status: if status:
query = query.where(FinanceTransaction.status == status) base_query = base_query.where(FinanceTransaction.status == status)
if year is not None or quarter is not None: if year is not None or quarter is not None:
_, _, _, period_start, period_end = _resolve_period_scope(year, quarter) _, _, _, period_start, period_end = _resolve_period_scope(year, quarter)
query = query.where(FinanceTransaction.txn_date >= period_start).where(FinanceTransaction.txn_date < period_end) base_query = base_query.where(FinanceTransaction.txn_date >= period_start).where(FinanceTransaction.txn_date < period_end)
query = query.offset(skip).limit(limit)
count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query) result = await db_session.execute(query)
rows = result.scalars().all() rows = result.scalars().all()
return [_build_transaction_response(item) for item in rows] return PaginatedResponse(
items=[_build_transaction_response(item) for item in rows],
total=total, skip=skip, limit=limit
)
@router.post("/transactions/{transaction_id}/void") @router.post("/transactions/{transaction_id}/void")
@@ -287,6 +298,10 @@ async def void_transaction(
txn.status = "voided" txn.status = "voided"
await db_session.commit() await db_session.commit()
logger.warning(
"财务单据已作废: txn_no=%s txn_type=%s amount=%s operator_id=%s",
txn.txn_no, txn.txn_type, txn.amount, current_user.id
)
return {"message": "单据已作废"} return {"message": "单据已作废"}
+15 -9
View File
@@ -9,18 +9,18 @@
""" """
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select, update, func
from typing import Optional, List from typing import Optional, List
from database.database import get_db_session from database.database import get_db_session
from services.auth_service import get_current_active_user from services.auth_service import get_current_active_user
from models.database import User, Product, Warehouse, Inventory from models.database import User, Product, Warehouse, Inventory
from .schemas import InventoryResponse, InventoryCreate, InventoryUpdate from .schemas import InventoryResponse, InventoryCreate, InventoryUpdate, PaginatedResponse
router = APIRouter(prefix="/inventory", tags=["库存管理"]) router = APIRouter(prefix="/inventory", tags=["库存管理"])
@router.get("", response_model=List[InventoryResponse]) @router.get("", response_model=PaginatedResponse[InventoryResponse])
async def list_inventory( async def list_inventory(
warehouse_id: Optional[int] = None, warehouse_id: Optional[int] = None,
product_id: Optional[int] = None, product_id: Optional[int] = None,
@@ -30,7 +30,7 @@ async def list_inventory(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
query = ( base_query = (
select(Inventory, Product, Warehouse) select(Inventory, Product, Warehouse)
.join(Product, Inventory.product_id == Product.id) .join(Product, Inventory.product_id == Product.id)
.join(Warehouse, Inventory.warehouse_id == Warehouse.id) .join(Warehouse, Inventory.warehouse_id == Warehouse.id)
@@ -40,13 +40,16 @@ async def list_inventory(
) )
if warehouse_id: if warehouse_id:
query = query.where(Inventory.warehouse_id == warehouse_id) base_query = base_query.where(Inventory.warehouse_id == warehouse_id)
if product_id: if product_id:
query = query.where(Inventory.product_id == product_id) base_query = base_query.where(Inventory.product_id == product_id)
if low_stock: if low_stock:
query = query.where(Inventory.quantity <= Product.min_stock) base_query = base_query.where(Inventory.quantity <= Product.min_stock)
query = query.offset(skip).limit(limit) count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query) result = await db_session.execute(query)
inventory_list = [] inventory_list = []
@@ -63,7 +66,7 @@ async def list_inventory(
available_quantity=inv.available_quantity available_quantity=inv.available_quantity
)) ))
return inventory_list return PaginatedResponse(items=inventory_list, total=total, skip=skip, limit=limit)
@router.post("", response_model=InventoryResponse, status_code=201) @router.post("", response_model=InventoryResponse, status_code=201)
@@ -140,6 +143,7 @@ async def update_inventory(
.join(Warehouse, Inventory.warehouse_id == Warehouse.id) .join(Warehouse, Inventory.warehouse_id == Warehouse.id)
.where(Inventory.id == inventory_id) .where(Inventory.id == inventory_id)
.where(Product.item_type == "material") .where(Product.item_type == "material")
.with_for_update(of=Inventory)
) )
row = result.first() row = result.first()
if not row: if not row:
@@ -187,6 +191,8 @@ async def delete_inventory(
inventory = result.scalar_one_or_none() inventory = result.scalar_one_or_none()
if not inventory: if not inventory:
raise HTTPException(status_code=404, detail="库存记录不存在") raise HTTPException(status_code=404, detail="库存记录不存在")
if inventory.quantity > 0:
raise HTTPException(status_code=400, detail="库存数量不为零,无法删除库存记录")
await db_session.delete(inventory) await db_session.delete(inventory)
await db_session.commit() await db_session.commit()
return {"message": "库存记录已删除"} return {"message": "库存记录已删除"}
+1
View File
@@ -252,6 +252,7 @@ async def replace_product_bom(
finished_product_id=product_id, finished_product_id=product_id,
material_product_id=item.material_id, material_product_id=item.material_id,
quantity=item.quantity, quantity=item.quantity,
loss_rate=item.loss_rate,
) )
) )
+51 -43
View File
@@ -10,7 +10,7 @@
""" """
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func from sqlalchemy import select, func, update
from typing import Optional, List from typing import Optional, List
from decimal import Decimal from decimal import Decimal
@@ -32,6 +32,7 @@ from .schemas import (
PurchaseOrderDetailResponse, PurchaseOrderDetailResponse,
PurchaseOrderItemResponse, PurchaseOrderItemResponse,
PurchaseOrderReceiveRequest, PurchaseOrderReceiveRequest,
PaginatedResponse,
) )
from .utils import generate_order_no from .utils import generate_order_no
@@ -39,10 +40,6 @@ router = APIRouter(prefix="/purchase-orders", tags=["采购订单"])
def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) -> PurchaseOrderResponse: def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) -> PurchaseOrderResponse:
# 检查字段是否存在,避免数据库中没有这些字段时的错误
received_date = getattr(order, 'received_date', None)
paid_date = getattr(order, 'paid_date', None)
return PurchaseOrderResponse( return PurchaseOrderResponse(
id=order.id, id=order.id,
order_no=order.order_no, order_no=order.order_no,
@@ -54,8 +51,8 @@ def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) ->
paid_amount=order.paid_amount, paid_amount=order.paid_amount,
remark=order.remark, remark=order.remark,
created_at=order.created_at, created_at=order.created_at,
received_date=received_date, received_date=order.received_date,
paid_date=paid_date paid_date=order.paid_date
) )
@@ -86,9 +83,6 @@ async def _build_purchase_order_detail(
.order_by(PurchaseOrderItem.id.asc()) .order_by(PurchaseOrderItem.id.asc())
) )
item_rows = item_result.all() item_rows = item_result.all()
# 检查字段是否存在,避免数据库中没有这些字段时的错误
received_date = getattr(order, 'received_date', None)
paid_date = getattr(order, 'paid_date', None)
return PurchaseOrderDetailResponse( return PurchaseOrderDetailResponse(
id=order.id, id=order.id,
@@ -102,8 +96,8 @@ async def _build_purchase_order_detail(
paid_amount=order.paid_amount, paid_amount=order.paid_amount,
remark=order.remark, remark=order.remark,
created_at=order.created_at, created_at=order.created_at,
received_date=received_date, received_date=order.received_date,
paid_date=paid_date, paid_date=order.paid_date,
items=[ items=[
PurchaseOrderItemResponse( PurchaseOrderItemResponse(
id=item.id, id=item.id,
@@ -146,8 +140,8 @@ async def _apply_order_items(
db_session: AsyncSession, db_session: AsyncSession,
order: PurchaseOrder, order: PurchaseOrder,
order_data: PurchaseOrderCreate order_data: PurchaseOrderCreate
) -> float: ) -> Decimal:
total_amount = 0.0 total_amount = Decimal("0")
for item_data in order_data.items: for item_data in order_data.items:
product_result = await db_session.execute( product_result = await db_session.execute(
select(Product).where(Product.id == item_data.product_id, Product.is_active == True) select(Product).where(Product.id == item_data.product_id, Product.is_active == True)
@@ -179,7 +173,7 @@ async def _apply_order_items(
return total_amount return total_amount
@router.get("", response_model=List[PurchaseOrderResponse]) @router.get("", response_model=PaginatedResponse[PurchaseOrderResponse])
async def list_purchase_orders( async def list_purchase_orders(
status: Optional[str] = None, status: Optional[str] = None,
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
@@ -187,23 +181,26 @@ async def list_purchase_orders(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
query = ( base_query = (
select(PurchaseOrder, Supplier) select(PurchaseOrder, Supplier)
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id) .join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
.order_by(PurchaseOrder.created_at.desc()) .order_by(PurchaseOrder.created_at.desc())
) )
if status: if status:
query = query.where(PurchaseOrder.status == status) base_query = base_query.where(PurchaseOrder.status == status)
query = query.offset(skip).limit(limit) count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query) result = await db_session.execute(query)
orders = [] orders = []
for order, supplier in result.all(): for order, supplier in result.all():
orders.append(_build_purchase_order_response(order, supplier.name)) orders.append(_build_purchase_order_response(order, supplier.name))
return orders return PaginatedResponse(items=orders, total=total, skip=skip, limit=limit)
@router.post("", response_model=PurchaseOrderResponse, status_code=201) @router.post("", response_model=PurchaseOrderResponse, status_code=201)
@@ -223,8 +220,12 @@ async def create_purchase_order(
db_session.add(order) db_session.add(order)
await db_session.flush() await db_session.flush()
order.total_amount = await _apply_order_items(db_session, order, order_data) try:
await db_session.commit() order.total_amount = await _apply_order_items(db_session, order, order_data)
await db_session.commit()
except (HTTPException, Exception):
await db_session.rollback()
raise
await db_session.refresh(order) await db_session.refresh(order)
supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id)) supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
@@ -266,13 +267,17 @@ async def update_purchase_order(
for item in existing_items: for item in existing_items:
await db_session.delete(item) await db_session.delete(item)
order.supplier_id = order_data.supplier_id try:
order.expected_date = order_data.expected_date order.supplier_id = order_data.supplier_id
order.remark = order_data.remark order.expected_date = order_data.expected_date
order.total_amount = await _apply_order_items(db_session, order, order_data) order.remark = order_data.remark
order.status = "pending" order.total_amount = await _apply_order_items(db_session, order, order_data)
order.status = "pending"
await db_session.commit() await db_session.commit()
except (HTTPException, Exception):
await db_session.rollback()
raise
await db_session.refresh(order) await db_session.refresh(order)
supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id)) supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
supplier = supplier_result.scalar_one_or_none() supplier = supplier_result.scalar_one_or_none()
@@ -311,7 +316,7 @@ async def update_purchase_order_status(
if not new_status: if not new_status:
raise HTTPException(status_code=400, detail="状态不能为空") raise HTTPException(status_code=400, detail="状态不能为空")
valid_statuses = ["pending", "received", "paid"] valid_statuses = ["pending", "partial_received", "received", "paid"]
if new_status not in valid_statuses: if new_status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"无效的状态值,有效值为: {valid_statuses}") raise HTTPException(status_code=400, detail=f"无效的状态值,有效值为: {valid_statuses}")
@@ -322,12 +327,14 @@ async def update_purchase_order_status(
if order.status == "received" and new_status != "paid": if order.status == "received" and new_status != "paid":
raise HTTPException(status_code=400, detail="已收货的采购订单只能标记为已付款") raise HTTPException(status_code=400, detail="已收货的采购订单只能标记为已付款")
if order.status == "partial_received" and new_status not in ("received", "paid"):
raise HTTPException(status_code=400, detail="部分收货的采购订单只能标记为已收货或已付款")
# 更新状态和对应时间 # 更新状态和对应时间
order.status = new_status order.status = new_status
# 检查字段是否存在,避免数据库中没有这些字段时的错误 if new_status == "received":
if hasattr(order, 'received_date') and new_status == "received":
order.received_date = func.now() order.received_date = func.now()
elif hasattr(order, 'paid_date') and new_status == "paid": elif new_status == "paid":
order.paid_date = func.now() order.paid_date = func.now()
await db_session.commit() await db_session.commit()
@@ -377,25 +384,28 @@ async def receive_purchase_order(
if not product: if not product:
raise HTTPException(status_code=400, detail=f"物料不存在: {item.product_id}") raise HTTPException(status_code=400, detail=f"物料不存在: {item.product_id}")
inv_result = await db_session.execute( upd_result = await db_session.execute(
select(Inventory) update(Inventory)
.where(Inventory.product_id == item.product_id) .where(Inventory.product_id == item.product_id)
.where(Inventory.warehouse_id == warehouse.id) .where(Inventory.warehouse_id == warehouse.id)
.values(quantity=Inventory.quantity + receive_item.receive_quantity)
.returning(Inventory.quantity)
) )
inventory = inv_result.scalar_one_or_none() after_qty = upd_result.scalar_one_or_none()
if not inventory: if after_qty is None:
inventory = Inventory( inventory = Inventory(
product_id=item.product_id, product_id=item.product_id,
warehouse_id=warehouse.id, warehouse_id=warehouse.id,
quantity=0, quantity=receive_item.receive_quantity,
locked_quantity=0 locked_quantity=0
) )
db_session.add(inventory) db_session.add(inventory)
await db_session.flush() await db_session.flush()
before_qty = 0
before_qty = inventory.quantity after_qty = receive_item.receive_quantity
inventory.quantity += receive_item.receive_quantity else:
after_qty = inventory.quantity after_qty = int(after_qty)
before_qty = after_qty - receive_item.receive_quantity
item.received_quantity = (item.received_quantity or 0) + receive_item.receive_quantity item.received_quantity = (item.received_quantity or 0) + receive_item.receive_quantity
movement = StockMovement( movement = StockMovement(
@@ -419,9 +429,7 @@ async def receive_purchase_order(
any_received = any((item.received_quantity or 0) > 0 for item in item_map.values()) any_received = any((item.received_quantity or 0) > 0 for item in item_map.values())
if all_received: if all_received:
order.status = "received" order.status = "received"
# 检查字段是否存在,避免数据库中没有这些字段时的错误 order.received_date = func.now()
if hasattr(order, 'received_date'):
order.received_date = func.now()
elif any_received: elif any_received:
order.status = "partial_received" order.status = "partial_received"
+66 -51
View File
@@ -38,12 +38,14 @@ from .schemas import (
ProductionMaterialPlanItemResponse, ProductionMaterialPlanItemResponse,
SalesOrderIssueRequest, SalesOrderIssueRequest,
SalesOrderIssueResponse, SalesOrderIssueResponse,
SalesOrderStatusUpdate SalesOrderStatusUpdate,
PaginatedResponse,
) )
from .utils import generate_order_no from .utils import generate_order_no
router = APIRouter(prefix="/sales-orders", tags=["销售订单"]) router = APIRouter(prefix="/sales-orders", tags=["销售订单"])
VALID_ORDER_STATUSES = {"manufacturing", "delivered", "paid"} VALID_ORDER_STATUSES = {"draft", "manufacturing", "delivered", "paid"}
PRODUCTION_STATUSES = {"not_started", "bom_missing", "material_issued", "completed"}
def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesOrderResponse: def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesOrderResponse:
@@ -225,7 +227,7 @@ async def _issue_materials_for_order_creation(
raise HTTPException(status_code=400, detail=f"物料库存不足:{shortage_text}") raise HTTPException(status_code=400, detail=f"物料库存不足:{shortage_text}")
production_no = order.production_no or generate_order_no("WO") production_no = order.production_no or generate_order_no("WO")
actual_material_cost = 0.0 actual_material_cost = Decimal("0")
movement_count = 0 movement_count = 0
for item in plan_items: for item in plan_items:
@@ -289,25 +291,28 @@ async def _rollback_issued_materials(
return return
for movement in movements: for movement in movements:
inv_result = await db_session.execute( upd_result = await db_session.execute(
select(Inventory) update(Inventory)
.where(Inventory.product_id == movement.product_id) .where(Inventory.product_id == movement.product_id)
.where(Inventory.warehouse_id == movement.warehouse_id) .where(Inventory.warehouse_id == movement.warehouse_id)
.values(quantity=Inventory.quantity + movement.quantity)
.returning(Inventory.quantity)
) )
inventory = inv_result.scalar_one_or_none() after_qty = upd_result.scalar_one_or_none()
if not inventory: if after_qty is None:
inventory = Inventory( inventory = Inventory(
product_id=movement.product_id, product_id=movement.product_id,
warehouse_id=movement.warehouse_id, warehouse_id=movement.warehouse_id,
quantity=0, quantity=movement.quantity,
locked_quantity=0 locked_quantity=0
) )
db_session.add(inventory) db_session.add(inventory)
await db_session.flush() await db_session.flush()
before_qty = 0
before_qty = inventory.quantity after_qty = movement.quantity
inventory.quantity += movement.quantity else:
after_qty = inventory.quantity after_qty = int(after_qty)
before_qty = after_qty - movement.quantity
revert_movement = StockMovement( revert_movement = StockMovement(
product_id=movement.product_id, product_id=movement.product_id,
@@ -331,8 +336,8 @@ async def _apply_order_items(
db_session: AsyncSession, db_session: AsyncSession,
order: SalesOrder, order: SalesOrder,
order_data: SalesOrderCreate order_data: SalesOrderCreate
) -> float: ) -> Decimal:
total_amount = 0.0 total_amount = Decimal("0")
for item_data in order_data.items: for item_data in order_data.items:
product = None product = None
if item_data.product_id is not None: if item_data.product_id is not None:
@@ -378,7 +383,7 @@ async def _apply_order_items(
return total_amount return total_amount
@router.get("", response_model=List[SalesOrderResponse]) @router.get("", response_model=PaginatedResponse[SalesOrderResponse])
async def list_sales_orders( async def list_sales_orders(
status: Optional[str] = None, status: Optional[str] = None,
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
@@ -386,23 +391,26 @@ async def list_sales_orders(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
query = ( base_query = (
select(SalesOrder, Customer) select(SalesOrder, Customer)
.join(Customer, SalesOrder.customer_id == Customer.id) .join(Customer, SalesOrder.customer_id == Customer.id)
.order_by(SalesOrder.created_at.desc()) .order_by(SalesOrder.created_at.desc())
) )
if status: if status:
query = query.where(SalesOrder.status == status) base_query = base_query.where(SalesOrder.status == status)
query = query.offset(skip).limit(limit) count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query) result = await db_session.execute(query)
orders = [] orders = []
for order, customer in result.all(): for order, customer in result.all():
orders.append(_build_sales_order_response(order, customer.name)) orders.append(_build_sales_order_response(order, customer.name))
return orders return PaginatedResponse(items=orders, total=total, skip=skip, limit=limit)
@router.post("", response_model=SalesOrderResponse, status_code=201) @router.post("", response_model=SalesOrderResponse, status_code=201)
@@ -418,7 +426,7 @@ async def create_sales_order(
customer_id=order_data.customer_id, customer_id=order_data.customer_id,
order_date=now, order_date=now,
delivery_date=order_data.delivery_date, delivery_date=order_data.delivery_date,
manufacturing_date=now.date(), manufacturing_date=now,
created_at=now, created_at=now,
remark=order_data.remark, remark=order_data.remark,
operator_id=current_user.id, operator_id=current_user.id,
@@ -427,9 +435,13 @@ async def create_sales_order(
db_session.add(order) db_session.add(order)
await db_session.flush() await db_session.flush()
order.total_amount = await _apply_order_items(db_session, order, order_data) try:
await _issue_materials_for_order_creation(db_session, order, current_user) order.total_amount = await _apply_order_items(db_session, order, order_data)
await db_session.commit() await _issue_materials_for_order_creation(db_session, order, current_user)
await db_session.commit()
except (HTTPException, Exception):
await db_session.rollback()
raise
await db_session.refresh(order) await db_session.refresh(order)
customer = await db_session.execute(select(Customer).where(Customer.id == order.customer_id)) customer = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
@@ -458,21 +470,25 @@ async def update_sales_order(
order, customer = await _get_sales_order_with_customer(db_session, order_id) order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "paid": if order.status == "paid":
raise HTTPException(status_code=400, detail="已收款的销售订单禁止修改") raise HTTPException(status_code=400, detail="已收款的销售订单禁止修改")
await _rollback_issued_materials(db_session, order, current_user) try:
await db_session.execute(delete(SalesOrderItem).where(SalesOrderItem.order_id == order.id)) await _rollback_issued_materials(db_session, order, current_user)
await db_session.execute(delete(SalesOrderItem).where(SalesOrderItem.order_id == order.id))
order.customer_id = order_data.customer_id order.customer_id = order_data.customer_id
order.delivery_date = order_data.delivery_date order.delivery_date = order_data.delivery_date
order.remark = order_data.remark order.remark = order_data.remark
order.production_status = "not_started" order.production_status = "not_started"
order.production_no = None order.production_no = None
order.planned_material_cost = 0 order.planned_material_cost = 0
order.actual_material_cost = 0 order.actual_material_cost = 0
order.status = "manufacturing" order.status = "manufacturing"
order.total_amount = await _apply_order_items(db_session, order, order_data) order.total_amount = await _apply_order_items(db_session, order, order_data)
await _issue_materials_for_order_creation(db_session, order, current_user) await _issue_materials_for_order_creation(db_session, order, current_user)
await db_session.commit() await db_session.commit()
except (HTTPException, Exception):
await db_session.rollback()
raise
await db_session.refresh(order) await db_session.refresh(order)
customer_result = await db_session.execute(select(Customer).where(Customer.id == order.customer_id)) customer_result = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
@@ -562,27 +578,26 @@ async def consume_materials(
cost = Decimal(str(material.cost_price or 0)) * item.quantity cost = Decimal(str(material.cost_price or 0)) * item.quantity
total_cost += cost total_cost += cost
# 更新物料库存 # 更新物料库存(原子操作防并发)
inventory_result = await db_session.execute( upd_result = await db_session.execute(
select(Inventory) update(Inventory)
.where(Inventory.product_id == material.id) .where(Inventory.product_id == material.id)
.where(Inventory.warehouse_id == default_warehouse.id) .where(Inventory.warehouse_id == default_warehouse.id)
.where(Inventory.quantity >= item.quantity)
.values(quantity=Inventory.quantity - item.quantity)
.returning(Inventory.quantity)
) )
inventory = inventory_result.scalar() after_qty = upd_result.scalar_one_or_none()
if inventory: if after_qty is None:
before_qty = inventory.quantity raise HTTPException(status_code=400, detail=f"物料 {material.name} 库存不足")
inventory.quantity -= item.quantity after_qty = int(after_qty)
after_qty = inventory.quantity before_qty = after_qty + int(item.quantity)
if after_qty < 0:
raise HTTPException(status_code=400, detail=f"物料 {material.name} 库存不足")
else:
raise HTTPException(status_code=400, detail=f"物料 {material.name} 没有库存记录")
# 记录物料消耗 # 记录物料消耗
movement = StockMovement( movement = StockMovement(
product_id=material.id, product_id=material.id,
warehouse_id=default_warehouse.id, warehouse_id=default_warehouse.id,
quantity=-item.quantity, quantity=item.quantity,
before_quantity=before_qty, before_quantity=before_qty,
after_quantity=after_qty, after_quantity=after_qty,
movement_type="consumption", movement_type="consumption",
@@ -661,7 +676,7 @@ async def issue_sales_order_materials(
production_no = payload.production_no or order.production_no or generate_order_no("WO") production_no = payload.production_no or order.production_no or generate_order_no("WO")
actual_material_cost = 0.0 actual_material_cost = Decimal("0")
movement_count = 0 movement_count = 0
for item in plan_items: for item in plan_items:
upd_result = await db_session.execute( upd_result = await db_session.execute(
+3
View File
@@ -55,7 +55,10 @@ from .material_schemas import (
MaterialPriceTrendResponse MaterialPriceTrendResponse
) )
from .common_schemas import PaginatedResponse
__all__ = [ __all__ = [
"PaginatedResponse",
"ProductCreate", "ProductResponse", "ProductMaterialItemUpdate", "ProductBOMUpdate", "ProductCreate", "ProductResponse", "ProductMaterialItemUpdate", "ProductBOMUpdate",
"ProductMaterialItemResponse", "ProductBOMResponse", "ProductMaterialItemResponse", "ProductBOMResponse",
"SupplierCreate", "SupplierResponse", "SupplierCreate", "SupplierResponse",
@@ -0,0 +1,11 @@
from typing import TypeVar, Generic, List
from pydantic import BaseModel
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: List[T]
total: int
skip: int
limit: int
@@ -40,6 +40,7 @@ class ProductResponse(BaseModel):
class ProductMaterialItemUpdate(BaseModel): class ProductMaterialItemUpdate(BaseModel):
material_id: int material_id: int
quantity: Decimal quantity: Decimal
loss_rate: Decimal = Decimal("0")
class ProductBOMUpdate(BaseModel): class ProductBOMUpdate(BaseModel):
+65 -33
View File
@@ -11,13 +11,13 @@
""" """
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select, update, func
from typing import Optional, List from typing import Optional, List
from database.database import get_db_session from database.database import get_db_session
from services.auth_service import get_current_active_user from services.auth_service import get_current_active_user
from models.database import User, Product, Warehouse, Inventory, StockMovement from models.database import User, Product, Warehouse, Inventory, StockMovement
from .schemas import StockMovementCreate, StockMovementResponse from .schemas import StockMovementCreate, StockMovementResponse, PaginatedResponse
from .utils import generate_order_no from .utils import generate_order_no
router = APIRouter(prefix="/stock-movements", tags=["库存变动"]) router = APIRouter(prefix="/stock-movements", tags=["库存变动"])
@@ -73,36 +73,65 @@ async def create_stock_movement(
resolved_product_id = product.id resolved_product_id = product.id
result = await db_session.execute(
select(Inventory)
.where(Inventory.product_id == resolved_product_id)
.where(Inventory.warehouse_id == movement_data.warehouse_id)
)
inventory = result.scalar_one_or_none()
if not inventory:
if movement_data.movement_type in OUTBOUND_TYPES:
raise HTTPException(status_code=400, detail="库存不足")
inventory = Inventory(
product_id=resolved_product_id,
warehouse_id=movement_data.warehouse_id,
quantity=0
)
db_session.add(inventory)
await db_session.flush()
before_qty = inventory.quantity
if movement_data.movement_type in INBOUND_TYPES: if movement_data.movement_type in INBOUND_TYPES:
inventory.quantity += movement_data.quantity upd_result = await db_session.execute(
update(Inventory)
.where(Inventory.product_id == resolved_product_id)
.where(Inventory.warehouse_id == movement_data.warehouse_id)
.values(quantity=Inventory.quantity + movement_data.quantity)
.returning(Inventory.quantity)
)
after_qty = upd_result.scalar_one_or_none()
if after_qty is None:
inventory = Inventory(
product_id=resolved_product_id,
warehouse_id=movement_data.warehouse_id,
quantity=movement_data.quantity,
)
db_session.add(inventory)
await db_session.flush()
before_qty = 0
after_qty = movement_data.quantity
else:
after_qty = int(after_qty)
before_qty = after_qty - movement_data.quantity
elif movement_data.movement_type in OUTBOUND_TYPES: elif movement_data.movement_type in OUTBOUND_TYPES:
if inventory.quantity < movement_data.quantity: upd_result = await db_session.execute(
update(Inventory)
.where(Inventory.product_id == resolved_product_id)
.where(Inventory.warehouse_id == movement_data.warehouse_id)
.where(Inventory.quantity >= movement_data.quantity)
.values(quantity=Inventory.quantity - movement_data.quantity)
.returning(Inventory.quantity)
)
after_qty = upd_result.scalar_one_or_none()
if after_qty is None:
raise HTTPException(status_code=400, detail="库存不足") raise HTTPException(status_code=400, detail="库存不足")
inventory.quantity -= movement_data.quantity after_qty = int(after_qty)
before_qty = after_qty + movement_data.quantity
else: else:
result = await db_session.execute(
select(Inventory)
.where(Inventory.product_id == resolved_product_id)
.where(Inventory.warehouse_id == movement_data.warehouse_id)
.with_for_update()
)
inventory = result.scalar_one_or_none()
if not inventory:
inventory = Inventory(
product_id=resolved_product_id,
warehouse_id=movement_data.warehouse_id,
quantity=0,
)
db_session.add(inventory)
await db_session.flush()
before_qty = 0
else:
before_qty = int(inventory.quantity)
inventory.quantity = movement_data.quantity inventory.quantity = movement_data.quantity
after_qty = movement_data.quantity
after_qty = inventory.quantity
movement = StockMovement( movement = StockMovement(
product_id=resolved_product_id, product_id=resolved_product_id,
@@ -135,7 +164,7 @@ async def create_stock_movement(
) )
@router.get("", response_model=List[StockMovementResponse]) @router.get("", response_model=PaginatedResponse[StockMovementResponse])
async def list_stock_movements( async def list_stock_movements(
product_id: Optional[int] = None, product_id: Optional[int] = None,
movement_type: Optional[str] = None, movement_type: Optional[str] = None,
@@ -144,18 +173,21 @@ async def list_stock_movements(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
query = ( base_query = (
select(StockMovement, Product) select(StockMovement, Product)
.join(Product, StockMovement.product_id == Product.id) .join(Product, StockMovement.product_id == Product.id)
.order_by(StockMovement.created_at.desc()) .order_by(StockMovement.created_at.desc())
) )
if product_id: if product_id:
query = query.where(StockMovement.product_id == product_id) base_query = base_query.where(StockMovement.product_id == product_id)
if movement_type: if movement_type:
query = query.where(StockMovement.movement_type == movement_type) base_query = base_query.where(StockMovement.movement_type == movement_type)
query = query.offset(skip).limit(limit) count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query) result = await db_session.execute(query)
movements = [] movements = []
@@ -174,4 +206,4 @@ async def list_stock_movements(
created_at=movement.created_at created_at=movement.created_at
)) ))
return movements return PaginatedResponse(items=movements, total=total, skip=skip, limit=limit)
+5 -5
View File
@@ -4,8 +4,8 @@
提供进销存系统通用的工具函数,包括: 提供进销存系统通用的工具函数,包括:
- 订单编号生成器(采购订单、销售订单、库存变动等) - 订单编号生成器(采购订单、销售订单、库存变动等)
""" """
from datetime import datetime from datetime import datetime, timezone
import uuid import secrets
def generate_order_no(prefix: str) -> str: def generate_order_no(prefix: str) -> str:
@@ -15,8 +15,8 @@ def generate_order_no(prefix: str) -> str:
prefix: 订单类型前缀,如 PO(采购订单)、SO(销售订单)、SM(库存变动) prefix: 订单类型前缀,如 PO(采购订单)、SO(销售订单)、SM(库存变动)
Returns: Returns:
格式为 {prefix}{YYYYMMDDHHMMSS}{4位随机字符} 的订单编号 格式为 {prefix}{YYYYMMDDHHMMSS}{8位随机字符} 的订单编号
""" """
date_str = datetime.now().strftime("%Y%m%d%H%M%S") date_str = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
random_str = uuid.uuid4().hex[:4].upper() random_str = secrets.token_hex(4).upper()
return f"{prefix}{date_str}{random_str}" return f"{prefix}{date_str}{random_str}"
+41 -114
View File
@@ -2,7 +2,7 @@ from typing import Dict, List, Any, Tuple, Optional
import math import math
import numpy as np import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common, BRepAlgoAPI_Fuse from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2 from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2
@@ -226,14 +226,14 @@ class BaseMoldGenerator:
def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]: def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
""" """
分离型腔和型芯 — 完全嵌入 + 突出贴合方式 用分型面将模具块切分为A板(上模/型腔)和B板(下模/型芯),
然后从每个板中减去产品形状的对应部分。
流程: 流程:
1. 创建完整模具块(产品包围盒 + 全方向余量) 1. 创建完整模具块(产品包围盒 + 全方向余量)
2. 型腔(凹模)= 模具块 - 产品 → 产品完全嵌入型腔块中 2. 用分型面将模具块切分为 A板 和 B板
3. 型芯(凸模)= 产品形状突出体 → 从芯块面突出贴合 3. A板 - 产品 = 型腔(凹模)
4. B板 - 产品 = 型芯(凸模)
不再使用分型面中间切开产品的方式。
""" """
try: try:
bbox = Bnd_Box() bbox = Bnd_Box()
@@ -247,110 +247,40 @@ class BaseMoldGenerator:
mold_ymax = ymax + margin mold_ymax = ymax + margin
mold_zmax = zmax + margin mold_zmax = zmax + margin
cavity_block = BRepPrimAPI_MakeBox( mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin), gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax) gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape() ).Shape()
cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔") parting_plane = self._get_parting_plane(parting_surface, shape)
if cavity is None: if parting_plane is None:
logger.warning("型腔布尔减运算失败,使用原始模具块") logger.warning("无法提取分型面平面,使用回退方案")
cavity = cavity_block center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
parting_normal = self._extract_parting_normal(parting_surface) a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
core = self._build_protruding_core( if a_plate is not None and b_plate is not None:
shape, cavity_block, cavity = self._subtract_product_from_plate(a_plate, shape, "型腔(A板)")
mold_xmin, mold_ymin, mold_zmin, core = self._subtract_product_from_plate(b_plate, shape, "型芯(B板)")
mold_xmax, mold_ymax, mold_zmax,
xmin, ymin, zmin, xmax, ymax, zmax,
parting_normal
)
logger.info("型腔/型芯分离完成(完全嵌入+突出贴合)") if cavity is not None and core is not None:
return cavity, core logger.info("型腔/型芯分离完成(分型面切分+布尔减)")
return cavity, core
elif cavity is not None:
logger.warning("型芯生成失败,使用产品形状")
return cavity, shape
elif core is not None:
logger.warning("型腔生成失败,使用模具块")
return mold_block, core
logger.warning("A/B板切分不完全,回退到原方案")
return self._split_cavity_core_fallback(shape, mold_block)
except Exception as e: except Exception as e:
logger.error(f"型腔分离失败: {e}") logger.error(f"型腔分离失败: {e}")
return self._split_cavity_core_fallback(shape, None) return self._split_cavity_core_fallback(shape, None)
def _extract_parting_normal(self, parting_surface: Any) -> List[float]:
"""从分型面提取法向量方向"""
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
plane = surface.Plane()
normal = plane.Axis().Direction()
return [float(normal.X()), float(normal.Y()), float(normal.Z())]
except Exception:
pass
return [0.0, 0.0, 1.0]
def _build_protruding_core(
self,
shape: Any,
cavity_block: Any,
mold_xmin: float, mold_ymin: float, mold_zmin: float,
mold_xmax: float, mold_ymax: float, mold_zmax: float,
xmin: float, ymin: float, zmin: float,
xmax: float, ymax: float, zmax: float,
parting_normal: List[float] = None,
) -> Any:
"""
构建突出贴合式型芯。
型芯 = 底座平板 + 产品形状突出体。
底座平板沿分型方向位于产品下方,产品形状融合在底座之上,
与型腔的凹入形状完美贴合。
"""
nx, ny, nz = (parting_normal or [0.0, 0.0, 1.0])
mold_min = [mold_xmin, mold_ymin, mold_zmin]
mold_max = [mold_xmax, mold_ymax, mold_zmax]
prod_min = [xmin, ymin, zmin]
prod_max = [xmax, ymax, zmax]
base_p1 = list(mold_min)
base_p2 = list(mold_max)
for i in range(3):
prod_span = prod_max[i] - prod_min[i]
overlap = max(prod_span * 0.03, 0.5)
if parting_normal[i] > 0:
base_p2[i] = prod_min[i] + overlap
elif parting_normal[i] < 0:
base_p1[i] = prod_max[i] - overlap
base_plate = None
try:
base_plate = BRepPrimAPI_MakeBox(
gp_Pnt(base_p1[0], base_p1[1], base_p1[2]),
gp_Pnt(base_p2[0], base_p2[1], base_p2[2])
).Shape()
logger.info("型芯底座平板构建完成")
except Exception as e:
logger.warning(f"底座平板构建失败: {e}")
return shape
try:
fuse_op = BRepAlgoAPI_Fuse(base_plate, shape)
if fuse_op.IsDone():
core = fuse_op.Shape()
logger.info("型芯(底座+产品突出体)融合成功")
return core
except Exception as e:
logger.warning(f"底座与产品融合失败: {e}")
try:
cut_op = BRepAlgoAPI_Cut(cavity_block, shape)
if cut_op.IsDone():
logger.info("型芯通过布尔减回退构建")
return cut_op.Shape()
except Exception:
pass
logger.info("型芯最终回退为产品形状")
return shape
def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]: def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]:
"""从分型面提取平面方程""" """从分型面提取平面方程"""
try: try:
@@ -443,12 +373,9 @@ class BaseMoldGenerator:
def _split_cavity_core_fallback(self, shape: Any, def _split_cavity_core_fallback(self, shape: Any,
mold_block: Optional[Any] = None) -> Tuple[Any, Any]: mold_block: Optional[Any] = None) -> Tuple[Any, Any]:
""" """
分模回退方案:完全嵌入+突出贴合,不切分模具块。 分模回退方案:用边界框中心面作为分型面切分模具块。
1. 创建完整模具块 → 型腔 = 模具块 - 产品(产品完全嵌入)
2. 型芯 = 产品形状突出体(突出贴合)
""" """
logger.warning("使用分模回退方案(完全嵌入+突出贴合)") logger.warning("使用分模回退方案")
try: try:
bbox = Bnd_Box() bbox = Bnd_Box()
@@ -462,19 +389,19 @@ class BaseMoldGenerator:
gp_Pnt(xmax + margin, ymax + margin, zmax + margin) gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape() ).Shape()
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(回退)") center_z = (zmin + zmax) / 2
if cavity is None: parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
cavity = mold_block a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
core = self._build_protruding_core( if a_plate is not None and b_plate is not None:
shape, mold_block, cavity = self._subtract_product_from_plate(a_plate, shape, "型腔(回退)")
xmin - margin, ymin - margin, zmin - margin, core = self._subtract_product_from_plate(b_plate, shape, "型芯(回退)")
xmax + margin, ymax + margin, zmax + margin, if cavity is not None and core is not None:
xmin, ymin, zmin, xmax, ymax, zmax logger.info("回退方案型腔/型芯分离完成")
) return cavity, core
logger.info("回退方案型腔/型芯分离完成") cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(兜底)")
return cavity or mold_block, core return cavity or mold_block, shape
except Exception as e: except Exception as e:
logger.error(f"分模回退方案失败: {e}") logger.error(f"分模回退方案失败: {e}")
+53
View File
@@ -197,6 +197,59 @@ async def ensure_schema_updates():
CREATE INDEX IF NOT EXISTS idx_mold_cavity_is_fallback CREATE INDEX IF NOT EXISTS idx_mold_cavity_is_fallback
ON mold_cavity_data (is_fallback) ON mold_cavity_data (is_fallback)
""")) """))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'uq_inventory_product_warehouse'
) THEN
ALTER TABLE inventory
ADD CONSTRAINT uq_inventory_product_warehouse UNIQUE (product_id, warehouse_id);
END IF;
END $$;
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'ck_inventory_qty_nonnegative'
) THEN
ALTER TABLE inventory
ADD CONSTRAINT ck_inventory_qty_nonnegative
CHECK (quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity);
END IF;
END $$;
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'ck_purchase_order_items_qty'
) THEN
ALTER TABLE purchase_order_items
ADD CONSTRAINT ck_purchase_order_items_qty
CHECK (quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity);
END IF;
END $$;
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'ck_sales_order_items_qty'
) THEN
ALTER TABLE sales_order_items
ADD CONSTRAINT ck_sales_order_items_qty
CHECK (quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity);
END IF;
END $$;
"""))
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS manufacturing_date TIMESTAMP WITHOUT TIME ZONE"))
await conn.execute(text("ALTER TABLE sales_orders ALTER COLUMN manufacturing_date TYPE TIMESTAMP WITHOUT TIME ZONE"))
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -762,7 +762,7 @@ class SalesOrder(Base):
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True) customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True)
order_date = Column(DateTime, default=func.now()) order_date = Column(DateTime, default=func.now())
delivery_date = Column(Date, nullable=True) delivery_date = Column(Date, nullable=True)
manufacturing_date = Column(Date, nullable=True) manufacturing_date = Column(DateTime, nullable=True)
actual_delivery_date = Column(DateTime, nullable=True) actual_delivery_date = Column(DateTime, nullable=True)
actual_payment_date = Column(DateTime, nullable=True) actual_payment_date = Column(DateTime, nullable=True)
status = Column(String(20), default="draft") status = Column(String(20), default="draft")