|
|
@@ -1,3 +1,4 @@
|
|
|
+using Admin.NET.Plugin.AiDOP.Rule;
|
|
|
using Yitter.IdGenerator;
|
|
|
|
|
|
namespace Admin.NET.Plugin.AiDOP.Supply;
|
|
|
@@ -15,17 +16,32 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
private readonly SqlSugarRepository<PolistDeliverySchedule> _rep;
|
|
|
private readonly UserManager _userManager;
|
|
|
private readonly NumberRuleService _numberRuleService;
|
|
|
+ private readonly PurchaseRequestMergeService _purchaseRequestMergeService;
|
|
|
+ private readonly PurchaseOrderTransferService _purchaseOrderTransferService;
|
|
|
+ private readonly PurchaseRequestExternalPushService _purchaseRequestExternalPushService;
|
|
|
+ private readonly S3OperationLogService _operationLogService;
|
|
|
+ private readonly RuleConfigService _ruleConfigService;
|
|
|
|
|
|
public DeliveryScheduleService(
|
|
|
ISqlSugarClient db,
|
|
|
SqlSugarRepository<PolistDeliverySchedule> rep,
|
|
|
UserManager userManager,
|
|
|
- NumberRuleService numberRuleService)
|
|
|
+ NumberRuleService numberRuleService,
|
|
|
+ PurchaseRequestMergeService purchaseRequestMergeService,
|
|
|
+ PurchaseOrderTransferService purchaseOrderTransferService,
|
|
|
+ PurchaseRequestExternalPushService purchaseRequestExternalPushService,
|
|
|
+ S3OperationLogService operationLogService,
|
|
|
+ RuleConfigService ruleConfigService)
|
|
|
{
|
|
|
_db = db;
|
|
|
_rep = rep;
|
|
|
_userManager = userManager;
|
|
|
_numberRuleService = numberRuleService;
|
|
|
+ _purchaseRequestMergeService = purchaseRequestMergeService;
|
|
|
+ _purchaseOrderTransferService = purchaseOrderTransferService;
|
|
|
+ _purchaseRequestExternalPushService = purchaseRequestExternalPushService;
|
|
|
+ _operationLogService = operationLogService;
|
|
|
+ _ruleConfigService = ruleConfigService;
|
|
|
}
|
|
|
|
|
|
[DisplayName("物料交货计划列表")]
|
|
|
@@ -217,7 +233,31 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
[HttpPost("delivery-schedule/generate")]
|
|
|
public async Task<object> Generate()
|
|
|
{
|
|
|
- return await GenerateDeliverySchedulesAsync(string.Empty);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var result = await GenerateDeliverySchedulesFromDemandAsync();
|
|
|
+ await _operationLogService.WriteAsync(new S3OperationLogDto
|
|
|
+ {
|
|
|
+ Action = "S3_DELIVERY_GENERATE",
|
|
|
+ DisplayTitle = "S3 生成交货单",
|
|
|
+ Message = result.Message,
|
|
|
+ Result = result,
|
|
|
+ Success = true
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ await _operationLogService.WriteAsync(new S3OperationLogDto
|
|
|
+ {
|
|
|
+ Action = "S3_DELIVERY_GENERATE",
|
|
|
+ DisplayTitle = "S3 生成交货单失败",
|
|
|
+ Message = ex.Message,
|
|
|
+ Exception = ex.ToString(),
|
|
|
+ Success = false
|
|
|
+ });
|
|
|
+ throw;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
[DisplayName("批量添加交货单")]
|
|
|
@@ -227,7 +267,7 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
if (string.IsNullOrWhiteSpace(input.PoNumber))
|
|
|
throw Oops.Oh("采购单号不能为空");
|
|
|
|
|
|
- return await GenerateDeliverySchedulesAsync(input.PoNumber.Trim());
|
|
|
+ return await GenerateDeliverySchedulesFromPurchaseOrdersAsync(input.PoNumber.Trim());
|
|
|
}
|
|
|
|
|
|
[DisplayName("发布交货单")]
|
|
|
@@ -338,7 +378,265 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
return new { total, page, pageSize, list };
|
|
|
}
|
|
|
|
|
|
- private async Task<DeliveryScheduleGenerateResult> GenerateDeliverySchedulesAsync(string poNumber)
|
|
|
+ private async Task<DeliveryScheduleGenerateResult> GenerateDeliverySchedulesFromDemandAsync()
|
|
|
+ {
|
|
|
+ var account = _userManager.Account ?? "system";
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var optTime = now;
|
|
|
+ var ruleResult = await _ruleConfigService.ResolveS3DeliveryGenerateOptionsAsync(_userManager.TenantId, null);
|
|
|
+ if (!ruleResult.Success)
|
|
|
+ {
|
|
|
+ throw Oops.Oh($"S3交货单生成规则配置无效:{string.Join(";", ruleResult.Errors)}");
|
|
|
+ }
|
|
|
+ var rules = ruleResult.Options;
|
|
|
+ var ruleSnapshot = _ruleConfigService.CreateSnapshot("S3", "DELIVERY_GENERATE", _userManager.TenantId, null, ruleResult, now);
|
|
|
+ var pars = new List<SugarParameter>();
|
|
|
+ var where = new List<string>
|
|
|
+ {
|
|
|
+ "IFNULL(a.status,'') = 'P'",
|
|
|
+ "IFNULL(a.tosechedqty,0) > 0",
|
|
|
+ "(IFNULL(a.ishistoryversion,'') = '' OR a.ishistoryversion = 'N')",
|
|
|
+ "IFNULL(a.IsDeleted,0)=0"
|
|
|
+ };
|
|
|
+ if (_userManager.TenantId > 0)
|
|
|
+ {
|
|
|
+ where.Add("a.tenant_id = @TenantId");
|
|
|
+ pars.Add(new SugarParameter("@TenantId", _userManager.TenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ var demands = await _db.Ado.SqlQueryAsync<DemandScheduleCandidateRow>(
|
|
|
+ $"""
|
|
|
+ SELECT
|
|
|
+ a.Id AS IcdsId,
|
|
|
+ IFNULL(TRIM(a.itemnum),'') AS ItemNum,
|
|
|
+ a.requestdate AS RequestDate,
|
|
|
+ a.arrivaldate AS ArrivalDate,
|
|
|
+ IFNULL(a.tosechedqty,0) AS DemandQty,
|
|
|
+ a.tenant_id AS TenantId,
|
|
|
+ a.factory_id AS FactoryId,
|
|
|
+ a.company_id AS CompanyId,
|
|
|
+ IFNULL(TRIM(im.PurMfg),'') AS PurMfg,
|
|
|
+ IFNULL(TRIM(im.UM),'') AS Um,
|
|
|
+ IFNULL(TRIM(im.Descr),'') AS ItemName,
|
|
|
+ ic.Id AS IcItemId
|
|
|
+ FROM ic_demandschedule a
|
|
|
+ LEFT JOIN ItemMaster im ON a.itemnum = im.ItemNum
|
|
|
+ LEFT JOIN ic_item ic ON a.itemnum = ic.number AND (a.tenant_id = ic.tenant_id OR a.tenant_id IS NULL)
|
|
|
+ WHERE {string.Join(" AND ", where)}
|
|
|
+ ORDER BY a.arrivaldate, a.requestdate, a.Id
|
|
|
+ """,
|
|
|
+ pars);
|
|
|
+
|
|
|
+ if (demands.Count == 0)
|
|
|
+ {
|
|
|
+ return new DeliveryScheduleGenerateResult
|
|
|
+ {
|
|
|
+ DemandCount = 0,
|
|
|
+ CandidateCount = 0,
|
|
|
+ CreatedCount = 0,
|
|
|
+ RuleSnapshot = ruleSnapshot,
|
|
|
+ Message = "无已发布且待生成交货单的需求计划"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ var demandIds = demands.Select(x => x.IcdsId).Distinct().ToList();
|
|
|
+ var existingUsages = await QueryExistingScheduleUsagesAsync(demandIds);
|
|
|
+ var itemNums = demands.Select(x => x.ItemNum).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
|
|
|
+ var sourceLists = await QuerySourceListsAsync(itemNums);
|
|
|
+ var supplierCodes = sourceLists.Select(x => x.SupplierCode).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
|
|
|
+ var poLines = supplierCodes.Count == 0
|
|
|
+ ? new List<PurchaseOrderAvailableRow>()
|
|
|
+ : await QueryAvailablePurchaseOrderLinesAsync(itemNums, supplierCodes, rules);
|
|
|
+
|
|
|
+ var exceptions = new List<DeliveryExceptionMaster>();
|
|
|
+ var schedules = new List<PolistDeliverySchedule>();
|
|
|
+ var purchaseRequests = new List<PurchaseRequestMain>();
|
|
|
+ var prMergeReducedCount = 0;
|
|
|
+ var purchaseOrderTransferResult = new PurchaseOrderTransferResult();
|
|
|
+ var externalPushResult = new PurchaseRequestExternalPushResult();
|
|
|
+ var allocatedByPoLine = new Dictionary<string, decimal>();
|
|
|
+ var skippedCount = 0;
|
|
|
+
|
|
|
+ foreach (var demand in demands)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(demand.ItemNum))
|
|
|
+ {
|
|
|
+ skippedCount++;
|
|
|
+ AddDeliveryException(exceptions, demand, optTime, "需求计划缺少物料编码,无法生成交货单", demand.DemandQty);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!string.Equals(demand.PurMfg, "P", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ skippedCount++;
|
|
|
+ AddDeliveryException(exceptions, demand, optTime, $"物料[{demand.ItemNum}]不是采购件,不需要生成交货单", demand.DemandQty);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ var usedQty = existingUsages.TryGetValue(demand.IcdsId, out var used) ? used : 0m;
|
|
|
+ var demandRestQty = demand.DemandQty - usedQty;
|
|
|
+ if (demandRestQty <= 0)
|
|
|
+ {
|
|
|
+ skippedCount++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ var itemSources = sourceLists
|
|
|
+ .Where(x => string.Equals(x.ItemNum, demand.ItemNum, StringComparison.OrdinalIgnoreCase))
|
|
|
+ .OrderByDescending(x => x.QuotaRate)
|
|
|
+ .ToList();
|
|
|
+ if (itemSources.Count == 0)
|
|
|
+ {
|
|
|
+ skippedCount++;
|
|
|
+ AddDeliveryException(exceptions, demand, optTime, $"物料交货计划[{demand.ItemNum}:{FormatDate(demand.RequestDate)}]没有维护货源清单,无法转交货单", demandRestQty);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ var quotaTotal = itemSources.Sum(x => x.QuotaRate);
|
|
|
+ if (Math.Abs(quotaTotal - rules.QuotaRequiredTotal) > rules.QuotaTolerance)
|
|
|
+ {
|
|
|
+ skippedCount++;
|
|
|
+ AddDeliveryException(exceptions, demand, optTime, $"物料交货计划[{demand.ItemNum}:{FormatDate(demand.RequestDate)}]的货源清单供应商配额之和不为{rules.QuotaRequiredTotal:0.####}%,无法转交货单", demandRestQty);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ var allocatedForDemand = 0m;
|
|
|
+ for (var i = 0; i < itemSources.Count; i++)
|
|
|
+ {
|
|
|
+ var source = itemSources[i];
|
|
|
+ var remainingDemand = demandRestQty - allocatedForDemand;
|
|
|
+ if (remainingDemand <= 0) break;
|
|
|
+
|
|
|
+ var supplierNeedQty = i == itemSources.Count - 1
|
|
|
+ ? remainingDemand
|
|
|
+ : Math.Min(remainingDemand, Math.Ceiling(demandRestQty * source.QuotaRate / 100m));
|
|
|
+ if (supplierNeedQty <= 0) continue;
|
|
|
+
|
|
|
+ var supplierAllocated = AllocateDemandToPurchaseLines(
|
|
|
+ demand,
|
|
|
+ source,
|
|
|
+ poLines,
|
|
|
+ allocatedByPoLine,
|
|
|
+ supplierNeedQty,
|
|
|
+ remainingDemand,
|
|
|
+ schedules,
|
|
|
+ now,
|
|
|
+ account,
|
|
|
+ rules);
|
|
|
+ allocatedForDemand += supplierAllocated;
|
|
|
+
|
|
|
+ var shortageQty = supplierNeedQty - supplierAllocated;
|
|
|
+ if (shortageQty > 0)
|
|
|
+ {
|
|
|
+ var prCreated = rules.ShortageCreatePr && TryAddPurchaseRequest(purchaseRequests, demand, source, shortageQty, now, account, rules);
|
|
|
+ AddDeliveryException(
|
|
|
+ exceptions,
|
|
|
+ demand,
|
|
|
+ optTime,
|
|
|
+ prCreated
|
|
|
+ ? $"物料交货计划[{demand.ItemNum}:{FormatDate(demand.RequestDate)}]对应供应商[{source.SupplierName}]采购订单不足,已生成采购申请,后续待合并/转单/推送"
|
|
|
+ : rules.ShortageCreatePr
|
|
|
+ ? $"物料交货计划[{demand.ItemNum}:{FormatDate(demand.RequestDate)}]对应供应商[{source.SupplierName}]采购订单不足,需补充 {shortageQty:0.####};因物料或供应商主键缺失,未自动生成PR"
|
|
|
+ : $"物料交货计划[{demand.ItemNum}:{FormatDate(demand.RequestDate)}]对应供应商[{source.SupplierName}]采购订单不足,需补充 {shortageQty:0.####};规则配置关闭自动生成PR",
|
|
|
+ shortageQty);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (schedules.Count == 0 && exceptions.Count == 0)
|
|
|
+ {
|
|
|
+ return new DeliveryScheduleGenerateResult
|
|
|
+ {
|
|
|
+ DemandCount = demands.Count,
|
|
|
+ CandidateCount = 0,
|
|
|
+ CreatedCount = 0,
|
|
|
+ SkippedCount = skippedCount,
|
|
|
+ RuleSnapshot = ruleSnapshot,
|
|
|
+ Message = "没有可生成交货单的需求计划"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ _db.Ado.BeginTran();
|
|
|
+ if (purchaseRequests.Count > 0 && rules.EnablePrMerge)
|
|
|
+ {
|
|
|
+ var mergeResult = _purchaseRequestMergeService.MergeGeneratedRequests(purchaseRequests);
|
|
|
+ purchaseRequests = mergeResult.Requests;
|
|
|
+ prMergeReducedCount = mergeResult.ReducedCount;
|
|
|
+ }
|
|
|
+ if (purchaseRequests.Count > 0)
|
|
|
+ {
|
|
|
+ await AssignPurchaseRequestNumbersAsync(purchaseRequests, account);
|
|
|
+ }
|
|
|
+ if (rules.EnableRequireGoodsToPo && purchaseRequests.Any(x => x.IsRequireGoods == 1))
|
|
|
+ {
|
|
|
+ purchaseOrderTransferResult = await _purchaseOrderTransferService.TransferGeneratedRequireGoodsAsync(purchaseRequests, account);
|
|
|
+ }
|
|
|
+ if (rules.EnableExternalPushTracking && purchaseRequests.Any(x => x.IsRequireGoods == 0))
|
|
|
+ {
|
|
|
+ externalPushResult = await _purchaseRequestExternalPushService.CreateQadTrackingForGeneratedRequestsAsync(purchaseRequests, account);
|
|
|
+ }
|
|
|
+ if (schedules.Count > 0)
|
|
|
+ {
|
|
|
+ var dsNumbersByKey = new Dictionary<string, string>();
|
|
|
+ foreach (var group in schedules.GroupBy(x => x.Domain ?? string.Empty))
|
|
|
+ {
|
|
|
+ var groupRows = group.ToList();
|
|
|
+ var dsNumbers = await _numberRuleService.NextBatchInCurrentTransactionAsync("M8", group.Key, groupRows.Count, account);
|
|
|
+ if (dsNumbers.Count < groupRows.Count || dsNumbers.Any(string.IsNullOrWhiteSpace))
|
|
|
+ throw Oops.Oh($"当前交货计划号生成失败,请检查交货计划编号规则维护。Domain={group.Key}");
|
|
|
+
|
|
|
+ for (var index = 0; index < groupRows.Count; index++)
|
|
|
+ {
|
|
|
+ dsNumbersByKey[BuildScheduleTempKey(groupRows[index])] = dsNumbers[index].Trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var schedule in schedules)
|
|
|
+ {
|
|
|
+ schedule.DsNum = dsNumbersByKey[BuildScheduleTempKey(schedule)];
|
|
|
+ }
|
|
|
+
|
|
|
+ await _db.Insertable(schedules).ExecuteCommandAsync();
|
|
|
+ await UpsertDeliveryScheduleMdpAsync(schedules, now);
|
|
|
+ }
|
|
|
+ if (purchaseRequests.Count > 0)
|
|
|
+ {
|
|
|
+ await InsertPurchaseRequestsAsync(purchaseRequests);
|
|
|
+ }
|
|
|
+ if (exceptions.Count > 0)
|
|
|
+ {
|
|
|
+ await _db.Insertable(exceptions).ExecuteCommandAsync();
|
|
|
+ }
|
|
|
+ _db.Ado.CommitTran();
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ _db.Ado.RollbackTran();
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new DeliveryScheduleGenerateResult
|
|
|
+ {
|
|
|
+ DemandCount = demands.Count,
|
|
|
+ CandidateCount = schedules.Count,
|
|
|
+ CreatedCount = schedules.Count,
|
|
|
+ ExceptionCount = exceptions.Count,
|
|
|
+ PurchaseRequestCount = purchaseRequests.Count,
|
|
|
+ PurchaseRequestMergeReducedCount = prMergeReducedCount,
|
|
|
+ PurchaseOrderCount = purchaseOrderTransferResult.CreatedOrderCount,
|
|
|
+ PurchaseOrderLineCount = purchaseOrderTransferResult.TransferredPrCount,
|
|
|
+ QadTrackingCount = externalPushResult.TrackingCount,
|
|
|
+ SkippedCount = skippedCount,
|
|
|
+ DsNums = schedules.Select(x => x.DsNum).Where(x => !string.IsNullOrWhiteSpace(x)).Take(20).ToList(),
|
|
|
+ RuleSnapshot = ruleSnapshot,
|
|
|
+ Message = schedules.Count > 0
|
|
|
+ ? $"生成交货单成功,共生成 {schedules.Count} 条;自动生成PR {purchaseRequests.Count} 条;合并减少 {prMergeReducedCount} 条;生成DO/PO {purchaseOrderTransferResult.CreatedOrderCount} 单;写QadTracking {externalPushResult.TrackingCount} 条;异常 {exceptions.Count} 条"
|
|
|
+ : $"未生成交货单,自动生成PR {purchaseRequests.Count} 条,合并减少 {prMergeReducedCount} 条,生成DO/PO {purchaseOrderTransferResult.CreatedOrderCount} 单,写QadTracking {externalPushResult.TrackingCount} 条,已记录异常 {exceptions.Count} 条"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<DeliveryScheduleGenerateResult> GenerateDeliverySchedulesFromPurchaseOrdersAsync(string poNumber)
|
|
|
{
|
|
|
var account = _userManager.Account ?? "system";
|
|
|
var pars = new List<SugarParameter>();
|
|
|
@@ -597,6 +895,404 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async Task<Dictionary<long, decimal>> QueryExistingScheduleUsagesAsync(List<long> demandIds)
|
|
|
+ {
|
|
|
+ if (demandIds.Count == 0) return new Dictionary<long, decimal>();
|
|
|
+
|
|
|
+ var pars = new List<SugarParameter>();
|
|
|
+ var inClause = BuildInClause(demandIds.Select(x => x.ToString()).ToList(), "DemandId", pars);
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<ExistingScheduleUsageRow>(
|
|
|
+ $"""
|
|
|
+ SELECT
|
|
|
+ icdsid AS IcdsId,
|
|
|
+ SUM(
|
|
|
+ CASE
|
|
|
+ WHEN IFNULL(status,'') = 'C' THEN IFNULL(sentqty,0)
|
|
|
+ WHEN IFNULL(schedqty,0) >= IFNULL(sentqty,0) THEN IFNULL(schedqty,0)
|
|
|
+ ELSE IFNULL(sentqty,0)
|
|
|
+ END
|
|
|
+ ) AS UsedQty
|
|
|
+ FROM srm_polist_ds
|
|
|
+ WHERE isactive = 1 AND icdsid IN ({inClause})
|
|
|
+ GROUP BY icdsid
|
|
|
+ """,
|
|
|
+ pars);
|
|
|
+
|
|
|
+ return rows.ToDictionary(x => x.IcdsId, x => x.UsedQty);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<SourceListRow>> QuerySourceListsAsync(List<string> itemNums)
|
|
|
+ {
|
|
|
+ if (itemNums.Count == 0) return new List<SourceListRow>();
|
|
|
+
|
|
|
+ var pars = new List<SugarParameter>();
|
|
|
+ var where = new List<string>
|
|
|
+ {
|
|
|
+ "IFNULL(sp.IsDeleted,0)=0",
|
|
|
+ "IFNULL(sp.supplier_number,'') <> ''",
|
|
|
+ "IFNULL(sp.quota_rate,0) > 0",
|
|
|
+ """
|
|
|
+ (
|
|
|
+ IFNULL(sp.is_active,'') = ''
|
|
|
+ OR sp.is_active = '否'
|
|
|
+ OR sp.is_active = 'N'
|
|
|
+ OR sp.is_active = '0'
|
|
|
+ OR LOWER(sp.is_active) = 'false'
|
|
|
+ )
|
|
|
+ """
|
|
|
+ };
|
|
|
+ if (_userManager.TenantId > 0)
|
|
|
+ {
|
|
|
+ where.Add("sp.tenant_id = @TenantId");
|
|
|
+ pars.Add(new SugarParameter("@TenantId", _userManager.TenantId));
|
|
|
+ }
|
|
|
+ var inClause = BuildInClause(itemNums, "ItemNum", pars);
|
|
|
+ where.Add($"sp.number IN ({inClause})");
|
|
|
+
|
|
|
+ return await _db.Ado.SqlQueryAsync<SourceListRow>(
|
|
|
+ $"""
|
|
|
+ SELECT
|
|
|
+ IFNULL(TRIM(sp.number),'') AS ItemNum,
|
|
|
+ IFNULL(TRIM(sp.supplier_number),'') AS SupplierCode,
|
|
|
+ IFNULL(TRIM(sp.supplier_name),'') AS SupplierName,
|
|
|
+ IFNULL(TRIM(sp.supplier_type),'') AS SupplierType,
|
|
|
+ IFNULL(sp.quota_rate,0) AS QuotaRate,
|
|
|
+ IFNULL(sp.packaging_qty,0) AS PackagingQty,
|
|
|
+ IFNULL(sp.lead_time,0) AS LeadTime,
|
|
|
+ IFNULL(sp.IsRequireGoods,0) AS IsRequireGoods,
|
|
|
+ sp.supplier_id AS SupplierId,
|
|
|
+ IFNULL(TRIM(sp.order_rector_name),'') AS OrderRectorName,
|
|
|
+ IFNULL(TRIM(sp.order_rector_num),'') AS OrderRectorNum,
|
|
|
+ IFNULL(TRIM(sp.purchase_unit),'') AS PurchaseUnit,
|
|
|
+ sp.currency_type AS CurrencyType
|
|
|
+ FROM srm_purchase sp
|
|
|
+ WHERE {string.Join(" AND ", where)}
|
|
|
+ ORDER BY sp.number, IFNULL(sp.quota_priority, 999999), sp.quota_rate DESC
|
|
|
+ """,
|
|
|
+ pars);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<PurchaseOrderAvailableRow>> QueryAvailablePurchaseOrderLinesAsync(
|
|
|
+ List<string> itemNums,
|
|
|
+ List<string> supplierCodes,
|
|
|
+ S3DeliveryGenerateRuleOptions rules)
|
|
|
+ {
|
|
|
+ var pars = new List<SugarParameter>();
|
|
|
+ var poBuyerCodesClause = BuildInClause(rules.PoBuyerCodes, "PoBuyerCode", pars);
|
|
|
+ var doUsagesClause = BuildInClause(rules.DoUsages, "DoUsage", pars);
|
|
|
+ var where = new List<string>
|
|
|
+ {
|
|
|
+ "IFNULL(m.Status,'') <> 'C'",
|
|
|
+ "IFNULL(d.Status,'') <> 'C'",
|
|
|
+ "IFNULL(d.ItemNum,'') <> ''",
|
|
|
+ "IFNULL(m.Supp,'') <> ''",
|
|
|
+ "(IFNULL(d.QtyOrded,0) - IFNULL(d.RctQty,0)) > 0",
|
|
|
+ """
|
|
|
+ (
|
|
|
+ (m.PurOrd NOT LIKE 'DO%' AND IFNULL(m.Buyer,'') IN ({0}))
|
|
|
+ OR
|
|
|
+ (m.PurOrd LIKE 'DO%' AND IFNULL(m.USAGE,'') IN ({1}))
|
|
|
+ )
|
|
|
+ """.Replace("{0}", poBuyerCodesClause).Replace("{1}", doUsagesClause)
|
|
|
+ };
|
|
|
+ if (_userManager.TenantId > 0)
|
|
|
+ {
|
|
|
+ where.Add("m.tenant_id = @TenantId");
|
|
|
+ pars.Add(new SugarParameter("@TenantId", _userManager.TenantId));
|
|
|
+ }
|
|
|
+ where.Add($"d.ItemNum IN ({BuildInClause(itemNums, "PoItemNum", pars)})");
|
|
|
+ where.Add($"m.Supp IN ({BuildInClause(supplierCodes, "SupplierCode", pars)})");
|
|
|
+
|
|
|
+ return await _db.Ado.SqlQueryAsync<PurchaseOrderAvailableRow>(
|
|
|
+ $"""
|
|
|
+ SELECT *
|
|
|
+ FROM (
|
|
|
+ SELECT
|
|
|
+ IFNULL(TRIM(m.Domain),'') AS Domain,
|
|
|
+ IFNULL(TRIM(m.PurOrd),'') AS PoNumber,
|
|
|
+ IFNULL(d.Line,0) AS PoLine,
|
|
|
+ d.RecID AS DetailRecId,
|
|
|
+ IFNULL(TRIM(d.ItemNum),'') AS ItemNum,
|
|
|
+ IFNULL(TRIM(d.UM),'') AS Um,
|
|
|
+ IFNULL(TRIM(m.Buyer),'') AS PurGroup,
|
|
|
+ IFNULL(TRIM(m.Supp),'') AS SupplierCode,
|
|
|
+ IFNULL(TRIM(s.Name),'') AS SupplierName,
|
|
|
+ d.DueDate AS DueDate,
|
|
|
+ IFNULL(d.QtyOrded,0) AS OrderQty,
|
|
|
+ IFNULL(d.RctQty,0) AS ReceiptQty,
|
|
|
+ m.tenant_id AS TenantId,
|
|
|
+ (
|
|
|
+ IFNULL(d.QtyOrded,0)
|
|
|
+ - IFNULL(d.RctQty,0)
|
|
|
+ - IFNULL((
|
|
|
+ SELECT SUM(
|
|
|
+ CASE
|
|
|
+ WHEN IFNULL(ds.status,'') = 'C' THEN IFNULL(ds.sentqty,0)
|
|
|
+ WHEN IFNULL(ds.schedqty,0) >= IFNULL(ds.sentqty,0) THEN IFNULL(ds.schedqty,0)
|
|
|
+ ELSE IFNULL(ds.sentqty,0)
|
|
|
+ END)
|
|
|
+ FROM srm_polist_ds ds
|
|
|
+ WHERE ds.ponumber = d.PurOrd
|
|
|
+ AND ds.poline = d.Line
|
|
|
+ AND ds.itemnum = d.ItemNum
|
|
|
+ AND ds.isactive = 1
|
|
|
+ ), 0)
|
|
|
+ ) AS AvailableQty
|
|
|
+ FROM PurOrdMaster m
|
|
|
+ INNER JOIN PurOrdDetail d ON m.RecID = d.PurOrdRecID
|
|
|
+ LEFT JOIN ConsigneeAddressMaster s ON m.Supp = s.Address AND s.Typed = 'Supp'
|
|
|
+ WHERE {string.Join(" AND ", where)}
|
|
|
+ ) t
|
|
|
+ WHERE t.AvailableQty > 0
|
|
|
+ ORDER BY t.ItemNum, t.SupplierCode, CASE WHEN t.PoNumber LIKE 'DO%' THEN 1 ELSE 0 END, t.DueDate, t.DetailRecId
|
|
|
+ """,
|
|
|
+ pars);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static decimal AllocateDemandToPurchaseLines(
|
|
|
+ DemandScheduleCandidateRow demand,
|
|
|
+ SourceListRow source,
|
|
|
+ List<PurchaseOrderAvailableRow> poLines,
|
|
|
+ Dictionary<string, decimal> allocatedByPoLine,
|
|
|
+ decimal supplierNeedQty,
|
|
|
+ decimal remainingDemand,
|
|
|
+ List<PolistDeliverySchedule> schedules,
|
|
|
+ DateTime now,
|
|
|
+ string account,
|
|
|
+ S3DeliveryGenerateRuleOptions rules)
|
|
|
+ {
|
|
|
+ var supplierAllocated = 0m;
|
|
|
+ var lines = poLines
|
|
|
+ .Where(x =>
|
|
|
+ string.Equals(x.ItemNum, demand.ItemNum, StringComparison.OrdinalIgnoreCase)
|
|
|
+ && string.Equals(x.SupplierCode, source.SupplierCode, StringComparison.OrdinalIgnoreCase))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ foreach (var line in lines)
|
|
|
+ {
|
|
|
+ if (supplierAllocated >= supplierNeedQty) break;
|
|
|
+
|
|
|
+ var key = BuildPoLineKey(line);
|
|
|
+ var runAllocated = allocatedByPoLine.TryGetValue(key, out var used) ? used : 0m;
|
|
|
+ var availableQty = line.AvailableQty - runAllocated;
|
|
|
+ if (availableQty <= 0) continue;
|
|
|
+
|
|
|
+ var requiredQty = supplierNeedQty - supplierAllocated;
|
|
|
+ var qty = Math.Min(availableQty, requiredQty);
|
|
|
+ if (rules.EnablePackagingRoundUp && source.PackagingQty > 0 && availableQty > qty)
|
|
|
+ {
|
|
|
+ var roundedQty = Math.Ceiling(qty / source.PackagingQty) * source.PackagingQty;
|
|
|
+ if (roundedQty <= availableQty && roundedQty <= remainingDemand - supplierAllocated)
|
|
|
+ {
|
|
|
+ qty = roundedQty;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (qty <= 0) continue;
|
|
|
+
|
|
|
+ allocatedByPoLine[key] = runAllocated + qty;
|
|
|
+ supplierAllocated += qty;
|
|
|
+ schedules.Add(new PolistDeliverySchedule
|
|
|
+ {
|
|
|
+ Id = YitIdHelper.NextId(),
|
|
|
+ Domain = line.Domain ?? string.Empty,
|
|
|
+ IcdsId = demand.IcdsId,
|
|
|
+ DsNum = string.Empty,
|
|
|
+ Status = "N",
|
|
|
+ ItemNum = demand.ItemNum ?? string.Empty,
|
|
|
+ Um = line.Um ?? demand.Um ?? string.Empty,
|
|
|
+ PurGroup = line.PurGroup ?? string.Empty,
|
|
|
+ SupplierCode = source.SupplierCode ?? line.SupplierCode ?? string.Empty,
|
|
|
+ Supplier = string.IsNullOrWhiteSpace(source.SupplierName) ? line.SupplierName ?? string.Empty : source.SupplierName,
|
|
|
+ RequestDate = demand.ArrivalDate ?? now,
|
|
|
+ NeedDate = demand.RequestDate,
|
|
|
+ PoNumber = line.PoNumber ?? string.Empty,
|
|
|
+ PoLine = line.PoLine,
|
|
|
+ SchedQty = qty,
|
|
|
+ LastSentQty = 0,
|
|
|
+ SentQty = 0,
|
|
|
+ RestQty = qty,
|
|
|
+ CreateUser = account,
|
|
|
+ UpdateUser = account,
|
|
|
+ CreateTime = now,
|
|
|
+ UpdateTime = now,
|
|
|
+ Remarks = "由系统按需求计划、货源配额和采购单可用量自动生成",
|
|
|
+ IsActive = 1,
|
|
|
+ ReturnQty = 0,
|
|
|
+ TenantId = demand.TenantId ?? line.TenantId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return supplierAllocated;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void AddDeliveryException(
|
|
|
+ List<DeliveryExceptionMaster> exceptions,
|
|
|
+ DemandScheduleCandidateRow demand,
|
|
|
+ DateTime optTime,
|
|
|
+ string remark,
|
|
|
+ decimal needQty)
|
|
|
+ {
|
|
|
+ exceptions.Add(new DeliveryExceptionMaster
|
|
|
+ {
|
|
|
+ RecID = YitIdHelper.NextId(),
|
|
|
+ Domain = demand.TenantId?.ToString() ?? string.Empty,
|
|
|
+ IcdsId = demand.IcdsId,
|
|
|
+ OptTime = optTime,
|
|
|
+ ItemNum = demand.ItemNum,
|
|
|
+ Remark = remark,
|
|
|
+ NeedQty = needQty,
|
|
|
+ TenantId = demand.TenantId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool TryAddPurchaseRequest(
|
|
|
+ List<PurchaseRequestMain> purchaseRequests,
|
|
|
+ DemandScheduleCandidateRow demand,
|
|
|
+ SourceListRow source,
|
|
|
+ decimal shortageQty,
|
|
|
+ DateTime now,
|
|
|
+ string account,
|
|
|
+ S3DeliveryGenerateRuleOptions rules)
|
|
|
+ {
|
|
|
+ if (shortageQty <= 0) return false;
|
|
|
+ if (!demand.IcItemId.HasValue || demand.IcItemId.Value <= 0) return false;
|
|
|
+ if (!source.SupplierId.HasValue || source.SupplierId.Value <= 0) return false;
|
|
|
+
|
|
|
+ var arriveDate = demand.ArrivalDate ?? demand.RequestDate ?? now.Date;
|
|
|
+ var leadDays = source.LeadTime > 0 ? (int)Math.Ceiling(source.LeadTime) : rules.DefaultLeadDays;
|
|
|
+ if (leadDays < 0) leadDays = 0;
|
|
|
+ var sendDate = arriveDate.Date.AddDays(-leadDays);
|
|
|
+ if (sendDate < now.Date) sendDate = now.Date;
|
|
|
+
|
|
|
+ purchaseRequests.Add(new PurchaseRequestMain
|
|
|
+ {
|
|
|
+ Id = YitIdHelper.NextId(),
|
|
|
+ PrPurchaseId = source.SupplierId.Value,
|
|
|
+ PrPurchaseNumber = source.SupplierCode,
|
|
|
+ PrPurchaseName = source.SupplierName,
|
|
|
+ PrPurchaser = source.OrderRectorName,
|
|
|
+ PrPurchaserNum = source.OrderRectorNum,
|
|
|
+ PrRqty = shortageQty,
|
|
|
+ PrAqty = shortageQty,
|
|
|
+ PrSqty = shortageQty,
|
|
|
+ IcitemId = demand.IcItemId.Value,
|
|
|
+ IcitemName = string.IsNullOrWhiteSpace(demand.ItemName) ? demand.ItemNum : demand.ItemName,
|
|
|
+ PrSsendDate = sendDate,
|
|
|
+ PrSarriveDate = arriveDate,
|
|
|
+ PrUnit = string.IsNullOrWhiteSpace(source.PurchaseUnit) ? demand.Um : source.PurchaseUnit,
|
|
|
+ State = 1,
|
|
|
+ PrType = 3,
|
|
|
+ CurrencyType = source.CurrencyType ?? 1,
|
|
|
+ CreateBy = null,
|
|
|
+ CreateByName = account,
|
|
|
+ CreateTime = now,
|
|
|
+ UpdateBy = null,
|
|
|
+ UpdateByName = account,
|
|
|
+ UpdateTime = now,
|
|
|
+ TenantId = demand.TenantId ?? 0,
|
|
|
+ FactoryId = demand.FactoryId,
|
|
|
+ OrgId = demand.FactoryId,
|
|
|
+ IsDeleted = false,
|
|
|
+ CompanyId = demand.CompanyId ?? 1000,
|
|
|
+ IsRequireGoods = source.IsRequireGoods,
|
|
|
+ SupplierType = source.SupplierType
|
|
|
+ });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task AssignPurchaseRequestNumbersAsync(List<PurchaseRequestMain> purchaseRequests, string account)
|
|
|
+ {
|
|
|
+ foreach (var group in purchaseRequests.GroupBy(x => x.TenantId.ToString()))
|
|
|
+ {
|
|
|
+ var groupRows = group.ToList();
|
|
|
+ var prNumbers = await _numberRuleService.NextBatchInCurrentTransactionAsync("PR", group.Key, groupRows.Count, account);
|
|
|
+ if (prNumbers.Count < groupRows.Count || prNumbers.Any(string.IsNullOrWhiteSpace))
|
|
|
+ throw Oops.Oh($"当前采购申请号生成失败,请检查采购申请编号规则维护。Domain={group.Key}");
|
|
|
+
|
|
|
+ for (var index = 0; index < groupRows.Count; index++)
|
|
|
+ {
|
|
|
+ groupRows[index].PrBillNo = prNumbers[index].Trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task InsertPurchaseRequestsAsync(List<PurchaseRequestMain> purchaseRequests)
|
|
|
+ {
|
|
|
+ if (purchaseRequests.Count == 0) return;
|
|
|
+
|
|
|
+ foreach (var pr in purchaseRequests)
|
|
|
+ {
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ INSERT INTO srm_pr_main
|
|
|
+ (Id,pr_billno,pr_purchaseid,pr_purchasenumber,pr_purchasename,pr_purchaser,pr_purchaser_num,
|
|
|
+ pr_rqty,pr_aqty,pr_sqty,icitem_id,icitem_name,pr_ssend_date,pr_sarrive_date,pr_unit,state,pr_type,currencytype,
|
|
|
+ create_by,create_by_name,create_time,update_by,update_by_name,update_time,tenant_id,factory_id,org_id,IsDeleted,company_id,IsRequireGoods,supplier_type)
|
|
|
+ VALUES
|
|
|
+ (@Id,@PrBillNo,@PrPurchaseId,@PrPurchaseNumber,@PrPurchaseName,@PrPurchaser,@PrPurchaserNum,
|
|
|
+ @PrRqty,@PrAqty,@PrSqty,@IcitemId,@IcitemName,@PrSsendDate,@PrSarriveDate,@PrUnit,@State,@PrType,@CurrencyType,
|
|
|
+ @CreateBy,@CreateByName,@CreateTime,@UpdateBy,@UpdateByName,@UpdateTime,@TenantId,@FactoryId,@OrgId,@IsDeleted,@CompanyId,@IsRequireGoods,@SupplierType)
|
|
|
+ """,
|
|
|
+ new SugarParameter("@Id", pr.Id),
|
|
|
+ new SugarParameter("@PrBillNo", pr.PrBillNo),
|
|
|
+ new SugarParameter("@PrPurchaseId", pr.PrPurchaseId),
|
|
|
+ new SugarParameter("@PrPurchaseNumber", pr.PrPurchaseNumber),
|
|
|
+ new SugarParameter("@PrPurchaseName", pr.PrPurchaseName),
|
|
|
+ new SugarParameter("@PrPurchaser", pr.PrPurchaser),
|
|
|
+ new SugarParameter("@PrPurchaserNum", pr.PrPurchaserNum),
|
|
|
+ new SugarParameter("@PrRqty", pr.PrRqty),
|
|
|
+ new SugarParameter("@PrAqty", pr.PrAqty),
|
|
|
+ new SugarParameter("@PrSqty", pr.PrSqty),
|
|
|
+ new SugarParameter("@IcitemId", pr.IcitemId),
|
|
|
+ new SugarParameter("@IcitemName", pr.IcitemName),
|
|
|
+ new SugarParameter("@PrSsendDate", pr.PrSsendDate),
|
|
|
+ new SugarParameter("@PrSarriveDate", pr.PrSarriveDate),
|
|
|
+ new SugarParameter("@PrUnit", pr.PrUnit),
|
|
|
+ new SugarParameter("@State", pr.State),
|
|
|
+ new SugarParameter("@PrType", pr.PrType),
|
|
|
+ new SugarParameter("@CurrencyType", pr.CurrencyType),
|
|
|
+ new SugarParameter("@CreateBy", pr.CreateBy),
|
|
|
+ new SugarParameter("@CreateByName", pr.CreateByName),
|
|
|
+ new SugarParameter("@CreateTime", pr.CreateTime),
|
|
|
+ new SugarParameter("@UpdateBy", pr.UpdateBy),
|
|
|
+ new SugarParameter("@UpdateByName", pr.UpdateByName),
|
|
|
+ new SugarParameter("@UpdateTime", pr.UpdateTime),
|
|
|
+ new SugarParameter("@TenantId", pr.TenantId),
|
|
|
+ new SugarParameter("@FactoryId", pr.FactoryId),
|
|
|
+ new SugarParameter("@OrgId", pr.OrgId),
|
|
|
+ new SugarParameter("@IsDeleted", pr.IsDeleted ? 1 : 0),
|
|
|
+ new SugarParameter("@CompanyId", pr.CompanyId),
|
|
|
+ new SugarParameter("@IsRequireGoods", pr.IsRequireGoods),
|
|
|
+ new SugarParameter("@SupplierType", pr.SupplierType));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string BuildInClause(List<string> values, string prefix, List<SugarParameter> pars)
|
|
|
+ {
|
|
|
+ if (values.Count == 0) return "NULL";
|
|
|
+
|
|
|
+ var names = new List<string>();
|
|
|
+ for (var i = 0; i < values.Count; i++)
|
|
|
+ {
|
|
|
+ var name = $"@{prefix}{i}";
|
|
|
+ names.Add(name);
|
|
|
+ pars.Add(new SugarParameter(name, values[i]));
|
|
|
+ }
|
|
|
+ return string.Join(",", names);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string BuildPoLineKey(PurchaseOrderAvailableRow row)
|
|
|
+ {
|
|
|
+ return $"{row.PoNumber}|{row.PoLine}|{row.ItemNum}|{row.DetailRecId}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string BuildScheduleTempKey(PolistDeliverySchedule row)
|
|
|
+ {
|
|
|
+ return $"{row.Id}|{row.PoNumber}|{row.PoLine}|{row.IcdsId}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string FormatDate(DateTime? value) => value?.ToString("yyyy-MM-dd") ?? "";
|
|
|
+
|
|
|
private static List<long> ParseIds(string ids)
|
|
|
{
|
|
|
return ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
|
@@ -687,9 +1383,18 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
|
|
|
private sealed class DeliveryScheduleGenerateResult
|
|
|
{
|
|
|
+ public int DemandCount { get; set; }
|
|
|
public int CandidateCount { get; set; }
|
|
|
public int CreatedCount { get; set; }
|
|
|
+ public int ExceptionCount { get; set; }
|
|
|
+ public int PurchaseRequestCount { get; set; }
|
|
|
+ public int PurchaseRequestMergeReducedCount { get; set; }
|
|
|
+ public int PurchaseOrderCount { get; set; }
|
|
|
+ public int PurchaseOrderLineCount { get; set; }
|
|
|
+ public int QadTrackingCount { get; set; }
|
|
|
+ public int SkippedCount { get; set; }
|
|
|
public List<string> DsNums { get; set; } = new();
|
|
|
+ public RuleConfigSnapshot<S3DeliveryGenerateRuleOptions>? RuleSnapshot { get; set; }
|
|
|
public string Message { get; set; } = string.Empty;
|
|
|
}
|
|
|
|
|
|
@@ -708,4 +1413,61 @@ public class DeliveryScheduleService : IDynamicApiController, ITransient
|
|
|
public decimal RestQty { get; set; }
|
|
|
public long? TenantId { get; set; }
|
|
|
}
|
|
|
+
|
|
|
+ private sealed class DemandScheduleCandidateRow
|
|
|
+ {
|
|
|
+ public long IcdsId { get; set; }
|
|
|
+ public string? ItemNum { get; set; }
|
|
|
+ public DateTime? RequestDate { get; set; }
|
|
|
+ public DateTime? ArrivalDate { get; set; }
|
|
|
+ public decimal DemandQty { get; set; }
|
|
|
+ public long? TenantId { get; set; }
|
|
|
+ public long? FactoryId { get; set; }
|
|
|
+ public long? CompanyId { get; set; }
|
|
|
+ public string? PurMfg { get; set; }
|
|
|
+ public string? Um { get; set; }
|
|
|
+ public string? ItemName { get; set; }
|
|
|
+ public long? IcItemId { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class ExistingScheduleUsageRow
|
|
|
+ {
|
|
|
+ public long IcdsId { get; set; }
|
|
|
+ public decimal UsedQty { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class SourceListRow
|
|
|
+ {
|
|
|
+ public string? ItemNum { get; set; }
|
|
|
+ public string? SupplierCode { get; set; }
|
|
|
+ public string? SupplierName { get; set; }
|
|
|
+ public string? SupplierType { get; set; }
|
|
|
+ public decimal QuotaRate { get; set; }
|
|
|
+ public decimal PackagingQty { get; set; }
|
|
|
+ public decimal LeadTime { get; set; }
|
|
|
+ public int IsRequireGoods { get; set; }
|
|
|
+ public long? SupplierId { get; set; }
|
|
|
+ public string? OrderRectorName { get; set; }
|
|
|
+ public string? OrderRectorNum { get; set; }
|
|
|
+ public string? PurchaseUnit { get; set; }
|
|
|
+ public long? CurrencyType { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class PurchaseOrderAvailableRow
|
|
|
+ {
|
|
|
+ public string? Domain { get; set; }
|
|
|
+ public string? PoNumber { get; set; }
|
|
|
+ public int PoLine { get; set; }
|
|
|
+ public long DetailRecId { get; set; }
|
|
|
+ public string? ItemNum { get; set; }
|
|
|
+ public string? Um { get; set; }
|
|
|
+ public string? PurGroup { get; set; }
|
|
|
+ public string? SupplierCode { get; set; }
|
|
|
+ public string? SupplierName { get; set; }
|
|
|
+ public DateTime? DueDate { get; set; }
|
|
|
+ public decimal OrderQty { get; set; }
|
|
|
+ public decimal ReceiptQty { get; set; }
|
|
|
+ public long? TenantId { get; set; }
|
|
|
+ public decimal AvailableQty { get; set; }
|
|
|
+ }
|
|
|
}
|