feat: 完成多模块迭代更新

- 新增过程巡检按步骤上报、数量不符上报、工位退料功能
- 增加工单工程编号三级归属字段
- 新增物料安全库存与缺货预警
- 新增附件管理、退库单管理模块
- 优化生产看板展示逻辑与前端页面文案
- 清理冗余的工艺路线相关代码与备份文件
This commit is contained in:
SunYF
2026-09-15 16:37:39 +08:00
parent 951f3dfecd
commit 4a5e2d7bac
494 changed files with 24702 additions and 139223 deletions
+183 -23
View File
@@ -36,19 +36,22 @@ func resolveInspectionTarget(ctx *svc.ServiceContext, targetId string) (*ent.Inv
func createInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
TargetType string `json:"targetType"`
TargetId string `json:"targetId"`
MaterialCode string `json:"materialCode"`
InspectionType string `json:"inspectionType"`
Status string `json:"status"` // 合格/不合格
ResultValue string `json:"resultValue"`
InspectQty int `json:"inspectQty"` // 检验数量
PassQty int `json:"passQty"` // 合格数量
CheckDesc string `json:"checkDesc"` // 检测说明
ReportNo string `json:"reportNo"` // 报检单号(生产报检关联)
Inspector string `json:"inspector"`
Remark string `json:"remark"`
InspectionNo string `json:"inspectionNo"`
TargetType string `json:"targetType"`
TargetId string `json:"targetId"`
MaterialCode string `json:"materialCode"`
InspectionType string `json:"inspectionType"`
Status string `json:"status"` // 合格/不合格
ResultValue string `json:"resultValue"`
InspectQty int `json:"inspectQty"` // 检验数量
PassQty int `json:"passQty"` // 合格数量
CheckDesc string `json:"checkDesc"` // 检测说明
ReportNo string `json:"reportNo"` // 报检单号(生产报检关联)
Inspector string `json:"inspector"`
Remark string `json:"remark"`
InspectionNo string `json:"inspectionNo"`
DisposalType string `json:"disposalType"` // NONE/RETURN/SCRAP/PARTIAL
DisposalRemark string `json:"disposalRemark"`
ReturnTrackingNo string `json:"returnTrackingNo"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
@@ -102,15 +105,19 @@ func createInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
SetNillableRemark(strPtr(req.Remark)).
SetNillableReportNo(strPtr(req.ReportNo)).
SetNillableInspectionNo(strPtr(req.InspectionNo)).
SetDisposalType(disposalTypeOrNone(req.DisposalType)).
SetNillableDisposalRemark(strPtr(req.DisposalRemark)).
SetNillableReturnTrackingNo(strPtr(req.ReturnTrackingNo)).
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
// 同步库存检验状态:直接更新定位到的那条库存行(精确,绝不误伤)
// 同步库存检验状态:直接更新定位到的那条库存行(精确,绝不误伤)
// 处置为"退货"时置 待退(隔离、禁止出库);否则不合格置 不合格(同样隔离)。
if _, err = tx.Inventory.UpdateOneID(inv.ID).
SetQualityStatus(req.Status).Save(ctx0()); err != nil {
SetQualityStatus(qualityAfterInspection(req.Status, req.DisposalType)).Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "同步库存检验状态失败: "+err.Error())
return
}
@@ -134,13 +141,18 @@ func createInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
func batchFlipInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
TargetType string `json:"targetType"`
TargetIds []string `json:"targetIds"`
Status string `json:"status"`
InspectQty int `json:"inspectQty"` // 检验数量
PassQty int `json:"passQty"` // 合格数量
CheckDesc string `json:"checkDesc"` // 检测说明
Inspector string `json:"inspector"`
TargetType string `json:"targetType"`
TargetIds []string `json:"targetIds"`
Status string `json:"status"`
InspectQty int `json:"inspectQty"` // 检验数量
PassQty int `json:"passQty"` // 合格数量
CheckDesc string `json:"checkDesc"` // 检测说明
Inspector string `json:"inspector"`
ReportNo string `json:"reportNo"` // 报检单号
InspectionNo string `json:"inspectionNo"` // 检测单号
DisposalType string `json:"disposalType"` // NONE/RETURN/SCRAP/PARTIAL
DisposalRemark string `json:"disposalRemark"` // 处置说明
ReturnTrackingNo string `json:"returnTrackingNo"` // 退货货运单号
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
@@ -195,12 +207,17 @@ func batchFlipInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
SetPassQty(req.PassQty).
SetNillableCheckDesc(strPtr(req.CheckDesc)).
SetNillableInspector(strPtr(req.Inspector)).
SetNillableReportNo(strPtr(req.ReportNo)).
SetNillableInspectionNo(strPtr(req.InspectionNo)).
SetDisposalType(disposalTypeOrNone(req.DisposalType)).
SetNillableDisposalRemark(strPtr(req.DisposalRemark)).
SetNillableReturnTrackingNo(strPtr(req.ReturnTrackingNo)).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建检验记录失败: "+err.Error())
return
}
if _, err = tx.Inventory.UpdateOneID(t.inv.ID).
SetQualityStatus(req.Status).Save(ctx0()); err != nil {
SetQualityStatus(qualityAfterInspection(req.Status, req.DisposalType)).Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "同步库存检验状态失败: "+err.Error())
return
}
@@ -359,6 +376,149 @@ func exportInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// disposalTypeOrNone 处置方式归一化(客户诉求 L137-145:退货/报废/部分入库)
func disposalTypeOrNone(v string) string {
switch v {
case "RETURN", "SCRAP", "PARTIAL":
return v
default:
return "NONE"
}
}
// qualityAfterInspection 检验后的库存质量状态:
// 合格 → 合格(可出库);不合格且选退货 → 待退(隔离);其余 → 不合格(隔离)。
func qualityAfterInspection(status, disposalType string) string {
if status == "合格" {
return "合格"
}
if disposalTypeOrNone(disposalType) == "RETURN" {
return "待退"
}
return "不合格"
}
// disposeInspectionHandler 不合格品处置(退货 / 报废 / 部分入库)
// POST /api/inspection/dispose body: { recordId, disposalType, disposalRemark, returnTrackingNo, operator }
//
// RETURN 退货 → 库存置「待退」隔离(保持数量,等待退货运单确认后出库)
// SCRAP 报废 → 库存置「不合格」且生命周期「报废」,退出可用库存
// PARTIAL 部分入库 → 仅批次管理:按合格数拆行(合格行入库 + 差额行隔离),数量守恒
func disposeInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
RecordID int `json:"recordId"`
DisposalType string `json:"disposalType"`
DisposalRemark string `json:"disposalRemark"`
ReturnTrackingNo string `json:"returnTrackingNo"`
Operator string `json:"operator"`
}
if err := parseJSON(r, &req); err != nil || req.RecordID <= 0 {
fail(w, http.StatusBadRequest, "参数错误:recordId 必填")
return
}
dt := disposalTypeOrNone(req.DisposalType)
if dt == "NONE" {
fail(w, http.StatusBadRequest, "请选择处置方式(退货/报废/部分入库)")
return
}
rec, err := ctx.EntClient.InspectionRecord.Get(ctx0(), req.RecordID)
if err != nil {
fail(w, http.StatusNotFound, "检验记录不存在")
return
}
if rec.Status != "不合格" {
fail(w, http.StatusBadRequest, "仅「不合格」记录可执行处置")
return
}
inv, err := resolveInspectionTarget(ctx, rec.TargetID)
if err != nil {
fail(w, http.StatusBadRequest, err.Error())
return
}
if dt == "RETURN" && req.ReturnTrackingNo == "" {
fail(w, http.StatusBadRequest, "退货处置必须填写货运单号")
return
}
tx, err := ctx.EntClient.Tx(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
return
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
if _, err = tx.InspectionRecord.UpdateOneID(rec.ID).
SetDisposalType(dt).
SetNillableDisposalRemark(strPtr(req.DisposalRemark)).
SetNillableReturnTrackingNo(strPtr(req.ReturnTrackingNo)).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新处置信息失败: "+err.Error())
return
}
switch dt {
case "RETURN":
if _, err = tx.Inventory.UpdateOneID(inv.ID).SetQualityStatus("待退").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新库存状态失败: "+err.Error())
return
}
case "SCRAP":
if _, err = tx.Inventory.UpdateOneID(inv.ID).SetQualityStatus("不合格").SetStatus("报废").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新库存状态失败: "+err.Error())
return
}
case "PARTIAL":
if inv.ManageMode != 1 {
fail(w, http.StatusBadRequest, "部分入库仅适用于批次管理物料(结构件)")
return
}
total := inv.Quantity
if rec.PassQty <= 0 || rec.PassQty >= total {
fail(w, http.StatusBadRequest, fmt.Sprintf("部分入库:合格数须在 0 与批次数量(%d) 之间", total))
return
}
bad := total - rec.PassQty
if _, err = tx.Inventory.UpdateOneID(inv.ID).
SetQuantity(rec.PassQty).SetQualityStatus("合格").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "拆分合格批次失败: "+err.Error())
return
}
if _, err = tx.Inventory.Create().
SetManageMode(1).
SetCategory(inv.Category).
SetMaterialCode(inv.MaterialCode).
SetNillableMaterialName(strPtr(inv.MaterialName)).
SetBatchNo(inv.BatchNo + "-NG").
SetQuantity(bad).SetLockedQty(0).
SetQualityStatus("不合格").
SetNillableZoneCode(strPtr(inv.ZoneCode)).
SetStatus("在库").
SetNillableInboundNo(strPtr(inv.InboundNo)).
SetRemark("不合格隔离(部分入库差额)").
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "生成不合格隔离行失败: "+err.Error())
return
}
}
if err = tx.Commit(); err != nil {
fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error())
return
}
committed = true
ctx.EventLog.Write(ctx0(), "inspection.dispose", r.Header.Get("X-Username"), "inspection", rec.TargetID,
"不合格处置 "+rec.TargetID+" → "+dt, map[string]any{"recordId": rec.ID, "disposalType": dt})
ok(w, map[string]any{"recordId": rec.ID, "disposalType": dt})
}
}
// statsInspectionHandler 检验统计:按物料汇总检验次数/合格数/不合格数/合格率
// 对应《问题记录》附二9 的替代方案(不合并记录,另出统计视图)
func statsInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {