|
|
@@ -1,15 +1,28 @@
|
|
|
+using Admin.NET.Plugin.AiDOP.Order;
|
|
|
+
|
|
|
namespace Admin.NET.Plugin.AiDOP.WorkOrder;
|
|
|
|
|
|
-/// <summary>工单齐套检查动作。</summary>
|
|
|
+/// <summary>
|
|
|
+/// 工单齐套检查动作:重新执行资源检查(BOM 展开 + 库存/在途缺口计算),
|
|
|
+/// 将新结果写入 b_examine_result / b_bom_child_examine,并更新 mes_morder.MaterialSituation。
|
|
|
+/// </summary>
|
|
|
public class WorkOrderKittingCheckService : ITransient
|
|
|
{
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
+ private readonly MaterialRequirementCalculator _calculator;
|
|
|
+ private readonly ResourceCheckResultWriter _writer;
|
|
|
|
|
|
- public WorkOrderKittingCheckService(ISqlSugarClient db)
|
|
|
+ public WorkOrderKittingCheckService(
|
|
|
+ ISqlSugarClient db,
|
|
|
+ MaterialRequirementCalculator calculator,
|
|
|
+ ResourceCheckResultWriter writer)
|
|
|
{
|
|
|
_db = db;
|
|
|
+ _calculator = calculator;
|
|
|
+ _writer = writer;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>批量齐套检查:租户下所有 Status='p'/'r' 的工单逐一重新检查。</summary>
|
|
|
public async Task<KittingCheckResult> CheckTenantWorkOrdersAsync(long tenantId, string account)
|
|
|
{
|
|
|
var workOrds = await _db.Ado.SqlQueryAsync<string>(
|
|
|
@@ -32,45 +45,196 @@ public class WorkOrderKittingCheckService : ITransient
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 单工单齐套检查:以工单为主,重新 BOM 展开 + 库存计算 → 写入检查结果。
|
|
|
+ /// BusinessID>0 时从订单明细行加载数据;BusinessID=0 时从工单/生产工单构造参数。
|
|
|
+ /// </summary>
|
|
|
public async Task<SingleKittingCheckResult> CheckSingleAsync(long tenantId, string workOrd, string account)
|
|
|
{
|
|
|
- var now = DateTime.Now;
|
|
|
- var shortage = await _db.Ado.GetIntAsync(
|
|
|
+ var wo = workOrd.Trim();
|
|
|
+
|
|
|
+ // 1. 从 WorkOrdMaster + mes_morder 获取工单数据
|
|
|
+ var wm = await LoadWorkOrdMasterAsync(tenantId, wo);
|
|
|
+ if (wm is null)
|
|
|
+ throw Oops.Oh($"工单 {wo} 不存在或已关闭");
|
|
|
+
|
|
|
+ OrderWorkOrderGenerationService.OrderEntryLine entry;
|
|
|
+ OrderWorkOrderGenerationService.OrderHeader order;
|
|
|
+
|
|
|
+ if (wm.BusinessId > 0)
|
|
|
+ {
|
|
|
+ // 2a. 有关联订单明细行:从 crm_seorderentry / crm_seorder 加载
|
|
|
+ var loadedEntry = await LoadOrderEntryAsync(wm.BusinessId, tenantId);
|
|
|
+ if (loadedEntry is not null)
|
|
|
+ {
|
|
|
+ entry = loadedEntry;
|
|
|
+ var loadedOrder = await LoadOrderHeaderAsync(entry.SeOrderId, tenantId);
|
|
|
+ order = loadedOrder ?? BuildFallbackOrderFromWorkOrder(wm);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 明细行已删除/不存在,回退到工单数据
|
|
|
+ entry = BuildEntryFromWorkOrder(wm);
|
|
|
+ order = BuildFallbackOrderFromWorkOrder(wm);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 2b. 无关联订单明细行(BusinessID=0):从工单构造检查参数
|
|
|
+ entry = BuildEntryFromWorkOrder(wm);
|
|
|
+ order = BuildFallbackOrderFromWorkOrder(wm);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 重新执行资源检查(BOM 展开 + 库存/在途缺口计算)
|
|
|
+ var warnings = new List<string>();
|
|
|
+ var lines = await _calculator.BuildLinesAsync(order, entry, warnings);
|
|
|
+
|
|
|
+ // 4. 将新结果写入 b_examine_result / b_bom_child_examine 并更新 mes_morder
|
|
|
+ var checkResult = await _writer.WriteAsync(order, entry, wo, wm.MorderId, lines, account, DateTime.Now);
|
|
|
+
|
|
|
+ return new SingleKittingCheckResult
|
|
|
+ {
|
|
|
+ WorkOrd = wo,
|
|
|
+ IsKitted = !checkResult.HasShortage,
|
|
|
+ ShortageLineCount = checkResult.HasShortage
|
|
|
+ ? lines.Count(x => x.IsUse == 1 && x.LackQty > 0)
|
|
|
+ : 0,
|
|
|
+ MaterialSituation = checkResult.HasShortage ? "缺料" : "齐套"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>从 WorkOrdMaster + mes_morder 数据构造 OrderEntryLine。</summary>
|
|
|
+ private static OrderWorkOrderGenerationService.OrderEntryLine BuildEntryFromWorkOrder(WorkOrdMasterRow wm)
|
|
|
+ {
|
|
|
+ return new OrderWorkOrderGenerationService.OrderEntryLine
|
|
|
+ {
|
|
|
+ Id = wm.BusinessId,
|
|
|
+ SeOrderId = 0,
|
|
|
+ BillNo = wm.SalesJob,
|
|
|
+ EntrySeq = 0,
|
|
|
+ ItemNumber = wm.WoItemNum,
|
|
|
+ ItemName = wm.WoItemName,
|
|
|
+ Specification = wm.MorderModel,
|
|
|
+ Unit = wm.MorderUnit,
|
|
|
+ BomNumber = wm.MorderBomNumber ?? wm.WoBomFormula,
|
|
|
+ Qty = wm.WoQtyOrded,
|
|
|
+ PlanDate = wm.WoDueDate,
|
|
|
+ SysCapacityDate = wm.WoDueDate,
|
|
|
+ Progress = null,
|
|
|
+ Urgent = wm.WoUrgent,
|
|
|
+ FactoryId = null,
|
|
|
+ CompanyId = null,
|
|
|
+ TenantId = wm.WoTenantId
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>从 WorkOrdMaster 数据构造兜底 OrderHeader。</summary>
|
|
|
+ private static OrderWorkOrderGenerationService.OrderHeader BuildFallbackOrderFromWorkOrder(WorkOrdMasterRow wm)
|
|
|
+ {
|
|
|
+ return new OrderWorkOrderGenerationService.OrderHeader
|
|
|
+ {
|
|
|
+ Id = 0,
|
|
|
+ BillNo = wm.SalesJob,
|
|
|
+ CustomNo = wm.WoCustNo,
|
|
|
+ Urgent = wm.WoUrgent,
|
|
|
+ FactoryId = null,
|
|
|
+ TenantId = wm.WoTenantId
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // ──────────────── 数据加载 ────────────────
|
|
|
+
|
|
|
+ private async Task<WorkOrdMasterRow?> LoadWorkOrdMasterAsync(long tenantId, string workOrd)
|
|
|
+ {
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<WorkOrdMasterRow>(
|
|
|
"""
|
|
|
- SELECT COUNT(*)
|
|
|
- FROM b_examine_result ber
|
|
|
- INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.is_use = 1
|
|
|
- WHERE ber.tenant_id = @TenantId
|
|
|
- AND ber.IsDeleted = 0
|
|
|
- AND ber.morder_no = @WorkOrd
|
|
|
- AND IFNULL(bce.lack_qty, 0) > 0
|
|
|
+ SELECT wm.RecID, wm.WorkOrd, wm.BusinessID, wm.SalesJob,
|
|
|
+ wm.ItemNum AS WoItemNum, wm.ItemName AS WoItemName,
|
|
|
+ wm.QtyOrded AS WoQtyOrded, wm.DueDate AS WoDueDate,
|
|
|
+ wm.Urgent AS WoUrgent, wm.CustNo AS WoCustNo,
|
|
|
+ wm.BOMFormula AS WoBomFormula,
|
|
|
+ wm.tenant_id AS WoTenantId,
|
|
|
+ morder.Id AS MorderId,
|
|
|
+ morder.bom_number AS MorderBomNumber,
|
|
|
+ morder.product_code AS MorderProductCode,
|
|
|
+ morder.product_name AS MorderProductName,
|
|
|
+ morder.fmodel AS MorderModel,
|
|
|
+ morder.unit AS MorderUnit
|
|
|
+ FROM WorkOrdMaster wm
|
|
|
+ LEFT JOIN mes_morder morder
|
|
|
+ ON morder.morder_no = wm.WorkOrd
|
|
|
+ AND morder.tenant_id = wm.tenant_id
|
|
|
+ AND morder.IsDeleted = 0
|
|
|
+ WHERE wm.tenant_id = @TenantId
|
|
|
+ AND wm.WorkOrd = @WorkOrd
|
|
|
+ AND LOWER(TRIM(IFNULL(wm.Status,''))) <> 'c'
|
|
|
+ ORDER BY wm.RecID DESC
|
|
|
+ LIMIT 1
|
|
|
""",
|
|
|
new SugarParameter("@TenantId", tenantId),
|
|
|
- new SugarParameter("@WorkOrd", workOrd.Trim()));
|
|
|
+ new SugarParameter("@WorkOrd", workOrd));
|
|
|
+ return rows.FirstOrDefault();
|
|
|
+ }
|
|
|
|
|
|
- var isKitted = shortage == 0;
|
|
|
- var situation = isKitted ? "齐套" : "缺料";
|
|
|
- await _db.Ado.ExecuteCommandAsync(
|
|
|
+ private async Task<OrderWorkOrderGenerationService.OrderEntryLine?> LoadOrderEntryAsync(long entryId, long tenantId)
|
|
|
+ {
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<OrderWorkOrderGenerationService.OrderEntryLine>(
|
|
|
"""
|
|
|
- UPDATE mes_morder
|
|
|
- SET MaterialSituation = @Situation,
|
|
|
- update_by_name = @User,
|
|
|
- update_time = @Now
|
|
|
- WHERE tenant_id = @TenantId AND morder_no = @WorkOrd AND IsDeleted = 0
|
|
|
+ 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 Id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
|
|
|
+ LIMIT 1
|
|
|
""",
|
|
|
- new SugarParameter("@Situation", situation),
|
|
|
- new SugarParameter("@User", account),
|
|
|
- new SugarParameter("@Now", now),
|
|
|
- new SugarParameter("@TenantId", tenantId),
|
|
|
- new SugarParameter("@WorkOrd", workOrd.Trim()));
|
|
|
+ new SugarParameter("@Id", entryId),
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
+ return rows.FirstOrDefault();
|
|
|
+ }
|
|
|
|
|
|
- return new SingleKittingCheckResult
|
|
|
- {
|
|
|
- WorkOrd = workOrd,
|
|
|
- IsKitted = isKitted,
|
|
|
- ShortageLineCount = shortage,
|
|
|
- MaterialSituation = situation
|
|
|
- };
|
|
|
+ private async Task<OrderWorkOrderGenerationService.OrderHeader?> LoadOrderHeaderAsync(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 sealed class WorkOrdMasterRow
|
|
|
+ {
|
|
|
+ public long RecID { get; set; }
|
|
|
+ public string WorkOrd { get; set; } = string.Empty;
|
|
|
+ public long BusinessId { get; set; }
|
|
|
+ public string? SalesJob { get; set; }
|
|
|
+ public long? MorderId { get; set; }
|
|
|
+ // WorkOrdMaster 字段
|
|
|
+ public string? WoItemNum { get; set; }
|
|
|
+ public string? WoItemName { get; set; }
|
|
|
+ public decimal WoQtyOrded { get; set; }
|
|
|
+ public DateTime? WoDueDate { get; set; }
|
|
|
+ public int WoUrgent { get; set; }
|
|
|
+ public string? WoCustNo { get; set; }
|
|
|
+ public string? WoBomFormula { get; set; }
|
|
|
+ public long WoTenantId { get; set; }
|
|
|
+ // mes_morder 字段
|
|
|
+ public string? MorderBomNumber { get; set; }
|
|
|
+ public string? MorderProductCode { get; set; }
|
|
|
+ public string? MorderProductName { get; set; }
|
|
|
+ public string? MorderModel { get; set; }
|
|
|
+ public string? MorderUnit { get; set; }
|
|
|
}
|
|
|
|
|
|
public sealed class KittingCheckResult
|