global without sharing class AssetMaintainAutoBatch implements Database.Batchable<Id>, Database.Stateful {
|
global Integer allCount = 0;
|
global Integer doneCount = 0;
|
global List<String> errorList = new List<String>();
|
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<Id> start(Database.BatchableContext bc) {
|
List<Id> headerIdList = new List<Id>();
|
List<Inventory_Header__c> invHeaderList = new List<Inventory_Header__c>();
|
// 确认传入的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<Id> headerIdList) {
|
allCount += headerIdList.size();
|
Savepoint sp = Database.setSavepoint();
|
try{
|
AssetInfoInHeader info = loadAssetIdSet(headerIdList[0]);
|
Map<Id, Integer> abandondLostMap = info.abandondLostMap; //<AssetId, 丢失数>
|
Map<Id, Integer> abandondDeficitMap = info.abandondDeficitMap; //<AssetId, 盘亏数>
|
Map<Id, Integer> profitMap = info.profitMap; //<AssetId, 盘盈数>
|
Map<Id, Integer> consumedMap = info.consumedMap; //<AssetId, 已消耗数>
|
Set<Id> oneToOneAccessoryAssetIdSet = info.oneToOneAccessoryAssetIdSet; //一对一附属品Asset
|
String yearMonthStart = info.yearMonthStart;
|
String yearMonthEnd = info.yearMonthEnd;
|
List<Asset> 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<AssetMaintainDetail__c> amdLostList = new List<AssetMaintainDetail__c> (); // 废弃(丢失)
|
List<AssetMaintainDetail__c> amdDeficitList = new List<AssetMaintainDetail__c> (); //废弃(盘亏)
|
List<AssetMaintainDetail__c> amdProfitList = new List<AssetMaintainDetail__c> (); // 盘盈
|
List<AssetMaintainDetail__c> amdConsumedList = new List<AssetMaintainDetail__c> (); // 耗材已消耗数
|
|
List<Database.SaveResult> updResultList = Database.update(assetList, false);
|
List<Asset> updateList = new List<Asset>();
|
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<Id, Integer> abandondLostMap = new Map<Id, Integer>(); //<AssetId, 丢失数>
|
Map<Id, Integer> abandondDeficitMap = new Map<Id, Integer>(); //<AssetId, 盘亏数>
|
Map<Id, Integer> profitMap = new Map<Id, Integer>(); //<AssetId, 盘盈数>
|
Map<Id, Integer> consumedMap = new Map<Id, Integer>(); //<AssetId, 已消耗数>
|
Set<Id> oneToOneAccessoryAssetIdSet = new Set<Id>(); //一对一附属品Asset
|
String location = null;
|
Datetime endDate = null;
|
Id recordTypeId = null;
|
|
List<Inventory_Detail__c> 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<Consum_Inventory_Detail__c> 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<Inventory_Header__c> 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<AssetMaintainDetail__c> amdList,
|
String yearMonthStart, String yearMonthEnd){
|
|
// 插入缺少的维护单表头
|
List<AssetMaintainHeader__c> 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<Inventory_Header__c> ihList = [
|
SELECT Id
|
FROM Inventory_Header__c
|
WHERE Id =:headerId
|
FOR UPDATE
|
];
|
List<Inventory_Header__c> updateList = new List <Inventory_Header__c>();
|
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<Id, Integer> abandondLostMap = new Map<Id, Integer>(); //<AssetId, 丢失数>
|
public Map<Id, Integer> abandondDeficitMap = new Map<Id, Integer>(); //<AssetId, 盘亏数>
|
public Map<Id, Integer> profitMap = new Map<Id, Integer>(); //<AssetId, 盘盈数>
|
public Map<Id, Integer> consumedMap = new Map<Id, Integer>(); //<AssetId, 已消耗数>
|
public Set<Id> oneToOneAccessoryAssetIdSet = new Set<Id>(); //一对一附属品Asset
|
public String yearMonthStart;
|
public String yearMonthEnd;
|
}
|
}
|