|
|
@@ -1,4 +1,6 @@
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
+using Admin.NET.Plugin.AiDOP.Production;
|
|
|
+using Admin.NET.Plugin.AiDOP.Supply;
|
|
|
using Admin.NET.Plugin.AiDOP.WorkOrder;
|
|
|
|
|
|
namespace Admin.NET.Plugin.AiDOP.Order;
|
|
|
@@ -23,6 +25,8 @@ public class OrderReviewOrchestrationService : ITransient
|
|
|
private readonly WorkOrderRoutingSyncService _routingSync;
|
|
|
private readonly S1MdpSyncTransformService _mdpSync;
|
|
|
private readonly AidopActionRunLogWriter _runLog;
|
|
|
+ private readonly ProductionScheduleGenerationService _scheduleGen;
|
|
|
+ private readonly ProcurementPipelineService _pipeline;
|
|
|
|
|
|
public OrderReviewOrchestrationService(
|
|
|
ISqlSugarClient db,
|
|
|
@@ -32,7 +36,9 @@ public class OrderReviewOrchestrationService : ITransient
|
|
|
WorkOrderMaterialDetailSyncService materialDetailSync,
|
|
|
WorkOrderRoutingSyncService routingSync,
|
|
|
S1MdpSyncTransformService mdpSync,
|
|
|
- AidopActionRunLogWriter runLog)
|
|
|
+ AidopActionRunLogWriter runLog,
|
|
|
+ ProductionScheduleGenerationService scheduleGen,
|
|
|
+ ProcurementPipelineService pipeline)
|
|
|
{
|
|
|
_db = db;
|
|
|
_userManager = userManager;
|
|
|
@@ -42,6 +48,8 @@ public class OrderReviewOrchestrationService : ITransient
|
|
|
_routingSync = routingSync;
|
|
|
_mdpSync = mdpSync;
|
|
|
_runLog = runLog;
|
|
|
+ _scheduleGen = scheduleGen;
|
|
|
+ _pipeline = pipeline;
|
|
|
}
|
|
|
|
|
|
public Task<SeOrderReviewExecuteResult> ReviewAsync(IReadOnlyList<long> orderIds) =>
|
|
|
@@ -53,10 +61,25 @@ public class OrderReviewOrchestrationService : ITransient
|
|
|
public Task<SeOrderReviewExecuteResult> RefreshPlanAsync(long orderId, string? reason) =>
|
|
|
ExecuteSingleAsync(ActionRefresh, orderId, async (order, result, warnings, account) =>
|
|
|
{
|
|
|
+ // ── 第0步:物料编码变更校验 ──
|
|
|
+ await ValidateMaterialNotChangedAsync(order.Id, order.TenantId);
|
|
|
+
|
|
|
+ // ── 第1步:清理旧占用记录,确保跨工单库存递减重新计算 ──
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM ic_item_stockoccupy WHERE tenant_id = @TenantId AND bang_id = @BangId",
|
|
|
+ new SugarParameter("@TenantId", order.TenantId),
|
|
|
+ new SugarParameter("@BangId", ReviewBangId));
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM srm_po_occupy WHERE tenant_id = @TenantId AND bang_id = @BangId",
|
|
|
+ new SugarParameter("@TenantId", order.TenantId),
|
|
|
+ new SugarParameter("@BangId", ReviewBangId));
|
|
|
+
|
|
|
+ // ── 第2步:加载可重排明细行(确认 / 再评审) ──
|
|
|
var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["3", "0"]);
|
|
|
if (entries.Count == 0)
|
|
|
throw Oops.Oh("订单没有可重排的确认/再评审明细行");
|
|
|
|
|
|
+ // ── 第3步:逐条处理(工单 + 资源检查 + 领料单 + 交期更新) ──
|
|
|
foreach (var entry in entries)
|
|
|
{
|
|
|
ValidateEntryForResourceCheck(entry);
|
|
|
@@ -71,9 +94,44 @@ public class OrderReviewOrchestrationService : ITransient
|
|
|
|
|
|
await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, wo.WorkOrd, account);
|
|
|
await _routingSync.EnsureFromRoutingAsync(entry.TenantId, wo.WorkOrd, account);
|
|
|
+
|
|
|
+ // 当工单状态为下达/投产/暂停(R、W、S)时,更新对应领料单数据
|
|
|
+ await UpdatePickingListForActiveWorkOrderAsync(entry.TenantId, wo.WorkOrd, account, warnings);
|
|
|
+
|
|
|
+ // 根据资源检查结果更新明细行系统建议交期
|
|
|
+ await UpdateEntrySysCapacityDateAsync(entry.Id, check.KittingTime, account);
|
|
|
}
|
|
|
|
|
|
- await UpdateEntriesProgressAsync(entries.Select(e => e.Id).ToList(), "0", account);
|
|
|
+ // ── 第4步:更新明细行进度为3 ──
|
|
|
+ await UpdateEntriesProgressAsync(entries.Select(e => e.Id).ToList(), "3", account);
|
|
|
+
|
|
|
+ // ── 第4.5步:重新进行生产排程(在事务提交前) ──
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var scheduleResult = await _scheduleGen.GenerateAsync(order.TenantId, order.TenantId.ToString(), account);
|
|
|
+ if (!string.IsNullOrWhiteSpace(scheduleResult.Message))
|
|
|
+ warnings.Add($"生产排程:{scheduleResult.Message}");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ warnings.Add($"生产排程失败:{ex.Message}");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 第4.5步:同步物料需求(MRP → PR → 采购闭环) ──
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var mrResult = await _pipeline.ExecuteCoreAsync(order.TenantId, account, createFromShortage: true);
|
|
|
+ if (!string.IsNullOrWhiteSpace(mrResult.Message))
|
|
|
+ warnings.Add($"物料需求同步:{mrResult.Message}");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ warnings.Add($"物料需求同步失败:{ex.Message}");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 第4.5步:生成交货单(出货计划) ──
|
|
|
+ await GenerateShippingPlanFromOrderAsync(order, entries, account, warnings);
|
|
|
+
|
|
|
result.EntryCount = entries.Count;
|
|
|
result.Message = string.IsNullOrWhiteSpace(reason)
|
|
|
? "3级计划重排完成"
|
|
|
@@ -538,4 +596,428 @@ public class OrderReviewOrchestrationService : ITransient
|
|
|
return warnings;
|
|
|
}
|
|
|
|
|
|
+ // ══════════════════════════════════════════════════════════════
|
|
|
+ // 3级计划重排 — 辅助方法
|
|
|
+ // ══════════════════════════════════════════════════════════════
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 校验订单明细行的物料编码是否与已生成工单的物料编码一致。
|
|
|
+ /// 若不一致则说明物料信息已变更,不允许重排。
|
|
|
+ /// </summary>
|
|
|
+ private async Task ValidateMaterialNotChangedAsync(long orderId, long tenantId)
|
|
|
+ {
|
|
|
+ var mismatches = await _db.Ado.SqlQueryAsync<dynamic>(
|
|
|
+ """
|
|
|
+ SELECT e.entry_seq AS EntrySeq,
|
|
|
+ e.item_number AS EntryItemNum,
|
|
|
+ w.ItemNum AS WoItemNum
|
|
|
+ FROM crm_seorderentry e
|
|
|
+ INNER JOIN WorkOrdMaster w
|
|
|
+ ON w.BusinessID = e.Id AND w.tenant_id = e.tenant_id
|
|
|
+ AND IFNULL(w.IsActive, 0) = 1
|
|
|
+ AND LOWER(TRIM(IFNULL(w.Status, ''))) <> 'c'
|
|
|
+ WHERE e.seorder_id = @OrderId AND e.tenant_id = @TenantId AND e.IsDeleted = 0
|
|
|
+ AND TRIM(IFNULL(e.item_number, '')) <> ''
|
|
|
+ AND TRIM(IFNULL(w.ItemNum, '')) <> ''
|
|
|
+ AND TRIM(e.item_number) <> TRIM(w.ItemNum)
|
|
|
+ """,
|
|
|
+ new SugarParameter("@OrderId", orderId),
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
+
|
|
|
+ if (mismatches.Count > 0)
|
|
|
+ throw Oops.Oh("此订单行的物料信息有变更无法重排");
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 当工单状态为下达(R)/投产(W)/暂停(S)时,更新对应领料单明细数据。
|
|
|
+ /// 重新从 WorkOrdDetail 汇总物料需求,覆盖 NbrDetail 中的 QtyOrd。
|
|
|
+ /// </summary>
|
|
|
+ private async Task UpdatePickingListForActiveWorkOrderAsync(
|
|
|
+ long tenantId, string workOrd, string account, List<string> warnings)
|
|
|
+ {
|
|
|
+ // 获取工单状态
|
|
|
+ var status = await _db.Ado.GetStringAsync(
|
|
|
+ """
|
|
|
+ SELECT IFNULL(LOWER(TRIM(Status)), '') FROM WorkOrdMaster
|
|
|
+ WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
|
|
|
+ LIMIT 1
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@WorkOrd", workOrd));
|
|
|
+
|
|
|
+ // 仅处理 下达(R)/投产(W)/暂停(S) 状态的工单
|
|
|
+ if (status is not ("r" or "w" or "s"))
|
|
|
+ return;
|
|
|
+
|
|
|
+ // 查找领料单主记录
|
|
|
+ var nbrRows = await _db.Ado.SqlQueryAsync<NbrMasterRow>(
|
|
|
+ """
|
|
|
+ SELECT RecID, Nbr, `Domain` FROM NbrMaster
|
|
|
+ WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND Type = 'SM'
|
|
|
+ AND IFNULL(TransType, '') = ''
|
|
|
+ AND IFNULL(IsActive, 0) = 1
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@WorkOrd", workOrd));
|
|
|
+
|
|
|
+ if (nbrRows.Count == 0)
|
|
|
+ {
|
|
|
+ warnings.Add($"工单 {workOrd} 状态为 {status.ToUpper()} 但无领料单,跳过领料单更新");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+
|
|
|
+ // 从 WorkOrdDetail 汇总最新物料需求
|
|
|
+ var details = await _db.Ado.SqlQueryAsync<PickDetailRow>(
|
|
|
+ """
|
|
|
+ SELECT
|
|
|
+ d.ItemNum,
|
|
|
+ SUM(d.QtyRequired) AS QtyRequired,
|
|
|
+ MAX(IFNULL(d.UM, im.Um)) AS Unit,
|
|
|
+ MAX(im.Descr) AS ItemName
|
|
|
+ FROM WorkOrdDetail d
|
|
|
+ LEFT JOIN ItemMaster im ON d.ItemNum = im.ItemNum AND im.tenant_id = d.tenant_id
|
|
|
+ WHERE d.tenant_id = @TenantId AND d.WorkOrd = @WorkOrd AND IFNULL(d.IsActive, 0) = 1
|
|
|
+ GROUP BY d.ItemNum
|
|
|
+ HAVING SUM(d.QtyRequired) > 0
|
|
|
+ ORDER BY d.ItemNum
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@WorkOrd", workOrd));
|
|
|
+
|
|
|
+ foreach (var nbr in nbrRows)
|
|
|
+ {
|
|
|
+ var domain = string.IsNullOrWhiteSpace(nbr.Domain) ? tenantId.ToString() : nbr.Domain!.Trim();
|
|
|
+ if (domain.Length > 8) domain = domain[..8];
|
|
|
+
|
|
|
+ // 加载当前领料单明细行(仅未关闭的)
|
|
|
+ var existingDetails = await _db.Ado.SqlQueryAsync<NbrDetailRow>(
|
|
|
+ """
|
|
|
+ SELECT RecID, ItemNum, QtyOrd, QtyRec, CurrQtyOpened, Line
|
|
|
+ FROM NbrDetail
|
|
|
+ WHERE tenant_id = @TenantId AND Nbr = @Nbr AND Type = 'SM'
|
|
|
+ AND IFNULL(IsActive, 0) = 1
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", tenantId),
|
|
|
+ new SugarParameter("@Nbr", nbr.Nbr));
|
|
|
+
|
|
|
+ var detailMap = details.ToDictionary(d => d.ItemNum.Trim(), d => d);
|
|
|
+ var existingMap = existingDetails.ToDictionary(d => (d.ItemNum ?? "").Trim(), d => d);
|
|
|
+
|
|
|
+ // ── 1. 处理已有明细行:按 ItemNum 匹配 ──
|
|
|
+ foreach (var existing in existingDetails)
|
|
|
+ {
|
|
|
+ var key = (existing.ItemNum ?? "").Trim();
|
|
|
+ if (detailMap.TryGetValue(key, out var newDetail))
|
|
|
+ {
|
|
|
+ // 物料在工单明细中存在
|
|
|
+ var newQty = newDetail.QtyRequired;
|
|
|
+ if (existing.QtyRec > 0)
|
|
|
+ {
|
|
|
+ // 已发料:判断新需求数是否大于已发料数
|
|
|
+ if (newQty > existing.QtyRec)
|
|
|
+ {
|
|
|
+ // 新需求 > 已发料 → 更新需求数
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE NbrDetail
|
|
|
+ SET QtyOrd = @QtyOrd, CurrQtyOpened = @CurrQtyOpened,
|
|
|
+ UM = @UM, ItemName = @ItemName,
|
|
|
+ UpdateUser = @User, UpdateTime = @Now
|
|
|
+ WHERE RecID = @RecId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@QtyOrd", newQty),
|
|
|
+ new SugarParameter("@CurrQtyOpened", newQty),
|
|
|
+ new SugarParameter("@UM", (object?)newDetail.Unit ?? DBNull.Value),
|
|
|
+ new SugarParameter("@ItemName", (object?)newDetail.ItemName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@RecId", existing.RecID));
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 新需求 <= 已发料 → 关闭当前行
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE NbrDetail
|
|
|
+ SET Status = 'C', IsActive = 0,
|
|
|
+ UpdateUser = @User, UpdateTime = @Now
|
|
|
+ WHERE RecID = @RecId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@RecId", existing.RecID));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 未发料 → 直接修改需求数
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE NbrDetail
|
|
|
+ SET QtyOrd = @QtyOrd, CurrQtyOpened = @CurrQtyOpened,
|
|
|
+ UM = @UM, ItemName = @ItemName,
|
|
|
+ UpdateUser = @User, UpdateTime = @Now
|
|
|
+ WHERE RecID = @RecId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@QtyOrd", newQty),
|
|
|
+ new SugarParameter("@CurrQtyOpened", newQty),
|
|
|
+ new SugarParameter("@UM", (object?)newDetail.Unit ?? DBNull.Value),
|
|
|
+ new SugarParameter("@ItemName", (object?)newDetail.ItemName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@RecId", existing.RecID));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 物料明细中没有,但领料单明细有 → 关闭当前领料单明细行
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE NbrDetail
|
|
|
+ SET Status = 'C', IsActive = 0,
|
|
|
+ UpdateUser = @User, UpdateTime = @Now
|
|
|
+ WHERE RecID = @RecId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@RecId", existing.RecID));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 2. 新增:物料明细中有,领料单明细没有的 ──
|
|
|
+ var nextDetailId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) + 1 FROM NbrDetail");
|
|
|
+ short newLine = (short)(existingDetails.Count > 0 ? existingDetails.Max(d => d.Line) + 1 : 1);
|
|
|
+
|
|
|
+ foreach (var d in details)
|
|
|
+ {
|
|
|
+ var key = d.ItemNum.Trim();
|
|
|
+ if (existingMap.ContainsKey(key))
|
|
|
+ continue; // 已处理
|
|
|
+
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ INSERT INTO NbrDetail (
|
|
|
+ RecID, `Domain`, Type, Nbr, Line, ItemNum, Dimension1, Dimension2,
|
|
|
+ LocationFrom, LocationTo, QtyFrom, QtyTo, QtyOrd, QtyRec,
|
|
|
+ CurrQtyOpened, UM, WorkOrd, ItemName, Status,
|
|
|
+ IsActive, IsConfirm, IsChanged, BusinessID, NbrRecID,
|
|
|
+ CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
|
|
|
+ ) VALUES (
|
|
|
+ @RecId, @Domain, 'SM', @Nbr, @Line, @ItemNum, '', '',
|
|
|
+ '', '', 0, 0, @QtyOrd, 0,
|
|
|
+ @CurrQtyOpened, @UM, @WorkOrd, @ItemName, '',
|
|
|
+ 1, 0, 1, 0, @NbrRecId,
|
|
|
+ @User, @Now, @User, @Now, @TenantId
|
|
|
+ )
|
|
|
+ """,
|
|
|
+ new SugarParameter("@RecId", nextDetailId++),
|
|
|
+ new SugarParameter("@Domain", domain),
|
|
|
+ new SugarParameter("@Nbr", nbr.Nbr),
|
|
|
+ new SugarParameter("@Line", newLine++),
|
|
|
+ new SugarParameter("@ItemNum", d.ItemNum),
|
|
|
+ new SugarParameter("@QtyOrd", d.QtyRequired),
|
|
|
+ new SugarParameter("@CurrQtyOpened", d.QtyRequired),
|
|
|
+ new SugarParameter("@UM", (object?)d.Unit ?? DBNull.Value),
|
|
|
+ new SugarParameter("@WorkOrd", workOrd),
|
|
|
+ new SugarParameter("@ItemName", (object?)d.ItemName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@NbrRecId", nbr.RecID),
|
|
|
+ new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 3. 更新领料单主记录的更新时间和数量 ──
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE NbrMaster
|
|
|
+ SET QtyOrd = (SELECT IFNULL(SUM(QtyOrd), 0) FROM NbrDetail
|
|
|
+ WHERE Nbr = @Nbr AND Type = 'SM' AND IFNULL(IsActive, 1) = 1),
|
|
|
+ UpdateUser = @User, UpdateTime = @Now
|
|
|
+ WHERE RecID = @RecId
|
|
|
+ """,
|
|
|
+ new SugarParameter("@Nbr", nbr.Nbr),
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@RecId", nbr.RecID));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 根据资源检查结果更新明细行系统建议交期(sys_capacity_date)。
|
|
|
+ /// </summary>
|
|
|
+ private async Task UpdateEntrySysCapacityDateAsync(long entryId, DateTime? kittingTime, string account)
|
|
|
+ {
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE crm_seorderentry
|
|
|
+ SET sys_capacity_date = @CapacityDate,
|
|
|
+ update_time = @Now
|
|
|
+ WHERE Id = @Id AND IsDeleted = 0
|
|
|
+ """,
|
|
|
+ new SugarParameter("@CapacityDate", kittingTime ?? (object)DBNull.Value),
|
|
|
+ new SugarParameter("@Now", DateTime.Now),
|
|
|
+ new SugarParameter("@Id", entryId));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 根据订单及明细行生成交货单(出货计划 ShippingPlan / ShippingPlanDetail)。
|
|
|
+ /// 若该订单已存在出货计划则更新明细,否则新建。
|
|
|
+ /// </summary>
|
|
|
+ private async Task GenerateShippingPlanFromOrderAsync(
|
|
|
+ OrderWorkOrderGenerationService.OrderHeader order,
|
|
|
+ List<OrderWorkOrderGenerationService.OrderEntryLine> entries,
|
|
|
+ string account,
|
|
|
+ List<string> warnings)
|
|
|
+ {
|
|
|
+ // 加载订单额外字段(客户名称、国家、日期)
|
|
|
+ var orderInfo = await _db.Ado.SqlQueryAsync<OrderShippingInfoRow>(
|
|
|
+ """
|
|
|
+ SELECT custom_name AS CustomName, country AS Country, date AS OrderDate
|
|
|
+ FROM crm_seorder
|
|
|
+ WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
|
|
|
+ LIMIT 1
|
|
|
+ """,
|
|
|
+ new SugarParameter("@Id", order.Id),
|
|
|
+ new SugarParameter("@TenantId", order.TenantId));
|
|
|
+ var info = orderInfo.FirstOrDefault() ?? new OrderShippingInfoRow();
|
|
|
+
|
|
|
+ // 检查是否已存在该订单的出货计划明细
|
|
|
+ var existingPlanId = await _db.Ado.GetIntAsync(
|
|
|
+ """
|
|
|
+ SELECT IFNULL(MAX(plan_id), 0) FROM ShippingPlanDetail
|
|
|
+ WHERE tenant_id = @TenantId AND seorder_id = @OrderId AND IFNULL(IsActive, 1) = 1
|
|
|
+ """,
|
|
|
+ new SugarParameter("@TenantId", order.TenantId),
|
|
|
+ new SugarParameter("@OrderId", order.Id));
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var domain = order.TenantId.ToString();
|
|
|
+ if (domain.Length > 8) domain = domain[..8];
|
|
|
+
|
|
|
+ if (existingPlanId == 0)
|
|
|
+ {
|
|
|
+ // ── 新建出货计划主表 ──
|
|
|
+ var planId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) + 1 FROM ShippingPlan");
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ INSERT INTO ShippingPlan (
|
|
|
+ RecID, `Domain`, LotSerial, ShippingDate, ShippingSite,
|
|
|
+ Consignee, Priority, Status, Remark,
|
|
|
+ IsActive, IsConfirm, CreateUser, CreateTime, tenant_id
|
|
|
+ ) VALUES (
|
|
|
+ @PlanId, @Domain, @LotSerial, @ShippingDate, '',
|
|
|
+ @Consignee, 0, '', '3级计划重排自动生成',
|
|
|
+ 1, 1, @User, @Now, @TenantId
|
|
|
+ )
|
|
|
+ """,
|
|
|
+ new SugarParameter("@PlanId", planId),
|
|
|
+ new SugarParameter("@Domain", domain),
|
|
|
+ new SugarParameter("@LotSerial", order.BillNo ?? string.Empty),
|
|
|
+ new SugarParameter("@ShippingDate", info.OrderDate ?? (object)DBNull.Value),
|
|
|
+ new SugarParameter("@Consignee", (object?)info.CustomName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@TenantId", order.TenantId));
|
|
|
+
|
|
|
+ // ── 新建出货计划明细 ──
|
|
|
+ var nextDetailId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) + 1 FROM ShippingPlanDetail");
|
|
|
+ foreach (var entry in entries)
|
|
|
+ {
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ INSERT INTO ShippingPlanDetail (
|
|
|
+ RecID, `Domain`, plan_id, OrdNbr, bill_no,
|
|
|
+ ItemNum, ItemName, Specification, Qty,
|
|
|
+ OrdDate, Country, CustomNo, CustomName,
|
|
|
+ seorder_id, sentry_id, Remark, Status,
|
|
|
+ IsActive, IsConfirm, CreateUser, CreateTime, tenant_id
|
|
|
+ ) VALUES (
|
|
|
+ @RecId, @Domain, @PlanId, @OrdNbr, @BillNo,
|
|
|
+ @ItemNum, @ItemName, @Spec, @Qty,
|
|
|
+ @OrdDate, @Country, @CustomNo, @CustomName,
|
|
|
+ @SeOrderId, @SentryId, '', '',
|
|
|
+ 1, 1, @User, @Now, @TenantId
|
|
|
+ )
|
|
|
+ """,
|
|
|
+ new SugarParameter("@RecId", nextDetailId++),
|
|
|
+ new SugarParameter("@Domain", domain),
|
|
|
+ new SugarParameter("@PlanId", planId),
|
|
|
+ new SugarParameter("@OrdNbr", order.BillNo ?? string.Empty),
|
|
|
+ new SugarParameter("@BillNo", entry.BillNo ?? order.BillNo ?? string.Empty),
|
|
|
+ new SugarParameter("@ItemNum", (object?)entry.ItemNumber ?? DBNull.Value),
|
|
|
+ new SugarParameter("@ItemName", (object?)entry.ItemName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@Spec", (object?)entry.Specification ?? DBNull.Value),
|
|
|
+ new SugarParameter("@Qty", entry.Qty ?? 0),
|
|
|
+ new SugarParameter("@OrdDate", entry.PlanDate ?? (object)DBNull.Value),
|
|
|
+ new SugarParameter("@Country", (object?)info.Country ?? DBNull.Value),
|
|
|
+ new SugarParameter("@CustomNo", (object?)order.CustomNo ?? DBNull.Value),
|
|
|
+ new SugarParameter("@CustomName", (object?)info.CustomName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@SeOrderId", order.Id),
|
|
|
+ new SugarParameter("@SentryId", entry.Id),
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@TenantId", order.TenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ warnings.Add($"已自动生成出货计划(ID={planId}),共 {entries.Count} 行明细");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // ── 更新已有出货计划明细 ──
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE ShippingPlanDetail
|
|
|
+ SET CustomName = @CustomName, Country = @Country,
|
|
|
+ UpdateUser = @User, UpdateTime = @Now
|
|
|
+ WHERE tenant_id = @TenantId AND seorder_id = @OrderId AND IFNULL(IsActive, 1) = 1
|
|
|
+ """,
|
|
|
+ new SugarParameter("@CustomName", (object?)info.CustomName ?? DBNull.Value),
|
|
|
+ new SugarParameter("@Country", (object?)info.Country ?? DBNull.Value),
|
|
|
+ new SugarParameter("@User", account),
|
|
|
+ new SugarParameter("@Now", now),
|
|
|
+ new SugarParameter("@TenantId", order.TenantId),
|
|
|
+ new SugarParameter("@OrderId", order.Id));
|
|
|
+
|
|
|
+ warnings.Add($"已更新出货计划(ID={existingPlanId})的明细数据");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ══════════════════════════════════════════════════════════════
|
|
|
+ // 3级计划重排 — 内部 DTO
|
|
|
+ // ══════════════════════════════════════════════════════════════
|
|
|
+
|
|
|
+ private sealed class NbrMasterRow
|
|
|
+ {
|
|
|
+ public int RecID { get; set; }
|
|
|
+ public string Nbr { get; set; } = string.Empty;
|
|
|
+ public string? Domain { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class NbrDetailRow
|
|
|
+ {
|
|
|
+ public int RecID { get; set; }
|
|
|
+ public string? ItemNum { get; set; }
|
|
|
+ public decimal QtyOrd { get; set; }
|
|
|
+ public decimal QtyRec { get; set; }
|
|
|
+ public decimal CurrQtyOpened { get; set; }
|
|
|
+ public short Line { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class PickDetailRow
|
|
|
+ {
|
|
|
+ public string ItemNum { get; set; } = string.Empty;
|
|
|
+ public decimal QtyRequired { get; set; }
|
|
|
+ public string? Unit { get; set; }
|
|
|
+ public string? ItemName { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class OrderShippingInfoRow
|
|
|
+ {
|
|
|
+ public string? CustomName { get; set; }
|
|
|
+ public string? Country { get; set; }
|
|
|
+ public DateTime? OrderDate { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
}
|