From cd9b244bc89f40adfe1afab24f8d13ffee69b0d2 Mon Sep 17 00:00:00 2001 From: SZCJW <792430652@qq.com> Date: Thu, 9 Apr 2026 23:15:56 +0800 Subject: [PATCH] init --- migrate_purchase_status_dates.py | 75 ++++++++++++++++++++ src/api/inventory/product_routes.py | 10 +-- src/api/inventory/purchase_order_routes.py | 31 +++++--- src/api/inventory/schemas/product_schemas.py | 2 - src/database/init_db.py | 2 + static/index.html | 2 +- static/vue-app.js | 59 ++++++++++++--- 7 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 migrate_purchase_status_dates.py diff --git a/migrate_purchase_status_dates.py b/migrate_purchase_status_dates.py new file mode 100644 index 0000000..6ae6c6c --- /dev/null +++ b/migrate_purchase_status_dates.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +数据库迁移脚本:为采购订单表添加状态变更时间字段 +- received_date: 实际到货时间 +- paid_date: 实际付款时间 +""" +import asyncio +import sys +from pathlib import Path + +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) +sys.path.insert(0, str(project_root / "src")) + +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy import text +from config.settings import settings + + +async def migrate(): + """执行数据库迁移""" + if not settings.DATABASE_URL: + print("错误:未配置数据库连接") + return + + engine = create_async_engine( + settings.DATABASE_URL, + echo=True + ) + + async with engine.begin() as conn: + # 检查字段是否已存在 + check_sql = """ + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'purchase_orders' + AND column_name = 'received_date' + """ + result = await conn.execute(text(check_sql)) + if result.fetchone(): + print("字段 received_date 已存在,跳过") + else: + # 添加实际到货时间字段 + alter_sql = """ + ALTER TABLE purchase_orders + ADD COLUMN received_date TIMESTAMP WITHOUT TIME ZONE + """ + await conn.execute(text(alter_sql)) + print("成功添加字段 received_date") + + # 检查 paid_date 字段是否已存在 + check_sql2 = """ + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'purchase_orders' + AND column_name = 'paid_date' + """ + result2 = await conn.execute(text(check_sql2)) + if result2.fetchone(): + print("字段 paid_date 已存在,跳过") + else: + # 添加实际付款时间字段 + alter_sql2 = """ + ALTER TABLE purchase_orders + ADD COLUMN paid_date TIMESTAMP WITHOUT TIME ZONE + """ + await conn.execute(text(alter_sql2)) + print("成功添加字段 paid_date") + + await engine.dispose() + print("迁移完成") + + +if __name__ == "__main__": + asyncio.run(migrate()) diff --git a/src/api/inventory/product_routes.py b/src/api/inventory/product_routes.py index 39cabae..1069201 100644 --- a/src/api/inventory/product_routes.py +++ b/src/api/inventory/product_routes.py @@ -36,7 +36,7 @@ async def _calculate_material_cost_map(db_session: AsyncSession, product_ids: Li ProductMaterial.finished_product_id, func.coalesce( func.sum( - Product.cost_price * ProductMaterial.quantity * (1 + ProductMaterial.loss_rate) + Product.cost_price * ProductMaterial.quantity ), 0 ) @@ -186,7 +186,7 @@ async def get_product_bom( items: List[ProductMaterialItemResponse] = [] total_material_cost = 0.0 for bom, material in bom_result.all(): - line_cost = float(material.cost_price or 0) * float(bom.quantity) * (1 + float(bom.loss_rate or 0)) + line_cost = float(material.cost_price or 0) * float(bom.quantity) total_material_cost += line_cost items.append( ProductMaterialItemResponse( @@ -194,7 +194,6 @@ async def get_product_bom( material_sku=material.sku, material_name=material.name, quantity=round(float(bom.quantity), 4), - loss_rate=round(float(bom.loss_rate or 0), 4), unit_cost=round(float(material.cost_price or 0), 4), line_cost=round(float(line_cost), 4), ) @@ -246,15 +245,12 @@ async def replace_product_bom( for item in payload.items: if item.quantity <= 0: - raise HTTPException(status_code=400, detail="物料数量必须大于0") - if item.loss_rate < 0: - raise HTTPException(status_code=400, detail="损耗率不能为负数") + raise HTTPException(status_code=400, detail="物料数量必须大于 0") db_session.add( ProductMaterial( finished_product_id=product_id, material_product_id=item.material_id, quantity=item.quantity, - loss_rate=item.loss_rate, ) ) diff --git a/src/api/inventory/purchase_order_routes.py b/src/api/inventory/purchase_order_routes.py index b3479c7..cfac12c 100644 --- a/src/api/inventory/purchase_order_routes.py +++ b/src/api/inventory/purchase_order_routes.py @@ -38,6 +38,10 @@ router = APIRouter(prefix="/purchase-orders", tags=["采购订单"]) 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( id=order.id, order_no=order.order_no, @@ -49,8 +53,8 @@ def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) -> paid_amount=order.paid_amount, remark=order.remark, created_at=order.created_at, - received_date=order.received_date, - paid_date=order.paid_date + received_date=received_date, + paid_date=paid_date ) @@ -81,6 +85,10 @@ async def _build_purchase_order_detail( .order_by(PurchaseOrderItem.id.asc()) ) item_rows = item_result.all() + # 检查字段是否存在,避免数据库中没有这些字段时的错误 + received_date = getattr(order, 'received_date', None) + paid_date = getattr(order, 'paid_date', None) + return PurchaseOrderDetailResponse( id=order.id, order_no=order.order_no, @@ -93,8 +101,8 @@ async def _build_purchase_order_detail( paid_amount=order.paid_amount, remark=order.remark, created_at=order.created_at, - received_date=order.received_date, - paid_date=order.paid_date, + received_date=received_date, + paid_date=paid_date, items=[ PurchaseOrderItemResponse( id=item.id, @@ -218,8 +226,10 @@ async def create_purchase_order( await db_session.commit() await db_session.refresh(order) - supplier = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id)) - supplier = supplier.scalar_one() + supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id)) + supplier = supplier_result.scalar_one_or_none() + if not supplier: + raise HTTPException(status_code=404, detail="供应商不存在") return _build_purchase_order_response(order, supplier.name) @@ -313,9 +323,10 @@ async def update_purchase_order_status( # 更新状态和对应时间 order.status = new_status - if new_status == "received": + # 检查字段是否存在,避免数据库中没有这些字段时的错误 + if hasattr(order, 'received_date') and new_status == "received": order.received_date = func.now() - elif new_status == "paid": + elif hasattr(order, 'paid_date') and new_status == "paid": order.paid_date = func.now() await db_session.commit() @@ -407,7 +418,9 @@ async def receive_purchase_order( any_received = any((item.received_quantity or 0) > 0 for item in item_map.values()) if all_received: order.status = "received" - order.received_date = func.now() + # 检查字段是否存在,避免数据库中没有这些字段时的错误 + if hasattr(order, 'received_date'): + order.received_date = func.now() elif any_received: order.status = "partial_received" diff --git a/src/api/inventory/schemas/product_schemas.py b/src/api/inventory/schemas/product_schemas.py index 81cdd34..f0dd524 100644 --- a/src/api/inventory/schemas/product_schemas.py +++ b/src/api/inventory/schemas/product_schemas.py @@ -39,7 +39,6 @@ class ProductResponse(BaseModel): class ProductMaterialItemUpdate(BaseModel): material_id: int quantity: float - loss_rate: float = 0 class ProductBOMUpdate(BaseModel): @@ -51,7 +50,6 @@ class ProductMaterialItemResponse(BaseModel): material_sku: str material_name: str quantity: float - loss_rate: float unit_cost: float line_cost: float diff --git a/src/database/init_db.py b/src/database/init_db.py index c722f39..a624be2 100644 --- a/src/database/init_db.py +++ b/src/database/init_db.py @@ -167,6 +167,8 @@ async def ensure_schema_updates(): await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_no VARCHAR(50)")) await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS planned_material_cost DOUBLE PRECISION DEFAULT 0")) await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS actual_material_cost DOUBLE PRECISION DEFAULT 0")) + await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS received_date TIMESTAMP WITHOUT TIME ZONE")) + await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS paid_date TIMESTAMP WITHOUT TIME ZONE")) await conn.execute(text(""" CREATE TABLE IF NOT EXISTS product_materials ( id SERIAL PRIMARY KEY, diff --git a/static/index.html b/static/index.html index a859a8a..7b645ad 100644 --- a/static/index.html +++ b/static/index.html @@ -77,6 +77,6 @@ - + diff --git a/static/vue-app.js b/static/vue-app.js index 860eb0d..736d9a8 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -2194,6 +2194,16 @@ const InventoryView = { warehouse_id: state.purchaseWarehouseId || state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null, remark: '' }; + } else if (type === 'product') { + state.form = { ...item }; + if (item.item_type === 'finished') { + await loadMaterials(); + const bom = await apiRequest(`/api/products/${item.id}/materials`); + state.productBomItems = (bom.items || []).map(bomItem => ({ + material_id: bomItem.material_id, + quantity: bomItem.quantity + })); + } } else { state.form = { ...item }; } @@ -2459,6 +2469,12 @@ const InventoryView = { method: 'PUT', body: JSON.stringify(state.form) }); + if (state.form.item_type === 'finished' && state.productBomItems.length > 0) { + await apiRequest(`/api/products/${state.editingItem.id}/materials`, { + method: 'PUT', + body: JSON.stringify({ items: state.productBomItems }) + }); + } addNotification('产品更新成功', 'success'); } else { await apiRequest('/api/products', { @@ -2495,20 +2511,18 @@ const InventoryView = { state.editingItem = product; state.productBomItems = (bom.items || []).map(item => ({ material_id: item.material_id, - quantity: item.quantity, - loss_rate: item.loss_rate + quantity: item.quantity })); state.showModal = true; } catch (e) { - handleApiError(e, '加载产品BOM'); + handleApiError(e, '加载产品 BOM'); } }; const addBomItem = () => { state.productBomItems.push({ material_id: state.materials[0]?.id || null, - quantity: 1, - loss_rate: 0 + quantity: 1 }); }; @@ -3116,7 +3130,6 @@ const InventoryView = { {{ formatCurrency(product.material_cost || 0) }}
-
@@ -3618,7 +3631,7 @@ const InventoryView = {