feat(production): 新增排产支撑功能并移除手动报工页面

- 在main.go中更新排产物料需求注释说明,明确WMS与MES系统间交互方式
- 从前端help.js中移除手动报工(helpScan)相关帮助文档
- 更新工艺流程、绩效报表等页面帮助文档中的报工流程说明
- 在主布局中添加排产支撑菜单项并移除手动报工菜单项
- 新增ProducibleSupport.vue组件实现排产支撑页面功能
- 移除Scan.vue手动报工页面组件
- 更新相关帮助文档内容以匹配新的业务流程
This commit is contained in:
SunYF
2026-09-21 14:06:05 +08:00
parent 132c3ed6bd
commit 0e769f3813
105 changed files with 2313 additions and 5314 deletions
+353 -175
View File
@@ -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
}