diff --git a/backend/app/api/plan.py b/backend/app/api/plan.py index 9d824bd..58814c1 100644 --- a/backend/app/api/plan.py +++ b/backend/app/api/plan.py @@ -6,7 +6,7 @@ from datetime import datetime from app.database import get_db from app.models.plan import ProductionPlan -from app.schemas.plan import PlanCreate, PlanUpdate, PlanOut +from app.schemas.plan import PlanCreate, PlanUpdate, PlanOut, PlanTemplate from app.schemas.common import Response, PageResponse from app.services.auth_service import get_current_user @@ -22,6 +22,16 @@ def _parse_dt(s): return None +TEMPLATE_FIELDS = ( + "steel_grade", "incoming_thickness", "product_thickness", + "deviation_upper", "deviation_lower", + "incoming_width", "product_width", + "packaging_req", "trim_req", "rolling_mode", + "coil_diameter", "split_count", "next_process", + "incoming_weight", "incoming_od", "split_weights", +) + + @router.get("/", response_model=Response[PageResponse[PlanOut]]) async def list_plans( page: int = 1, @@ -48,6 +58,16 @@ async def list_plans( return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items)) +@router.get("/last-template", response_model=Response[PlanTemplate]) +async def get_last_template(db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)): + """最近一条计划的工艺字段(不含计划号/卷号/时间),用于新增时回填。""" + result = await db.execute(select(ProductionPlan).order_by(desc(ProductionPlan.created_at)).limit(1)) + p = result.scalar_one_or_none() + if not p: + return Response.ok(PlanTemplate()) + return Response.ok(PlanTemplate(**{k: getattr(p, k, None) for k in TEMPLATE_FIELDS})) + + @router.post("/", response_model=Response[PlanOut]) async def create_plan( body: PlanCreate, @@ -98,3 +118,24 @@ async def confirm_plan(plan_id: int, db: AsyncSession = Depends(get_db), _ = Dep plan.status = "online" await db.flush() return Response.ok(PlanOut.model_validate(plan)) + + +@router.patch("/{plan_id}/start", response_model=Response[PlanOut]) +async def start_producing(plan_id: int, db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)): + """移动到入口,开始生产:本条→producing,其它 producing→online(单卷在产)。""" + result = await db.execute(select(ProductionPlan).where(ProductionPlan.id == plan_id)) + plan = result.scalar_one_or_none() + if not plan: + raise HTTPException(status_code=404, detail="计划不存在") + # 其它正在生产的全部回退为在线(强制单卷在产) + others = await db.execute( + select(ProductionPlan).where( + ProductionPlan.status == "producing", + ProductionPlan.id != plan_id, + ) + ) + for o in others.scalars(): + o.status = "online" + plan.status = "producing" + await db.flush() + return Response.ok(PlanOut.model_validate(plan)) diff --git a/backend/app/api/production.py b/backend/app/api/production.py index 59acd06..33499e3 100644 --- a/backend/app/api/production.py +++ b/backend/app/api/production.py @@ -6,10 +6,28 @@ from datetime import datetime from app.database import get_db from app.models.production import ProductionRecord +from app.models.plan import ProductionPlan from app.schemas.production import ProductionRecordCreate, ProductionRecordUpdate, ProductionRecordOut from app.schemas.common import Response, PageResponse from app.services.auth_service import get_current_user + +async def _mark_plan_produced(db: AsyncSession, record: ProductionRecord): + """根据实绩记录的卷号自动把对应计划标记为 produced。""" + candidates = [] + for v in (getattr(record, "hot_coil_no", None), record.coil_no, getattr(record, "sub_coil_no", None)): + if v and v not in candidates: + candidates.append(v) + if not candidates: + return + q = select(ProductionPlan).where( + (ProductionPlan.cold_coil_no.in_(candidates)) | (ProductionPlan.hot_coil_no.in_(candidates)) + ) + res = await db.execute(q) + for plan in res.scalars(): + if plan.status != "produced": + plan.status = "produced" + router = APIRouter() @@ -60,6 +78,8 @@ async def create_record( record = ProductionRecord(**body.model_dump()) db.add(record) await db.flush() + await _mark_plan_produced(db, record) + await db.flush() return Response.ok(ProductionRecordOut.model_validate(record)) @@ -86,4 +106,6 @@ async def update_record( for k, v in body.model_dump(exclude_none=True).items(): setattr(record, k, v) await db.flush() + await _mark_plan_produced(db, record) + await db.flush() return Response.ok(ProductionRecordOut.model_validate(record)) diff --git a/backend/app/database.py b/backend/app/database.py index e214b75..f6b0488 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -56,6 +56,9 @@ async def _run_migrations(conn): "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS split_count INTEGER DEFAULT 1", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS coil_diameter DOUBLE PRECISION", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS next_process VARCHAR(30)", + "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_weight DOUBLE PRECISION", + "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_od DOUBLE PRECISION", + "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS split_weights JSONB", # 状态列改为 VARCHAR 以适配新值 "ALTER TABLE production_plans ALTER COLUMN status TYPE VARCHAR(20) USING status::text", # production_records 新字段 diff --git a/backend/app/models/plan.py b/backend/app/models/plan.py index 82ef45b..dc3332a 100644 --- a/backend/app/models/plan.py +++ b/backend/app/models/plan.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Float, DateTime, Text, func +from sqlalchemy import Column, Integer, String, Float, DateTime, Text, func, JSON from app.database import Base @@ -31,6 +31,9 @@ class ProductionPlan(Base): coil_diameter = Column(Float, comment="卷径 mm") split_count = Column(Integer, default=1, comment="分卷数") next_process = Column(String(30), comment="下工序") + incoming_weight = Column(Float, comment="来料重量 t") + incoming_od = Column(Float, comment="来料外径 mm") + split_weights = Column(JSON, comment="分卷重量 [t,...]") # 兼容历史字段 shift = Column(String(10), comment="班次") diff --git a/backend/app/schemas/plan.py b/backend/app/schemas/plan.py index 8eebe47..0c53922 100644 --- a/backend/app/schemas/plan.py +++ b/backend/app/schemas/plan.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Optional +from typing import Optional, List from datetime import datetime @@ -21,7 +21,10 @@ class PlanCreate(BaseModel): coil_diameter: Optional[float] = None split_count: Optional[int] = 1 next_process: Optional[str] = None - status: Optional[str] = "ready" + incoming_weight: Optional[float] = None + incoming_od: Optional[float] = None + split_weights: Optional[List[Optional[float]]] = None + status: Optional[str] = "online" remark: Optional[str] = None @@ -42,6 +45,9 @@ class PlanUpdate(BaseModel): coil_diameter: Optional[float] = None split_count: Optional[int] = None next_process: Optional[str] = None + incoming_weight: Optional[float] = None + incoming_od: Optional[float] = None + split_weights: Optional[List[Optional[float]]] = None status: Optional[str] = None remark: Optional[str] = None @@ -66,9 +72,32 @@ class PlanOut(BaseModel): coil_diameter: Optional[float] = None split_count: Optional[int] = 1 next_process: Optional[str] = None + incoming_weight: Optional[float] = None + incoming_od: Optional[float] = None + split_weights: Optional[List[Optional[float]]] = None remark: Optional[str] = None created_by: Optional[str] = None created_at: datetime class Config: from_attributes = True + + +class PlanTemplate(BaseModel): + """新增计划时回填的"上次录入"模板(不含 plan_no/卷号/时间)""" + steel_grade: Optional[str] = None + incoming_thickness: Optional[float] = None + product_thickness: Optional[float] = None + deviation_upper: Optional[float] = None + deviation_lower: Optional[float] = None + incoming_width: Optional[float] = None + product_width: Optional[float] = None + packaging_req: Optional[str] = None + trim_req: Optional[str] = None + rolling_mode: Optional[str] = None + coil_diameter: Optional[float] = None + split_count: Optional[int] = 1 + next_process: Optional[str] = None + incoming_weight: Optional[float] = None + incoming_od: Optional[float] = None + split_weights: Optional[List[Optional[float]]] = None diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index cd9eeef..96951f3 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -20,6 +20,8 @@ export const getPlans = params => request.get('/plan/', { params }) export const createPlan = data => request.post('/plan/', data) export const updatePlan = (id, data) => request.put(`/plan/${id}`, data) export const confirmPlan = id => request.patch(`/plan/${id}/confirm`) +export const startProducing = id => request.patch(`/plan/${id}/start`) +export const getLastPlanTemplate = () => request.get('/plan/last-template') // 停机管理 export const getDowntimeCategories = () => request.get('/downtime/categories') diff --git a/frontend/src/views/Material.vue b/frontend/src/views/Material.vue index ac53c69..13e4fbb 100644 --- a/frontend/src/views/Material.vue +++ b/frontend/src/views/Material.vue @@ -32,6 +32,41 @@ + +
| 冷卷号 | 钢种 | 厚度 | 宽度 | 分卷 | 下达时间 | 操作 |
|---|---|---|---|---|---|---|
| {{ p.cold_coil_no || p.plan_no }} | +{{ p.steel_grade || '—' }} | +{{ fmt(p.product_thickness) }} | +{{ fmt(p.product_width, 0) }} | +{{ p.split_count || 1 }} | +{{ fmtTime(p.plan_date) }} | ++ + | +