feat(production): 新增排产支撑功能并移除手动报工页面
- 在main.go中更新排产物料需求注释说明,明确WMS与MES系统间交互方式 - 从前端help.js中移除手动报工(helpScan)相关帮助文档 - 更新工艺流程、绩效报表等页面帮助文档中的报工流程说明 - 在主布局中添加排产支撑菜单项并移除手动报工菜单项 - 新增ProducibleSupport.vue组件实现排产支撑页面功能 - 移除Scan.vue手动报工页面组件 - 更新相关帮助文档内容以匹配新的业务流程
This commit is contained in:
@@ -6,12 +6,11 @@ import (
|
||||
"time"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
"bj_power_wms/ent/inboundorder"
|
||||
"bj_power_wms/ent/inspectionrecord"
|
||||
"bj_power_wms/ent/inventory"
|
||||
"bj_power_wms/ent/material"
|
||||
"bj_power_wms/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// resolveInspectionTarget 通过编号(批次号或 SN)定位唯一库存行。
|
||||
@@ -31,170 +30,75 @@ func resolveInspectionTarget(ctx *svc.ServiceContext, targetId string) (*ent.Inv
|
||||
return nil, fmt.Errorf("未找到对应库存记录(批次号/SN):%s,请确认编号是否已入库、或该记录是否已出库", targetId)
|
||||
}
|
||||
|
||||
// createInspectionHandler 创建检验记录
|
||||
// body: { targetType: BATCH/SN, targetId, status, inspector }
|
||||
// 同步库存检验状态:BATCH → inventory(manage_mode=1, batch_no);SN → inventory(manage_mode=2, sn_code)
|
||||
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"`
|
||||
DisposalType string `json:"disposalType"` // NONE/RETURN/SCRAP/PARTIAL
|
||||
DisposalRemark string `json:"disposalRemark"`
|
||||
ReturnTrackingNo string `json:"returnTrackingNo"`
|
||||
// 归属 / 相关标准 / 生产厂家(客户诉求 问题记录 L107/L149/L494)
|
||||
OwnershipType string `json:"ownershipType"` // workorder/project/other
|
||||
OwnershipNo string `json:"ownershipNo"`
|
||||
RelatedStandard string `json:"relatedStandard"` // 相关标准(检验细则号)
|
||||
Manufacturer string `json:"manufacturer"` // 生产厂家
|
||||
}
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
if req.TargetId == "" || req.Status == "" {
|
||||
fail(w, http.StatusBadRequest, "targetId 和 status 必填")
|
||||
return
|
||||
}
|
||||
if req.Status != "合格" && req.Status != "不合格" {
|
||||
fail(w, http.StatusBadRequest, "status 只能为 合格/不合格")
|
||||
return
|
||||
}
|
||||
|
||||
// 定位真实库存行:自动按 批次号/SN 解析(前端选错类型也能正确命中)
|
||||
inv, err := resolveInspectionTarget(ctx, req.TargetId)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// 物料编码必填字段由库存行回填,杜绝 missing required field
|
||||
req.MaterialCode = inv.MaterialCode
|
||||
req.TargetType = "BATCH"
|
||||
if inv.ManageMode == 2 {
|
||||
req.TargetType = "SN"
|
||||
}
|
||||
// 物料名称冗余(列表直接展示/名称模糊查询,客户诉求 L507-510 三框制)
|
||||
materialName := inv.MaterialName
|
||||
if materialName == "" {
|
||||
if m, merr := ctx.EntClient.Material.Query().Where(material.CodeEQ(inv.MaterialCode)).First(ctx0()); merr == nil && m != nil {
|
||||
materialName = m.Name
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := ctx.EntClient.Tx(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
rec, err := tx.InspectionRecord.Create().
|
||||
SetTargetType(req.TargetType).
|
||||
SetTargetID(req.TargetId).
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetNillableMaterialName(strPtr(materialName)).
|
||||
SetNillableOwnershipType(strPtr(ownershipTypeOrNone(req.OwnershipType))).
|
||||
SetNillableOwnershipNo(strPtr(req.OwnershipNo)).
|
||||
SetNillableRelatedStandard(strPtr(req.RelatedStandard)).
|
||||
SetNillableManufacturer(strPtr(req.Manufacturer)).
|
||||
SetInspectionType(req.InspectionType).
|
||||
SetStatus(req.Status).
|
||||
SetNillableResultValue(strPtr(req.ResultValue)).
|
||||
SetInspectQty(req.InspectQty).
|
||||
SetPassQty(req.PassQty).
|
||||
SetNillableCheckDesc(strPtr(req.CheckDesc)).
|
||||
SetNillableInspector(strPtr(req.Inspector)).
|
||||
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(qualityAfterInspection(req.Status, req.DisposalType)).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.create", r.Header.Get("X-Username"), "inspection", req.TargetId,
|
||||
"质量检验 "+req.MaterialCode+" → "+req.Status, map[string]any{
|
||||
"targetType": req.TargetType, "inspectQty": req.InspectQty, "passQty": req.PassQty})
|
||||
logx.Infof("inspection: %s %s -> %s by %s", req.TargetType, req.TargetId, req.Status, req.Inspector)
|
||||
ok(w, rec)
|
||||
}
|
||||
}
|
||||
|
||||
// batchFlipInspectionHandler 批量翻转检验状态
|
||||
// body: { targetType: BATCH/SN, targetIds: [], status, inspector }
|
||||
// batchFlipInspectionHandler 逐个判定批量翻转检验状态(问题记录 L150 三问题之三)
|
||||
// body: { items: [ { targetId, status, inspectQty, passQty, disposalType,
|
||||
//
|
||||
// returnTrackingNo, disposalRemark, ownershipType, ownershipNo,
|
||||
// relatedStandard, manufacturer, checkDesc, inspector, reportNo, inspectionNo } ] }
|
||||
//
|
||||
// 每个对象按自身数量写记录、按自身结论翻库存状态(修复旧版"所有记录同数量"的 bug)。
|
||||
// reject_qty 后端复核 = inspectQty - passQty,不信任前端传入值。
|
||||
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"`
|
||||
ReportNo string `json:"reportNo"` // 报检单号
|
||||
InspectionNo string `json:"inspectionNo"` // 检测单号
|
||||
DisposalType string `json:"disposalType"` // NONE/RETURN/SCRAP/PARTIAL
|
||||
DisposalRemark string `json:"disposalRemark"` // 处置说明
|
||||
ReturnTrackingNo string `json:"returnTrackingNo"` // 退货货运单号
|
||||
OwnershipType string `json:"ownershipType"`
|
||||
OwnershipNo string `json:"ownershipNo"`
|
||||
RelatedStandard string `json:"relatedStandard"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Items []struct {
|
||||
TargetId string `json:"targetId"`
|
||||
Status string `json:"status"` // 合格/不合格
|
||||
InspectQty int `json:"inspectQty"`
|
||||
PassQty int `json:"passQty"`
|
||||
DisposalType string `json:"disposalType"`
|
||||
ReturnTrackingNo string `json:"returnTrackingNo"`
|
||||
DisposalRemark string `json:"disposalRemark"`
|
||||
OwnershipType string `json:"ownershipType"`
|
||||
OwnershipNo string `json:"ownershipNo"`
|
||||
RelatedStandard string `json:"relatedStandard"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
CheckDesc string `json:"checkDesc"`
|
||||
Inspector string `json:"inspector"`
|
||||
ReportNo string `json:"reportNo"`
|
||||
InspectionNo string `json:"inspectionNo"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
if len(req.TargetIds) == 0 || (req.Status != "合格" && req.Status != "不合格") {
|
||||
fail(w, http.StatusBadRequest, "targetIds 和 status(合格/不合格) 必填")
|
||||
if len(req.Items) == 0 {
|
||||
fail(w, http.StatusBadRequest, "items 必填")
|
||||
return
|
||||
}
|
||||
|
||||
// 预校验:所有编号必须能定位到库存行,任意一条找不到则整体拒绝(避免部分成功、假闭环)
|
||||
type target struct {
|
||||
// 预校验:每条必须含 targetId 且结论合法、能定位到库存行;任意一条失败整体拒绝(避免部分成功假闭环)
|
||||
type item struct {
|
||||
id string
|
||||
inv *ent.Inventory
|
||||
targetType string
|
||||
material string
|
||||
status string
|
||||
inspectQty int
|
||||
passQty int
|
||||
rejectQty int
|
||||
disposal string
|
||||
retTrack string
|
||||
dispRemark string
|
||||
ownType string
|
||||
ownNo string
|
||||
relStd string
|
||||
mfr string
|
||||
checkDesc string
|
||||
inspector string
|
||||
reportNo string
|
||||
inspNo string
|
||||
}
|
||||
targets := make([]target, 0, len(req.TargetIds))
|
||||
for _, id := range req.TargetIds {
|
||||
inv, err := resolveInspectionTarget(ctx, id)
|
||||
items := make([]item, 0, len(req.Items))
|
||||
codes := make([]string, 0, len(req.Items))
|
||||
seenCode := map[string]bool{}
|
||||
for _, it := range req.Items {
|
||||
if it.TargetId == "" || (it.Status != "合格" && it.Status != "不合格") {
|
||||
fail(w, http.StatusBadRequest, "每条必须含 targetId 与 status(合格/不合格): "+it.TargetId)
|
||||
return
|
||||
}
|
||||
inv, err := resolveInspectionTarget(ctx, it.TargetId)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -203,7 +107,45 @@ func batchFlipInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if inv.ManageMode == 2 {
|
||||
tt = "SN"
|
||||
}
|
||||
targets = append(targets, target{id: id, inv: inv, targetType: tt, material: inv.MaterialCode})
|
||||
inspectQty := it.InspectQty
|
||||
passQty := it.PassQty
|
||||
if inspectQty <= 0 {
|
||||
inspectQty = 1 // 电气件/其他:单件判定默认检验数=1
|
||||
}
|
||||
if passQty < 0 || passQty > inspectQty {
|
||||
fail(w, http.StatusBadRequest, "合格数必须在 0~检验数 之间: "+it.TargetId)
|
||||
return
|
||||
}
|
||||
// 后端复核不合格数 = 检验数 - 合格数(不信任前端)
|
||||
rejectQty := inspectQty - passQty
|
||||
items = append(items, item{
|
||||
id: it.TargetId, inv: inv, targetType: tt, material: inv.MaterialCode,
|
||||
status: it.Status, inspectQty: inspectQty, passQty: passQty, rejectQty: rejectQty,
|
||||
disposal: disposalTypeOrNone(it.DisposalType), retTrack: it.ReturnTrackingNo, dispRemark: it.DisposalRemark,
|
||||
ownType: it.OwnershipType, ownNo: it.OwnershipNo, relStd: it.RelatedStandard, mfr: it.Manufacturer,
|
||||
checkDesc: it.CheckDesc, inspector: it.Inspector, reportNo: it.ReportNo, inspNo: it.InspectionNo,
|
||||
})
|
||||
if inv.MaterialCode != "" && !seenCode[inv.MaterialCode] {
|
||||
seenCode[inv.MaterialCode] = true
|
||||
codes = append(codes, inv.MaterialCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 「其他」类物料(item_type=4)单独记对象类型 OTHER(§B1:其他类此前混在批次(结构件)里无法单独识别)
|
||||
if len(codes) > 0 {
|
||||
if mats, e := ctx.EntClient.Material.Query().Where(material.CodeIn(codes...)).All(ctx0()); e == nil {
|
||||
otherSet := map[string]bool{}
|
||||
for _, m := range mats {
|
||||
if m.ItemType == invItemOther {
|
||||
otherSet[m.Code] = true
|
||||
}
|
||||
}
|
||||
for i := range items {
|
||||
if items[i].targetType == "BATCH" && otherSet[items[i].material] {
|
||||
items[i].targetType = "OTHER"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := ctx.EntClient.Tx(ctx0())
|
||||
@@ -218,39 +160,47 @@ func batchFlipInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}()
|
||||
|
||||
// 同库存行被多条记录引用的极端情况:批量送检若勾选了同一批次多行,按 id 去重取首条,
|
||||
// 避免重复写记录(正常待检清单按入库单分组,行级唯一,此去重仅为防御)。
|
||||
seen := map[string]bool{}
|
||||
flipped := 0
|
||||
for _, t := range targets {
|
||||
matName := t.inv.MaterialName
|
||||
for _, it := range items {
|
||||
if seen[it.id] {
|
||||
continue
|
||||
}
|
||||
seen[it.id] = true
|
||||
matName := it.inv.MaterialName
|
||||
if matName == "" {
|
||||
if m, merr := tx.Material.Query().Where(material.CodeEQ(t.material)).First(ctx0()); merr == nil && m != nil {
|
||||
if m, merr := tx.Material.Query().Where(material.CodeEQ(it.material)).First(ctx0()); merr == nil && m != nil {
|
||||
matName = m.Name
|
||||
}
|
||||
}
|
||||
if _, err = tx.InspectionRecord.Create().
|
||||
SetTargetType(t.targetType).
|
||||
SetTargetID(t.id).
|
||||
SetMaterialCode(t.material).
|
||||
SetTargetType(it.targetType).
|
||||
SetTargetID(it.id).
|
||||
SetMaterialCode(it.material).
|
||||
SetNillableMaterialName(strPtr(matName)).
|
||||
SetNillableOwnershipType(strPtr(ownershipTypeOrNone(req.OwnershipType))).
|
||||
SetNillableOwnershipNo(strPtr(req.OwnershipNo)).
|
||||
SetNillableRelatedStandard(strPtr(req.RelatedStandard)).
|
||||
SetNillableManufacturer(strPtr(req.Manufacturer)).
|
||||
SetStatus(req.Status).
|
||||
SetInspectQty(req.InspectQty).
|
||||
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)).
|
||||
SetNillableOwnershipType(strPtr(ownershipTypeOrNone(it.ownType))).
|
||||
SetNillableOwnershipNo(strPtr(it.ownNo)).
|
||||
SetNillableRelatedStandard(strPtr(it.relStd)).
|
||||
SetNillableManufacturer(strPtr(it.mfr)).
|
||||
SetStatus(it.status).
|
||||
SetInspectQty(it.inspectQty).
|
||||
SetPassQty(it.passQty).
|
||||
SetRejectQty(it.rejectQty).
|
||||
SetNillableCheckDesc(strPtr(it.checkDesc)).
|
||||
SetNillableInspector(strPtr(it.inspector)).
|
||||
SetNillableReportNo(strPtr(it.reportNo)).
|
||||
SetNillableInspectionNo(strPtr(it.inspNo)).
|
||||
SetDisposalType(it.disposal).
|
||||
SetNillableDisposalRemark(strPtr(it.dispRemark)).
|
||||
SetNillableReturnTrackingNo(strPtr(it.retTrack)).
|
||||
Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建检验记录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if _, err = tx.Inventory.UpdateOneID(t.inv.ID).
|
||||
SetQualityStatus(qualityAfterInspection(req.Status, req.DisposalType)).Save(ctx0()); err != nil {
|
||||
if _, err = tx.Inventory.UpdateOneID(it.inv.ID).
|
||||
SetQualityStatus(qualityAfterInspection(it.status, it.disposal)).Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "同步库存检验状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -298,6 +248,17 @@ func queryInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if v := r.URL.Query().Get("ownershipNo"); v != "" {
|
||||
q = q.Where(inspectionrecord.OwnershipNoContainsFold(v))
|
||||
}
|
||||
if v := r.URL.Query().Get("itemType"); v != "" {
|
||||
// 按品类(itemType)过滤:先取对应物料编码集再 IN(避免逐行 join,同 materialCodesBySpec 套路)
|
||||
if code := atoi(v, 0); code > 0 {
|
||||
codes := materialCodesByItemType(ctx, code)
|
||||
if len(codes) == 0 {
|
||||
ok(w, map[string]any{"total": 0, "list": []any{}, "page": page, "pageSize": pageSize})
|
||||
return
|
||||
}
|
||||
q = q.Where(inspectionrecord.MaterialCodeIn(codes...))
|
||||
}
|
||||
}
|
||||
if v := r.URL.Query().Get("spec"); v != "" {
|
||||
// 型号=物料档案规格型号,需 join 物料表取编码集合
|
||||
codes := materialCodesBySpec(ctx, v)
|
||||
@@ -379,6 +340,17 @@ func exportInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if v := r.URL.Query().Get("ownershipNo"); v != "" {
|
||||
q = q.Where(inspectionrecord.OwnershipNoContainsFold(v))
|
||||
}
|
||||
if v := r.URL.Query().Get("itemType"); v != "" {
|
||||
// 按品类(itemType)过滤:先取对应物料编码集再 IN(避免逐行 join,同 materialCodesBySpec 套路)
|
||||
if code := atoi(v, 0); code > 0 {
|
||||
codes := materialCodesByItemType(ctx, code)
|
||||
if len(codes) == 0 {
|
||||
ok(w, map[string]any{"list": []any{}})
|
||||
return
|
||||
}
|
||||
q = q.Where(inspectionrecord.MaterialCodeIn(codes...))
|
||||
}
|
||||
}
|
||||
if v := r.URL.Query().Get("spec"); v != "" {
|
||||
codes := materialCodesBySpec(ctx, v)
|
||||
if len(codes) == 0 {
|
||||
@@ -416,7 +388,7 @@ func exportInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if r.URL.Query().Get("format") == "xlsx" {
|
||||
// 列名按客户口径(问题记录 L150):检验说明→相关标准、检验员→记录人、检验日期→记录日期、增加归属;
|
||||
// 图号=物料编码(L18 合并裁决)。
|
||||
headers := []string{"ID", "对象", "批次/SN", "图号", "名称", "检验类型", "结论", "检验数", "合格数", "相关标准", "报检单号", "检测单号", "归属单号", "生产厂家", "记录人", "备注", "记录日期"}
|
||||
headers := []string{"ID", "对象", "批次/SN", "图号", "名称", "检验类型", "结论", "检验数", "合格数", "不合格数", "相关标准", "报检单号", "检测单号", "归属单号", "生产厂家", "记录人", "备注", "记录日期"}
|
||||
matrix := make([][]any, 0, len(list))
|
||||
for _, rec := range list {
|
||||
obj := rec.TargetType
|
||||
@@ -428,7 +400,7 @@ func exportInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
matrix = append(matrix, []any{
|
||||
rec.ID, obj, rec.TargetID, rec.MaterialCode, rec.MaterialName,
|
||||
inspectionTypeLabel(rec.InspectionType), rec.Status,
|
||||
rec.InspectQty, rec.PassQty, rec.RelatedStandard, rec.ReportNo,
|
||||
rec.InspectQty, rec.PassQty, rec.RejectQty, rec.RelatedStandard, rec.ReportNo,
|
||||
rec.InspectionNo, rec.OwnershipNo, rec.Manufacturer, rec.Inspector, rec.Remark,
|
||||
unixFmt(rec.CreatedAt),
|
||||
})
|
||||
@@ -678,3 +650,209 @@ func statsInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
ok(w, map[string]any{"list": list})
|
||||
}
|
||||
}
|
||||
|
||||
// materialCodesByItemType 按品类(itemType)取物料编码集合(来料检验「其他」类过滤用,B1)。
|
||||
// 先查 material 取编码集,再 IN 查 inventory/inspection_record,避免逐行 join(同 materialCodesBySpec 套路)。
|
||||
func materialCodesByItemType(ctx *svc.ServiceContext, itemType int) []string {
|
||||
mats, err := ctx.EntClient.Material.Query().Where(material.ItemTypeEQ(itemType)).All(ctx0())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
codes := make([]string, 0, len(mats))
|
||||
for _, m := range mats {
|
||||
codes = append(codes, m.Code)
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
// inboundNosByType 取符合入库类型的入库单号集合(待检清单按入库类型过滤用,B1)。
|
||||
func inboundNosByType(ctx *svc.ServiceContext, inboundType string) ([]string, error) {
|
||||
orders, err := ctx.EntClient.InboundOrder.Query().Where(inboundorder.InboundTypeEQ(inboundType)).All(ctx0())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nos := make([]string, 0, len(orders))
|
||||
for _, o := range orders {
|
||||
if o.InboundNo != "" {
|
||||
nos = append(nos, o.InboundNo)
|
||||
}
|
||||
}
|
||||
return nos, nil
|
||||
}
|
||||
|
||||
// pendingInspectionItem 待检清单展开明细行
|
||||
type pendingInspectionItem struct {
|
||||
ID int `json:"id"` // inventory.id(真实主键,禁止伪 id)
|
||||
TargetId string `json:"targetId"` // 批次号或 SN
|
||||
ManageMode int `json:"manageMode"` // 1结构件/2电气件
|
||||
ItemType int `json:"itemType"` // 品类 1原材料/2半成品/3成品/4其他(§E:带入录入页判定其他类)
|
||||
Quantity int `json:"quantity"`
|
||||
ZoneCode string `json:"zoneCode"`
|
||||
}
|
||||
|
||||
// pendingInspectionRow 待检清单主行(按 入库单 + 物料 分组,§E2)
|
||||
type pendingInspectionRow struct {
|
||||
InboundNo string `json:"inboundNo"`
|
||||
InboundType string `json:"inboundType"`
|
||||
OwnershipType string `json:"ownershipType"`
|
||||
OwnershipNo string `json:"ownershipNo"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
UnInspectedQty int `json:"unInspectedQty"` // 未检数量(结构件=数量求和;电气件=SN 行数)
|
||||
BatchSnCount int `json:"batchSnCount"` // 未检批次数/SN 数
|
||||
InboundTime int64 `json:"inboundTime"`
|
||||
Details []pendingInspectionItem `json:"details"`
|
||||
}
|
||||
|
||||
// pendingInspectionHandler 待检清单(来料检验问题2):列出 quality_status=未检 的库存行,
|
||||
// 按入库单为主行分组(展开看该单下批次/SN 明细),支持 物料/入库类型/目标类型(结构件·电气件·其他) 筛选。
|
||||
// 勾选带入录入页时,归属/入库类型/物料一并从入库单与库存行回填(§E1/E2)。
|
||||
// GET /api/inspection/pending?materialCode=&inboundType=&targetType=&page=&pageSize=
|
||||
func pendingInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
inboundType := r.URL.Query().Get("inboundType") // purchase/semi/finished/return/other
|
||||
targetType := r.URL.Query().Get("targetType") // BATCH(结构件)/SN(电气件)/OTHER(其他)
|
||||
page := atoi(r.URL.Query().Get("page"), 1)
|
||||
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
|
||||
|
||||
q := ctx.EntClient.Inventory.Query().
|
||||
Where(inventory.StatusIn("在库", "锁定"), inventory.QualityStatusEQ("未检"))
|
||||
if materialCode != "" {
|
||||
q = q.Where(inventory.MaterialCodeContainsFold(materialCode))
|
||||
}
|
||||
switch targetType {
|
||||
case "BATCH":
|
||||
q = q.Where(inventory.ManageModeEQ(1))
|
||||
case "SN":
|
||||
q = q.Where(inventory.ManageModeEQ(2))
|
||||
case "OTHER":
|
||||
// 其他入库物料:itemType=4(先取编码集再 IN,避免逐行 join)
|
||||
codes := materialCodesByItemType(ctx, 4)
|
||||
if len(codes) == 0 {
|
||||
ok(w, map[string]any{"list": []any{}, "total": 0})
|
||||
return
|
||||
}
|
||||
q = q.Where(inventory.MaterialCodeIn(codes...))
|
||||
}
|
||||
if inboundType != "" {
|
||||
// 入库类型在 inbound_orders 表,先取符合 inbound_type 的 inbound_no 集再 IN
|
||||
nos, err := inboundNosByType(ctx, inboundType)
|
||||
if err != nil || len(nos) == 0 {
|
||||
ok(w, map[string]any{"list": []any{}, "total": 0})
|
||||
return
|
||||
}
|
||||
q = q.Where(inventory.InboundNoIn(nos...))
|
||||
}
|
||||
|
||||
all, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 入库单维度信息(类型/归属/入库时间)map
|
||||
inboundNos := []string{}
|
||||
seenNo := map[string]bool{}
|
||||
for _, inv := range all {
|
||||
if inv.InboundNo != "" && !seenNo[inv.InboundNo] {
|
||||
seenNo[inv.InboundNo] = true
|
||||
inboundNos = append(inboundNos, inv.InboundNo)
|
||||
}
|
||||
}
|
||||
inboundMap := map[string]*ent.InboundOrder{}
|
||||
if len(inboundNos) > 0 {
|
||||
if orders, e := ctx.EntClient.InboundOrder.Query().Where(inboundorder.InboundNoIn(inboundNos...)).All(ctx0()); e == nil {
|
||||
for _, o := range orders {
|
||||
inboundMap[o.InboundNo] = o
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 物料名称/品类 map
|
||||
matName := map[string]string{}
|
||||
matItemType := map[string]int{}
|
||||
codeSet := []string{}
|
||||
seenC := map[string]bool{}
|
||||
for _, inv := range all {
|
||||
if !seenC[inv.MaterialCode] {
|
||||
seenC[inv.MaterialCode] = true
|
||||
codeSet = append(codeSet, inv.MaterialCode)
|
||||
}
|
||||
}
|
||||
if len(codeSet) > 0 {
|
||||
if mats, e := ctx.EntClient.Material.Query().Where(material.CodeIn(codeSet...)).All(ctx0()); e == nil {
|
||||
for _, m := range mats {
|
||||
matName[m.Code] = m.Name
|
||||
matItemType[m.Code] = m.ItemType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分组:主行 = (入库单号, 物料编码)
|
||||
group := map[string]*pendingInspectionRow{}
|
||||
order := []string{}
|
||||
for _, inv := range all {
|
||||
o := inboundMap[inv.InboundNo]
|
||||
key := inv.InboundNo + "|" + inv.MaterialCode
|
||||
g, ok := group[key]
|
||||
if !ok {
|
||||
ownType, ownNo := "", ""
|
||||
if o != nil {
|
||||
ownType = o.OwnershipType
|
||||
ownNo = o.OwnershipNo
|
||||
}
|
||||
g = &pendingInspectionRow{
|
||||
InboundNo: inv.InboundNo,
|
||||
InboundType: strOrEmpty(o),
|
||||
OwnershipType: ownershipTypeOrNone(ownType),
|
||||
OwnershipNo: ownNo,
|
||||
MaterialCode: inv.MaterialCode,
|
||||
MaterialName: matName[inv.MaterialCode],
|
||||
}
|
||||
group[key] = g
|
||||
order = append(order, key)
|
||||
}
|
||||
qty := 1
|
||||
if inv.ManageMode == 1 {
|
||||
qty = inv.Quantity
|
||||
}
|
||||
g.UnInspectedQty += qty
|
||||
g.BatchSnCount++
|
||||
tid := inv.BatchNo
|
||||
if inv.ManageMode == 2 {
|
||||
tid = inv.SnCode
|
||||
}
|
||||
g.Details = append(g.Details, pendingInspectionItem{
|
||||
ID: inv.ID, TargetId: tid, ManageMode: inv.ManageMode, ItemType: matItemType[inv.MaterialCode],
|
||||
Quantity: inv.Quantity, ZoneCode: inv.ZoneCode,
|
||||
})
|
||||
if o != nil && o.CreatedAt > g.InboundTime {
|
||||
g.InboundTime = o.CreatedAt
|
||||
}
|
||||
}
|
||||
|
||||
total := len(order)
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
rows := make([]*pendingInspectionRow, 0, end-start)
|
||||
for i := start; i < end; i++ {
|
||||
rows = append(rows, group[order[i]])
|
||||
}
|
||||
ok(w, map[string]any{"list": rows, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
}
|
||||
|
||||
// strOrEmpty 取入库单的入库类型(nil 安全)
|
||||
func strOrEmpty(o *ent.InboundOrder) string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
return o.InboundType
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user