feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统 - 为BomItem实体添加relatedStandard字段及相关CRUD方法 - 为InspectionRecord实体添加reportNo、materialCode、materialName等字段 - 更新ent schema确保新字段的验证和默认值设置 - 添加必要的数据库迁移和字段映射逻辑
This commit is contained in:
@@ -3,6 +3,7 @@ package logic
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/dailyplan"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/ent/workpiece"
|
||||
)
|
||||
|
||||
// parseDate 解析 yyyy-MM-dd 或标准时间
|
||||
@@ -63,14 +65,19 @@ func (s *Service) SaveDailyPlan(ctx context.Context, req DailyPlanReq, operator
|
||||
}
|
||||
total += p.PlanQty
|
||||
}
|
||||
if total > wo.Quantity {
|
||||
return errors.New("排产合计数量超过工单总数量,无法保存")
|
||||
// 跨天在制品:只排「未开工数量 = 工单总数量 − 已完工 − 已进线未完工」,在产工件跨天继续、不被重排冲掉
|
||||
remain, err := s.unscheduledQty(ctx, wo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total > remain {
|
||||
return errors.New("排产合计数量超过未开工数量(工单总数量−已完工−在产未完工),无法保存")
|
||||
}
|
||||
exist, err := s.ctx.EntClient.DailyPlan.Query().
|
||||
Where(dailyplan.OrderNo(req.OrderNo), dailyplan.PlanDate(req.PlanDate)).First(ctx)
|
||||
status := req.Status
|
||||
switch status {
|
||||
case "", "PENDING", "PROCESSING", "DONE", "CANCELLED":
|
||||
case "", "PENDING", "PROCESSING", "DONE", "CANCELLED", "CONFIRMED":
|
||||
if status == "" {
|
||||
status = "PENDING"
|
||||
}
|
||||
@@ -219,3 +226,170 @@ func (s *Service) ListDailyPlans(ctx context.Context, orderNo, planDate string)
|
||||
func (s *Service) DeleteDailyPlan(ctx context.Context, id int) error {
|
||||
return s.ctx.EntClient.DailyPlan.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
// ---------- 自动排产(引导式) ----------
|
||||
|
||||
// DateSegment 日期区间 [start, end](含两端),yyyy-MM-dd
|
||||
type DateSegment struct {
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
}
|
||||
|
||||
// AutoScheduleReq 引导式自动排产请求
|
||||
// Mode: AVG(平均分摊到区间各天) / FIXED(固定日产 dailyQty)
|
||||
// Segments: 一个或多个日期区间(如 6/23-26、6/28-29),区间并集去重排序
|
||||
type AutoScheduleReq struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
Mode string `json:"mode"`
|
||||
DailyQty int `json:"dailyQty"` // FIXED 模式每日产量
|
||||
Segments []DateSegment `json:"segments"`
|
||||
}
|
||||
|
||||
// unscheduledQty 未开工数量 = 工单总数量 − 已完工 − 已进线未完工(跨天在制品不重排)
|
||||
func (s *Service) unscheduledQty(ctx context.Context, wo *ent.WorkOrder) (int, error) {
|
||||
inLine, err := s.ctx.EntClient.Workpiece.Query().
|
||||
Where(workpiece.OrderNo(wo.WorkOrderNo), workpiece.StatusIn("ONLINE", "PROCESSING")).Count(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
remain := wo.Quantity - wo.FinishedNum - inLine
|
||||
if remain < 0 {
|
||||
remain = 0
|
||||
}
|
||||
return remain, nil
|
||||
}
|
||||
|
||||
// expandDates 把多个日期区间展开为去重、升序的日期列表(yyyy-MM-dd)
|
||||
func expandDates(segments []DateSegment) ([]string, error) {
|
||||
seen := map[string]bool{}
|
||||
out := []string{}
|
||||
for _, seg := range segments {
|
||||
start, e1 := time.ParseInLocation("2006-01-02", seg.Start, time.Local)
|
||||
end, e2 := time.ParseInLocation("2006-01-02", seg.End, time.Local)
|
||||
if e1 != nil || e2 != nil || !end.After(start) && !end.Equal(start) {
|
||||
return nil, errors.New("日期区间格式错误(应为 yyyy-MM-dd,且结束≥开始):" + seg.Start + "~" + seg.End)
|
||||
}
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
ds := d.Format("2006-01-02")
|
||||
if !seen[ds] {
|
||||
seen[ds] = true
|
||||
out = append(out, ds)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AutoSchedule 引导式自动排产:生成多条 daily_plan(status=PENDING,未启用),供人工确认后启用。
|
||||
// 排产合计(含已有排产)≤ 未开工数量;已进线未完工工件跨天继续、不被重排冲掉。
|
||||
func (s *Service) AutoSchedule(ctx context.Context, req AutoScheduleReq, operator string) error {
|
||||
if req.OrderNo == "" {
|
||||
return errors.New("工单号必填")
|
||||
}
|
||||
if req.Mode != "AVG" && req.Mode != "FIXED" {
|
||||
return errors.New("排产模式仅支持 AVG(平均分摊) / FIXED(固定日产)")
|
||||
}
|
||||
dates, err := expandDates(req.Segments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(dates) == 0 {
|
||||
return errors.New("请至少选择一个日期区间")
|
||||
}
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(req.OrderNo)).First(ctx)
|
||||
if err != nil {
|
||||
return errors.New("工单不存在")
|
||||
}
|
||||
if wo.Status == "DONE" || wo.Status == "CANCELLED" {
|
||||
return errors.New("该工单已完成或已取消,不能排产")
|
||||
}
|
||||
if wo.Status == "PAUSED" {
|
||||
return errors.New("该工单已暂停,恢复执行前不能排产")
|
||||
}
|
||||
remain, err := s.unscheduledQty(ctx, wo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 已有排产(非本次区间内的)已占用部分未开工数量,本次可排上限 = remain − 既有排产合计
|
||||
exist, _ := s.ctx.EntClient.DailyPlan.Query().Where(dailyplan.OrderNo(req.OrderNo)).All(ctx)
|
||||
existTotal := 0
|
||||
for _, p := range exist {
|
||||
existTotal += p.PlanQty
|
||||
}
|
||||
quota := remain - existTotal
|
||||
if quota < 0 {
|
||||
quota = 0
|
||||
}
|
||||
|
||||
days := len(dates)
|
||||
var planQty []int
|
||||
switch req.Mode {
|
||||
case "AVG":
|
||||
base := quota / days
|
||||
planQty = make([]int, days)
|
||||
for i := range planQty {
|
||||
planQty[i] = base
|
||||
}
|
||||
// 余数补到前面几天,保证合计=quota
|
||||
for i := 0; i < quota-base*days; i++ {
|
||||
planQty[i]++
|
||||
}
|
||||
case "FIXED":
|
||||
if req.DailyQty <= 0 {
|
||||
return errors.New("固定日产模式需填写每日产量(>0)")
|
||||
}
|
||||
planQty = make([]int, days)
|
||||
left := quota
|
||||
for i := range planQty {
|
||||
if left >= req.DailyQty {
|
||||
planQty[i] = req.DailyQty
|
||||
left -= req.DailyQty
|
||||
} else {
|
||||
planQty[i] = left
|
||||
left = 0
|
||||
}
|
||||
}
|
||||
if quota > req.DailyQty*days {
|
||||
return errors.New("固定日产×天数超过未开工数量,无法排产")
|
||||
}
|
||||
}
|
||||
sum := 0
|
||||
for _, q := range planQty {
|
||||
sum += q
|
||||
}
|
||||
if sum == 0 {
|
||||
return errors.New("未开工数量为 0,无需排产(已在产/已完工)")
|
||||
}
|
||||
|
||||
// 生成/更新排产(本次区间内的日期覆盖式写入,status=PENDING 待人工启用;执行中/已完工的日计划受保护不被覆盖)
|
||||
for i, ds := range dates {
|
||||
q := planQty[i]
|
||||
if q <= 0 {
|
||||
continue
|
||||
}
|
||||
existOne, _ := s.ctx.EntClient.DailyPlan.Query().
|
||||
Where(dailyplan.OrderNo(req.OrderNo), dailyplan.PlanDate(ds)).First(ctx)
|
||||
if existOne != nil {
|
||||
// 保护执行中/已完成的排产(方案 K / P2-3):不打回 PENDING、不覆盖数量,
|
||||
// 避免自动排产把正在产线执行(PROCESSING)或已完工(DONE)的日计划冲掉;
|
||||
// 这些计划的既有数量已在 existTotal 中计入、quota 已相应扣减,跳过不会超排。
|
||||
if existOne.Status == "PROCESSING" || existOne.Status == "DONE" {
|
||||
continue
|
||||
}
|
||||
_, err = s.ctx.EntClient.DailyPlan.UpdateOneID(existOne.ID).
|
||||
SetPlanQty(q).SetStatus("PENDING").SetOperator(operator).Save(ctx)
|
||||
} else {
|
||||
_, err = s.ctx.EntClient.DailyPlan.Create().
|
||||
SetOrderNo(req.OrderNo).SetPlanDate(ds).SetPlanQty(q).
|
||||
SetStatus("PENDING").SetOperator(operator).Save(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "daily.plan.auto", req.OrderNo, operator, "daily_plan", req.OrderNo,
|
||||
"自动排产(模式="+req.Mode+")", map[string]any{"days": days, "total": sum, "quota": quota})
|
||||
s.notifyDashboard()
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user