feat: 对账件重尺导入与应收只留一套
All checks were successful
Build and Deploy / deploy (push) Successful in 1m45s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m45s
新增 downstream 重量导入并锁定渠道重算应付;重算应收清空全部阶段;费用软删与查询过滤,避免预报入库叠算。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
43fcb4136a
commit
87abcecb65
@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺(downstream)Excel 导入入参。
|
||||
/// </summary>
|
||||
public class OrderDownstreamWeightImportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Excel 文件。
|
||||
/// </summary>
|
||||
public IFormFile File { get; set; } = null!;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺导入结果。
|
||||
/// </summary>
|
||||
public class OrderDownstreamWeightImportOutput
|
||||
{
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
public int FailCount { get; set; }
|
||||
|
||||
public List<OrderDownstreamWeightImportFailRow> Failures { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入失败行。
|
||||
/// </summary>
|
||||
public class OrderDownstreamWeightImportFailRow
|
||||
{
|
||||
public int RowNumber { get; set; }
|
||||
|
||||
public string? WaybillNo { get; set; }
|
||||
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
@ -256,11 +256,85 @@ public partial class OrderService
|
||||
public async Task<OrderOutput> RerunInboundBillingAsync(OrderActionInput input)
|
||||
{
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
EnsureReceivableMutable(order);
|
||||
await ProcessInboundBillingForBatchAsync(order.Id);
|
||||
order = await GetOrderOrThrowAsync(input.Id);
|
||||
return (await MapOrderOutputsAsync([order])).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载对账件重尺(downstream)导入模板。导入后只重算应付,不改应收。
|
||||
/// </summary>
|
||||
[HttpGet("/order/weight/downstream-import-template")]
|
||||
public async Task<FileContentResult> DownloadDownstreamWeightImportTemplateAsync()
|
||||
{
|
||||
var bytes = await OrderDownstreamWeightImportHelper.BuildTemplateBytesAsync();
|
||||
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
{
|
||||
FileDownloadName = OrderDownstreamWeightImportHelper.TemplateFileName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入对账件重尺:写入 Type=downstream,锁定当前渠道重算应付;不改应收。
|
||||
/// </summary>
|
||||
[HttpPost("/order/weight/downstream-import")]
|
||||
public async Task<OrderDownstreamWeightImportOutput> ImportDownstreamWeightsAsync(
|
||||
[FromForm] OrderDownstreamWeightImportInput input)
|
||||
{
|
||||
if (input.File == null || input.File.Length == 0)
|
||||
{
|
||||
throw new BusinessException("请上传 Excel 文件");
|
||||
}
|
||||
|
||||
await using var stream = input.File.OpenReadStream();
|
||||
List<(int RowNumber, OrderDownstreamWeightImportRow Row)> parsedRows;
|
||||
try
|
||||
{
|
||||
parsedRows = await OrderDownstreamWeightImportHelper.ParseImportRowsAsync(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new BusinessException($"Excel 解析失败:{ex.Message}");
|
||||
}
|
||||
|
||||
if (parsedRows.Count == 0)
|
||||
{
|
||||
throw new BusinessException("Excel 中无有效数据行");
|
||||
}
|
||||
|
||||
if (parsedRows.Count > OrderDownstreamWeightImportHelper.MaxRows)
|
||||
{
|
||||
throw new BusinessException(
|
||||
$"单次最多导入 {OrderDownstreamWeightImportHelper.MaxRows} 行,当前 {parsedRows.Count} 行");
|
||||
}
|
||||
|
||||
var output = new OrderDownstreamWeightImportOutput();
|
||||
foreach (var (rowNumber, row) in parsedRows)
|
||||
{
|
||||
try
|
||||
{
|
||||
OrderDownstreamWeightImportHelper.ValidateRow(row);
|
||||
await ApplyDownstreamWeightAndRecalculatePayableAsync(row);
|
||||
output.SuccessCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
output.FailCount++;
|
||||
output.Failures.Add(new OrderDownstreamWeightImportFailRow
|
||||
{
|
||||
RowNumber = rowNumber,
|
||||
WaybillNo = row.WaybillNo,
|
||||
Reason = ex is BusinessException or InvalidOperationException
|
||||
? ex.Message
|
||||
: $"导入失败:{ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询排货过程:优先返回快照;无快照或 live=true 时按当前规则现算(不落库)。
|
||||
/// </summary>
|
||||
@ -701,7 +775,121 @@ public partial class OrderService
|
||||
|
||||
private async Task RecalculatePayableCoreAsync(Order order)
|
||||
{
|
||||
await ReallocateCoreAsync(order, scheme: null, serviceChannelId: null, enforceReallocateGate: false);
|
||||
// 已有渠道时锁定渠道只重算金额,避免「重算应付」误换渠道清下游单号。
|
||||
await ReallocateCoreAsync(
|
||||
order,
|
||||
scheme: null,
|
||||
serviceChannelId: null,
|
||||
enforceReallocateGate: false,
|
||||
lockCurrentChannel: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入对账件重尺并锁定当前渠道重算应付(不改应收)。
|
||||
/// </summary>
|
||||
private async Task ApplyDownstreamWeightAndRecalculatePayableAsync(OrderDownstreamWeightImportRow row)
|
||||
{
|
||||
var order = await OrderRepository.GetByWaybillNoAsync(row.WaybillNo!.Trim());
|
||||
if (order == null)
|
||||
{
|
||||
throw new BusinessException("未找到对应订单,请检查单号");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ChannelCode))
|
||||
{
|
||||
throw new BusinessException("订单未确认服务渠道,无法按对账重量重算应付");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定销售产品,无法重算应付");
|
||||
}
|
||||
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product == null)
|
||||
{
|
||||
throw new BusinessException("销售产品不存在");
|
||||
}
|
||||
|
||||
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
var downstream = weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, OrderWeightTypeCodes.Downstream, StringComparison.OrdinalIgnoreCase));
|
||||
if (downstream == null)
|
||||
{
|
||||
throw new BusinessException("订单缺少下游件重尺记录");
|
||||
}
|
||||
|
||||
// 尺寸可选:未填则沿用下游已有,仍缺则回落收货→发货→预报。
|
||||
var length = row.Length ?? downstream.Length;
|
||||
var width = row.Width ?? downstream.Width;
|
||||
var height = row.Height ?? downstream.Height;
|
||||
if (length is not > 0 || width is not > 0 || height is not > 0)
|
||||
{
|
||||
FillDimensionsFromFallback(weights, ref length, ref width, ref height);
|
||||
}
|
||||
|
||||
if (length is not > 0 || width is not > 0 || height is not > 0)
|
||||
{
|
||||
throw new BusinessException("件重尺尺寸不完整,请在 Excel 填写长/宽/高,或先完善订单收货/发货尺寸");
|
||||
}
|
||||
|
||||
ApplyMeasuredDimensions(downstream, length.Value, width.Value, height.Value, row.ActualWeight!.Value);
|
||||
|
||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, downstream);
|
||||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||||
|
||||
await ReallocateCoreAsync(
|
||||
order,
|
||||
scheme: null,
|
||||
serviceChannelId: null,
|
||||
enforceReallocateGate: false,
|
||||
lockCurrentChannel: true,
|
||||
preferDownstreamWeight: true);
|
||||
}
|
||||
|
||||
private static void FillDimensionsFromFallback(
|
||||
IReadOnlyList<OrderWeight> weights,
|
||||
ref decimal? length,
|
||||
ref decimal? width,
|
||||
ref decimal? height)
|
||||
{
|
||||
foreach (var type in new[]
|
||||
{
|
||||
OrderWeightTypeCodes.Receive,
|
||||
OrderWeightTypeCodes.Ship,
|
||||
OrderWeightTypeCodes.Forecast
|
||||
})
|
||||
{
|
||||
var src = weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
|
||||
if (src == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (length is not > 0)
|
||||
{
|
||||
length = src.Length;
|
||||
}
|
||||
|
||||
if (width is not > 0)
|
||||
{
|
||||
width = src.Width;
|
||||
}
|
||||
|
||||
if (height is not > 0)
|
||||
{
|
||||
height = src.Height;
|
||||
}
|
||||
|
||||
if (length is > 0 && width is > 0 && height is > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -711,7 +899,9 @@ public partial class OrderService
|
||||
Order order,
|
||||
string? scheme,
|
||||
int? serviceChannelId,
|
||||
bool enforceReallocateGate = true)
|
||||
bool enforceReallocateGate = true,
|
||||
bool lockCurrentChannel = false,
|
||||
bool preferDownstreamWeight = false)
|
||||
{
|
||||
string? schemeOverride = null;
|
||||
int? forceChannelId = null;
|
||||
@ -723,6 +913,18 @@ public partial class OrderService
|
||||
? AllocationSchemeCodes.Designated
|
||||
: normalizedScheme;
|
||||
}
|
||||
else if (lockCurrentChannel && !string.IsNullOrWhiteSpace(order.ChannelCode))
|
||||
{
|
||||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x =>
|
||||
x.ChannelCode == order.ChannelCode.Trim());
|
||||
if (channel == null)
|
||||
{
|
||||
throw new BusinessException("订单服务渠道不存在,无法重算应付");
|
||||
}
|
||||
|
||||
forceChannelId = channel.Id;
|
||||
schemeOverride = AllocationSchemeCodes.Designated;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
@ -736,13 +938,18 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
||||
var weightRow = ResolvePayableWeightRow(order, weights);
|
||||
var weightRow = preferDownstreamWeight
|
||||
? weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, OrderWeightTypeCodes.Downstream, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new BusinessException("缺少件重尺类型 downstream")
|
||||
: ResolvePayableWeightRow(order, weights);
|
||||
if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0)
|
||||
{
|
||||
throw new BusinessException("计费重无效,请先完善件重尺");
|
||||
}
|
||||
|
||||
// 重新排货:产品规则/分区与渠道买价均取当前有效发布,避免沿用下单时旧快照。
|
||||
// 对账重算 / 普通重算应付:沿用订单快照绑定(分区等),买价仍由排货评估取当前有效发布。
|
||||
var snapshotBinding = enforceReallocateGate
|
||||
? await ResolveSnapshotBindingAsync(product, DateTime.Now)
|
||||
: await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
@ -753,6 +960,11 @@ public partial class OrderService
|
||||
snapshotBinding);
|
||||
|
||||
var (_, chargingStage) = ResolveReceivableBillingStage(order);
|
||||
// 对账件重尺导入:费用阶段记 outbound(终稿成本),与入库应收阶段解耦。
|
||||
if (preferDownstreamWeight)
|
||||
{
|
||||
chargingStage = OrderFeeChargingStageCodes.Outbound;
|
||||
}
|
||||
|
||||
await ConfirmServiceChannelAndPayableAsync(
|
||||
order,
|
||||
@ -823,11 +1035,12 @@ public partial class OrderService
|
||||
var weightType = chargingStage switch
|
||||
{
|
||||
OrderFeeChargingStageCodes.Inbound => OrderWeightTypeCodes.Receive,
|
||||
_ => OrderWeightTypeCodes.Downstream
|
||||
OrderFeeChargingStageCodes.Outbound => OrderWeightTypeCodes.Ship,
|
||||
_ => OrderWeightTypeCodes.Forecast
|
||||
};
|
||||
|
||||
return weights.FirstOrDefault(x => x.Type == weightType)
|
||||
?? weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Downstream)
|
||||
return weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, weightType, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new BusinessException($"缺少件重尺类型 {weightType}");
|
||||
}
|
||||
|
||||
|
||||
@ -774,7 +774,8 @@ public partial class OrderService
|
||||
{
|
||||
if (replaceExistingReceivable)
|
||||
{
|
||||
await OrderRepository.DeleteReceivableFeesByStageAsync(order.Id, chargingStage);
|
||||
// 有效应收只保留一套:清空全部阶段后再写入当前阶段,避免预报+入库叠算。
|
||||
await OrderRepository.DeleteReceivableFeesAsync(order.Id);
|
||||
}
|
||||
|
||||
await OrderRepository.UpsertFeesAsync(order.Id, fees);
|
||||
|
||||
251
src/Oms.Application/Shared/OrderDownstreamWeightImportHelper.cs
Normal file
251
src/Oms.Application/Shared/OrderDownstreamWeightImportHelper.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using System.Globalization;
|
||||
using MiniExcelLibs;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺(downstream)Excel 模板与解析。
|
||||
/// 仅写入下游对账重量,触发应付重算;不改应收。
|
||||
/// </summary>
|
||||
public static class OrderDownstreamWeightImportHelper
|
||||
{
|
||||
public const int MaxRows = 2000;
|
||||
|
||||
private static readonly Dictionary<string, string> HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["运单号"] = "WaybillNo",
|
||||
["单号"] = "WaybillNo",
|
||||
["服务商单号"] = "WaybillNo",
|
||||
["WaybillNo"] = "WaybillNo",
|
||||
["waybillNo"] = "WaybillNo",
|
||||
["ServiceNo"] = "WaybillNo",
|
||||
["serviceNo"] = "WaybillNo",
|
||||
["长"] = "Length",
|
||||
["长度"] = "Length",
|
||||
["Length"] = "Length",
|
||||
["length"] = "Length",
|
||||
["宽"] = "Width",
|
||||
["宽度"] = "Width",
|
||||
["Width"] = "Width",
|
||||
["width"] = "Width",
|
||||
["高"] = "Height",
|
||||
["高度"] = "Height",
|
||||
["Height"] = "Height",
|
||||
["height"] = "Height",
|
||||
["实重"] = "ActualWeight",
|
||||
["重量"] = "ActualWeight",
|
||||
["ActualWeight"] = "ActualWeight",
|
||||
["actualWeight"] = "ActualWeight",
|
||||
["Weight"] = "ActualWeight",
|
||||
["weight"] = "ActualWeight",
|
||||
};
|
||||
|
||||
public static async Task<byte[]> BuildTemplateBytesAsync()
|
||||
{
|
||||
var rows = new List<Dictionary<string, object?>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
["运单号"] = "OMS202601010001",
|
||||
["长"] = 30,
|
||||
["宽"] = 20,
|
||||
["高"] = 10,
|
||||
["实重"] = 1.25
|
||||
}
|
||||
};
|
||||
|
||||
await using var stream = new MemoryStream();
|
||||
await MiniExcel.SaveAsAsync(stream, rows);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public static string TemplateFileName => "对账件重尺导入模板.xlsx";
|
||||
|
||||
public static async Task<List<(int RowNumber, OrderDownstreamWeightImportRow Row)>> ParseImportRowsAsync(
|
||||
Stream fileStream)
|
||||
{
|
||||
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true);
|
||||
var result = new List<(int RowNumber, OrderDownstreamWeightImportRow Row)>();
|
||||
var rowNumber = 1;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
rowNumber++;
|
||||
if (row is not IDictionary<string, object> rowDict)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsEmptyRow(rowDict))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.Add((rowNumber, ParseRow(rowDict)));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
result.Add((rowNumber, new OrderDownstreamWeightImportRow
|
||||
{
|
||||
WaybillNo = TryPeekWaybillNo(rowDict),
|
||||
ParseError = ex.Message
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void ValidateRow(OrderDownstreamWeightImportRow row)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(row.ParseError))
|
||||
{
|
||||
throw new InvalidOperationException(row.ParseError);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(row.WaybillNo))
|
||||
{
|
||||
throw new InvalidOperationException("运单号不能为空");
|
||||
}
|
||||
|
||||
row.WaybillNo = row.WaybillNo.Trim();
|
||||
if (row.WaybillNo.Length > 64)
|
||||
{
|
||||
throw new InvalidOperationException("运单号不可超过64个字符");
|
||||
}
|
||||
|
||||
if (!row.ActualWeight.HasValue || row.ActualWeight.Value <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("实重必须大于0");
|
||||
}
|
||||
|
||||
ValidateOptionalDimension(row.Length, "长");
|
||||
ValidateOptionalDimension(row.Width, "宽");
|
||||
ValidateOptionalDimension(row.Height, "高");
|
||||
|
||||
var hasAnyDim = row.Length.HasValue || row.Width.HasValue || row.Height.HasValue;
|
||||
var hasAllDim = row.Length.HasValue && row.Width.HasValue && row.Height.HasValue;
|
||||
if (hasAnyDim && !hasAllDim)
|
||||
{
|
||||
throw new InvalidOperationException("长/宽/高需同时填写或全部留空(留空则沿用订单已有尺寸)");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryPeekWaybillNo(IDictionary<string, object> rowDict)
|
||||
{
|
||||
foreach (var (header, value) in rowDict)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HeaderAliases.TryGetValue(header.Trim(), out var field)
|
||||
&& string.Equals(field, "WaybillNo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NormalizeCell(value);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ValidateOptionalDimension(decimal? value, string label)
|
||||
{
|
||||
if (!value.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Value <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{label}必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private static OrderDownstreamWeightImportRow ParseRow(IDictionary<string, object> rowDict)
|
||||
{
|
||||
var mapped = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (header, value) in rowDict)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!HeaderAliases.TryGetValue(header.Trim(), out var field))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
mapped[field] = NormalizeCell(value);
|
||||
}
|
||||
|
||||
return new OrderDownstreamWeightImportRow
|
||||
{
|
||||
WaybillNo = mapped.GetValueOrDefault("WaybillNo"),
|
||||
Length = ParseDecimal(mapped.GetValueOrDefault("Length")),
|
||||
Width = ParseDecimal(mapped.GetValueOrDefault("Width")),
|
||||
Height = ParseDecimal(mapped.GetValueOrDefault("Height")),
|
||||
ActualWeight = ParseDecimal(mapped.GetValueOrDefault("ActualWeight"))
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal? ParseDecimal(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("zh-CN"), out value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"数值格式无效:{text}");
|
||||
}
|
||||
|
||||
private static string? NormalizeCell(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = Convert.ToString(value, CultureInfo.InvariantCulture)?.Trim();
|
||||
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||||
}
|
||||
|
||||
private static bool IsEmptyRow(IDictionary<string, object> rowDict) =>
|
||||
rowDict.Values.All(value => string.IsNullOrWhiteSpace(NormalizeCell(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺导入行。
|
||||
/// </summary>
|
||||
public sealed class OrderDownstreamWeightImportRow
|
||||
{
|
||||
public string? WaybillNo { get; set; }
|
||||
|
||||
public decimal? Length { get; set; }
|
||||
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
public decimal? Height { get; set; }
|
||||
|
||||
public decimal? ActualWeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Excel 单元格解析错误(校验阶段直接抛出)。
|
||||
/// </summary>
|
||||
public string? ParseError { get; set; }
|
||||
}
|
||||
@ -996,6 +996,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
fee.OrderId = orderId;
|
||||
var existingList = await db.Queryable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& !x.IsDeleted
|
||||
&& x.Type == fee.Type
|
||||
&& x.CostItem == fee.CostItem
|
||||
&& x.ChargingStage == fee.ChargingStage)
|
||||
@ -1004,6 +1005,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
var existing = existingList.FirstOrDefault();
|
||||
if (existing == null)
|
||||
{
|
||||
fee.IsDeleted = false;
|
||||
await db.Insertable(fee).ExecuteCommandAsync(cancellationToken);
|
||||
continue;
|
||||
}
|
||||
@ -1013,6 +1015,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
existing.PriceNo = fee.PriceNo;
|
||||
existing.PriceEffectiveTime = fee.PriceEffectiveTime;
|
||||
existing.DownstreamProvider = fee.DownstreamProvider;
|
||||
existing.IsDeleted = false;
|
||||
existing.UpdateTime = DateTime.Now;
|
||||
await db.Updateable(existing).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
@ -1021,36 +1024,46 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
public async Task DeleteReceivableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId && x.Type == OrderFeeTypeCodes.Receivable)
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Receivable
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteReceivableFeesByStageAsync(int orderId, string chargingStage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Receivable
|
||||
&& x.ChargingStage == chargingStage)
|
||||
&& x.ChargingStage == chargingStage
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePayableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId && x.Type == OrderFeeTypeCodes.Payable)
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Payable
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePayableFeesByStageAsync(int orderId, string chargingStage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Payable
|
||||
&& x.ChargingStage == chargingStage)
|
||||
&& x.ChargingStage == chargingStage
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@ -1078,7 +1091,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
return await db.Queryable<OrderFee>()
|
||||
.Where(x => ids.Contains(x.OrderId))
|
||||
.Where(x => ids.Contains(x.OrderId) && !x.IsDeleted)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user