using Business.Business.Dto; using Business.Core.Utilities; using Business.Dto; using Business.EntityFrameworkCore; using Business.EntityFrameworkCore.SqlRepositories; using Business.Model.MES.IC; using Business.Model.Production; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Volo.Abp.Application.Services; namespace Business.Quartz { /// /// 生产排产服务 /// public class ProductionScheduleAppService : ApplicationService { #region 服务 /// /// 物料 /// private ISqlRepository _itemMaster; /// /// 工单 /// private ISqlRepository _workOrdMaster; /// /// 工单物料明细 /// private ISqlRepository _workOrdDetail; /// /// 工单工艺路线明细 /// private ISqlRepository _workOrdRouting; /// /// 库存主数据 /// private ISqlRepository _invMaster; /// /// 生产线明细 /// private ISqlRepository _prodLineDetail; /// /// 生产周期明细 /// private ISqlRepository _periodSequenceDet; /// /// 排产结果明细 /// private ISqlRepository _scheduleResultOpMaster; /// /// 工作日历数据 /// private ISqlRepository _shopCalendarWorkCtr; /// /// 排产异常记录表 /// private ISqlRepository _scheduleExceptionMaster; /// /// 产线休息时间记录表 /// private ISqlRepository _qualityLineWorkDetail; /// /// 节假日记录表 /// private ISqlRepository _holidayMaster; /// /// 雪花算法 /// SnowFlake help = new SnowFlake(); /// /// 工作日历数据 /// private List calendars; /// /// 产线休息记录数据 /// private List qualityLines; /// /// 节假日记录数据 /// private List holidays; private readonly BusinessDbContext _businessDbContext; #endregion #region 构造函数 /// /// 构造函数 /// public ProductionScheduleAppService( ISqlRepository itemMaster, ISqlRepository workOrdMaster, ISqlRepository workOrdDetail, ISqlRepository workOrdRouting, ISqlRepository prodLineDetail, ISqlRepository periodSequenceDet, ISqlRepository scheduleResultOpMaster, ISqlRepository invMaster, ISqlRepository shopCalendarWorkCtr, ISqlRepository scheduleExceptionMaster, ISqlRepository qualityLineWorkDetail, ISqlRepository holidayMaster, BusinessDbContext businessDbContext ) { _itemMaster= itemMaster; _workOrdMaster= workOrdMaster; _workOrdDetail= workOrdDetail; _workOrdRouting= workOrdRouting; _prodLineDetail = prodLineDetail; _periodSequenceDet = periodSequenceDet; _scheduleResultOpMaster= scheduleResultOpMaster; _invMaster= invMaster; _shopCalendarWorkCtr= shopCalendarWorkCtr; _scheduleExceptionMaster= scheduleExceptionMaster; _qualityLineWorkDetail= qualityLineWorkDetail; _holidayMaster = holidayMaster; _businessDbContext = businessDbContext; } #endregion /// /// 执行生产排产 /// public async void DoExt() { await DoProductShcedule(); } /// /// 生产排产 /// public async Task DoProductShcedule() { //1、获取需要排产的工单:Status为空且IsActive==1 List workOrds = _workOrdMaster.Select(p => string.IsNullOrEmpty(p.Status) && p.IsActive == 1).Result; if (workOrds.Count == 0) { return; } //获取排产工单的最早计划开工日期 DateTime earlist = workOrds.Min(p => p.OrdDate.GetValueOrDefault()).Date; //2、获取数据 //获取工单工艺路径数据 List workOrdRoutings = _workOrdRouting.Select(p => workOrds.Select(m => m.WorkOrd).Contains(p.WorkOrd) && p.Domain == "1001" && p.Status != "C" && p.IsActive == 1).Result; //获取物料对应的生产线信息:物料、工序对应的生产线 List prodLines = _prodLineDetail.Select(p => workOrds.Select(m => m.ItemNum).Contains(p.Part) && p.Domain == "1001" && p.IsActive == 1).Result; //获取生产周期数据 List dbPeriodSequences = _periodSequenceDet.Select(p=> workOrds.Select(m => m.ItemNum).Contains(p.ItemNum) && p.PlanDate >= earlist && p.Domain == "1001" && p.IsActive == 1).Result; //获取当前日期往后的排产记录数据 List dbSchedules = _scheduleResultOpMaster.Select(p => workOrds.Select(m => m.ItemNum).Contains(p.ItemNum) && p.WorkDate >= earlist && p.Domain == "1001").Result; //获取工作日历数据 calendars = _shopCalendarWorkCtr.Select(p=>p.Domain == "1001" && p.IsActive == 1).Result; //获取产线休息记录数据 qualityLines = _qualityLineWorkDetail.Select(p => p.Domain == "1001" && p.IsActive == 1).Result; //获取节假日记录数据 holidays = _holidayMaster.Select(p => p.Domain == "1001" && p.IsActive == 1 && p.Dated >= earlist).Result; //3、排产 //排产异常记录 List scheduleExceptions = new List(); //生产周期 List periodSequenceDtls = new List(); //排产记录表 List scheduleMasters = new List(); foreach (var item in workOrds) { //当前工单的排产计划开始时间:年-月-日 DateTime planStart = item.OrdDate.GetValueOrDefault().Date; //当前工单的对应的产线排产记录 var curSchedules = dbSchedules.Where(p => p.ItemNum == item.ItemNum).ToList(); //工序预处理:确定每层级工序对应的产线 List routingDtos = ProcPretreatment(item, workOrdRoutings.Where(p => p.WorkOrd == item.WorkOrd).ToList(), prodLines, curSchedules); //排产前的数据校验 if (routingDtos.Count() == 0)//没有维护主工序 { //记录排产异常原因 scheduleExceptions.Add(new ScheduleExceptionMaster { RecID = help.NextId(), Domain = "1001", WorkOrd = item.WorkOrd, Remark = "工单的工序数据维护错误", CreatTime = DateTime.Now }); continue; } //校验每层级工序是否都维护了产线 if (routingDtos.Exists(p=> string.IsNullOrEmpty(p.Line))) { //记录排产异常原因 scheduleExceptions.Add(new ScheduleExceptionMaster { RecID = help.NextId(), Domain = "1001", WorkOrd = item.WorkOrd, Remark = "工单的产线数据维护错误", CreatTime = DateTime.Now }); continue; } //校验每个层级是否维护了工作日历 bool flag = false; foreach (var rut in routingDtos) { var lineCals = calendars.Where(p => p.ProdLine == rut.Line).ToList(); if (lineCals.Select(p=>p.WeekDay).Distinct().Count() !=7) { flag = true; break; } } if (flag) { //记录排产异常原因 scheduleExceptions.Add(new ScheduleExceptionMaster { RecID = help.NextId(), Domain = "1001", WorkOrd = item.WorkOrd, Remark = "工单产线的工作日历数据维护错误", CreatTime = DateTime.Now }); continue; } //产线排产 LineSchedule(item, routingDtos.OrderBy(p => p.level).ToList(), periodSequenceDtls, scheduleMasters); //更新工单表 item.Status = "w"; await _workOrdMaster.Update(item); } //记录排产数据 await _businessDbContext.PeriodSequenceDet.BulkInsertAsync(periodSequenceDtls); await _businessDbContext.ScheduleResultOpMaster.BulkInsertAsync(scheduleMasters); await _businessDbContext.ScheduleExceptionMaster.BulkInsertAsync(scheduleExceptions); } /// /// 排产 /// /// 工单 /// 每层级工序对应的产线信息,从小到大排序 /// 生产周期 /// 排产结果 public void LineSchedule(WorkOrdMaster workOrd,List routingDtos,List periodsDet, List scheduleResults) { //生产周期 List curSequences = new List(); //排产明细 List curScheduleRsts = new List(); //产线排产开始时间 List lineStarts = new List(); //循环产线,排产 foreach (var item in routingDtos) { //当前产线的工作日历 var mLCalendars = calendars.Where(p => p.ProdLine == item.Line).ToList(); //当前产线的每天休息时间记录 var mlqtyWorkDtls = qualityLines.Where(p => p.ProdLine == item.Line).ToList(); //产线已排产数量 decimal sumQty = 0m; //产线实际排产开始时间 DateTime workStartTime; if (item.level == 1)//主产线 { workStartTime = DealStartTime(item.StartTime, mLCalendars, mlqtyWorkDtls); } else { //子产线获取实际排产开始日期 //获取父级排产开始时间 DateTime parentStartTime = lineStarts.First(p => p.Op == item.ParentOp).StartTime; workStartTime = DealChildStartTime(parentStartTime, item.SetupTime, mLCalendars, mlqtyWorkDtls); } //记录产线排产开始时间 lineStarts.Add(new LineStartDto { level = item.level, Line = item.Line, Op= item.Op, StartTime = workStartTime }); //排产 while (sumQty < workOrd.QtyOrded) { //获取当天的产能 LineScheduledDto dto = GetScheduledPoint(item, workStartTime, mLCalendars, mlqtyWorkDtls); //判断已排产数量+当天的产能是否超过工单数量 if (sumQty + dto.ProductQty <= workOrd.QtyOrded)//当天的产能需要全部排产 { //记录生产周期 curSequences.Add(new PeriodSequenceDet { Domain = "1001", Line = item.Line, ItemNum = workOrd.ItemNum, PlanDate = workStartTime.Date, Period = 1,//目前只考虑一班制 OrdQty = dto.ProductQty, WorkOrds = workOrd.WorkOrd, IsActive = 1 }); //记录排产记录 curScheduleRsts.Add(new ScheduleResultOpMaster { Domain = "1001", WorkOrd = workOrd.WorkOrd, Line = item.Line, ItemNum = workOrd.ItemNum, Op = item.Op, WorkDate = workStartTime.Date, WorkQty = dto.ProductQty, WorkStartTime = dto.StartTime, WorkEndTime = dto.EndTime, CreatTime = DateTime.Now }); //累计已排产数量 sumQty += dto.ProductQty; //继续排下一个工作日 workStartTime = GetNextWorkDay((int)workStartTime.DayOfWeek, workStartTime, mLCalendars); } else// 最后一天的产能只能占用一部分 { //剩余需要排产的数量 decimal residueQty = workOrd.QtyOrded - sumQty; //剩余数量生产需要时长(分钟) decimal workTime = residueQty / item.Rate * 60; //获取当天的工作时间段 List workPoints = DealWorkDayToLevels(workStartTime, mLCalendars.First(p => p.WeekDay == (int)workStartTime.DayOfWeek), mlqtyWorkDtls); var curPoint = workPoints.First(p => p.StartPoint >= workStartTime && workStartTime <= p.EndPoint); TimeSpan span = curPoint.EndPoint - workStartTime; //当天工作时间段的有效生产时间 decimal effMins = (decimal)span.TotalMinutes; DateTime workEndTime = workStartTime; if (effMins >= workTime)//当前工作时间段即可满足产能 { workEndTime = workStartTime.AddMinutes((double)workTime); } else { //获取后续生产时间段 var nextPoints = workPoints.Where(p => p.Level > curPoint.Level).ToList(); //剩余需要工作时长 decimal nextMins = workTime - effMins; foreach (var p in nextPoints) { if (p.WorkMinutes >= nextMins) { workEndTime = p.StartPoint.AddMinutes((double)nextMins); break; } nextMins -= p.WorkMinutes; } } //记录生产周期 curSequences.Add(new PeriodSequenceDet { Domain = "1001", Line = item.Line, ItemNum = workOrd.ItemNum, PlanDate = workStartTime.Date, Period = 1,//目前只考虑一班制 OrdQty = residueQty, WorkOrds = workOrd.WorkOrd, IsActive = 1 }); //记录排产记录 curScheduleRsts.Add(new ScheduleResultOpMaster { Domain = "1001", WorkOrd = workOrd.WorkOrd, Line = item.Line, ItemNum = workOrd.ItemNum, Op = item.Op, WorkDate = workStartTime.Date, WorkQty = residueQty, WorkStartTime = workStartTime, WorkEndTime = workEndTime, CreatTime = DateTime.Now }); } } } //记录排产结果 periodsDet.AddRange(curSequences); scheduleResults.AddRange(curScheduleRsts); } /// /// 获取产线当天的开工时间,结束时间,有效工作时长,生产数量 /// /// 产线 /// /// /// /// public LineScheduledDto GetScheduledPoint(WorkOrdRoutingDto routingDto,DateTime startTime, List curCalendars, List curQtyDtls) { LineScheduledDto scheduledDto = new LineScheduledDto(); //当天排产开始时间 scheduledDto.StartTime = startTime; //开始时间是周几 int weekDay = (int)startTime.DayOfWeek; //当天的工作日历 var shopCal = curCalendars.Where(p => p.WeekDay == weekDay).First(); //当前日期的工作时间段 List workPoints = DealWorkDayToLevels(startTime, shopCal, curQtyDtls); //当天排产结束时间 scheduledDto.EndTime = workPoints.Last().EndPoint; //计算starttime处于那个工作时间段 var curPoint = workPoints.Where(p => startTime >= p.StartPoint && startTime <= p.EndPoint).First(); TimeSpan span = curPoint.EndPoint - startTime; scheduledDto.EffTime = (decimal)span.TotalHours; //获取后续工作时间段的有效工作时间 var nextPoints = workPoints.Where(p => p.Level > curPoint.Level).ToList(); foreach (var item in nextPoints) { span = item.EndPoint - item.StartPoint; scheduledDto.EffTime += (decimal)span.TotalHours; } //计算当天的产能 scheduledDto.ProductQty = scheduledDto.EffTime * routingDto.Rate; return scheduledDto; } /// /// 计算主产线实际排产开始时间 /// /// 工单排产开始时间 /// 当前产线工作日历 /// 当前产线休息记录 /// public DateTime DealStartTime(DateTime startTime, List curCalendars, List curQtyDtls) { //实际排产开始时间 DateTime actStart = startTime; //开始时间是周几 int weekDay = (int)startTime.DayOfWeek; //当天的工作日历 var shopCal = curCalendars.Where(p => p.WeekDay == weekDay).First(); //当前日期的工作时间段 List workPoints = DealWorkDayToLevels(startTime, shopCal, curQtyDtls); //计算starttime处于那个工作时间段 var curPoint = workPoints.Where(p => startTime >= p.StartPoint && startTime <= p.EndPoint).FirstOrDefault(); if (startTime != curPoint.EndPoint) { return actStart; } //查询下一时间段的开始时间点 var nextPoint = workPoints.Where(p => p.Level == curPoint.Level + 1).FirstOrDefault(); if (nextPoint != null) { return nextPoint.StartPoint; } //开始时间为今天下班时间,实际排产开始时间为下一个工作日的开始时间 actStart = GetNextWorkDay(weekDay, startTime, curCalendars); return actStart; } /// /// 计算子产线实际排产开始时间 /// /// 父级工单排产开始时间 /// 当前产线提前期(小时)-需要提前生产时长,不包括休息时间 /// 当前产线工作日历 /// 当前产线休息记录 /// public DateTime DealChildStartTime(DateTime startTime,decimal setupTime, List curCalendars, List curQtyDtls) { //提前期转换成分钟 decimal needMinute = setupTime * 60; //实际排产开始时间 DateTime actStart = startTime; //开始时间是周几 int weekDay = (int)startTime.DayOfWeek; //当天的工作日历 var shopCal = curCalendars.Where(p => p.WeekDay == weekDay).First(); //当前日期的工作时间段 List workPoints = DealWorkDayToLevels(startTime, shopCal, curQtyDtls); //计算starttime处于那个工作时间段 var curPoint = workPoints.Where(p => startTime >= p.StartPoint && startTime <= p.EndPoint).FirstOrDefault(); //当前时间段可用提前期 TimeSpan span = startTime - curPoint.StartPoint; decimal curMins = (decimal)span.TotalMinutes; if (curMins >= needMinute)//当前时间段的可用提前期满足 { actStart = startTime.AddMinutes((double)-needMinute); return actStart; } //当前时间段的可用提前期不满足 //剩余提前期 needMinute -= curMins; //获取前层级时间段 var prePoints = workPoints.Where(p => p.Level < curPoint.Level).ToList(); foreach (var item in prePoints) { if (item.WorkMinutes >= needMinute)//当前时间段的可用提前期满足 { actStart = item.EndPoint.AddMinutes((double)-needMinute); break; } needMinute -= item.WorkMinutes; } //今天可用提前期不够,往前工作日找 DateTime perStartTime = startTime; bool flag = true;//标志位 while (flag) { //获取前一个工作日 perStartTime = GetPreWorkDay(perStartTime, curCalendars); //获取前一个工作日的工作时间段数据,倒序排 workPoints = DealWorkDayToLevels(perStartTime, shopCal, curQtyDtls).OrderByDescending(p=>p.Level).ToList(); //当天的工作时长(分钟) decimal sumWorkMins = workPoints.Sum(p => p.WorkMinutes); if (sumWorkMins >= needMinute)//当天可用提前期满足 { //获取开始时间 foreach (var item in workPoints) { if (item.WorkMinutes >= needMinute)//当前时间段满足 { actStart = item.EndPoint.AddMinutes((double)-needMinute); break; } needMinute -= item.WorkMinutes; } flag = false; } //当天可用提前期不满足 needMinute -= sumWorkMins; } return actStart; } /// /// 获取下一个工作日开始时间 /// /// 当前周几 /// 开始时间 /// 当前产线的工作日历 /// public DateTime GetNextWorkDay(int weekDay, DateTime startTime, List curCalendars) { DateTime rtnData = startTime; //下一天 DateTime nextDate = startTime.Date.AddDays(1); //下一天是周几 int nextWeekDay = (weekDay + 1) % 7; var calendar = curCalendars.FirstOrDefault(p=>p.WeekDay == nextWeekDay); //判断下一天是否是工作日 if (nextWeekDay == 0 || nextWeekDay == 6)//下一天是周六或者周日,需要判断是否调休,需要加班 { if (!holidays.Exists(p => p.Dated.GetValueOrDefault().Date == nextDate && p.Ufld1 == "调休"))//下一天是周末 { //递归继续找下一个工作日 GetNextWorkDay(nextWeekDay, nextDate, curCalendars); } rtnData = nextDate.AddHours((double)calendar.ShiftsStart1); return rtnData; } //下一天不是周六周日,需要判断是不是节假日 if (holidays.Exists(p => p.Dated.GetValueOrDefault().Date == nextDate && p.Ufld1 == "休假"))//是节假日 { //递归继续找下一个工作日 GetNextWorkDay(nextWeekDay, nextDate, curCalendars); } rtnData = nextDate.AddHours((double)calendar.ShiftsStart1); return rtnData; } /// /// 获取上一个工作日开始时间 /// /// 开始时间 /// 当前产线的工作日历 /// public DateTime GetPreWorkDay(DateTime startTime, List curCalendars) { DateTime rtnData = startTime; //前一天 DateTime preDate = startTime.Date.AddDays(-1); //前一天是周几 int preWeekDay = (int)preDate.DayOfWeek; var calendar = curCalendars.FirstOrDefault(p => p.WeekDay == preWeekDay); //判断前一天是否是工作日 if (preWeekDay == 0 || preWeekDay == 6)//前一天是周六或者周日,需要判断是否调休,需要加班 { if (!holidays.Exists(p => p.Dated.GetValueOrDefault().Date == preDate && p.Ufld1 == "调休"))//前一天是非工作日 { //递归继续找下一个工作日 GetPreWorkDay(preDate, curCalendars); } rtnData = preDate.AddHours((double)calendar.ShiftsStart1); return rtnData; } //前一天不是周六周日,需要判断是不是节假日 if (holidays.Exists(p => p.Dated.GetValueOrDefault().Date == preDate && p.Ufld1 == "休假"))//是节假日 { //递归继续找前一个工作日 GetPreWorkDay(preDate, curCalendars); } rtnData = preDate.AddHours((double)calendar.ShiftsStart1); return rtnData; } /// /// 处理当前日期的工作时间段 /// /// /// 当前产线的工作日历-周几 /// 每天休息记录 /// public List DealWorkDayToLevels(DateTime startTime, ShopCalendarWorkCtr shopCal, List curQtyDtls) { //年-月-日 string date = startTime.Date.ToString("yyyy-MM-dd"); //排产记录结束日期是周几 int weekDay = (int)startTime.DayOfWeek; //计算当天的开工时间点,停工时间点 DateTime dayStartPoint = startTime.Date.AddHours(Convert.ToDouble(shopCal.ShiftsStart1)); DateTime dayEndPoint = dayStartPoint.AddHours(Convert.ToDouble(shopCal.ShiftsHours1)); //工作时间段 List workPoints = new List(); LineWorkPointDto dto = new LineWorkPointDto(); dto.Level = 1; dto.Line = shopCal.ProdLine; dto.WeekDay = weekDay; dto.StartPoint = dayStartPoint; int level = 1; TimeSpan span = TimeSpan.Zero; foreach (var item in curQtyDtls) { DateTime endPoint = Convert.ToDateTime(date + " " + item.RestTimePoint); dto.EndPoint= endPoint; span = dto.EndPoint - dto.StartPoint; dto.WorkMinutes = (decimal)span.TotalMinutes; workPoints.Add(dto); level++; dto = new LineWorkPointDto(); dto.Level = level; dto.Line = shopCal.ProdLine; dto.WeekDay = weekDay; dto.StartPoint = endPoint.AddMinutes(item.RestTime); } dto.EndPoint = dayEndPoint; span = dto.EndPoint - dto.StartPoint; dto.WorkMinutes = (decimal)span.TotalMinutes; workPoints.Add(dto); return workPoints.OrderBy(p => p.Level).ToList(); } /// /// 工单工艺路线预处理 /// /// 工单 /// 当前工单对应的工序 /// 产线 /// 当前工单对应产品的排产记录 /// public List ProcPretreatment(WorkOrdMaster workOrd,List woRuntings, List prodLines, List schedules) { List routingDtos = new List(); //当前工单计划开始时间 DateTime planStart = workOrd.OrdDate.GetValueOrDefault(); //取主工序(第一层级工序) var firsts = woRuntings.Where(p =>p.ParentOp == 0).OrderByDescending(p => p.OP).ToList(); if (firsts.Count() == 0) { return routingDtos; } WorkOrdRoutingDto dto = new WorkOrdRoutingDto(); //主工序按照Op排序,取最大Op var lastOp = firsts.First(); dto.ParentOp = lastOp.ParentOp; dto.level = 1; dto.Op = lastOp.OP; //主工序对应的产线(目前只考虑一个产品对应一条产线的情况) var line = prodLines.Where(p => p.Part == lastOp.ItemNum && p.Op == lastOp.OP).FirstOrDefault(); if (line != null) { dto.Line = line.Line; dto.Rate = line.Rate; dto.SetupTime = 0; //获取产线占用结束时间 var schedule = schedules.Where(p => p.Line == line.Line).OrderByDescending(p => p.WorkEndTime).FirstOrDefault(); dto.StartTime = schedule == null ? planStart : (schedule.WorkEndTime <= planStart ? planStart : schedule.WorkEndTime); } routingDtos.Add(dto); //递归处理其他层级工序 RecursionProc(woRuntings, firsts, 1, routingDtos, prodLines); return routingDtos; } /// /// 递归处理工序 /// /// 工单工序 /// 上-层级工序 /// 层级 /// 返回结果 /// 产线 public void RecursionProc(List woRuntings, List preLevels, int level, List routingDtos, List prodLines) { //获取当前层级工序 var curLevels = woRuntings.Where(p => preLevels.Select(m=>m.OP).Contains(p.ParentOp)).ToList(); if (curLevels.Count() == 0) { return; } //获取父级Op-当前层级有几条子产线 var parentOps = curLevels.Select(m => m.ParentOp).Distinct().ToList(); foreach (var item in parentOps) { var dto = new WorkOrdRoutingDto(); var lastOp = curLevels.Where(p=>p.ParentOp == item).OrderByDescending(m=>m.OP).FirstOrDefault(); if (lastOp == null){ continue; } dto.Op = lastOp.OP; dto.ParentOp = lastOp.ParentOp; dto.level = level + 1; //当前层级工序对应的产线 var maxRateLine = prodLines.Where(p => p.Part == lastOp.ItemNum && p.Op == lastOp.OP).OrderByDescending(p => p.Rate).FirstOrDefault(); if (maxRateLine != null) { dto.Line = maxRateLine.Line; dto.Rate = maxRateLine.Rate; dto.SetupTime = maxRateLine.SetupTime; } routingDtos.Add(dto); } //递归 RecursionProc(woRuntings, curLevels, level + 1, routingDtos,prodLines); } } }