using System.Globalization;
using Yitter.IdGenerator;
namespace Admin.NET.Plugin.AiDOP.Order;
///
/// 订单评审通过后生成/更新工单(WorkOrdMaster、mes_morder、mes_moentry)。
///
public class OrderWorkOrderGenerationService : ITransient
{
private readonly ISqlSugarClient _db;
public OrderWorkOrderGenerationService(ISqlSugarClient db)
{
_db = db;
}
public sealed class OrderHeader
{
public long Id { get; set; }
public string? BillNo { get; set; }
public string? CustomNo { get; set; }
public int? Urgent { get; set; }
public long? FactoryId { get; set; }
public long TenantId { get; set; }
}
public sealed class OrderEntryLine
{
public long Id { 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 sealed class WorkOrderUpsertResult
{
public string WorkOrd { get; set; } = string.Empty;
public bool Created { get; set; }
public List Warnings { get; set; } = new();
}
public async Task CreateOrUpdateForEntryAsync(
OrderHeader order,
OrderEntryLine entry,
string account,
List warnings)
{
var domain = await ResolveDomainAsync(order, entry);
var dueDate = entry.SysCapacityDate ?? entry.PlanDate
?? throw Oops.Oh($"订单行 {entry.EntrySeq} 缺少计划交期,无法生成工单");
var qty = entry.Qty ?? throw Oops.Oh($"订单行 {entry.EntrySeq} 数量无效");
if (qty <= 0)
throw Oops.Oh($"订单行 {entry.EntrySeq} 数量必须大于 0");
var itemNum = entry.ItemNumber?.Trim()
?? throw Oops.Oh($"订单行 {entry.EntrySeq} 物料编码不能为空");
var itemCnt = await _db.Ado.GetIntAsync(
"SELECT COUNT(*) FROM ItemMaster WHERE ItemNum = @ItemNum",
new SugarParameter("@ItemNum", itemNum));
if (itemCnt == 0)
throw Oops.Oh($"物料 {itemNum} 不存在于 ItemMaster,订单行 {entry.EntrySeq} 无法评审");
if (string.IsNullOrWhiteSpace(entry.BomNumber))
warnings.Add($"订单行 {entry.EntrySeq}({itemNum})无 BOM 编号,工单已生成但未展开物料明细");
var urgent = entry.Urgent ?? order.Urgent ?? 0;
var priority = urgent switch
{
2 => 20m,
1 => 10m,
_ => 0m
};
var existing = await FindOpenWorkOrderByEntryAsync(entry.TenantId, entry.Id);
var now = DateTime.Now;
var workOrd = existing?.WorkOrd ?? await GenerateWorkOrdNoAsync(entry.TenantId);
var created = existing is null;
if (existing is null)
{
await InsertWorkOrdMasterAsync(order, entry, workOrd, domain, itemNum, qty, dueDate, priority, urgent, account, now);
var morderId = await InsertMesMorderAsync(order, entry, workOrd, domain, itemNum, qty, urgent, account, now);
await InsertMesMoentryAsync(order, entry, workOrd, morderId, qty, account, now);
}
else
{
await UpdateWorkOrdMasterAsync(existing, entry, itemNum, qty, dueDate, priority, urgent, account, now);
await UpdateMesMorderAsync(existing.WorkOrd, domain, entry, itemNum, qty, urgent, account, now);
}
return new WorkOrderUpsertResult
{
WorkOrd = workOrd,
Created = created,
Warnings = warnings
};
}
///
/// WorkOrdMaster.Domain 为 varchar(8) 工厂编码(如 8010)。
/// UAT 订单行 factory_id 可能误存租户 ID,不可直接 ToString 写入 Domain。
///
private async Task ResolveDomainAsync(OrderHeader order, OrderEntryLine entry)
{
var fid = entry.FactoryId ?? order.FactoryId;
if (fid is > 0)
{
var fidText = fid.Value.ToString(CultureInfo.InvariantCulture);
if (fidText.Length <= 8)
return fidText;
}
var fallback = await _db.Ado.GetStringAsync(
"""
SELECT `Domain` FROM WorkOrdMaster
WHERE tenant_id = @TenantId
AND `Domain` IS NOT NULL AND TRIM(`Domain`) <> ''
GROUP BY `Domain`
ORDER BY COUNT(*) DESC
LIMIT 1
""",
new SugarParameter("@TenantId", entry.TenantId));
if (!string.IsNullOrWhiteSpace(fallback))
return fallback.Trim();
return "8010";
}
private async Task FindOpenWorkOrderByEntryAsync(long tenantId, long entryId)
{
var rows = await _db.Ado.SqlQueryAsync(
"""
SELECT RecID AS RecId, WorkOrd, `Domain`, Status
FROM WorkOrdMaster
WHERE tenant_id = @TenantId
AND BusinessID = @EntryId
AND LOWER(TRIM(IFNULL(Status,''))) <> 'c'
ORDER BY RecID DESC
LIMIT 1
""",
new SugarParameter("@TenantId", tenantId),
new SugarParameter("@EntryId", entryId));
return rows.FirstOrDefault();
}
private async Task GenerateWorkOrdNoAsync(long tenantId)
{
const string sql = """
SELECT IFNULL(MAX(CAST(SUBSTRING(TRIM(WorkOrd), 2) AS UNSIGNED)), 0)
FROM WorkOrdMaster
WHERE tenant_id = @TenantId
AND TRIM(WorkOrd) REGEXP '^M[0-9]+$'
""";
var maxSeq = await _db.Ado.GetIntAsync(sql, new SugarParameter("@TenantId", tenantId));
for (var attempt = 0; attempt < 5; attempt++)
{
var next = maxSeq + 1 + attempt;
var candidate = "M" + next.ToString("D9", CultureInfo.InvariantCulture);
var dup = await _db.Ado.GetIntAsync(
"SELECT COUNT(*) FROM WorkOrdMaster WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd",
new SugarParameter("@TenantId", tenantId),
new SugarParameter("@WorkOrd", candidate));
if (dup == 0)
return candidate;
}
throw Oops.Oh("生成工单号失败:流水号冲突,请重试");
}
private async Task InsertWorkOrdMasterAsync(
OrderHeader order,
OrderEntryLine entry,
string workOrd,
string domain,
string itemNum,
decimal qty,
DateTime dueDate,
decimal priority,
int urgent,
string account,
DateTime now)
{
await _db.Ado.ExecuteCommandAsync(
"""
INSERT INTO WorkOrdMaster (
`Domain`, WorkOrd, ItemNum, ItemName, Status, Typed,
QtyOrded, QtyCompleted, LbrVar, OrdDate, DueDate, Priority, Urgent,
CustNo, SalesJob, BusinessID, CreateUser, CreateTime, UpdateUser, UpdateTime,
IsActive, IsConfirm, tenant_id, Remark
) VALUES (
@Domain, @WorkOrd, @ItemNum, @ItemName, 'p', 'M',
@Qty, 0, 0, @OrdDate, @DueDate, @Priority, @Urgent,
@CustNo, @SalesJob, @BusinessId, @User, @Now, @User, @Now,
1, 0, @TenantId, @Remark
)
""",
new SugarParameter("@Domain", domain),
new SugarParameter("@WorkOrd", workOrd),
new SugarParameter("@ItemNum", itemNum),
new SugarParameter("@ItemName", entry.ItemName ?? string.Empty),
new SugarParameter("@Qty", qty),
new SugarParameter("@OrdDate", now.Date),
new SugarParameter("@DueDate", dueDate.Date),
new SugarParameter("@Priority", priority),
new SugarParameter("@Urgent", urgent),
new SugarParameter("@CustNo", order.CustomNo ?? (object)DBNull.Value),
new SugarParameter("@SalesJob", order.BillNo ?? (object)DBNull.Value),
new SugarParameter("@BusinessId", entry.Id),
new SugarParameter("@User", account),
new SugarParameter("@Now", now),
new SugarParameter("@TenantId", entry.TenantId),
new SugarParameter("@Remark", $"S1评审生成 订单行{entry.EntrySeq}"));
}
private async Task UpdateWorkOrdMasterAsync(
ExistingWorkOrderRow existing,
OrderEntryLine entry,
string itemNum,
decimal qty,
DateTime dueDate,
decimal priority,
int urgent,
string account,
DateTime now)
{
await _db.Ado.ExecuteCommandAsync(
"""
UPDATE WorkOrdMaster
SET ItemNum = @ItemNum,
ItemName = @ItemName,
QtyOrded = @Qty,
DueDate = @DueDate,
Priority = @Priority,
Urgent = @Urgent,
UpdateUser = @User,
UpdateTime = @Now
WHERE RecID = @RecId
""",
new SugarParameter("@ItemNum", itemNum),
new SugarParameter("@ItemName", entry.ItemName ?? string.Empty),
new SugarParameter("@Qty", qty),
new SugarParameter("@DueDate", dueDate.Date),
new SugarParameter("@Priority", priority),
new SugarParameter("@Urgent", urgent),
new SugarParameter("@User", account),
new SugarParameter("@Now", now),
new SugarParameter("@RecId", existing.RecId));
}
private async Task InsertMesMorderAsync(
OrderHeader order,
OrderEntryLine entry,
string workOrd,
string domain,
string itemNum,
decimal qty,
int urgent,
string account,
DateTime now)
{
var factoryId = entry.FactoryId ?? order.FactoryId ?? entry.TenantId;
var companyId = entry.CompanyId ?? factoryId;
var morderId = YitIdHelper.NextId();
await _db.Ado.ExecuteCommandAsync(
"""
INSERT INTO mes_morder (
Id, morder_no, morder_type, morder_date, morder_state,
product_code, product_name, fmodel, bom_number, unit,
need_number, morder_production_number, MaterialSituation, urgent,
create_by_name, create_time, update_by_name, update_time,
tenant_id, factory_id, company_id, IsDeleted
) VALUES (
@Id, @MorderNo, '生产工单', @Now, '新建',
@ProductCode, @ProductName, @Fmodel, @BomNumber, @Unit,
@Qty, @Qty, '待检查', @Urgent,
@User, @Now, @User, @Now,
@TenantId, @FactoryId, @CompanyId, 0
)
""",
new SugarParameter("@Id", morderId),
new SugarParameter("@MorderNo", workOrd),
new SugarParameter("@Now", now),
new SugarParameter("@ProductCode", itemNum),
new SugarParameter("@ProductName", entry.ItemName ?? string.Empty),
new SugarParameter("@Fmodel", entry.Specification ?? (object)DBNull.Value),
new SugarParameter("@BomNumber", entry.BomNumber ?? (object)DBNull.Value),
new SugarParameter("@Unit", entry.Unit ?? (object)DBNull.Value),
new SugarParameter("@Qty", qty),
new SugarParameter("@Urgent", urgent),
new SugarParameter("@User", account),
new SugarParameter("@TenantId", entry.TenantId),
new SugarParameter("@FactoryId", factoryId),
new SugarParameter("@CompanyId", companyId));
return morderId;
}
private async Task InsertMesMoentryAsync(
OrderHeader order,
OrderEntryLine entry,
string workOrd,
long morderId,
decimal qty,
string account,
DateTime now)
{
var factoryId = entry.FactoryId ?? order.FactoryId ?? entry.TenantId;
var companyId = entry.CompanyId ?? factoryId;
var moentryId = YitIdHelper.NextId();
await _db.Ado.ExecuteCommandAsync(
"""
INSERT INTO mes_moentry (
Id, moentry_moid, moentry_mono, soentry_id, fbill_no, unit,
need_number, morder_production_number, remaining_number,
create_by_name, create_time, update_by_name, update_time,
tenant_id, factory_id, company_id, IsDeleted
) VALUES (
@Id, @Moid, @Mono, @SoentryId, @BillNo, @Unit,
@Qty, @Qty, @Qty,
@User, @Now, @User, @Now,
@TenantId, @FactoryId, @CompanyId, 0
)
""",
new SugarParameter("@Id", moentryId),
new SugarParameter("@Moid", morderId),
new SugarParameter("@Mono", workOrd),
new SugarParameter("@SoentryId", entry.Id),
new SugarParameter("@BillNo", order.BillNo ?? entry.BillNo ?? string.Empty),
new SugarParameter("@Unit", entry.Unit ?? (object)DBNull.Value),
new SugarParameter("@Qty", qty),
new SugarParameter("@User", account),
new SugarParameter("@Now", now),
new SugarParameter("@TenantId", entry.TenantId),
new SugarParameter("@FactoryId", factoryId),
new SugarParameter("@CompanyId", companyId));
}
private async Task UpdateMesMorderAsync(
string workOrd,
string domain,
OrderEntryLine entry,
string itemNum,
decimal qty,
int urgent,
string account,
DateTime now)
{
var factoryId = entry.FactoryId ?? entry.TenantId;
await _db.Ado.ExecuteCommandAsync(
"""
UPDATE mes_morder
SET product_code = @ProductCode,
product_name = @ProductName,
fmodel = @Fmodel,
bom_number = @BomNumber,
unit = @Unit,
need_number = @Qty,
morder_production_number = @Qty,
urgent = @Urgent,
update_by_name = @User,
update_time = @Now
WHERE morder_no = @MorderNo
AND tenant_id = @TenantId
AND IsDeleted = 0
""",
new SugarParameter("@ProductCode", itemNum),
new SugarParameter("@ProductName", entry.ItemName ?? string.Empty),
new SugarParameter("@Fmodel", entry.Specification ?? (object)DBNull.Value),
new SugarParameter("@BomNumber", entry.BomNumber ?? (object)DBNull.Value),
new SugarParameter("@Unit", entry.Unit ?? (object)DBNull.Value),
new SugarParameter("@Qty", qty),
new SugarParameter("@Urgent", urgent),
new SugarParameter("@User", account),
new SugarParameter("@Now", now),
new SugarParameter("@MorderNo", workOrd),
new SugarParameter("@TenantId", entry.TenantId));
}
private sealed class ExistingWorkOrderRow
{
public long RecId { get; set; }
public string? WorkOrd { get; set; }
public string? Domain { get; set; }
public string? Status { get; set; }
}
}