global without sharing class AssetMaintainAutoBatch implements Database.Batchable, Database.Stateful { global Integer allCount = 0; global Integer doneCount = 0; global List errorList = new List(); global Date today = System.today(); global String headerId; global Integer round; /** @description 调试用:传入盘点表头Id或遗失报告Id,得到需要维护的保有设备,无视批准情况 @param headerId 盘点表头Id或遗失报告Id */ global AssetMaintainAutoBatch(String headerId) { this.headerId = headerId; this.round = 4; } /** @description 5工作日前批准的盘点报告和遗失报告 */ global AssetMaintainAutoBatch() { this.headerId = ''; this.round = 4; } global AssetMaintainAutoBatch(String headerId, Integer round){ this.headerId = headerId; this.round = round; } global Iterable start(Database.BatchableContext bc) { List headerIdList = new List(); List invHeaderList = new List(); // 确认传入的Id是否有效,Id无效时返回空列表,execute不会执行 if(String.isNotBlank(this.headerId)){ invHeaderList = [ SELECT Id FROM Inventory_Header__c WHERE Id=:this.headerId ]; if(!invHeaderList.isEmpty()){ headerIdList.add(this.headerId); } return headerIdList; } OlympusCalendar__c oc = [ SELECT Id , Before_5_WorkDay__c , Before_4_WorkDay__c FROM OlympusCalendar__c WHERE Date__c =:today ]; invHeaderList = [ SELECT Id FROM Inventory_Header__c WHERE AutoMaintained__c = false AND Inventory_Header_approval_time__c <: oc.Before_4_WorkDay__c ]; for(Inventory_Header__c header:invHeaderList){ headerIdList.add(header.Id); } return headerIdList; } // schedule里scope设为1,保证是以表头为单位做修改 // Database.executeBatch(new AssetMaintainAutoBatch(), 1); global void execute(Database.BatchableContext BC, List headerIdList) { allCount += headerIdList.size(); Savepoint sp = Database.setSavepoint(); try{ AssetInfoInHeader info = loadAssetIdSet(headerIdList[0]); Map abandondLostMap = info.abandondLostMap; // Map abandondDeficitMap = info.abandondDeficitMap; // Map profitMap = info.profitMap; // Map consumedMap = info.consumedMap; // Set oneToOneAccessoryAssetIdSet = info.oneToOneAccessoryAssetIdSet; //一对一附属品Asset String yearMonthStart = info.yearMonthStart; String yearMonthEnd = info.yearMonthEnd; List assetList = [ SELECT Id , Abandoned_Inventory__c , Appended_Inventory_Frozen_Quantity__c , Appended_Inventory_Profit_Quantity__c , Asset_loaner_category__c , Consumable_Guaranteen_end__c , Consumed_Count__c , Fixture_Model_No_F__c , Fixture_OneToOne_Link__r.Main_Asset__c , Internal_asset_location__c , Internal_Asset_number_key__c , Manage_Type__c , Quantity , Salesdepartment__c , SerialNumber FROM Asset WHERE Id IN:abandondLostMap.keySet() OR Id IN: abandondDeficitMap.keySet() OR Id IN: profitMap.keySet() OR Id IN: consumedMap.keySet() ]; // 维护明细 List amdLostList = new List (); // 废弃(丢失) List amdDeficitList = new List (); //废弃(盘亏) List amdProfitList = new List (); // 盘盈 List amdConsumedList = new List (); // 耗材已消耗数 List updResultList = Database.update(assetList, false); List updateList = new List(); for(Integer i = 0; i < updResultList.size(); i++){ Database.SaveResult sr = updResultList[i]; if(!sr.isSuccess()){ for (Database.Error err : sr.getErrors()) { errorList.add(err.getStatusCode() + ':' + err.getMessage() + err.getFields()); } continue; } Asset ass = assetList[i]; Boolean isOneToOneAccessory = oneToOneAccessoryAssetIdSet.contains(ass.Id); if(abandondLostMap.containsKey(ass.Id)){ AssetMaintainDetail__c amd = createAssetMaintainDetail(ass, isOneToOneAccessory); amd.AbandonCount__c = abandondLostMap.get(ass.Id); amdLostList.add(amd); ass.Quantity = intValueOf(ass.Quantity) - abandondLostMap.get(ass.Id); ass.ChangeQuantityReason__c = '废弃'; ass.Abandoned_Inventory__c = intValueOf(ass.Abandoned_Inventory__c) - abandondLostMap.get(ass.Id); if(ass.Quantity <= 0) { if(ass.Manage_Type__c == '个体管理' || (ass.Consumable_Guaranteen_end__c != null && ass.Consumable_Guaranteen_end__c < today)){ ass.Status = FixtureUtil.assetStatusMap.get(FixtureUtil.AssetStatus.Fei_Qi.ordinal()); } } } if(abandondDeficitMap.containsKey(ass.Id)){ if(String.isBlank(ass.Internal_Asset_number_key__c)){ AssetMaintainDetail__c amd = createAssetMaintainDetail(ass, isOneToOneAccessory); amd.AbandonCount__c = abandondDeficitMap.get(ass.Id); amdDeficitList.add(amd); ass.Quantity = intValueOf(ass.Quantity) - abandondDeficitMap.get(ass.Id); ass.Abandoned_Inventory__c = intValueOf(ass.Abandoned_Inventory__c) - abandondDeficitMap.get(ass.Id); ass.ChangeQuantityReason__c = '盘亏'; } if(ass.Quantity <= 0) { if(ass.Manage_Type__c == '个体管理' || (ass.Consumable_Guaranteen_end__c != null && ass.Consumable_Guaranteen_end__c < today)){ ass.Status = FixtureUtil.assetStatusMap.get(FixtureUtil.AssetStatus.Fei_Qi.ordinal()); } } } if(profitMap.containsKey(ass.Id)){ AssetMaintainDetail__c amd = createAssetMaintainDetail(ass, isOneToOneAccessory); amd.MaintainCount__c = profitMap.get(ass.Id); amdProfitList.add(amd); ass.Quantity = intValueOf(ass.Quantity) + profitMap.get(ass.Id); ass.Appended_Inventory_Profit_Quantity__c = intValueOf(ass.Appended_Inventory_Profit_Quantity__c) - profitMap.get(ass.Id); ass.ChangeQuantityReason__c = '盘盈'; } if(consumedMap.containsKey(ass.Id)){ AssetMaintainDetail__c amd = createAssetMaintainDetail(ass, isOneToOneAccessory); amd.MaintainCount__c = consumedMap.get(ass.Id); amdConsumedList.add(amd); ass.Consumed_Count__c = intValueOf(ass.Consumed_Count__c) - consumedMap.get(ass.Id); } updateList.add(ass); } if(!updateList.isEmpty()){ update updateList; } if(!amdLostList.isEmpty()){ insertAssetMaintainDetails('废弃(丢失)', amdLostList, yearMonthStart, yearMonthEnd); } if(!amdDeficitList.isEmpty()){ insertAssetMaintainDetails('废弃(盘亏)', amdDeficitList, yearMonthStart, yearMonthEnd); } if(!amdProfitList.isEmpty()){ insertAssetMaintainDetails('盘盈(数量管理)', amdProfitList, yearMonthStart, yearMonthEnd); } if(!amdConsumedList.isEmpty()){ insertAssetMaintainDetails('耗材已消耗数', amdConsumedList, yearMonthStart, yearMonthEnd); } updateHeaders(headerIdList[0]); doneCount += headerIdList.size(); } catch (Exception e) { Database.rollback(sp); errorList.add(e.getMessage() + '\n' + e.getStackTraceString()); System.debug(LoggingLevel.ERROR, e.getMessage() + '\n' + e.getStackTraceString()); throw e; } } /** @description 盘点明细和遗失报告明细中取出保有设备 @param headerId 盘点报告或遗失报告的Id @return 表头下保有设备的更新信息 */ private AssetInfoInHeader loadAssetIdSet(Id headerId){ Map abandondLostMap = new Map(); // Map abandondDeficitMap = new Map(); // Map profitMap = new Map(); // Map consumedMap = new Map(); // Set oneToOneAccessoryAssetIdSet = new Set(); //一对一附属品Asset String location = null; Datetime endDate = null; Id recordTypeId = null; List invdList = [SELECT Id , Asset__c , Abandoned_Inventory_Start__c , Inventory_Deviation__c , OneToOne_Accsessary__c , Inventory_Header__r.CreatedDate , Inventory_Header__r.Internal_asset_location__c , Inventory_Header__r.RecordTypeId , Inventory_Count__c FROM Inventory_Detail__c WHERE Inventory_Header__c =:headerId AND (Abandoned_Inventory_Start__c > 0 OR (Inventory_Count__c != null AND Inventory_Deviation__c != 0 ) ) ]; List cinvdList = [SELECT Id , Asset__c , Abandoned_Inventory_Start__c , Consumed_Count_Start__c , Inventory_Header__r.CreatedDate , Inventory_Header__r.Internal_asset_location__c , Inventory_Header__r.RecordTypeId , Inventory_Deviation__c , Inventory_Count__c FROM Consum_Inventory_Detail__c WHERE Inventory_Header__c =:headerId AND (Abandoned_Inventory_Start__c > 0 OR (Inventory_Count__c != null AND Inventory_Deviation__c != 0) OR Consumed_Count_Start__c > 0 ) ]; for(Inventory_Detail__c invd:invdList){ endDate = invd.Inventory_Header__r.CreatedDate; location = invd.Inventory_Header__r.Internal_asset_location__c; recordTypeId = invd.Inventory_Header__r.RecordTypeId; if(invd.Abandoned_Inventory_Start__c > 0) { Integer c = intValueOf(invd.Abandoned_Inventory_Start__c); if(abandondLostMap.containsKey(invd.Asset__c)) { c += abandondLostMap.get(invd.Asset__c); } abandondLostMap.put(invd.Asset__c, c); } if(invd.Inventory_Count__c != null && invd.Inventory_Deviation__c < 0){ Integer c = -intValueOf(invd.Inventory_Deviation__c); if(abandondDeficitMap.containsKey(invd.Asset__c)) { c += abandondDeficitMap.get(invd.Asset__c); } abandondDeficitMap.put(invd.Asset__c, c); } if(invd.Inventory_Count__c != null && invd.Inventory_Deviation__c > 0){ Integer c = intValueOf(invd.Inventory_Deviation__c); if(profitMap.containsKey(invd.Asset__c)) { c += profitMap.get(invd.Asset__c); } profitMap.put(invd.Asset__c, c); } if(invd.OneToOne_Accsessary__c){ oneToOneAccessoryAssetIdSet.add(invd.Asset__c); } } for(Consum_Inventory_Detail__c cinvd:cinvdList){ endDate = cinvd.Inventory_Header__r.CreatedDate; location = cinvd.Inventory_Header__r.Internal_asset_location__c; recordTypeId = cinvd.Inventory_Header__r.RecordTypeId; if(cinvd.Abandoned_Inventory_Start__c > 0){ abandondLostMap.put(cinvd.Asset__c, intValueOf(cinvd.Abandoned_Inventory_Start__c)); } if(cinvd.Inventory_Count__c != null && cinvd.Inventory_Deviation__c < 0){ abandondDeficitMap.put(cinvd.Asset__c, -intValueOf(cinvd.Inventory_Deviation__c)); } if(cinvd.Inventory_Count__c != null && cinvd.Inventory_Deviation__c > 0){ profitMap.put(cinvd.Asset__c, intValueOf(cinvd.Inventory_Deviation__c)); } if(cinvd.Consumed_Count_Start__c > 0){ consumedMap.put(cinvd.Asset__c, intValueOf(cinvd.Consumed_Count_Start__c)); } } // 获取同一存放地,同一记录类型的上一次盘点表头 List lastHeader = [ SELECT CreatedDate FROM Inventory_Header__c WHERE CreatedDate <: endDate AND RecordTypeId =: recordTypeId AND Internal_asset_location__c =: location ORDER BY CreatedDate DESC LIMIT 1 ]; String yearMonthStart = ''; String yearMonthEnd = ''; if(lastHeader.size() == 1){ yearMonthStart = getYearMonthStr(lastHeader[0].CreatedDate); } yearMonthEnd = getYearMonthStr(endDate); AssetInfoInHeader info = new AssetInfoInHeader(); info.abandondLostMap = abandondLostMap; info.abandondDeficitMap = abandondDeficitMap; info.profitMap = profitMap; info.consumedMap = consumedMap; info.oneToOneAccessoryAssetIdSet = oneToOneAccessoryAssetIdSet; info.yearMonthStart = yearMonthStart; info.yearMonthEnd = yearMonthEnd; return info; } private Integer intValueOf(Decimal d) { if(d == null) { return 0; } return Integer.valueOf(d); } /** @description 根据asset造出维护明细 @param ass 保有设备 @param isOneToOneAccessory 是否是一对一附属品,来自盘点明细里的OneToOne_Accsessary__c字段 @return 维护明细 */ private AssetMaintainDetail__c createAssetMaintainDetail(Asset ass, Boolean isOneToOneAccessory){ AssetMaintainDetail__c amd = new AssetMaintainDetail__c(); amd.Asset__c = ass.Id; amd.Internal_asset_location__c = ass.Internal_asset_location__c; amd.Salesdepartment__c = ass.Salesdepartment__c; amd.Fixture_Model_No__c = ass.Fixture_Model_No_F__c; amd.Internal_Asset_number_key__c = ass.Internal_Asset_number_key__c; amd.Manage_type__c = ass.Manage_type__c; amd.SerialNumber__c = ass.SerialNumber; if(isOneToOneAccessory) { amd.Is_OneToOne_Accessory__c = true; amd.OneToOne_Main__c = ass.Fixture_OneToOne_Link__r.Main_Asset__c; } return amd; } /** @description 造出维护表单和维护明细,并更新 @param maintainType 维护类型 @param amdList 维护表明细 @param yearMonthStart 盘点开始日,来自盘点报告的Inventory_Date_From__c @param yearMonthEnd 盘点结束日,来自盘点报告的Inventory_Date_To__c */ private void insertAssetMaintainDetails(String maintainType, List amdList, String yearMonthStart, String yearMonthEnd){ // 插入缺少的维护单表头 List amhList = [ SELECT Id , DetailCount__c FROM AssetMaintainHeader__c WHERE MaintainType__c =: maintainType AND Date__c =: today AND Status__c='已完成' LIMIT 1]; Integer startIndex = 1; if(amhList.isEmpty()){ AssetMaintainHeader__c newAmh = new AssetMaintainHeader__c ( MaintainType__c = maintainType , Date__c = today , Status__c = '已完成'); insert newAmh; amhList.add(newAmh); } else { startIndex = intValueOf(amhList[0].DetailCount__c); } // 插入明细 String maintainReason = getMaintainReason(maintainType, yearMonthStart, yearMonthEnd); Datetime now = System.now(); for (AssetMaintainDetail__c amd: amdList) { amd.AssetMaintainHeader__c = amhList[0].Id; amd.MaintainType__c = maintainType; amd.Batch_Status__c = '完成'; if (maintainType == '废弃(丢失)' || maintainType == '废弃(盘亏)') { amd.AbandonReason__c = maintainReason; amd.AbandonedTime__c = now; } else { amd.MaintainReason__c = maintainReason; amd.MaintainedTime__c = now; } amd.OrderNumber__c = startIndex++; } insert amdList; } private void updateHeaders(Id headerId){ List ihList = [ SELECT Id FROM Inventory_Header__c WHERE Id =:headerId FOR UPDATE ]; List updateList = new List (); for(Inventory_Header__c ih: ihList){ ih.AutoMaintained__c = true; updateList.add(ih); } if(!updateList.isEmpty()){ update updateList; } } /** @description 根据维护类型生成维护原因 @param maintainType 维护类型 @param yearMonthStart 盘点开始日,来自盘点报告的Inventory_Date_From__c @param yearMonthEnd 盘点结束日,来自盘点报告的Inventory_Date_To__c */ private String getMaintainReason(String maintainType, String yearMonthStart, String yearMonthEnd){ String reason = ''; switch on maintainType{ when '废弃(丢失)'{ if(String.isNotBlank(yearMonthStart)){ reason += yearMonthStart + '盘点开始后至'; } reason += yearMonthEnd + '盘点开始前的现场丢失数量'; } when '废弃(盘亏)'{ reason = yearMonthEnd + '盘点盘亏数量'; } when '盘盈(数量管理)'{ reason = yearMonthEnd + '盘点数量管理备品盘盈数量'; } when '耗材已消耗数'{ if(String.isNotBlank(yearMonthStart)){ reason += yearMonthStart + '盘点开始后至'; } reason += yearMonthEnd + '盘点开始前的耗材消耗数量'; } } return reason; } /** @description 日期转成年度月 @param d 日期 */ private String getYearMonthStr(Datetime d){ String res = ''; if(d == null){ return res; } if(d.month() <= 3){ res += (d.year() - 1 - 1867); } else{ res += (d.year() - 1867); } res += 'P' + d.month() + '月'; return res; } global void finish(Database.BatchableContext BC) { this.round -= 1; // 当所有错误都是lock引起时才要重跑 Boolean lockFailure = this.errorList.size() > 0; for(String errorMsg :this.errorList){ if (!errorMsg.contains('Record Currently Unavailable') && !errorMsg.contains('记录当前不可用') && !errorMsg.contains('レコードは現在利用できません')) { lockFailure = false; break; } } if(lockFailure && this.round > 0){ AssetMaintainAutoBatch bat = new AssetMaintainAutoBatch(this.headerId, this.round); Database.executeBatch(bat); } else{ if (allCount != doneCount || this.errorList.size() > 0) { BatchEmailUtil be = new BatchEmailUtil(); String[] toList = new String[]{}; String title = '自动数据维护batch'; String[] ccList = new String[]{}; for (OrgWideEmailAddress tmpEmailObj : [SELECT Id, Address, DisplayName FROM OrgWideEmailAddress WHERE DisplayName like 'BatchNotify']) { toList.add(tmpEmailObj.Address); } for (Consum_Apply_Meta__mdt camd : [SELECT Id , Key__c , ValueLong__c FROM Consum_Apply_Meta__mdt WHERE Package__c = 'AssetMaintainAutoBatch' AND Key__c = 'ErrorMailAddress' ORDER BY Key__c]) { for (String email:camd.ValueLong__c.split(',')) { if (!toList.contains(email)) { toList.add(email); } } } be.failedMail(toList, ccList, title, String.join(this.errorList, '\n'), allCount, doneCount, errorList.size()); be.send(); } } } class AssetInfoInHeader { public Map abandondLostMap = new Map(); // public Map abandondDeficitMap = new Map(); // public Map profitMap = new Map(); // public Map consumedMap = new Map(); // public Set oneToOneAccessoryAssetIdSet = new Set(); //一对一附属品Asset public String yearMonthStart; public String yearMonthEnd; } }