docs(deployment): 更新部署手册和业务架构说明
- 更新MES和WMS服务端口配置说明 - 详细说明产线工位设备配置(扫码枪+拧紧枪) - 明确AGV配送和接驳台的模拟模式与真实对接方案 - 更新WMS中AGV配送默认模拟模式说明 - 完善厂内物流(AGV/接驳台)的业务架构约束 - 修订临时文档为详细的项目问题和解决方案规划
This commit is contained in:
@@ -251,7 +251,7 @@ func ListMaterialRequestsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
TargetDock: q.Get("targetDock"),
|
||||
StationNo: q.Get("stationNo"),
|
||||
Source: q.Get("source"),
|
||||
Keyword: q.Get("keyword"),
|
||||
RequestNo: q.Get("requestNo"),
|
||||
})
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3104, err.Error())
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -165,6 +166,19 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
|
||||
}
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产+工位派工自动生成工位级备料单", map[string]any{"planDate": planDate, "count": created})
|
||||
// 需求10:把每个工单的「工单级需求总量」同步给 WMS 台账(幂等,失败降级不阻塞)。
|
||||
// 必须先同步 total_qty,后续自动出库才能在台账上累加 out_qty 并判定缺口/领料完结。
|
||||
synced := map[string]bool{}
|
||||
for _, g := range groups {
|
||||
if synced[g.plan.OrderNo] {
|
||||
continue
|
||||
}
|
||||
synced[g.plan.OrderNo] = true
|
||||
if e := s.SyncOrderLedger(ctx, g.plan.OrderNo, operator); e != nil {
|
||||
s.ctx.EventLog.Write(ctx, "material.ledger.sync_fail", "", operator, "material_request", g.plan.OrderNo,
|
||||
"工单需求量同步 WMS 台账失败", map[string]any{"orderNo": g.plan.OrderNo, "error": e.Error()})
|
||||
}
|
||||
}
|
||||
// 生成后立即自动出库(客户第1条:平时无需操作)。缺料/失败不阻塞生成,仅记预警等待补库存。
|
||||
if _, short := s.AutoOutboundRequests(ctx, planDate, "", operator); len(short) > 0 {
|
||||
s.ctx.EventLog.Write(ctx, "material.outbound.short", "", operator, "material_request", planDate,
|
||||
@@ -174,6 +188,58 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
|
||||
return created, nil, nil
|
||||
}
|
||||
|
||||
// SyncOrderLedger 计算并向 WMS 下发某工单的「工单级物料需求总量」(需求10)。
|
||||
//
|
||||
// 需求量口径(临时.md §四 工单域):需求量 = 工单数量 × 单台用量 × (1+损耗率),向上取整。
|
||||
// 工单级而非日级——用日级会误卡跨天生产(临时.md L283-285)。
|
||||
// 多级 BOM 展开到叶子后按物料编码聚合(同编码合并),一个物料一行下发。
|
||||
// WMS 侧按 orderNo+materialCode 幂等 upsert total_qty,可重复调用。
|
||||
func (s *Service) SyncOrderLedger(ctx context.Context, orderNo, operator string) error {
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Query().
|
||||
Where(workorder.WorkOrderNo(orderNo)).Only(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
leaves := s.expandBomToLeaf(ctx, wo.ProductCode, bomNameOrDefault(""), 1, 0, map[string]bool{})
|
||||
if len(leaves) == 0 {
|
||||
return nil
|
||||
}
|
||||
type agg struct {
|
||||
name string
|
||||
qty float64
|
||||
}
|
||||
m := map[string]*agg{}
|
||||
order := make([]string, 0, len(leaves))
|
||||
for _, it := range leaves {
|
||||
if it.materialCode == "" {
|
||||
continue
|
||||
}
|
||||
need := float64(wo.Quantity) * it.unitQty * (1 + it.lossRate/100)
|
||||
if need <= 0 {
|
||||
continue
|
||||
}
|
||||
if a, ok := m[it.materialCode]; ok {
|
||||
a.qty += need
|
||||
} else {
|
||||
m[it.materialCode] = &agg{name: it.materialName, qty: need}
|
||||
order = append(order, it.materialCode)
|
||||
}
|
||||
}
|
||||
items := make([]wmsclient.LedgerSyncItem, 0, len(order))
|
||||
for _, code := range order {
|
||||
a := m[code]
|
||||
items = append(items, wmsclient.LedgerSyncItem{
|
||||
MaterialCode: code,
|
||||
MaterialName: a.name,
|
||||
TotalQty: int(math.Ceil(a.qty - 1e-9)),
|
||||
})
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ctx.Wms.LedgerSync(ctx, orderNo, wo.ProductCode, operator, items)
|
||||
}
|
||||
|
||||
// AutoOutboundRequests 工单备料自动出库(客户第1条:备料出库根据 MES 排产自动生成,平时无需操作)。
|
||||
//
|
||||
// 对指定排产日 / 工单下的备料单,按 FIFO 从 WMS「合格」在库库存自动领料:
|
||||
@@ -356,7 +422,7 @@ type MaterialRequestQuery struct {
|
||||
TargetDock string
|
||||
StationNo string
|
||||
Source string // PLAN / REFILL
|
||||
Keyword string // 兜底匹配 备料单号/工单号/物料编码
|
||||
RequestNo string // 备料单号(独立模糊筛选;列表页禁用关键字混搜)
|
||||
}
|
||||
|
||||
// ListMaterialRequests 备料单查询(大小写不敏感)。
|
||||
@@ -383,12 +449,8 @@ func (s *Service) ListMaterialRequests(ctx context.Context, o MaterialRequestQue
|
||||
if o.Source != "" {
|
||||
q = q.Where(materialrequest.Source(o.Source))
|
||||
}
|
||||
if o.Keyword != "" {
|
||||
q = q.Where(materialrequest.Or(
|
||||
materialrequest.RequestNoContainsFold(o.Keyword),
|
||||
materialrequest.OrderNoContainsFold(o.Keyword),
|
||||
materialrequest.MaterialCodeContainsFold(o.Keyword),
|
||||
))
|
||||
if o.RequestNo != "" {
|
||||
q = q.Where(materialrequest.RequestNoContainsFold(o.RequestNo))
|
||||
}
|
||||
if o.StationNo != "" {
|
||||
if n, e := strconv.Atoi(o.StationNo); e == nil {
|
||||
|
||||
@@ -572,6 +572,57 @@ func (c *Client) LedgerProgress(ctx context.Context, orderNo, materialCode strin
|
||||
return out.List, out.Total, nil
|
||||
}
|
||||
|
||||
// LedgerSyncItem 工单物料台账同步项(工单级需求量,一个物料一行)。
|
||||
type LedgerSyncItem struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
TotalQty int `json:"totalQty"`
|
||||
}
|
||||
|
||||
// LedgerSync 向 WMS 下发「该工单每种物料总共要多少料」(工单级需求量)。
|
||||
//
|
||||
// 需求10根因修复:MES 生成备料单后必须把工单级需求量同步给 WMS,
|
||||
// WMS 才知道 total_qty(台账需求量),否则出库/缺口/领料完结判定都无据可依。
|
||||
// WMS 侧按 orderNo+materialCode 幂等 upsert total_qty(详见 ledgerSyncHandler)。
|
||||
func (c *Client) LedgerSync(ctx context.Context, orderNo, productCode, operator string, items []LedgerSyncItem) error {
|
||||
if orderNo == "" || len(items) == 0 {
|
||||
return errors.New("orderNo 和 items 必填")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"orderNo": orderNo,
|
||||
"productCode": productCode,
|
||||
"operator": operator,
|
||||
"items": items,
|
||||
}
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/internal/ledger/sync", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-TOKEN", c.token)
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&body)
|
||||
if resp.StatusCode >= 300 || body.Code != 0 {
|
||||
if body.Message != "" {
|
||||
return errors.New(body.Message)
|
||||
}
|
||||
return &RespError{Status: resp.StatusCode}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) post(ctx context.Context, path string, body any) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user