|
|
@@ -124,6 +124,24 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
var master = await _masterRep.GetFirstAsync(x => x.Id == id && x.TenantId == tenantId) ?? throw Oops.Oh("发货单不存在");
|
|
|
var details = await _detailRep.AsQueryable().Where(x => x.Glid == id.ToString() && x.TenantId == tenantId).OrderBy(x => x.Hh).ToListAsync();
|
|
|
+
|
|
|
+ // 待交数量按订单行实时重算(排除本发货单自身明细,避免重复扣减),查不到订单行时沿用明细原值
|
|
|
+ var djslOverrides = new Dictionary<long, decimal>();
|
|
|
+ var remainCache = new Dictionary<string, decimal?>();
|
|
|
+ foreach (var d in details)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(d.PoBill) || !int.TryParse(d.PoBillLine?.Trim(), out var line))
|
|
|
+ continue;
|
|
|
+ var cacheKey = $"{d.PoBill.Trim()}|{line}";
|
|
|
+ if (!remainCache.TryGetValue(cacheKey, out var remain))
|
|
|
+ {
|
|
|
+ remain = await GetPoRemainQtyAsync(d.PoBill.Trim(), line, id.ToString(), tenantId);
|
|
|
+ remainCache[cacheKey] = remain;
|
|
|
+ }
|
|
|
+ if (remain is not null)
|
|
|
+ djslOverrides[d.Id] = remain.Value;
|
|
|
+ }
|
|
|
+
|
|
|
return new
|
|
|
{
|
|
|
id = master.Id,
|
|
|
@@ -155,7 +173,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
scrq = d.Scrq,
|
|
|
scph = d.Scph,
|
|
|
remarks = d.Remarks,
|
|
|
- djsl = d.Djsl,
|
|
|
+ djsl = djslOverrides.TryGetValue(d.Id, out var liveDjsl) ? liveDjsl : d.Djsl,
|
|
|
jybb = d.Jybb,
|
|
|
jhdbh = d.Jhdbh
|
|
|
})
|
|
|
@@ -180,18 +198,27 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
p.ItemNum AS shMaterialCode,
|
|
|
im.Descr AS shMaterialName,
|
|
|
IFNULL(im.Drawing, p.Drawing) AS th,
|
|
|
- IFNULL(ds.SchedQty, p.QtyOrded - p.RctQty - p.ReceiptQty + p.QtyReturned) AS shDeliveryQuantity,
|
|
|
p.StdPackQty AS bzsl,
|
|
|
- CASE
|
|
|
- WHEN IFNULL(p.StdPackQty, 0) > 0 THEN CEILING(IFNULL(ds.SchedQty, p.QtyOrded - p.RctQty - p.ReceiptQty + p.QtyReturned) / p.StdPackQty)
|
|
|
- ELSE 0
|
|
|
- END AS bqsl,
|
|
|
p.UM AS shMaterialDw,
|
|
|
- (p.QtyOrded - p.RctQty - p.ReceiptQty + p.QtyReturned) AS djsl,
|
|
|
ds.DSNum AS jhdbh,
|
|
|
- im.Descr1 AS shMaterialGgxh
|
|
|
+ im.Descr1 AS shMaterialGgxh,
|
|
|
+ p.QtyOrded AS QtyOrded,
|
|
|
+ p.RctQty AS RctQty,
|
|
|
+ p.ReceiptQty AS ReceiptQty,
|
|
|
+ p.QtyReturned AS QtyReturned,
|
|
|
+ ds.SchedQty AS SchedQty,
|
|
|
+ IFNULL(s.fhsl, 0) AS ShippedFhsl
|
|
|
FROM PurOrdDetail p
|
|
|
LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline AND ds.tenant_id = p.tenant_id
|
|
|
+ LEFT JOIN (
|
|
|
+ SELECT d.po_bill, d.po_billline, SUM(d.sh_delivery_quantity) AS fhsl
|
|
|
+ FROM scm_shdzb d
|
|
|
+ INNER JOIN scm_shd m ON CAST(m.id AS CHAR) = d.glid
|
|
|
+ WHERE m.tenant_id = @TenantId
|
|
|
+ AND IFNULL(m.state, 1) <> 0
|
|
|
+ AND IFNULL(d.po_bill, '') <> ''
|
|
|
+ GROUP BY d.po_bill, d.po_billline
|
|
|
+ ) s ON s.po_bill = p.PurOrd AND s.po_billline = p.Line
|
|
|
LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum AND (im.tenant_id = p.tenant_id OR p.tenant_id IS NULL)
|
|
|
WHERE FIND_IN_SET(CAST(IFNULL(ds.id, p.RecID) AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
|
|
|
AND p.tenant_id = @TenantId
|
|
|
@@ -213,26 +240,35 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
sfpc = 0,
|
|
|
chbg = string.Empty,
|
|
|
pcsm = string.Empty,
|
|
|
- details = rows.Select((r, i) => new
|
|
|
+ details = rows.Select((r, i) =>
|
|
|
{
|
|
|
- id = (long?)null,
|
|
|
- hh = i + 1,
|
|
|
- poBill = r.PoBill,
|
|
|
- poBillLine = r.PoBillLine?.ToString(),
|
|
|
- orderType = r.OrderType,
|
|
|
- shMaterialCode = r.ShMaterialCode,
|
|
|
- shMaterialName = r.ShMaterialName,
|
|
|
- th = r.Th,
|
|
|
- shDeliveryQuantity = r.ShDeliveryQuantity,
|
|
|
- bzsl = r.Bzsl,
|
|
|
- bqsl = r.Bqsl ?? 0,
|
|
|
- shMaterialDw = r.ShMaterialDw,
|
|
|
- scrq = string.Empty,
|
|
|
- scph = string.Empty,
|
|
|
- remarks = string.Empty,
|
|
|
- djsl = r.Djsl,
|
|
|
- jybb = string.Empty,
|
|
|
- jhdbh = r.Jhdbh
|
|
|
+ // 待交数量需扣除已有发货单的累计送货量(含已收货),剩余的才是本次可发数量
|
|
|
+ var djsl = CalcPendingDeliveryQty(
|
|
|
+ r.QtyOrded ?? 0m, r.RctQty ?? 0m, r.ReceiptQty ?? 0m, r.QtyReturned ?? 0m, r.ShippedFhsl ?? 0m);
|
|
|
+ var poPending = (r.QtyOrded ?? 0m) - (r.RctQty ?? 0m) - (r.ReceiptQty ?? 0m) + (r.QtyReturned ?? 0m);
|
|
|
+ var defaultQty = Math.Min(r.SchedQty ?? poPending, djsl);
|
|
|
+ var bqsl = (r.Bzsl ?? 0m) > 0m ? Math.Ceiling(defaultQty / r.Bzsl!.Value) : 0m;
|
|
|
+ return new
|
|
|
+ {
|
|
|
+ id = (long?)null,
|
|
|
+ hh = i + 1,
|
|
|
+ poBill = r.PoBill,
|
|
|
+ poBillLine = r.PoBillLine?.ToString(),
|
|
|
+ orderType = r.OrderType,
|
|
|
+ shMaterialCode = r.ShMaterialCode,
|
|
|
+ shMaterialName = r.ShMaterialName,
|
|
|
+ th = r.Th,
|
|
|
+ shDeliveryQuantity = defaultQty,
|
|
|
+ bzsl = r.Bzsl,
|
|
|
+ bqsl,
|
|
|
+ shMaterialDw = r.ShMaterialDw,
|
|
|
+ scrq = string.Empty,
|
|
|
+ scph = string.Empty,
|
|
|
+ remarks = string.Empty,
|
|
|
+ djsl,
|
|
|
+ jybb = string.Empty,
|
|
|
+ jhdbh = r.Jhdbh
|
|
|
+ };
|
|
|
})
|
|
|
};
|
|
|
}
|
|
|
@@ -247,6 +283,9 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
var userName = _userManager.Account ?? "system";
|
|
|
var shipDate = string.IsNullOrWhiteSpace(input.Jhshrq) ? now.ToString("yyyy-MM-dd") : input.Jhshrq!.Trim();
|
|
|
|
|
|
+ // 送货数量不得超过待交数量(订单数量 - 已有发货单累计送货量 + 退货量;编辑时排除本单自身明细)
|
|
|
+ await ValidateDeliveryQtyAsync(input.Id, input.Details, tenantId);
|
|
|
+
|
|
|
if (input.Id is null or 0)
|
|
|
{
|
|
|
var newId = YitIdHelper.NextId();
|
|
|
@@ -1141,6 +1180,95 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 待交数量:订单数量 - 未删除发货单累计送货量 + 退货量,并以(订单数量 - 已收货 - 已入库 + 退货量)封顶、0 保底。
|
|
|
+ /// 已有发货单(无论是否已收货)的数量一律计入已发,避免“已发货未收货”时待交数量虚高。
|
|
|
+ /// </summary>
|
|
|
+ private static decimal CalcPendingDeliveryQty(decimal qtyOrded, decimal rctQty, decimal receiptQty, decimal qtyReturned, decimal shipped)
|
|
|
+ {
|
|
|
+ var poPending = qtyOrded - rctQty - receiptQty + qtyReturned;
|
|
|
+ var pending = qtyOrded - shipped + qtyReturned;
|
|
|
+ if (pending > poPending)
|
|
|
+ pending = poPending;
|
|
|
+ return pending < 0 ? 0 : pending;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 查询采购订单行当前待交数量;未找到订单行返回 null(调用方跳过校验/沿用旧值)。
|
|
|
+ /// excludeGlid 传当前发货单 id 字符串,编辑场景排除本单自身明细,避免重复扣减。
|
|
|
+ /// </summary>
|
|
|
+ private async Task<decimal?> GetPoRemainQtyAsync(string poBill, int line, string excludeGlid, long tenantId)
|
|
|
+ {
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<PoRemainRow>(
|
|
|
+ """
|
|
|
+ SELECT
|
|
|
+ p.QtyOrded AS QtyOrded,
|
|
|
+ p.RctQty AS RctQty,
|
|
|
+ p.ReceiptQty AS ReceiptQty,
|
|
|
+ p.QtyReturned AS QtyReturned,
|
|
|
+ IFNULL((
|
|
|
+ SELECT SUM(d.sh_delivery_quantity)
|
|
|
+ FROM scm_shdzb d
|
|
|
+ INNER JOIN scm_shd m ON CAST(m.id AS CHAR) = d.glid
|
|
|
+ WHERE d.po_bill = p.PurOrd
|
|
|
+ AND d.po_billline = p.Line
|
|
|
+ AND m.tenant_id = p.tenant_id
|
|
|
+ AND IFNULL(m.state, 1) <> 0
|
|
|
+ AND IFNULL(d.glid, '') <> @ExcludeGlid
|
|
|
+ ), 0) AS Shipped
|
|
|
+ FROM PurOrdDetail p
|
|
|
+ WHERE p.PurOrd = @PoBill
|
|
|
+ AND p.Line = @Line
|
|
|
+ AND p.tenant_id = @TenantId
|
|
|
+ LIMIT 1
|
|
|
+ """,
|
|
|
+ new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@PoBill", poBill),
|
|
|
+ new("@Line", line),
|
|
|
+ new("@ExcludeGlid", excludeGlid),
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ });
|
|
|
+ var row = rows.FirstOrDefault();
|
|
|
+ if (row?.QtyOrded is null)
|
|
|
+ return null;
|
|
|
+
|
|
|
+ return CalcPendingDeliveryQty(
|
|
|
+ row.QtyOrded ?? 0m, row.RctQty ?? 0m, row.ReceiptQty ?? 0m, row.QtyReturned ?? 0m, row.Shipped ?? 0m);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 保存前校验:同一采购订单行的送货数量(含本次表单多行合计)不得超过待交数量,
|
|
|
+ /// 即已有发货单时待交数量 = 订单待交数量 - 发货单数量。
|
|
|
+ /// </summary>
|
|
|
+ private async Task ValidateDeliveryQtyAsync(long? shipmentId, List<SupplierShipmentDetailInput> details, long tenantId)
|
|
|
+ {
|
|
|
+ if (details.Count == 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var excludeGlid = (shipmentId ?? 0) > 0 ? shipmentId!.ToString() : string.Empty;
|
|
|
+ var groups = details
|
|
|
+ .Where(d => !string.IsNullOrWhiteSpace(d.PoBill) && !string.IsNullOrWhiteSpace(d.PoBillLine))
|
|
|
+ .GroupBy(d => new { PoBill = d.PoBill!.Trim(), PoBillLine = d.PoBillLine!.Trim() })
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ foreach (var g in groups)
|
|
|
+ {
|
|
|
+ if (!int.TryParse(g.Key.PoBillLine, out var line))
|
|
|
+ continue;
|
|
|
+ var qty = g.Sum(x => x.ShDeliveryQuantity ?? 0m);
|
|
|
+ if (qty <= 0)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ var remain = await GetPoRemainQtyAsync(g.Key.PoBill, line, excludeGlid, tenantId);
|
|
|
+ if (remain is null)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ if (qty > remain.Value)
|
|
|
+ throw Oops.Oh($"采购订单 {g.Key.PoBill} 第 {line} 行送货数量({qty})超过待交数量({remain.Value}),请调整");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private static string BuildListOrderBy(string? sortField, string? sortOrder)
|
|
|
{
|
|
|
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
|
@@ -1295,13 +1423,25 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
|
|
|
public string? ShMaterialCode { get; set; }
|
|
|
public string? ShMaterialName { get; set; }
|
|
|
public string? Th { get; set; }
|
|
|
- public decimal? ShDeliveryQuantity { get; set; }
|
|
|
public decimal? Bzsl { get; set; }
|
|
|
- public decimal? Bqsl { get; set; }
|
|
|
public string? ShMaterialDw { get; set; }
|
|
|
- public decimal? Djsl { get; set; }
|
|
|
public string? Jhdbh { get; set; }
|
|
|
public string? ShMaterialGgxh { get; set; }
|
|
|
+ public decimal? QtyOrded { get; set; }
|
|
|
+ public decimal? RctQty { get; set; }
|
|
|
+ public decimal? ReceiptQty { get; set; }
|
|
|
+ public decimal? QtyReturned { get; set; }
|
|
|
+ public decimal? SchedQty { get; set; }
|
|
|
+ public decimal? ShippedFhsl { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class PoRemainRow
|
|
|
+ {
|
|
|
+ public decimal? QtyOrded { get; set; }
|
|
|
+ public decimal? RctQty { get; set; }
|
|
|
+ public decimal? ReceiptQty { get; set; }
|
|
|
+ public decimal? QtyReturned { get; set; }
|
|
|
+ public decimal? Shipped { get; set; }
|
|
|
}
|
|
|
|
|
|
private sealed class ShippingNoteMasterRow
|