| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- using System.Globalization;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.AiDOP.Order;
- namespace Admin.NET.Plugin.AiDOP.Production;
- /// <summary>工单排产动作:生成排程、优先级调整并重检。</summary>
- [ApiDescriptionSettings(Order = 264, Description = "工单排产动作")]
- [Route("api/Production")]
- [AllowAnonymous]
- [NonUnify]
- public class ProductionSchedulingActionService : IDynamicApiController, ITransient
- {
- private readonly ISqlSugarClient _db;
- private readonly UserManager _userManager;
- private readonly OrderResourceCheckService _resourceCheck;
- private readonly ProductionScheduleGenerationService _scheduleGen;
- private readonly AidopActionRunLogWriter _runLog;
- public ProductionSchedulingActionService(
- ISqlSugarClient db,
- UserManager userManager,
- OrderResourceCheckService resourceCheck,
- ProductionScheduleGenerationService scheduleGen,
- AidopActionRunLogWriter runLog)
- {
- _db = db;
- _userManager = userManager;
- _resourceCheck = resourceCheck;
- _scheduleGen = scheduleGen;
- _runLog = runLog;
- }
- /// <summary>生成生产排程计划(写入 PeriodSequenceDet)。</summary>
- [DisplayName("生成生产排程")]
- [HttpPost("scheduling/generate")]
- public async Task<object> GenerateSchedule([FromQuery] string domain)
- {
- var tenantId = ResolveTenantId(domain);
- var account = _userManager.Account ?? "system";
- var logId = await _runLog.StartAsync("S2_SCHEDULE_GENERATE", tenantId, "WorkOrdMaster", null, $"domain={domain}");
- try
- {
- var result = await _scheduleGen.GenerateAsync(tenantId, domain, account);
- await _runLog.SuccessAsync(logId, result.Message, new
- {
- tenantId,
- domain,
- result.WorkOrderCount,
- result.ScheduleRowCount,
- result.UsedWorkCenterCalendar,
- result.SkippedWorkOrders
- });
- return new
- {
- message = result.Message,
- workOrderCount = result.WorkOrderCount,
- scheduleRowCount = result.ScheduleRowCount,
- usedWorkCenterCalendar = result.UsedWorkCenterCalendar,
- skippedWorkOrders = result.SkippedWorkOrders
- };
- }
- catch (Exception ex)
- {
- await _runLog.FailedAsync(logId, ex.Message, new { tenantId, domain });
- throw;
- }
- }
- /// <summary>优先级/数量/交期调整并重做资源检查。</summary>
- [DisplayName("优先级调整并重检")]
- [HttpPost("scheduling/update-priority-and-recheck")]
- public async Task<object> UpdatePriorityAndRecheck([FromBody] WorkOrderPriorityRecheckInput input)
- {
- var tenantId = ResolveTenantId(input.Domain);
- var account = string.IsNullOrWhiteSpace(input.UserAccount)
- ? (_userManager.Account ?? "system")
- : input.UserAccount.Trim();
- var workOrd = input.Workord.Trim();
- var logId = await _runLog.StartAsync("S2_PRIORITY_RECHECK", tenantId, "WorkOrdMaster", null, workOrd);
- try
- {
- var before = await LoadWorkOrderSnapshotAsync(tenantId, workOrd);
- DateTime? dueDate = null;
- if (!string.IsNullOrWhiteSpace(input.Instockdate))
- dueDate = DateTime.Parse(input.Instockdate.Trim(), CultureInfo.InvariantCulture).Date;
- decimal? qty = null;
- if (!string.IsNullOrWhiteSpace(input.Qty) && decimal.TryParse(input.Qty.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var q))
- qty = q;
- var pars = new List<SugarParameter>
- {
- new("@WorkOrd", workOrd),
- new("@TenantId", tenantId),
- new("@Priority", string.IsNullOrWhiteSpace(input.Priority) ? (object)DBNull.Value : input.Priority.Trim()),
- new("@LotSerial", string.IsNullOrWhiteSpace(input.LotSerial) ? (object)DBNull.Value : input.LotSerial.Trim()),
- new("@Qty", qty ?? (object)DBNull.Value),
- new("@DueDate", dueDate ?? (object)DBNull.Value),
- new("@User", account),
- new("@Now", DateTime.Now)
- };
- var affected = await _db.Ado.ExecuteCommandAsync(
- """
- UPDATE WorkOrdMaster
- SET Priority = COALESCE(@Priority, Priority),
- LotSerial = COALESCE(@LotSerial, LotSerial),
- QtyOrded = COALESCE(@Qty, QtyOrded),
- DueDate = COALESCE(@DueDate, DueDate),
- UpdateUser = @User,
- UpdateTime = @Now
- WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
- """,
- pars);
- if (affected == 0)
- throw Oops.Oh("工单不存在或未更新");
- var link = await LoadWorkOrderEntryLinkAsync(tenantId, workOrd);
- var warnings = new List<string>();
- var resourceRechecked = false;
- if (link is not null && (qty.HasValue || dueDate.HasValue))
- {
- if (qty.HasValue)
- link.Entry.Qty = qty;
- if (dueDate.HasValue)
- link.Entry.SysCapacityDate = dueDate;
- await _resourceCheck.RunForEntryAsync(link.Order, link.Entry, workOrd, account, warnings);
- resourceRechecked = true;
- }
- var qtyChanged = qty.HasValue && before?.QtyOrded != qty;
- var dueChanged = dueDate.HasValue && before?.DueDate?.Date != dueDate.Value.Date;
- var priorityChanged = !string.IsNullOrWhiteSpace(input.Priority)
- && !string.Equals(before?.Priority?.Trim(), input.Priority.Trim(), StringComparison.Ordinal);
- ProductionScheduleGenerationService.ScheduleGenerationResult? reschedule = null;
- if (qtyChanged || dueChanged)
- {
- reschedule = await _scheduleGen.RegenerateForWorkOrderAsync(tenantId, workOrd, input.Domain, account);
- warnings.Add(reschedule.Message);
- }
- else if (priorityChanged)
- {
- var woDomain = before?.Domain ?? input.Domain;
- await _scheduleGen.DeactivateExistingScheduleAsync(tenantId, workOrd, woDomain);
- warnings.Add("优先级已变更,原排程已失效,请重新执行批量排程");
- }
- await _runLog.SuccessAsync(logId, "优先级调整并重检完成", new
- {
- workOrd,
- before,
- after = new
- {
- qty,
- dueDate,
- priority = input.Priority,
- lotSerial = input.LotSerial
- },
- qtyChanged,
- dueChanged,
- priorityChanged,
- resourceRechecked,
- reschedule = reschedule is null
- ? null
- : new
- {
- reschedule.WorkOrderCount,
- reschedule.ScheduleRowCount,
- reschedule.UsedWorkCenterCalendar
- },
- warnings
- });
- return new
- {
- message = "ok",
- warnings,
- resourceRechecked,
- rescheduleTriggered = reschedule is not null,
- scheduleRowCount = reschedule?.ScheduleRowCount ?? 0
- };
- }
- catch (Exception ex)
- {
- await _runLog.FailedAsync(logId, ex.Message, new { workOrd, tenantId });
- throw;
- }
- }
- private long ResolveTenantId(string? domain)
- {
- if (!string.IsNullOrWhiteSpace(domain) && long.TryParse(domain.Trim(), out var tid) && tid > 0)
- return tid;
- return AidopTenantHelper.Resolve(App.HttpContext);
- }
- private async Task<WorkOrderSnapshotRow?> LoadWorkOrderSnapshotAsync(long tenantId, string workOrd)
- {
- var rows = await _db.Ado.SqlQueryAsync<WorkOrderSnapshotRow>(
- """
- SELECT WorkOrd, Priority, QtyOrded, DueDate, LotSerial, `Domain`
- FROM WorkOrdMaster
- WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
- LIMIT 1
- """,
- new SugarParameter("@TenantId", tenantId),
- new SugarParameter("@WorkOrd", workOrd));
- return rows.FirstOrDefault();
- }
- private async Task<WorkOrderEntryLink?> LoadWorkOrderEntryLinkAsync(long tenantId, string workOrd)
- {
- var rows = await _db.Ado.SqlQueryAsync<WorkOrderEntryLinkRow>(
- """
- SELECT
- w.BusinessID AS EntryId,
- e.seorder_id AS SeOrderId,
- e.bill_no AS BillNo,
- e.entry_seq AS EntrySeq,
- e.item_number AS ItemNumber,
- e.item_name AS ItemName,
- e.specification AS Specification,
- e.unit AS Unit,
- e.bom_number AS BomNumber,
- e.qty AS Qty,
- e.plan_date AS PlanDate,
- e.sys_capacity_date AS SysCapacityDate,
- e.progress AS Progress,
- e.urgent AS Urgent,
- e.factory_id AS FactoryId,
- e.company_id AS CompanyId,
- e.tenant_id AS TenantId,
- o.Id AS OrderId,
- o.bill_no AS OrderBillNo,
- o.custom_no AS CustomNo,
- o.urgent AS OrderUrgent,
- o.factory_id AS OrderFactoryId
- FROM WorkOrdMaster w
- INNER JOIN crm_seorderentry e ON e.Id = w.BusinessID AND e.tenant_id = w.tenant_id AND e.IsDeleted = 0
- INNER JOIN crm_seorder o ON o.Id = e.seorder_id AND o.tenant_id = e.tenant_id AND o.IsDeleted = 0
- WHERE w.tenant_id = @TenantId AND w.WorkOrd = @WorkOrd
- LIMIT 1
- """,
- new SugarParameter("@TenantId", tenantId),
- new SugarParameter("@WorkOrd", workOrd));
- var row = rows.FirstOrDefault();
- if (row is null || row.EntryId <= 0)
- return null;
- return new WorkOrderEntryLink
- {
- Order = new OrderWorkOrderGenerationService.OrderHeader
- {
- Id = row.OrderId,
- BillNo = row.OrderBillNo ?? row.BillNo,
- CustomNo = row.CustomNo,
- Urgent = row.OrderUrgent,
- FactoryId = row.OrderFactoryId,
- TenantId = tenantId
- },
- Entry = new OrderWorkOrderGenerationService.OrderEntryLine
- {
- Id = row.EntryId,
- SeOrderId = row.SeOrderId,
- BillNo = row.BillNo,
- EntrySeq = row.EntrySeq,
- ItemNumber = row.ItemNumber,
- ItemName = row.ItemName,
- Specification = row.Specification,
- Unit = row.Unit,
- BomNumber = row.BomNumber,
- Qty = row.Qty,
- PlanDate = row.PlanDate,
- SysCapacityDate = row.SysCapacityDate,
- Progress = row.Progress,
- Urgent = row.Urgent,
- FactoryId = row.FactoryId,
- CompanyId = row.CompanyId,
- TenantId = row.TenantId
- }
- };
- }
- private sealed class WorkOrderSnapshotRow
- {
- public string? WorkOrd { get; set; }
- public string? Priority { get; set; }
- public decimal? QtyOrded { get; set; }
- public DateTime? DueDate { get; set; }
- public string? LotSerial { get; set; }
- public string? Domain { get; set; }
- }
- private sealed class WorkOrderEntryLink
- {
- public OrderWorkOrderGenerationService.OrderHeader Order { get; set; } = new();
- public OrderWorkOrderGenerationService.OrderEntryLine Entry { get; set; } = new();
- }
- private sealed class WorkOrderEntryLinkRow
- {
- public long EntryId { get; set; }
- public long SeOrderId { get; set; }
- public string? BillNo { get; set; }
- public int? EntrySeq { get; set; }
- public string? ItemNumber { get; set; }
- public string? ItemName { get; set; }
- public string? Specification { get; set; }
- public string? Unit { get; set; }
- public string? BomNumber { get; set; }
- public decimal? Qty { get; set; }
- public DateTime? PlanDate { get; set; }
- public DateTime? SysCapacityDate { get; set; }
- public string? Progress { get; set; }
- public int? Urgent { get; set; }
- public long? FactoryId { get; set; }
- public long? CompanyId { get; set; }
- public long TenantId { get; set; }
- public long OrderId { get; set; }
- public string? OrderBillNo { get; set; }
- public string? CustomNo { get; set; }
- public int? OrderUrgent { get; set; }
- public long? OrderFactoryId { get; set; }
- }
- }
- public class WorkOrderPriorityRecheckInput
- {
- public string Workord { get; set; } = string.Empty;
- public string? Qty { get; set; }
- public string? Instockdate { get; set; }
- public string? Priority { get; set; }
- public string Domain { get; set; } = string.Empty;
- public string? UserAccount { get; set; }
- public string? LotSerial { get; set; }
- }
|