using Admin.NET.Plugin.AiDOP.Order;
namespace Admin.NET.Plugin.AiDOP.WorkOrder;
///
/// 工单齐套检查动作:重新执行资源检查(BOM 展开 + 库存/在途缺口计算),
/// 将新结果写入 b_examine_result / b_bom_child_examine,并更新 mes_morder.MaterialSituation。
///
public class WorkOrderKittingCheckService : ITransient
{
private readonly ISqlSugarClient _db;
private readonly MaterialRequirementCalculator _calculator;
private readonly ResourceCheckResultWriter _writer;
public WorkOrderKittingCheckService(
ISqlSugarClient db,
MaterialRequirementCalculator calculator,
ResourceCheckResultWriter writer)
{
_db = db;
_calculator = calculator;
_writer = writer;
}
/// 批量齐套检查:租户下所有 Status='p'/'r' 的工单逐一重新检查。
public async Task CheckTenantWorkOrdersAsync(long tenantId, string account)
{
var workOrds = await _db.Ado.SqlQueryAsync(
"""
SELECT WorkOrd FROM WorkOrdMaster
WHERE tenant_id = @TenantId
AND LOWER(TRIM(IFNULL(Status,''))) IN ('p', 'r')
ORDER BY WorkOrd
""",
new SugarParameter("@TenantId", tenantId));
var result = new KittingCheckResult();
foreach (var wo in workOrds)
{
var one = await CheckSingleAsync(tenantId, wo, account);
result.CheckedCount++;
if (one.IsKitted) result.KittedCount++;
else result.ShortageCount++;
}
return result;
}
///
/// 单工单齐套检查:以工单为主,重新 BOM 展开 + 库存计算 → 写入检查结果。
/// BusinessID>0 时从订单明细行加载数据;BusinessID=0 时从工单/生产工单构造参数。
///
public async Task CheckSingleAsync(long tenantId, string workOrd, string account)
{
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();
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 ? "缺料" : "齐套"
};
}
/// 从 WorkOrdMaster + mes_morder 数据构造 OrderEntryLine。
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
};
}
/// 从 WorkOrdMaster 数据构造兜底 OrderHeader。
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 LoadWorkOrdMasterAsync(long tenantId, string workOrd)
{
var rows = await _db.Ado.SqlQueryAsync(
"""
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));
return rows.FirstOrDefault();
}
private async Task LoadOrderEntryAsync(long entryId, long tenantId)
{
var rows = await _db.Ado.SqlQueryAsync(
"""
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("@Id", entryId),
new SugarParameter("@TenantId", tenantId));
return rows.FirstOrDefault();
}
private async Task LoadOrderHeaderAsync(long orderId, long tenantId)
{
var rows = await _db.Ado.SqlQueryAsync(
"""
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
{
public int CheckedCount { get; set; }
public int KittedCount { get; set; }
public int ShortageCount { get; set; }
}
public sealed class SingleKittingCheckResult
{
public string WorkOrd { get; set; } = string.Empty;
public bool IsKitted { get; set; }
public int ShortageLineCount { get; set; }
public string MaterialSituation { get; set; } = string.Empty;
}
}