Files
kunshan-bzfm-mes-backend/DOAN.Service/MES/Material/MmRecordInboundService.cs
git_rabbit cc1fe5f967 feat(工单管理): 新增工单物料查询及生产进度功能
新增工单物料服务接口及实现,包括领料清单、成品入库清单和出货清单查询
添加工单生产进度查询功能及相关DTO定义
完善工单修改日志记录功能
规范工单号字段使用,区分原材料工单号和当前工单号
2026-01-30 18:28:21 +08:00

624 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using DOAN.Model.BZFM;
using DOAN.Model.BZFM.Dto;
using DOAN.Model.MES.product;
using DOAN.Repository;
using DOAN.Service.BZFM.IBZFMService;
using Infrastructure.Attribute;
using Infrastructure.Converter;
using Infrastructure.Extensions;
using JinianNet.JNTemplate;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace DOAN.Service.BZFM
{
/// <summary>
/// 入库记录表Service业务层处理
/// </summary>
[AppService(
ServiceType = typeof(IMmRecordInboundService),
ServiceLifetime = LifeTime.Transient
)]
public class MmRecordInboundService : BaseService<MmRecordInbound>, IMmRecordInboundService
{
/// <summary>
/// 查询入库记录表列表
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public PagedInfo<MmRecordInboundDto> GetList(MmRecordInboundQueryDto parm)
{
var predicate = QueryExp(parm);
var response = Queryable()
.Where(predicate.ToExpression())
.OrderByDescending(it => it.CreatedTime)
.ToPage<MmRecordInbound, MmRecordInboundDto>(parm);
return response;
}
/// <summary>
/// 获取详情
/// </summary>
/// <param name="Id"></param>
/// <returns></returns>
public MmRecordInbound GetInfo(int Id)
{
var response = Queryable().Where(x => x.Id == Id).First();
return response;
}
/// <summary>
/// 添加入库记录表
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public MmRecordInbound AddMmRecordInbound(MmRecordInbound model)
{
return Insertable(model).ExecuteReturnEntity();
}
/// <summary>
/// 修改入库记录表
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public int UpdateMmRecordInbound(MmRecordInbound model)
{
return Update(model, true);
}
/// <summary>
/// 查询导出表达式
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
private static Expressionable<MmRecordInbound> QueryExp(MmRecordInboundQueryDto parm)
{
if (parm.CreatedTime != null && parm.CreatedTime.Length > 0)
{
parm.CreatedTime[0] = DOANConvertDate.ConvertLocalDate(parm.CreatedTime[0]);
parm.CreatedTime[1] = DOANConvertDate.ConvertLocalDate(parm.CreatedTime[1]);
parm.CreatedTime[0] = parm.CreatedTime[0].Date;
parm.CreatedTime[1] = parm.CreatedTime[1].Date;
}
var predicate = Expressionable
.Create<MmRecordInbound>()
.AndIF(
!string.IsNullOrEmpty(parm.SupplierCode),
it => it.SupplierCode.Contains(parm.SupplierCode)
)
.AndIF(
!string.IsNullOrEmpty(parm.Operator),
it => it.Operator.Contains(parm.Operator)
)
.AndIF(
!string.IsNullOrEmpty(parm.MaterialCode),
it => it.MaterialCode.Contains(parm.MaterialCode)
)
.AndIF(
!string.IsNullOrEmpty(parm.StoveCode),
it => it.StoveCode.Contains(parm.StoveCode)
)
.AndIF(
!string.IsNullOrEmpty(parm.InboundNo),
it => it.InboundNo.Contains(parm.InboundNo)
)
.AndIF(
!string.IsNullOrEmpty(parm.TransactionType),
it => it.TransactionType.Contains(parm.TransactionType)
)
.AndIF(
parm.CreatedTime != null && parm.CreatedTime[0] > DateTime.MinValue,
it => it.CreatedTime >= parm.CreatedTime[0]
)
.AndIF(
parm.CreatedTime != null && parm.CreatedTime[1] > DateTime.MinValue,
it => it.CreatedTime <= parm.CreatedTime[1]
);
;
return predicate;
}
/// <summary>
/// 导入数据
/// </summary>
/// <param name="formFile"></param>
/// <returns></returns>
public ImportResultDto ImportRecordinbound(IFormFile formFile, string username)
{
string message = "导入成功";
List<MmRecordinboundExcelDto> recordinboundList = new();
using (var stream = formFile.OpenReadStream())
{
try
{
IWorkbook workbook = new XSSFWorkbook(stream);
ISheet sheet = workbook.GetSheetAt(0);
#region excel
// 遍历每一行
for (int row = 1; row <= sheet.LastRowNum; row++)
{
IRow currentRow = sheet.GetRow(row);
if (currentRow != null) // 确保行不为空
{
MmRecordinboundExcelDto recordinbound = new MmRecordinboundExcelDto();
//00 ID
NPOI.SS.UserModel.ICell currentCell_00 = currentRow.GetCell(0);
recordinbound.Id = (int)currentCell_00?.NumericCellValue;
//01 创建时间
NPOI.SS.UserModel.ICell currentCell_01 = currentRow.GetCell(1);
recordinbound.CreatedTime =
currentCell_01?.DateCellValue ?? DateTime.Now;
//02 入库单号
NPOI.SS.UserModel.ICell currentCell_02 = currentRow.GetCell(2);
recordinbound.InboundNo = currentCell_02?.ToString();
if (
currentCell_02 == null
|| string.IsNullOrEmpty(recordinbound.InboundNo)
)
{
message = $"入库单号不可为空,第{row + 1}行";
break;
}
//03 物料编码
NPOI.SS.UserModel.ICell currentCell_03 = currentRow.GetCell(3);
recordinbound.MaterialCode = currentCell_03?.ToString();
if (
currentCell_03 == null
|| string.IsNullOrEmpty(recordinbound.MaterialCode)
)
{
message = $"物料编码不可为空,第{row + 1}行";
break;
}
//04 物料名称
NPOI.SS.UserModel.ICell currentCell_04 = currentRow.GetCell(4);
recordinbound.MaterialName = currentCell_04?.ToString();
if (
currentCell_04 == null
|| string.IsNullOrEmpty(recordinbound.MaterialName)
)
{
message = $"物料名称不可为空,第{row + 1}行";
break;
}
//05 库位编码
NPOI.SS.UserModel.ICell currentCell_05 = currentRow.GetCell(5);
recordinbound.LocationCode = currentCell_05?.ToString();
if (
currentCell_05 == null
|| string.IsNullOrEmpty(recordinbound.LocationCode)
)
{
message = $"库位编码不可为空,第{row + 1}行";
break;
}
//06 库位名称
NPOI.SS.UserModel.ICell currentCell_06 = currentRow.GetCell(6);
recordinbound.LocationName = currentCell_06?.ToString();
if (
currentCell_06 == null
|| string.IsNullOrEmpty(recordinbound.LocationName)
)
{
message = $"库位名称不可为空,第{row + 1}行";
break;
}
//07 入库数量
NPOI.SS.UserModel.ICell currentCell_07 = currentRow.GetCell(7);
string QuantityStr = currentCell_07?.ToString();
if (currentCell_07 == null || string.IsNullOrWhiteSpace(QuantityStr))
{
message = $"入库数量不可为空,第{row + 1}行";
break;
}
// 尝试转换为decimal
if (!decimal.TryParse(QuantityStr, out decimal Quantity))
{
message = $"入库数量格式错误(必须是数字),第{row + 1}行";
break;
}
//验证数值范围(可根据业务需求调整)
if (Quantity < 0)
{
message = $"入库数量不能为负数,第{row + 1}行";
break;
}
recordinbound.Quantity = Quantity;
//08 入库类型
NPOI.SS.UserModel.ICell currentCell_08 = currentRow.GetCell(8);
recordinbound.TransactionType = currentCell_08?.ToString();
if (
currentCell_08 == null
|| string.IsNullOrEmpty(recordinbound.TransactionType)
)
{
message = $"入库类型不可为空,第{row + 1}行";
break;
}
//09 仓库编码
NPOI.SS.UserModel.ICell currentCell_09 = currentRow.GetCell(9);
recordinbound.WarehouseCode = currentCell_09?.ToString();
if (
currentCell_09 == null
|| string.IsNullOrEmpty(recordinbound.WarehouseCode)
)
{
message = $"仓库编码不可为空,第{row + 1}行";
break;
}
//10 批次号
NPOI.SS.UserModel.ICell currentCell_10 = currentRow.GetCell(10);
recordinbound.BatchNo = currentCell_10?.ToString() ?? string.Empty;
//11 操作员
NPOI.SS.UserModel.ICell currentCell_11 = currentRow.GetCell(11);
recordinbound.Operator = currentCell_11?.ToString() ?? string.Empty;
//12 供应商编码
NPOI.SS.UserModel.ICell currentCell_12 = currentRow.GetCell(12);
recordinbound.SupplierCode = currentCell_12?.ToString() ?? string.Empty;
//13 供应商名称
NPOI.SS.UserModel.ICell currentCell_13 = currentRow.GetCell(13);
recordinbound.SupplierName = currentCell_13?.ToString() ?? string.Empty;
recordinboundList.Add(recordinbound);
}
}
#endregion
}
catch (Exception ex)
{
return null;
}
}
// TODO 3.调用SplitInsert方法实现导入操作,注意主键列的配置(建议优化为ID相同则修改不同则新增)
var x = Context
.Storageable(recordinboundList)
//.SplitInsert(it => !it.Any())
//.WhereColumns(it => it.Id) //如果不是主键可以这样实现多字段it=>new{it.x1,it.x2}
.ToStorage();
var result = x.AsInsertable.ExecuteCommand();
var result2 = x.AsUpdateable.IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommand(); //插入可插入部分;
var importResult = new ImportResultDto
{
Message = message,
Inserted = x.InsertList.Count,
Updated = x.UpdateList.Count,
ErrorCount = x.ErrorList.Count,
IgnoredCount = x.IgnoreList.Count,
Deleted = x.DeleteList.Count,
Total = x.TotalList.Count,
};
//输出统计
Console.WriteLine(importResult);
// 4.收集错误与忽略信息返回导入结果ImportResultDto 提示需要修改IServer相关返回格式
foreach (var item in x.ErrorList)
{
importResult.Errors.Add(
new ImportErrorDto
{
MaterialCode = item.Item.MaterialCode,
Message = item.StorageMessage,
}
);
}
foreach (var item in x.IgnoreList)
{
importResult.Ignored.Add(
new ImportErrorDto
{
MaterialCode = item.Item.MaterialCode,
Message = item.StorageMessage,
}
);
}
return importResult;
}
/// <summary>
/// 导出入库记录列表
/// </summary>
/// <returns></returns>
public PagedInfo<MmRecordinboundExcelDto> SelectRecordinboundList(
MmRecordInboundQueryDto recordinbound,
PagerInfo pager
)
{
// Use the same predicate builder as GetList to support consistent filtering
var predicate = QueryExp(recordinbound);
var query = Queryable()
.Where(predicate.ToExpression())
.Select(it => new MmRecordinboundExcelDto
{
Id = it.Id,
CreatedTime = it.CreatedTime,
InboundNo = it.InboundNo,
MaterialCode = it.MaterialCode,
MaterialName = it.MaterialName,
LocationCode = it.LocationCode,
LocationName = it.LocationName,
Quantity = it.Quantity,
TransactionType = it.TransactionType,
WarehouseCode = it.WarehouseCode,
BatchNo = it.BatchNo,
Operator = it.Operator,
SupplierCode = it.SupplierCode,
SupplierName = it.SupplierName,
});
return query.ToPage(pager);
}
/// <summary>
/// 领料接口(临时)
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public string TakeMaterial(TakeMaterialRequestDto parm)
{
try
{
// 初始化领料记录参数
parm = InitializeMaterialTakeRecord(parm);
var inboundInfo = Queryable().Where(x => x.Id == parm.Id).First();
if (inboundInfo == null)
{
return "该入库记录不存在!";
}
if (inboundInfo.Remarks == "已撤销")
{
return "该记录已撤销,无法领料!";
}
if (parm.Quantity > inboundInfo.Quantity)
{
return "领料数量超过记录最大值!";
}
var workorderInfo = Context.Queryable<ProWorkorder>()
.Where(it => it.Workorder == inboundInfo.Workorder)
.First();
if (workorderInfo == null)
{
return "该入库记录对应的工单不存在!";
}
Context.Ado.BeginTran();
// 根据规范WorkorderRaw字段用于永久性记录原材料最初关联的工单号信息保持不变
// 因此这里不应该修改WorkorderRaw而是保持其原值
// 只更新Workorder字段动态记录当前操作环节所对应的工单号
inboundInfo.Workorder = parm.Workorder;
inboundInfo.Remarks += $"[已领料{parm.Quantity}]";
Context.Updateable(inboundInfo).ExecuteCommand();
// 填写出库单
OutboundReceiptDto revokeRecepitDto = new()
{
ReceiptType = 1,
MaterialCode = inboundInfo.MaterialCode,
BatchNo = inboundInfo.BatchNo,
LocationCode = inboundInfo.LocationCode,
WarehouseCode = inboundInfo.WarehouseCode,
OrderNo = workorderInfo.CustomerOrder,
// 原材料工单号:保持原材料最初关联的工单号
Workorder = inboundInfo.WorkorderRaw, // 使用WorkorderRaw作为原材料工单号
// 产成品工单号:动态记录当前操作对应的工单号
WorkorderRaw = parm.Workorder, // 使用传入的Workorder作为产成品工单号
Operator = parm.Operator,
Quantity = parm.Quantity,
TransactionType = "领料出库",
Remarks = $"领料出库,产成品领料出库,原入库单号{inboundInfo.InboundNo},原工单{inboundInfo.Workorder}",
};
MmInventoryService mmInventoryService = new MmInventoryService();
string result = mmInventoryService.CreateOutboundReceipt(revokeRecepitDto);
Context.Ado.CommitTran();
return result;
}
catch (Exception e)
{
Context.Ado.RollbackTran();
return e.Message;
}
}
/// <summary>
/// 初始化领料记录
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public TakeMaterialRequestDto InitializeMaterialTakeRecord(TakeMaterialRequestDto parm)
{
try
{
// 数据验证
if (parm == null)
{
throw new ArgumentNullException(nameof(parm), "领料请求参数不能为空");
}
// 验证必填字段
if (parm.Id <= 0)
{
throw new ArgumentException("记录ID不能为空且必须大于0", nameof(parm.Id));
}
if (string.IsNullOrEmpty(parm.Workorder))
{
throw new ArgumentException("成品工单号不能为空", nameof(parm.Workorder));
}
if (string.IsNullOrEmpty(parm.Operator))
{
throw new ArgumentException("领料人不能为空", nameof(parm.Operator));
}
if (parm.Quantity <= 0)
{
throw new ArgumentException("领料数量必须大于0", nameof(parm.Quantity));
}
// 默认值设置
if (string.IsNullOrEmpty(parm.InboundNo))
{
// 尝试从入库记录中获取入库单号
var inboundInfo = Queryable().Where(x => x.Id == parm.Id).First();
if (inboundInfo != null)
{
parm.InboundNo = inboundInfo.InboundNo;
}
else
{
parm.InboundNo = string.Empty;
}
}
// 确保WorkorderRaw和Workorder字段的正确设置
// 这里Workorder已经通过验证确保不为空
// WorkorderRaw将在TakeMaterial方法中从入库记录中获取并保持不变
return parm;
}
catch (Exception ex)
{
// 异常处理
throw new Exception($"初始化领料记录失败: {ex.Message}", ex);
}
}
/// <summary>
/// 初始化入库记录数据(适用于成品入库和原材料入库)
/// </summary>
/// <param name="inboundRecord">入库记录对象</param>
/// <param name="currentWorkorder">当前操作工单号</param>
/// <param name="rawMaterialWorkorder">原材料工单号(如果有)</param>
/// <returns>初始化后的入库记录对象</returns>
/// <exception cref="ArgumentException">参数验证异常</exception>
public MmRecordInbound InitializeProductStorageRecord(MmRecordInbound inboundRecord, string currentWorkorder, string rawMaterialWorkorder = null)
{
// 参数验证
if (inboundRecord == null)
{
throw new ArgumentNullException(nameof(inboundRecord), "入库记录对象不能为空");
}
if (string.IsNullOrWhiteSpace(currentWorkorder))
{
throw new ArgumentException("当前操作工单号不能为空", nameof(currentWorkorder));
}
if (string.IsNullOrWhiteSpace(inboundRecord.MaterialCode))
{
throw new ArgumentException("物料编码不能为空");
}
if (string.IsNullOrWhiteSpace(inboundRecord.WarehouseCode))
{
throw new ArgumentException("仓库编码不能为空");
}
if (inboundRecord.Quantity <= 0)
{
throw new ArgumentException("入库数量必须大于0");
}
if (string.IsNullOrWhiteSpace(inboundRecord.TransactionType))
{
throw new ArgumentException("入库类型不能为空");
}
// 设置默认值
if (string.IsNullOrWhiteSpace(inboundRecord.InboundNo))
{
// 生成入库单号IN + 年月日时分秒
inboundRecord.InboundNo = "IN" + DateTime.Now.ToString("yyyyMMddHHmmss");
}
if (inboundRecord.CreatedTime == null)
{
inboundRecord.CreatedTime = DateTime.Now;
}
if (string.IsNullOrWhiteSpace(inboundRecord.Unit))
{
inboundRecord.Unit = "个";
}
// 字段使用规范:
// WorkorderRaw永久性记录原材料最初关联的工单号信息保持不变
// Workorder动态记录当前操作环节所对应的工单号根据实际业务操作场景实时更新
if (!string.IsNullOrWhiteSpace(rawMaterialWorkorder))
{
// 如果提供了原材料工单号设置为WorkorderRaw
inboundRecord.WorkorderRaw = rawMaterialWorkorder;
}
else if (string.IsNullOrWhiteSpace(inboundRecord.WorkorderRaw))
{
// 如果没有提供原材料工单号且WorkorderRaw为空则使用当前工单号作为初始值
inboundRecord.WorkorderRaw = currentWorkorder;
}
// 注意如果WorkorderRaw已经有值保持不变确保其在整个生命周期中保持恒定
// 更新当前操作工单号
inboundRecord.Workorder = currentWorkorder;
return inboundRecord;
}
/// <summary>
/// 创建并初始化入库记录
/// </summary>
/// <param name="inboundRecord">入库记录对象</param>
/// <param name="currentWorkorder">当前操作工单号</param>
/// <param name="rawMaterialWorkorder">原材料工单号(如果有)</param>
/// <returns>创建的入库记录对象</returns>
public MmRecordInbound CreateAndInitializeProductStorageRecord(MmRecordInbound inboundRecord, string currentWorkorder, string rawMaterialWorkorder = null)
{
try
{
// 初始化入库记录数据
var initializedRecord = InitializeProductStorageRecord(inboundRecord, currentWorkorder, rawMaterialWorkorder);
// 保存到数据库
return Insertable(initializedRecord).ExecuteReturnEntity();
}
catch (ArgumentException ex)
{
// 处理参数验证异常
throw new Exception($"入库记录初始化失败:{ex.Message}", ex);
}
catch (Exception ex)
{
// 处理其他异常
throw new Exception($"创建入库记录失败:{ex.Message}", ex);
}
}
}
}