JhHardwareWRS_BackPoint/app/routers/products.py
2026-06-24 15:19:14 +08:00

361 lines
15 KiB
Python

from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from fastapi.responses import Response
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from app.database import get_db
from app.deps import require_roles
from app.models import DeviceQRCode, MoldLockFeedback, Personnel, Product, ProductionReportItem, Role, WorkSessionDevice
from app.schemas import ImportResult, MoldNameOut, PageResponse, ProductCreate, ProductOut
from app.services.attendance_points import accessible_point_names, require_attendance_point_access
from app.services.common import normalize_device_no, paginate
from app.services.excel_import import ProductImportValidationError, import_products
from app.services.excel_export import export_products
from app.services.cleaning import LEGACY_CLEANING_PROCESS_NAME, is_cleaning_stamping_method
from app.services.continuous_die import is_continuous_die_product
from app.services.display_names import mold_process_display_name
from app.services.misc_work import ensure_misc_work_products, is_misc_stamping_method
from app.services.multi_person import is_multi_person_product
from app.services.process_names import MISC_PROCESS_NAME, normalize_process_name
from app.services.serializers import product_out
router = APIRouter(prefix="/api/products", tags=["products"])
def _cascade_product_key_change(db: Session, original_key: dict, next_key: dict) -> None:
if original_key == next_key:
return
old_point = original_key["attendance_point_name"]
old_product = original_key["product_name"]
old_process = original_key["process_name"]
next_point = next_key["attendance_point_name"]
next_product = next_key["product_name"]
next_process = next_key["process_name"]
old_qr = db.get(DeviceQRCode, {
"attendance_point_name": old_point,
"device_no": old_product,
"process_name": old_process,
})
if old_qr is not None:
qr_target = db.get(DeviceQRCode, {
"attendance_point_name": next_point,
"device_no": next_product,
"process_name": next_process,
})
if qr_target is not None:
raise HTTPException(status_code=409, detail="目标产品/工序二维码已存在,不能合并覆盖")
old_qr.attendance_point_name = next_point
old_qr.device_no = next_product
old_qr.process_name = next_process
for device in db.scalars(select(WorkSessionDevice).where(
WorkSessionDevice.attendance_point_name == old_point,
WorkSessionDevice.device_no == old_product,
WorkSessionDevice.process_name == old_process,
)).all():
device.attendance_point_name = next_point
device.device_no = next_product
device.process_name = next_process
for item in db.scalars(select(ProductionReportItem).where(
ProductionReportItem.attendance_point_name == old_point,
ProductionReportItem.project_no == original_key["project_no"],
ProductionReportItem.product_name == old_product,
ProductionReportItem.process_name == old_process,
)).all():
item.attendance_point_name = next_point
item.project_no = next_key["project_no"]
item.product_name = next_product
item.process_name = next_process
for feedback in db.scalars(select(MoldLockFeedback).where(
MoldLockFeedback.attendance_point_name == old_point,
MoldLockFeedback.mold_name == old_product,
MoldLockFeedback.process_name == old_process,
)).all():
feedback.attendance_point_name = next_point
feedback.mold_name = next_product
feedback.process_name = next_process
@router.get("/molds", response_model=list[MoldNameOut])
def list_mold_names(
keyword: str = "",
limit: int = Query(100, ge=1, le=500),
user: Personnel = Depends(require_roles(Role.admin, Role.manager)),
db: Session = Depends(get_db),
) -> list[MoldNameOut]:
point_names = accessible_point_names(db, user)
ensure_misc_work_products(db, point_names)
db.commit()
query = (
select(Product)
.where(
Product.attendance_point_name.in_(point_names),
Product.product_name != "",
Product.process_name != "",
Product.device_no == "",
)
.distinct()
.order_by(Product.attendance_point_name, Product.product_name, Product.process_name)
)
if keyword:
like = f"%{keyword}%"
query = query.where(or_(Product.product_name.like(like), Product.process_name.like(like), Product.stamping_method.like(like)))
rows = db.scalars(query.limit(limit)).all()
return [
MoldNameOut(
attendance_point_name=product.attendance_point_name,
name=product.product_name,
process_name=product.process_name or "",
stamping_method=product.stamping_method,
operator_count=product.operator_count,
is_cleaning=is_cleaning_stamping_method(product.stamping_method),
is_misc=is_misc_stamping_method(product.stamping_method),
is_continuous_die=is_continuous_die_product(product),
is_multi_person=is_multi_person_product(product),
display_name=mold_process_display_name(product.product_name, product.process_name, product.stamping_method),
)
for product in rows
]
@router.get("", response_model=PageResponse)
def list_products(
attendance_point_name: str = "",
keyword: str = "",
device_no: str = "",
product_name: str = "",
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
user: Personnel = Depends(require_roles(Role.worker, Role.admin, Role.manager)),
db: Session = Depends(get_db),
) -> PageResponse:
point_names = accessible_point_names(db, user)
ensure_misc_work_products(db, point_names)
db.commit()
query = (
select(Product)
.where(Product.device_no == "", Product.attendance_point_name.in_(point_names))
.order_by(
Product.updated_at.desc(),
Product.attendance_point_name.asc(),
Product.project_no.asc(),
Product.product_name.asc(),
Product.process_name.asc(),
)
)
if attendance_point_name:
try:
point_name = require_attendance_point_access(db, user, attendance_point_name)
except (ValueError, PermissionError) as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
query = query.where(Product.attendance_point_name == point_name)
if product_name:
query = query.where(Product.product_name == product_name)
if keyword:
like = f"%{keyword}%"
query = query.where(
or_(
Product.project_no.like(like),
Product.product_name.like(like),
Product.attendance_point_name.like(like),
Product.material_code.like(like),
Product.material_name.like(like),
Product.supplier.like(like),
Product.process_name.like(like),
Product.stamping_method.like(like),
)
)
result = paginate(db, query, page, page_size)
return PageResponse(**{**result, "rows": [product_out(row) for row in result["rows"]]})
@router.post("", response_model=ProductOut)
def save_product(
payload: ProductCreate,
user: Personnel = Depends(require_roles(Role.admin, Role.manager)),
db: Session = Depends(get_db),
) -> ProductOut:
try:
point_name = require_attendance_point_access(db, user, payload.attendance_point_name)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
raw_process_name = str(payload.process_name or "").strip()
if raw_process_name == LEGACY_CLEANING_PROCESS_NAME:
raise HTTPException(status_code=400, detail="清洗请填写在冲压方式中,工序只能填写数字,处理杂活除外")
try:
process_name = normalize_process_name(raw_process_name)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
is_cleaning = is_cleaning_stamping_method(payload.stamping_method)
is_misc = is_misc_stamping_method(payload.stamping_method)
if is_misc and process_name != MISC_PROCESS_NAME:
raise HTTPException(status_code=400, detail="处理杂活的工序必须填写杂活")
if not is_misc and process_name == MISC_PROCESS_NAME:
raise HTTPException(status_code=400, detail="只有处理杂活的工序可以填写杂活")
if not is_cleaning and not is_misc and payload.standard_beat <= 0:
raise HTTPException(status_code=400, detail="非清洗工序必须填写标准节拍")
next_key = {
"attendance_point_name": point_name,
"project_no": payload.project_no,
"product_name": payload.product_name,
"device_no": "",
"process_name": process_name,
}
original_key = {
"attendance_point_name": payload.original_attendance_point_name or point_name,
"project_no": payload.original_project_no or payload.project_no,
"product_name": payload.original_product_name or payload.product_name,
"device_no": normalize_device_no(payload.original_device_no if payload.original_device_no is not None else ""),
"process_name": payload.original_process_name if payload.original_process_name is not None else process_name,
}
product = db.get(Product, original_key)
if product is not None and original_key != next_key:
existed = db.get(Product, next_key)
if existed is not None:
raise HTTPException(status_code=409, detail="目标产品/工序已存在,不能合并覆盖")
else:
_cascade_product_key_change(db, original_key, next_key)
product.project_no = next_key["project_no"]
product.product_name = next_key["product_name"]
product.device_no = next_key["device_no"]
product.process_name = next_key["process_name"]
elif product is None:
product = db.get(Product, next_key)
if product is None:
product = Product(
attendance_point_name=next_key["attendance_point_name"],
project_no=next_key["project_no"],
product_name=next_key["product_name"],
device_no=next_key["device_no"],
process_name=next_key["process_name"],
)
db.add(product)
aux_values = (
payload.product_net_weight_kg,
payload.product_gross_weight_kg,
payload.scrap_loss_rate,
payload.waste_price_yuan_per_kg,
)
if all(value is None for value in aux_values):
sibling = next(
(
item for item in db.scalars(
select(Product).where(Product.product_name == next_key["product_name"], Product.device_no == "")
.where(Product.attendance_point_name == point_name)
).all()
if item.product_net_weight_kg is not None
and item.product_gross_weight_kg is not None
and item.scrap_loss_rate is not None
and item.waste_price_yuan_per_kg is not None
),
None,
)
if sibling is not None:
aux_values = (
sibling.product_net_weight_kg,
sibling.product_gross_weight_kg,
sibling.scrap_loss_rate,
sibling.waste_price_yuan_per_kg,
)
product.profile_no = payload.profile_no
product.attendance_point_name = next_key["attendance_point_name"]
product.material_code = payload.material_code
product.material_name = payload.material_name
product.supplier = payload.supplier
product.product_net_weight_kg = aux_values[0]
product.product_gross_weight_kg = aux_values[1]
product.scrap_loss_rate = aux_values[2]
product.waste_price_yuan_per_kg = aux_values[3]
product.device_no = next_key["device_no"]
product.process_name = next_key["process_name"]
product.stamping_method = payload.stamping_method
product.operator_count = payload.operator_count or 1
product.process_unit_price_yuan = payload.process_unit_price_yuan or 0
product.standard_beat = 0 if (is_cleaning or is_misc) else payload.standard_beat
product.standard_workload = 0
for sibling in db.scalars(select(Product).where(
Product.attendance_point_name == point_name,
Product.product_name == next_key["product_name"],
Product.device_no == "",
)).all():
sibling.product_net_weight_kg = product.product_net_weight_kg
sibling.product_gross_weight_kg = product.product_gross_weight_kg
sibling.scrap_loss_rate = product.scrap_loss_rate
sibling.waste_price_yuan_per_kg = product.waste_price_yuan_per_kg
db.commit()
db.refresh(product)
return product_out(product)
@router.delete("", status_code=204)
def delete_product(
attendance_point_name: str,
project_no: str,
product_name: str,
device_no: str = "",
process_name: str = "",
user: Personnel = Depends(require_roles(Role.admin, Role.manager)),
db: Session = Depends(get_db),
) -> None:
try:
point_name = require_attendance_point_access(db, user, attendance_point_name)
except (ValueError, PermissionError) as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
product = db.get(
Product,
{
"attendance_point_name": point_name,
"project_no": project_no,
"product_name": product_name,
"device_no": "",
"process_name": process_name,
},
)
if product is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="产品不存在")
qrcode = db.get(DeviceQRCode, {
"attendance_point_name": point_name,
"device_no": product.product_name,
"process_name": product.process_name or "",
})
if qrcode is not None:
db.delete(qrcode)
db.delete(product)
db.commit()
@router.post("/import", response_model=ImportResult)
async def import_product_excel(
file: UploadFile = File(...),
user: Personnel = Depends(require_roles(Role.admin, Role.manager)),
db: Session = Depends(get_db),
) -> ImportResult:
content = await file.read()
try:
result = import_products(db, content, user=user)
except ProductImportValidationError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ImportResult(**result.as_dict())
@router.get("/export")
def export_product_excel(
user: Personnel = Depends(require_roles(Role.admin, Role.manager)),
db: Session = Depends(get_db),
) -> Response:
content = export_products(db, user=user)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": 'attachment; filename="products.xlsx"'},
)