2026-08-28 15:06:01 +08:00
|
|
|
|
package logic
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2026-09-19 17:04:58 +08:00
|
|
|
|
"sort"
|
2026-09-19 07:50:21 +08:00
|
|
|
|
"strconv"
|
2026-09-19 17:43:14 +08:00
|
|
|
|
"strings"
|
2026-08-28 15:06:01 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"bj_power_mes/ent"
|
|
|
|
|
|
"bj_power_mes/ent/materialrequest"
|
2026-09-19 07:50:21 +08:00
|
|
|
|
"bj_power_mes/ent/station"
|
2026-08-29 15:30:25 +08:00
|
|
|
|
"bj_power_mes/ent/workorder"
|
2026-09-19 17:04:58 +08:00
|
|
|
|
"bj_power_mes/internal/wmsclient"
|
2026-08-28 15:06:01 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-19 07:50:21 +08:00
|
|
|
|
// GenerateMaterialRequest 按日排产 + 今日工位派工(station_process) 自动算料,生成工位级备料单。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 算法:
|
|
|
|
|
|
// - 路线来源:今日 station_process 派工(工位号→工序编号)。某工序(1~12)今日派工到哪些工位,
|
|
|
|
|
|
// 该工序的 BOM 物料就拆到这些工位各生成一行备料单(物料随工序上料)。
|
|
|
|
|
|
// - 数量:单工位量 = 该工位分配产量(日排产 stationPlanQty,未配置则日总量平均到各派工工位)
|
|
|
|
|
|
// × BOM 单台用量 ×(1+损耗率);同工序多工位时日总量按分配/平均拆分,不重复放大。
|
|
|
|
|
|
// - 目标接驳台:取该派工工位的 station.dockCode(无接驳台则留空待仓管指定)。
|
|
|
|
|
|
// - processCode=0 的 BOM 物料(不参与工位绑定):按日总量生成一行,工位/接驳台留空。
|
|
|
|
|
|
//
|
2026-09-04 14:18:53 +08:00
|
|
|
|
// 生成前校验:BOM 内所有物料编码须在 WMS 物料档案存在,有缺失返回 missing(不生成);
|
|
|
|
|
|
// WMS 不可达/异常时降级为"全部存在"并记 eventlog,避免 WMS 抖动阻塞产线。
|
|
|
|
|
|
// 返回值:count=本次生成条数;missing=WMS 中不存在的物料编码(非空时 count=0)。
|
|
|
|
|
|
func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string, operator string) (int, []string, error) {
|
2026-08-28 15:06:01 +08:00
|
|
|
|
plans, err := s.ListDailyPlans(ctx, "", planDate)
|
|
|
|
|
|
if err != nil {
|
2026-09-04 14:18:53 +08:00
|
|
|
|
return 0, nil, err
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if len(plans) == 0 {
|
2026-09-04 14:18:53 +08:00
|
|
|
|
return 0, nil, errors.New("该日期没有排产计划")
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
|
// 今日工位派工:工位号 → 工序编号
|
|
|
|
|
|
procMap, err := RouteStationProcessCodeMap(ctx, s.ctx.EntClient, TodayStr(), "")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(procMap) == 0 {
|
|
|
|
|
|
return 0, nil, errors.New("今日未配置工位派工(station_process),无法生成备料单")
|
|
|
|
|
|
}
|
|
|
|
|
|
// 工序编号 → 工位号列表(反查,供 BOM 物料按工序拆到工位)
|
|
|
|
|
|
stationsByProcess := map[int][]int{}
|
|
|
|
|
|
stNos := make([]int, 0, len(procMap))
|
|
|
|
|
|
for stNo, pc := range procMap {
|
|
|
|
|
|
stationsByProcess[pc] = append(stationsByProcess[pc], stNo)
|
|
|
|
|
|
stNos = append(stNos, stNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 工位号 → 接驳台编码
|
|
|
|
|
|
dockByStation := map[int]string{}
|
|
|
|
|
|
if sts, e := s.ctx.EntClient.Station.Query().Where(station.StationNoIn(stNos...)).All(ctx); e == nil {
|
|
|
|
|
|
for _, st := range sts {
|
|
|
|
|
|
dockByStation[st.StationNo] = st.DockCode
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 14:18:53 +08:00
|
|
|
|
type planBom struct {
|
|
|
|
|
|
plan *ent.DailyPlan
|
|
|
|
|
|
wo *ent.WorkOrder
|
|
|
|
|
|
bom []*ent.BomItem
|
|
|
|
|
|
}
|
|
|
|
|
|
groups := make([]planBom, 0, len(plans))
|
|
|
|
|
|
codeSet := map[string]bool{}
|
2026-08-28 15:06:01 +08:00
|
|
|
|
for _, plan := range plans {
|
2026-09-04 14:18:53 +08:00
|
|
|
|
if plan.Status == "CANCELLED" || plan.Status == "DONE" {
|
|
|
|
|
|
continue // 已取消/已完成的排产不再算料
|
|
|
|
|
|
}
|
2026-08-29 15:30:25 +08:00
|
|
|
|
wo, err := s.ctx.EntClient.WorkOrder.Query().
|
|
|
|
|
|
Where(workorder.WorkOrderNo(plan.OrderNo)).Only(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-09-10 16:59:15 +08:00
|
|
|
|
bom, err := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault(""))
|
2026-08-28 15:06:01 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-09-04 14:18:53 +08:00
|
|
|
|
if len(bom) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-28 15:06:01 +08:00
|
|
|
|
for _, item := range bom {
|
2026-09-04 14:18:53 +08:00
|
|
|
|
if item.MaterialCode != "" {
|
|
|
|
|
|
codeSet[item.MaterialCode] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
groups = append(groups, planBom{plan: plan, wo: wo, bom: bom})
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(groups) == 0 {
|
|
|
|
|
|
return 0, nil, errors.New("该日期排产均无有效 BOM 物料,无法生成备料单")
|
|
|
|
|
|
}
|
|
|
|
|
|
// 阶段1:WMS 物料存在性校验(全部存在才生成)
|
|
|
|
|
|
codes := make([]string, 0, len(codeSet))
|
|
|
|
|
|
for c := range codeSet {
|
|
|
|
|
|
codes = append(codes, c)
|
|
|
|
|
|
}
|
|
|
|
|
|
missing, wmsErr := s.ctx.Wms.MaterialExists(ctx, codes)
|
|
|
|
|
|
if wmsErr != nil {
|
|
|
|
|
|
// 降级:WMS 不可达按"全部存在"放行,记日志供事后核查
|
|
|
|
|
|
s.ctx.EventLog.Write(ctx, "material.request.wms_skip", "", operator, "material_request", planDate,
|
|
|
|
|
|
"WMS 物料校验不可达,已降级放行", map[string]any{"planDate": planDate, "error": wmsErr.Error()})
|
|
|
|
|
|
} else if len(missing) > 0 {
|
|
|
|
|
|
return 0, missing, nil
|
|
|
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
|
// 阶段2:全部存在 → 按工位拆料生成
|
2026-09-04 14:18:53 +08:00
|
|
|
|
created := 0
|
|
|
|
|
|
tsBase := time.Now().Format("20060102150405")
|
|
|
|
|
|
seq := 1
|
|
|
|
|
|
for _, g := range groups {
|
2026-09-19 07:50:21 +08:00
|
|
|
|
// 工位产量分配(日排产层):stationNo(字符串键) → 计划产量
|
|
|
|
|
|
stationQty := map[int]int{}
|
|
|
|
|
|
for k, v := range g.plan.StationPlanQty {
|
|
|
|
|
|
if n, e := strconv.Atoi(k); e == nil {
|
|
|
|
|
|
stationQty[n] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 成品可作零部件:BOM 子项若为成品,递归展开到叶子(原材料/外购件)再算料
|
|
|
|
|
|
leaves := s.expandBomToLeaf(ctx, g.wo.ProductCode, bomNameOrDefault(""), 1, 0, map[string]bool{})
|
|
|
|
|
|
for _, item := range leaves {
|
|
|
|
|
|
pc := item.processCode // 该物料所属装配工序(1~12),0=不参与工位绑定
|
|
|
|
|
|
if pc == 0 {
|
|
|
|
|
|
reqQty := float64(g.plan.PlanQty) * item.unitQty * (1 + item.lossRate/100)
|
|
|
|
|
|
if reqQty <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq)
|
|
|
|
|
|
seq++
|
|
|
|
|
|
_ = s.ctx.EntClient.MaterialRequest.Create().
|
|
|
|
|
|
SetRequestNo(reqNo).SetOrderNo(g.plan.OrderNo).SetPlanDate(g.plan.PlanDate).
|
|
|
|
|
|
SetMaterialCode(item.materialCode).SetMaterialName(item.materialName).
|
|
|
|
|
|
SetUnit(item.unit).SetManageMode(item.manageMode).SetReqQty(reqQty).
|
|
|
|
|
|
SetStatus("PENDING").SetOperator(operator).Exec(ctx)
|
|
|
|
|
|
created++
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
stations := stationsByProcess[pc]
|
|
|
|
|
|
if len(stations) == 0 {
|
|
|
|
|
|
// 该工序今日未派工到任何工位,本日无需上料(非缺料)
|
2026-08-28 15:06:01 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
|
qtys := splitQtyToStations(stations, g.plan.PlanQty, stationQty)
|
|
|
|
|
|
for i, stNo := range stations {
|
|
|
|
|
|
q := qtys[i]
|
|
|
|
|
|
if q <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
reqQty := float64(q) * item.unitQty * (1 + item.lossRate/100)
|
|
|
|
|
|
if reqQty <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq)
|
|
|
|
|
|
seq++
|
|
|
|
|
|
_ = s.ctx.EntClient.MaterialRequest.Create().
|
|
|
|
|
|
SetRequestNo(reqNo).SetOrderNo(g.plan.OrderNo).SetPlanDate(g.plan.PlanDate).
|
|
|
|
|
|
SetMaterialCode(item.materialCode).SetMaterialName(item.materialName).
|
|
|
|
|
|
SetUnit(item.unit).SetManageMode(item.manageMode).SetReqQty(reqQty).
|
|
|
|
|
|
SetStatus("PENDING").SetOperator(operator).
|
|
|
|
|
|
SetStationNo(stNo).SetProcessCode(pc).SetTargetDock(dockByStation[stNo]).Exec(ctx)
|
|
|
|
|
|
created++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产+工位派工自动生成工位级备料单", map[string]any{"planDate": planDate, "count": created})
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// 生成后立即自动出库(客户第1条:平时无需操作)。缺料/失败不阻塞生成,仅记预警等待补库存。
|
|
|
|
|
|
if _, short := s.AutoOutboundRequests(ctx, planDate, "", operator); len(short) > 0 {
|
|
|
|
|
|
s.ctx.EventLog.Write(ctx, "material.outbound.short", "", operator, "material_request", planDate,
|
|
|
|
|
|
"备料自动出库未齐套", map[string]any{"planDate": planDate, "short": strings.Join(short, "; ")})
|
|
|
|
|
|
}
|
2026-09-04 17:31:18 +08:00
|
|
|
|
s.notifyDashboard()
|
2026-09-04 14:18:53 +08:00
|
|
|
|
return created, nil, nil
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// AutoOutboundRequests 工单备料自动出库(客户第1条:备料出库根据 MES 排产自动生成,平时无需操作)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 对指定排产日 / 工单下的备料单,按 FIFO 从 WMS「合格」在库库存自动领料:
|
|
|
|
|
|
// - 结构件(manageMode=1):按批次号升序逐批出库,直到补足未出数量;
|
|
|
|
|
|
// - 电气件(manageMode=2):按可用 SN 逐件出库。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 幂等:只出「未出数量 = 需求量 − 已发数量」,重复调用不会超领。
|
|
|
|
|
|
// 库存不足的行已出部分照常计入 sentQty,差额维持待备料并触发「备料未齐套」预警,
|
|
|
|
|
|
// 库存补齐后再次调用即可补出(补出由 WMS 台账强约束兜底,不会超出工单 BOM 需求)。
|
|
|
|
|
|
// 返回:本次补足(出库总量达标)的备料单条数、未出齐的明细文案。
|
|
|
|
|
|
func (s *Service) AutoOutboundRequests(ctx context.Context, planDate, orderNo, operator string) (int, []string) {
|
|
|
|
|
|
list, err := s.ListMaterialRequests(ctx, MaterialRequestQuery{OrderNo: orderNo, PlanDate: planDate})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, []string{"查询备料单失败: " + err.Error()}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 补料单(refill)优先于排产备料(plan):先把补料发出,避免产线因缺料停线。
|
|
|
|
|
|
sort.SliceStable(list, func(i, j int) bool {
|
|
|
|
|
|
return list[i].Source == "REFILL" && list[j].Source != "REFILL"
|
|
|
|
|
|
})
|
|
|
|
|
|
done := 0
|
|
|
|
|
|
short := make([]string, 0)
|
|
|
|
|
|
for _, mr := range list {
|
|
|
|
|
|
need := int(mr.ReqQty - mr.SentQty + 0.5)
|
|
|
|
|
|
if need <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
batches, sns, err := s.ctx.Wms.StockBatches(ctx, mr.MaterialCode)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
short = append(short, fmt.Sprintf("%s %s 查询 WMS 库存失败", mr.OrderNo, mr.MaterialCode))
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
dock := mr.TargetDock
|
|
|
|
|
|
if dock == "" {
|
|
|
|
|
|
dock = "DOCK01"
|
|
|
|
|
|
}
|
|
|
|
|
|
remain := need
|
|
|
|
|
|
outboundNos := make([]string, 0, 4)
|
|
|
|
|
|
// 出库参数:补料(source=REFILL) 记为 refill 类型并默认叫车,且携带超领原因
|
|
|
|
|
|
// (补料常因损耗/报废超出 BOM 需求,WMS 侧有原因才放行)。
|
|
|
|
|
|
obType := "workorder"
|
|
|
|
|
|
if mr.Source == "REFILL" {
|
|
|
|
|
|
obType = "refill"
|
|
|
|
|
|
}
|
|
|
|
|
|
opts := wmsclient.StockDeductOpts{
|
|
|
|
|
|
Qty: need, Operator: operator, TargetDock: dock,
|
|
|
|
|
|
OutboundType: obType, NeedAgv: mr.NeedAgv, OverReason: mr.OverReason,
|
|
|
|
|
|
}
|
|
|
|
|
|
if mr.ManageMode == "2" {
|
|
|
|
|
|
for _, sn := range sns {
|
|
|
|
|
|
if remain <= 0 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
opts.SnList = []string{sn}
|
|
|
|
|
|
opts.BatchNo = ""
|
|
|
|
|
|
opts.Qty = 1
|
|
|
|
|
|
no, _, e := s.ctx.Wms.StockDeduct(ctx, mr.OrderNo, mr.MaterialCode, opts)
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
outboundNos = append(outboundNos, no)
|
|
|
|
|
|
remain--
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for _, b := range batches {
|
|
|
|
|
|
if remain <= 0 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
take := b.Avail
|
|
|
|
|
|
if take > remain {
|
|
|
|
|
|
take = remain
|
|
|
|
|
|
}
|
|
|
|
|
|
if take <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
opts.SnList = nil
|
|
|
|
|
|
opts.BatchNo = b.BatchNo
|
|
|
|
|
|
opts.Qty = take
|
|
|
|
|
|
opts.ZoneCode = b.ZoneCode
|
|
|
|
|
|
no, _, e := s.ctx.Wms.StockDeduct(ctx, mr.OrderNo, mr.MaterialCode, opts)
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
outboundNos = append(outboundNos, no)
|
|
|
|
|
|
remain -= take
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sent := need - remain
|
|
|
|
|
|
if sent > 0 {
|
|
|
|
|
|
upd := s.ctx.EntClient.MaterialRequest.UpdateOneID(mr.ID).AddSentQty(float64(sent))
|
|
|
|
|
|
if remain == 0 {
|
|
|
|
|
|
// 出库量已达标:备料完成,等 AGV 配送(WMS 下发后回写 DELIVERING)
|
|
|
|
|
|
upd = upd.SetStatus("LOCKED")
|
|
|
|
|
|
done++
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, e := upd.Save(ctx); e == nil {
|
|
|
|
|
|
s.ctx.EventLog.Write(ctx, "material.outbound.auto", mr.OrderNo, operator, "material_request", mr.RequestNo,
|
|
|
|
|
|
"备料自动出库", map[string]any{"materialCode": mr.MaterialCode, "qty": sent, "outboundNos": strings.Join(outboundNos, ",")})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if remain > 0 {
|
|
|
|
|
|
short = append(short, fmt.Sprintf("%s %s 缺 %d", mr.OrderNo, mr.MaterialCode, remain))
|
|
|
|
|
|
s.Evaluate(ctx, "material_shortage", 1, "备料未齐套",
|
|
|
|
|
|
fmt.Sprintf("工单 %s 物料 %s 自动出库后仍缺 %d(已出 %d/%d)", mr.OrderNo, mr.MaterialCode, remain, sent, need),
|
|
|
|
|
|
"material_request", mr.RequestNo, mr.OrderNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if done > 0 {
|
|
|
|
|
|
s.notifyDashboard()
|
|
|
|
|
|
}
|
|
|
|
|
|
return done, short
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 07:50:21 +08:00
|
|
|
|
// bomLeaf 递归展开后的叶子物料行(原材料/外购件)
|
|
|
|
|
|
type bomLeaf struct {
|
|
|
|
|
|
materialCode string
|
|
|
|
|
|
materialName string
|
|
|
|
|
|
unit string
|
|
|
|
|
|
manageMode string
|
|
|
|
|
|
unitQty float64 // 已折算父级倍率的单台用量
|
|
|
|
|
|
lossRate float64
|
|
|
|
|
|
processCode int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// expandBomToLeaf 递归展开产品物料清单到叶子(原材料/外购件),供备料算料。
|
2026-09-19 07:50:21 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 业务口径:成品可以作为零部件被另一个成品再次加工(多级嵌套)。备料算料时必须把
|
|
|
|
|
|
// 成品子项按其自身的物料清单继续展开,直到原材料/外购件,否则产线会缺料。
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// 用量按倍率相乘(父级单台用量 × 子级单台用量)。
|
2026-09-19 07:50:21 +08:00
|
|
|
|
//
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// R1(2026-09-19):递归骨架统一到 bom_expand.go 的 expandBomTree,与追溯侧
|
|
|
|
|
|
// (expandBindBom)共用同一份实现;下钻判定由 wmsFinishedResolver 注入,
|
|
|
|
|
|
// WMS 不可达时按叶子处理,不阻塞备料生成。
|
2026-09-19 07:50:21 +08:00
|
|
|
|
func (s *Service) expandBomToLeaf(ctx context.Context, productCode, bomName string, mult float64, depth int, path map[string]bool) []bomLeaf {
|
2026-09-19 17:04:58 +08:00
|
|
|
|
return bomTreeToLeaf(s.expandBomTree(ctx, productCode, bomName, mult, depth, path, s.wmsFinishedResolver))
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// splitQtyToStations 把日总量 dayQty 拆分到 stations 各工位:
|
|
|
|
|
|
// 优先使用 stationQty 显式分配(其和>0 时直接采用);否则平均分配(余数补到前面工位)。
|
|
|
|
|
|
func splitQtyToStations(stations []int, dayQty int, stationQty map[int]int) []int {
|
|
|
|
|
|
n := len(stations)
|
|
|
|
|
|
out := make([]int, n)
|
|
|
|
|
|
usePlan := true
|
|
|
|
|
|
sum := 0
|
|
|
|
|
|
for _, st := range stations {
|
|
|
|
|
|
v, ok := stationQty[st]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
usePlan = false
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
sum += v
|
|
|
|
|
|
}
|
|
|
|
|
|
if usePlan && sum > 0 {
|
|
|
|
|
|
for i, st := range stations {
|
|
|
|
|
|
out[i] = stationQty[st]
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
if n == 0 {
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
base := dayQty / n
|
|
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
|
|
out[i] = base
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := 0; i < dayQty-base*n; i++ {
|
|
|
|
|
|
out[i]++
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// MaterialRequestQuery 备料单查询条件(零值=不过滤)。
|
|
|
|
|
|
// 统一入口:原 ListMaterialRequests / ListMaterialRequestsFiltered 两套位置参数合并到本结构,
|
|
|
|
|
|
// 避免参数越加越长、调用点易错。
|
|
|
|
|
|
type MaterialRequestQuery struct {
|
|
|
|
|
|
OrderNo string
|
|
|
|
|
|
PlanDate string
|
|
|
|
|
|
Status string
|
|
|
|
|
|
MaterialCode string
|
|
|
|
|
|
MaterialName string
|
|
|
|
|
|
TargetDock string
|
|
|
|
|
|
StationNo string
|
|
|
|
|
|
Source string // PLAN / REFILL
|
|
|
|
|
|
Keyword string // 兜底匹配 备料单号/工单号/物料编码
|
2026-09-08 11:44:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// ListMaterialRequests 备料单查询(大小写不敏感)。
|
|
|
|
|
|
func (s *Service) ListMaterialRequests(ctx context.Context, o MaterialRequestQuery) ([]*ent.MaterialRequest, error) {
|
2026-08-28 15:06:01 +08:00
|
|
|
|
q := s.ctx.EntClient.MaterialRequest.Query()
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.OrderNo != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.OrderNoEqualFold(o.OrderNo))
|
|
|
|
|
|
}
|
|
|
|
|
|
if o.PlanDate != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.PlanDate(o.PlanDate))
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.Status != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.Status(o.Status))
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.MaterialCode != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.MaterialCodeContainsFold(o.MaterialCode))
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.MaterialName != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.MaterialNameContainsFold(o.MaterialName))
|
2026-09-08 11:44:04 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.TargetDock != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.TargetDockEqualFold(o.TargetDock))
|
2026-09-08 11:44:04 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.Source != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.Source(o.Source))
|
2026-09-08 11:44:04 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if o.Keyword != "" {
|
|
|
|
|
|
q = q.Where(materialrequest.Or(
|
|
|
|
|
|
materialrequest.RequestNoContainsFold(o.Keyword),
|
|
|
|
|
|
materialrequest.OrderNoContainsFold(o.Keyword),
|
|
|
|
|
|
materialrequest.MaterialCodeContainsFold(o.Keyword),
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
if o.StationNo != "" {
|
|
|
|
|
|
if n, e := strconv.Atoi(o.StationNo); e == nil {
|
2026-09-19 07:50:21 +08:00
|
|
|
|
q = q.Where(materialrequest.StationNo(n))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-08 11:44:04 +08:00
|
|
|
|
return q.Order(ent.Desc(materialrequest.FieldCreatedAt), ent.Desc(materialrequest.FieldID)).All(ctx)
|
2026-08-28 15:06:01 +08:00
|
|
|
|
}
|
2026-09-04 14:18:53 +08:00
|
|
|
|
|
2026-09-19 07:50:21 +08:00
|
|
|
|
// ReceiveMaterialRequest 接料确认(两个入口共用:工位终端扫码接料 / MES 备料页人工点「已接料」)。
|
2026-09-19 17:04:58 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 字段口径(2026-09-19 定稿):写 receivedQty(已接料),**不再累加 sentQty**。
|
2026-09-19 17:43:14 +08:00
|
|
|
|
//
|
|
|
|
|
|
// sentQty 已发出量 [写者:自动出库] —— 幂等基准「还需发多少 = reqQty − sentQty」
|
|
|
|
|
|
// receivedQty 已接料量 [写者:本方法] —— 在途 = sentQty − receivedQty
|
2026-09-19 17:04:58 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 上限 = 已发出量(工位只能接到仓库已发出来的料),不超接。
|
|
|
|
|
|
// qty<=0 表示「确认本批全部剩余」,适合扫码/一键接料;>0 为本次实收批次量。
|
|
|
|
|
|
// 累计接到 sentQty 且已发齐 reqQty → 状态置 DONE(送达并与接料双完结)。
|
2026-09-19 07:50:21 +08:00
|
|
|
|
func (s *Service) ReceiveMaterialRequest(ctx context.Context, requestNo string, qty int, operator string) error {
|
|
|
|
|
|
mr, err := s.ctx.EntClient.MaterialRequest.Query().
|
|
|
|
|
|
Where(materialrequest.RequestNo(requestNo)).Only(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.New("备料单不存在")
|
|
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
sent := int(mr.SentQty)
|
|
|
|
|
|
already := int(mr.ReceivedQty)
|
|
|
|
|
|
pending := sent - already // 已发出但尚未接料(在途)
|
|
|
|
|
|
if pending < 0 {
|
|
|
|
|
|
pending = 0
|
|
|
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
|
add := qty
|
|
|
|
|
|
if add <= 0 {
|
2026-09-19 17:04:58 +08:00
|
|
|
|
add = pending // 确认本批全部剩余
|
|
|
|
|
|
}
|
|
|
|
|
|
if add > pending {
|
|
|
|
|
|
add = pending // 不超接:上限是仓库已发出的量
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
if add < 0 {
|
|
|
|
|
|
add = 0
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
newReceived := already + add
|
2026-09-19 07:50:21 +08:00
|
|
|
|
upd := s.ctx.EntClient.MaterialRequest.UpdateOneID(mr.ID).
|
2026-09-19 17:04:58 +08:00
|
|
|
|
SetReceivedQty(float64(newReceived)).
|
|
|
|
|
|
SetReceivedAt(time.Now().Unix()).
|
|
|
|
|
|
SetReceiveBy(operator)
|
|
|
|
|
|
if mr.Status != "DONE" && sent >= int(mr.ReqQty) && newReceived >= sent {
|
2026-09-19 07:50:21 +08:00
|
|
|
|
upd.SetStatus("DONE")
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err = upd.Save(ctx); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
s.ctx.EventLog.Write(ctx, "material.request.receive", requestNo, operator, "material_request", requestNo,
|
2026-09-19 17:04:58 +08:00
|
|
|
|
"接料确认", map[string]any{"add": add, "receivedQty": newReceived, "sentQty": sent,
|
|
|
|
|
|
"reqQty": int(mr.ReqQty), "awaiting": sent - newReceived})
|
2026-09-19 07:50:21 +08:00
|
|
|
|
s.notifyDashboard()
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 14:18:53 +08:00
|
|
|
|
// SetMaterialRequestStatus 推进备料单状态(WMS 下发 AGV DELIVERING / 到位 DONE)
|
|
|
|
|
|
func (s *Service) SetMaterialRequestStatus(ctx context.Context, requestNo, status string) error {
|
|
|
|
|
|
n, err := s.ctx.EntClient.MaterialRequest.Update().
|
|
|
|
|
|
Where(materialrequest.RequestNo(requestNo)).
|
|
|
|
|
|
SetStatus(status).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if n == 0 {
|
|
|
|
|
|
return errors.New("备料单不存在")
|
|
|
|
|
|
}
|
2026-09-04 17:31:18 +08:00
|
|
|
|
s.notifyDashboard()
|
2026-09-04 14:18:53 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|