| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023 |
- 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;
- /// <summary>
- /// S1 订单评审编排:资源检查、状态更新、工单生成、运行日志。
- /// </summary>
- public class OrderReviewOrchestrationService : ITransient
- {
- public const string ActionReview = "S1_ORDER_REVIEW";
- public const string ActionConfirm = "S1_DELIVERY_CONFIRM";
- public const string ActionRefresh = "S1_ORDER_REFRESH_PLAN";
- /// <summary>订单评审资源检查批次ID,用于跨工单库存占用递减。</summary>
- private const long ReviewBangId = 2;
- private readonly ISqlSugarClient _db;
- private readonly UserManager _userManager;
- private readonly OrderWorkOrderGenerationService _workOrderGen;
- private readonly OrderResourceCheckService _resourceCheck;
- private readonly WorkOrderMaterialDetailSyncService _materialDetailSync;
- private readonly WorkOrderRoutingSyncService _routingSync;
- private readonly S1MdpSyncTransformService _mdpSync;
- private readonly AidopActionRunLogWriter _runLog;
- private readonly ProductionScheduleGenerationService _scheduleGen;
- private readonly ProcurementPipelineService _pipeline;
- public OrderReviewOrchestrationService(
- ISqlSugarClient db,
- UserManager userManager,
- OrderWorkOrderGenerationService workOrderGen,
- OrderResourceCheckService resourceCheck,
- WorkOrderMaterialDetailSyncService materialDetailSync,
- WorkOrderRoutingSyncService routingSync,
- S1MdpSyncTransformService mdpSync,
- AidopActionRunLogWriter runLog,
- ProductionScheduleGenerationService scheduleGen,
- ProcurementPipelineService pipeline)
- {
- _db = db;
- _userManager = userManager;
- _workOrderGen = workOrderGen;
- _resourceCheck = resourceCheck;
- _materialDetailSync = materialDetailSync;
- _routingSync = routingSync;
- _mdpSync = mdpSync;
- _runLog = runLog;
- _scheduleGen = scheduleGen;
- _pipeline = pipeline;
- }
- public Task<SeOrderReviewExecuteResult> ReviewAsync(IReadOnlyList<long> orderIds) =>
- ExecuteBatchAsync(ActionReview, orderIds, ReviewOneOrderAsync);
- public Task<SeOrderReviewExecuteResult> ConfirmDeliveryAsync(IReadOnlyList<long> orderIds) =>
- ExecuteBatchAsync(ActionConfirm, orderIds, ConfirmOneOrderAsync);
- 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);
- var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
- if (wo.Created) result.WorkOrderCreatedCount++;
- else result.WorkOrderUpdatedCount++;
- result.WorkOrders.Add(wo.WorkOrd);
- var check = await _resourceCheck.RunForEntryAsync(order, entry, wo.WorkOrd, account, warnings, ReviewBangId);
- result.ResourceCheckCount++;
- result.ResourceCheckLineCount += check.LineCount;
- 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);
- }
- // ── 第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级计划重排完成"
- : $"3级计划重排完成:{reason.Trim()}";
- });
- private async Task<SeOrderReviewExecuteResult> ExecuteBatchAsync(
- string actionCode,
- IReadOnlyList<long> orderIds,
- Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
- {
- if (orderIds is null || orderIds.Count == 0)
- throw Oops.Oh("至少选择一条订单");
- var tenantId = _userManager.TenantId > 0
- ? _userManager.TenantId
- : AidopTenantHelper.Resolve(App.HttpContext);
- var account = _userManager.Account ?? "system";
- var distinctIds = orderIds.Distinct().ToList();
- var aggregate = new SeOrderReviewExecuteResult
- {
- ActionCode = actionCode,
- OrderCount = distinctIds.Count,
- Message = "执行成功"
- };
- var allWarnings = new List<string>();
- long? firstLogId = null;
- // 评审/重排前清理旧占用记录,确保跨工单库存递减正确
- if (actionCode == ActionReview || actionCode == ActionRefresh)
- {
- await _db.Ado.ExecuteCommandAsync(
- "DELETE FROM ic_item_stockoccupy WHERE tenant_id = @TenantId AND bang_id = @BangId",
- new SugarParameter("@TenantId", 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", tenantId),
- new SugarParameter("@BangId", ReviewBangId));
- }
- foreach (var orderId in distinctIds)
- {
- var order = await LoadOrderAsync(orderId, tenantId)
- ?? throw Oops.Oh($"订单 {orderId} 不存在或不属于当前租户");
- var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
- if (firstLogId is null)
- firstLogId = runLogId;
- var perOrder = new SeOrderReviewExecuteResult { ActionCode = actionCode };
- var warnings = new List<string>();
- try
- {
- await _db.Ado.BeginTranAsync();
- await handler(order, perOrder, warnings, account);
- await _db.Ado.CommitTranAsync();
- aggregate.EntryCount += perOrder.EntryCount;
- aggregate.WorkOrderCreatedCount += perOrder.WorkOrderCreatedCount;
- aggregate.WorkOrderUpdatedCount += perOrder.WorkOrderUpdatedCount;
- aggregate.WorkOrderClosedCount += perOrder.WorkOrderClosedCount;
- aggregate.ResourceCheckCount += perOrder.ResourceCheckCount;
- aggregate.ResourceCheckLineCount += perOrder.ResourceCheckLineCount;
- aggregate.WorkOrders.AddRange(perOrder.WorkOrders);
- allWarnings.AddRange(warnings);
- await _runLog.SuccessAsync(runLogId, perOrder.Message, new
- {
- orderId = order.Id,
- billNo = order.BillNo,
- perOrder.EntryCount,
- perOrder.WorkOrderCreatedCount,
- perOrder.WorkOrderUpdatedCount,
- workOrders = perOrder.WorkOrders,
- warnings
- });
- }
- catch (Exception ex)
- {
- await _db.Ado.RollbackTranAsync();
- await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
- throw Oops.Oh(ex.Message);
- }
- }
- aggregate.RunLogId = firstLogId ?? 0;
- aggregate.Warnings = allWarnings.Distinct().ToList();
- aggregate.Message = BuildAggregateMessage(actionCode, aggregate);
- if (actionCode == ActionReview && aggregate.ResourceCheckCount > 0)
- aggregate.Warnings.AddRange(await TryTriggerMdpRefreshAsync());
- return aggregate;
- }
- private async Task<SeOrderReviewExecuteResult> ExecuteSingleAsync(
- string actionCode,
- long orderId,
- Func<OrderWorkOrderGenerationService.OrderHeader, SeOrderReviewExecuteResult, List<string>, string, Task> handler)
- {
- var tenantId = _userManager.TenantId > 0
- ? _userManager.TenantId
- : AidopTenantHelper.Resolve(App.HttpContext);
- var account = _userManager.Account ?? "system";
- var order = await LoadOrderAsync(orderId, tenantId)
- ?? throw Oops.Oh("订单不存在或不属于当前租户");
- var result = new SeOrderReviewExecuteResult
- {
- ActionCode = actionCode,
- OrderCount = 1
- };
- var warnings = new List<string>();
- var runLogId = await _runLog.StartAsync(actionCode, tenantId, "crm_seorder", order.Id, order.BillNo);
- result.RunLogId = runLogId;
- try
- {
- await _db.Ado.BeginTranAsync();
- await handler(order, result, warnings, account);
- await _db.Ado.CommitTranAsync();
- result.Warnings = warnings;
- result.Message = BuildSingleMessage(result);
- await _runLog.SuccessAsync(runLogId, result.Message, new
- {
- orderId = order.Id,
- billNo = order.BillNo,
- result.EntryCount,
- result.WorkOrderCreatedCount,
- result.WorkOrderUpdatedCount,
- workOrders = result.WorkOrders,
- warnings
- });
- if (actionCode == ActionRefresh && result.ResourceCheckCount > 0)
- result.Warnings.AddRange(await TryTriggerMdpRefreshAsync());
- return result;
- }
- catch (Exception ex)
- {
- await _db.Ado.RollbackTranAsync();
- await _runLog.FailedAsync(runLogId, ex.Message, new { orderId = order.Id, billNo = order.BillNo });
- throw Oops.Oh(ex.Message);
- }
- }
- private async Task ReviewOneOrderAsync(
- OrderWorkOrderGenerationService.OrderHeader order,
- SeOrderReviewExecuteResult result,
- List<string> warnings,
- string account)
- {
- var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["1", "0", "2"]);
- if (entries.Count == 0)
- throw Oops.Oh($"订单 {order.BillNo} 没有可评审的明细行(须为新建,评审,再评审状态)");
- foreach (var entry in entries)
- {
- ValidateEntryForResourceCheck(entry);
- if (entry.PlanDate is null)
- throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少客户要求交期(plan_date)");
- // 1. 先做资源检查(纯 BOM 展开 + 库存计算,不依赖工单),记录库存占用
- var (check, lines) = await _resourceCheck.CheckOnlyAsync(order, entry, warnings, ReviewBangId);
- result.ResourceCheckCount++;
- result.ResourceCheckLineCount += check.LineCount;
- // 2. 有缺料 → 才生成工单并写入资源检查结果;库存可满足则跳过工单
- if (check.HasShortage)
- {
- var wo = await _workOrderGen.CreateOrUpdateForEntryAsync(order, entry, account, warnings);
- if (wo.Created) result.WorkOrderCreatedCount++;
- else result.WorkOrderUpdatedCount++;
- result.WorkOrders.Add(wo.WorkOrd);
- await _resourceCheck.WriteResultAsync(order, entry, wo.WorkOrd, lines, account);
- await _materialDetailSync.EnsureFromResourceCheckAsync(entry.TenantId, wo.WorkOrd, account);
- await _routingSync.EnsureFromRoutingAsync(entry.TenantId, wo.WorkOrd, account);
- warnings.Add($"订单行 {entry.EntrySeq} 存在缺料(工单 {wo.WorkOrd})");
- }
- else
- {
- // 3. 不缺料 → 若之前已生成工单则关闭(Status='C', IsActive=0)
- var closedCount = await CloseExistingWorkOrdersAsync(entry.TenantId, entry.Id, account);
- if (closedCount > 0)
- {
- result.WorkOrderClosedCount += closedCount;
- warnings.Add($"订单行 {entry.EntrySeq} 库存可满足,已关闭 {closedCount} 个历史工单");
- }
- }
- await UpdateEntryAfterReviewAsync(entry.Id, check.KittingTime, account);
- }
- result.EntryCount = entries.Count;
- result.Message = $"订单 {order.BillNo} 评审完成(资源检查 {result.ResourceCheckCount} 条)";
- }
- private async Task ConfirmOneOrderAsync(
- OrderWorkOrderGenerationService.OrderHeader order,
- SeOrderReviewExecuteResult result,
- List<string> warnings,
- string account)
- {
- var entries = await LoadReviewableEntriesAsync(order.Id, order.TenantId, ["2"]);
- if (entries.Count == 0)
- throw Oops.Oh($"订单 {order.BillNo} 没有处于评审状态的明细行,请先完成订单评审");
- foreach (var entry in entries)
- {
- var woCnt = await _db.Ado.GetIntAsync(
- """
- SELECT COUNT(*) FROM WorkOrdMaster
- WHERE tenant_id = @TenantId AND BusinessID = @EntryId
- AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
- """,
- new SugarParameter("@TenantId", entry.TenantId),
- new SugarParameter("@EntryId", entry.Id));
- // 无活跃工单:可能从未生成(库存可满足)或已关闭 → 仍允许交期确认
- var confirmDate = entry.SysCapacityDate ?? entry.PlanDate;
- await UpdateEntryAfterConfirmAsync(entry.Id, confirmDate, account);
- result.EntryCount++;
- if (woCnt > 0)
- {
- var workOrd = await LoadWorkOrdForEntryAsync(entry.TenantId, entry.Id);
- result.WorkOrders.Add(workOrd);
- // 交期确认后将工单状态设为 p
- if (!string.IsNullOrWhiteSpace(workOrd))
- {
- await _db.Ado.ExecuteCommandAsync(
- """
- UPDATE WorkOrdMaster
- SET Status = 'p', UpdateUser = @User, UpdateTime = @Now
- WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
- AND IFNULL(TRIM(Status), '') = ''
- """,
- new SugarParameter("@TenantId", entry.TenantId),
- new SugarParameter("@WorkOrd", workOrd),
- new SugarParameter("@User", account),
- new SugarParameter("@Now", DateTime.Now));
- }
- }
- else
- warnings.Add($"订单行 {entry.EntrySeq} 无活跃工单(库存可满足或已关闭),已直接确认交期");
- }
- result.WorkOrders = result.WorkOrders.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
- result.Message = $"订单 {order.BillNo} 交期确认完成";
- }
- private async Task<string> LoadWorkOrdForEntryAsync(long tenantId, long entryId)
- {
- var rows = await _db.Ado.SqlQueryAsync<string>(
- """
- SELECT WorkOrd FROM WorkOrdMaster
- WHERE tenant_id = @TenantId AND BusinessID = @EntryId
- ORDER BY RecID DESC LIMIT 1
- """,
- new SugarParameter("@TenantId", tenantId),
- new SugarParameter("@EntryId", entryId));
- return rows.FirstOrDefault() ?? string.Empty;
- }
- /// <summary>
- /// 关闭指定订单行已有的未关闭工单(WorkOrdMaster.Status='C', IsActive=0;mes_morder.morder_state='关闭')。
- /// 返回被关闭的工单数量。
- /// </summary>
- private async Task<int> CloseExistingWorkOrdersAsync(long tenantId, long entryId, string account)
- {
- // 查找该订单行下所有未关闭的工单号
- var openWorkOrds = await _db.Ado.SqlQueryAsync<string>(
- """
- SELECT WorkOrd FROM WorkOrdMaster
- WHERE tenant_id = @TenantId AND BusinessID = @EntryId
- AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
- AND IFNULL(IsActive, 0) = 1
- """,
- new SugarParameter("@TenantId", tenantId),
- new SugarParameter("@EntryId", entryId));
- if (openWorkOrds.Count == 0) return 0;
- var workOrdList = string.Join(",", openWorkOrds.Select(w => $"'{w}'"));
- var now = DateTime.Now;
- // 关闭 WorkOrdMaster
- await _db.Ado.ExecuteCommandAsync(
- $"""
- UPDATE WorkOrdMaster
- SET Status = 'C', IsActive = 0,
- UpdateUser = @User, UpdateTime = @Now
- WHERE tenant_id = @TenantId AND BusinessID = @EntryId
- AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
- AND IFNULL(IsActive, 0) = 1
- """,
- new SugarParameter("@TenantId", tenantId),
- new SugarParameter("@EntryId", entryId),
- new SugarParameter("@User", account),
- new SugarParameter("@Now", now));
- // 同步关闭 mes_morder
- await _db.Ado.ExecuteCommandAsync(
- $"""
- UPDATE mes_morder
- SET morder_state = '关闭',
- update_by_name = @User, update_time = @Now
- WHERE tenant_id = @TenantId AND morder_no IN ({workOrdList})
- AND IFNULL(morder_state, '') <> '关闭'
- """,
- new SugarParameter("@TenantId", tenantId),
- new SugarParameter("@User", account),
- new SugarParameter("@Now", now));
- return openWorkOrds.Count;
- }
- private static void ValidateEntryForResourceCheck(OrderWorkOrderGenerationService.OrderEntryLine entry)
- {
- if (string.IsNullOrWhiteSpace(entry.ItemNumber))
- throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空");
- if (entry.Qty is null or <= 0)
- throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0");
- if (entry.PlanDate is null && entry.SysCapacityDate is null)
- throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期");
- }
- private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderAsync(long orderId, long tenantId)
- {
- var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderHeader>(
- """
- SELECT Id, bill_no AS BillNo, custom_no AS CustomNo, urgent AS Urgent,
- factory_id AS FactoryId, tenant_id AS TenantId
- FROM crm_seorder
- WHERE Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
- LIMIT 1
- """,
- new SugarParameter("@Id", orderId),
- new SugarParameter("@TenantId", tenantId));
- return rows.FirstOrDefault();
- }
- private async Task<List<OrderWorkOrderGenerationService.OrderEntryLine>> LoadReviewableEntriesAsync(
- long orderId,
- long tenantId,
- IReadOnlyList<string> progressList)
- {
- if (progressList.Count == 0)
- return new List<OrderWorkOrderGenerationService.OrderEntryLine>();
- var inClause = string.Join(", ", progressList.Select((_, i) => $"@P{i}"));
- var pars = new List<SugarParameter>
- {
- new("@OrderId", orderId),
- new("@TenantId", tenantId)
- };
- for (var i = 0; i < progressList.Count; i++)
- pars.Add(new SugarParameter($"@P{i}", progressList[i]));
- return await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
- $"""
- SELECT
- Id, seorder_id AS SeOrderId, bill_no AS BillNo, entry_seq AS EntrySeq,
- item_number AS ItemNumber, item_name AS ItemName, specification AS Specification,
- unit AS Unit, bom_number AS BomNumber, qty AS Qty,
- plan_date AS PlanDate, sys_capacity_date AS SysCapacityDate,
- progress AS Progress, urgent AS Urgent,
- factory_id AS FactoryId, company_id AS CompanyId, tenant_id AS TenantId
- FROM crm_seorderentry
- WHERE seorder_id = @OrderId AND tenant_id = @TenantId AND IsDeleted = 0
- AND COALESCE(NULLIF(progress, ''), '1') IN ({inClause})
- ORDER BY entry_seq, Id
- """,
- pars);
- }
- private async Task UpdateEntryAfterReviewAsync(long entryId, DateTime? capacityDate, string account)
- {
- await _db.Ado.ExecuteCommandAsync(
- """
- UPDATE crm_seorderentry
- SET progress = '2',
- sys_capacity_date = @CapacityDate,
- update_time = @Now
- WHERE Id = @Id AND IsDeleted = 0
- """,
- new SugarParameter("@CapacityDate", capacityDate ?? (object)DBNull.Value),
- new SugarParameter("@Now", DateTime.Now),
- new SugarParameter("@Id", entryId));
- }
- private async Task UpdateEntryAfterConfirmAsync(long entryId, DateTime? confirmDate, string account)
- {
- await _db.Ado.ExecuteCommandAsync(
- """
- UPDATE crm_seorderentry
- SET progress = '3',
- date = COALESCE(date, @ConfirmDate),
- update_time = @Now
- WHERE Id = @Id AND IsDeleted = 0
- """,
- new SugarParameter("@ConfirmDate", confirmDate ?? (object)DBNull.Value),
- new SugarParameter("@Now", DateTime.Now),
- new SugarParameter("@Id", entryId));
- }
- private async Task UpdateEntriesProgressAsync(IReadOnlyList<long> entryIds, string progress, string account)
- {
- if (entryIds.Count == 0) return;
- var idList = string.Join(",", entryIds);
- await _db.Ado.ExecuteCommandAsync(
- $"""
- UPDATE crm_seorderentry
- SET progress = @Progress, update_time = @Now
- WHERE Id IN ({idList}) AND IsDeleted = 0
- """,
- new SugarParameter("@Progress", progress),
- new SugarParameter("@Now", DateTime.Now));
- }
- private static string BuildAggregateMessage(string actionCode, SeOrderReviewExecuteResult r)
- {
- var woPart = r.WorkOrders.Count > 0
- ? $",工单:{string.Join("、", r.WorkOrders.Distinct())}"
- : string.Empty;
- var closedPart = r.WorkOrderClosedCount > 0
- ? $"、关闭 {r.WorkOrderClosedCount}"
- : string.Empty;
- return actionCode switch
- {
- ActionReview => $"评审完成 {r.OrderCount} 单、{r.EntryCount} 行,新建工单 {r.WorkOrderCreatedCount}、更新 {r.WorkOrderUpdatedCount}{closedPart}、资源检查 {r.ResourceCheckCount} 条{woPart}",
- ActionConfirm => $"交期确认完成 {r.OrderCount} 单、{r.EntryCount} 行{woPart}",
- _ => r.Message
- };
- }
- private static string BuildSingleMessage(SeOrderReviewExecuteResult r)
- {
- if (r.WorkOrders.Count == 0)
- return r.Message;
- return $"{r.Message},工单:{string.Join("、", r.WorkOrders.Distinct())}";
- }
- private async Task<List<string>> TryTriggerMdpRefreshAsync()
- {
- var warnings = new List<string>();
- try
- {
- await _mdpSync.RunFullAsync(triggerType: "ORDER_REVIEW");
- }
- catch (Exception ex)
- {
- warnings.Add($"S1 MDP/DWD 刷新未完成:{ex.Message}");
- }
- 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; }
- }
- }
|