人力资源
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

586 lines
29 KiB

const schedule = require('node-schedule');
const moment = require('moment')
module.exports = function (app, opts) {
const updateAttendance = app.fs.scheduleInit(
// 妥妥流水账 (*^▽^*)
{
// interval: '34 21 4 * * *',
interval: '34 */15 * * * *',
immediate: true,
proRun: true,
},
async () => {
console.log('假勤数据更新 ', moment().format('YYYY-MM-DD HH:mm:ss'));
const { workFlow: { processState } } = opts
try {
const startTime = moment()
const { models } = app.fs.dc
const { judgeHoliday } = app.fs.utils
const { clickHouse } = app.fs
const { database: camWorkflow } = clickHouse.camWorkflow.opts.config
const { } = app.fs.utils
let overtimeNeedData = {
reason: {
keyWord: ['加班事由']
},
begainTime: {
keyWord: ['加班开始时间'],
require: true,
},
endTime: {
keyWord: ['加班结束时间'],
require: true,
},
duration: {
keyWord: ['总时长(小时)']
},
compensate: {
keyWord: ['加班补偿'],
require: true,
},
hrAffirmDuration: {
keyWord: ['人事核定加班小时', '核定加班小时'],
require: true,
},
}
let vacateNeedData = {
reason: {
keyWord: ['请假事由']
},
begainTime: {
keyWord: ['请假开始时间', '请假起始时间'],
keys: ['leaveStartTime'],
require: true,
},
endTime: {
keyWord: ['请假结束时间', '请假终止时间'],
keys: ['leaveEndTime'],
require: true,
},
type: {
keyWord: ['请假类别', '请假类型'],
keys: ['leaveType'],
require: true,
},
hrAffirmType: {
keyWord: ['核定请假类别'],
},
duration: {
keyWord: ['请假时长(小时)', '请假小时'],
keys: ['leaveTime']
},
hrAffirmDuration: {
keyWord: ['核定请假小时'],
},
}
const schemaRecursionObj = ({
jsonSchema, keyWord, targetKeys = [], schemaPath, require
}) => {
let schemaPath_ = JSON.parse(JSON.stringify(schemaPath))
if (jsonSchema.properties) {
for (let prKey in jsonSchema.properties) {
if (
keyWord.includes(jsonSchema.properties[prKey].title)
|| targetKeys.includes(prKey)
) {
schemaPath_.push({
prKey,
...jsonSchema.properties[prKey]
})
return schemaPath_
} else if (jsonSchema.properties[prKey].properties) {
schemaPath_.push({
prKey,
...jsonSchema.properties[prKey]
})
schemaPath_ = schemaRecursionObj({
jsonSchema: jsonSchema.properties[prKey],
keyWord,
targetKeys,
schemaPath: schemaPath_,
require,
})
if (!schemaPath_.length && require) {
console.warn('数据字段查找错误:', jsonSchema.properties);
}
if (schemaPath_.length > schemaPath.length) {
return schemaPath_
}
}
}
} else {
return schemaPath_
}
}
const dataRecursionObj = (dataObj, index, needData, lastKeyObj, nd) => {
const keyObj = needData[nd].schemaPath[index]
if (dataObj.hasOwnProperty(keyObj.prKey)) {
if (lastKeyObj.prKey == keyObj.prKey) {
let gotValue = dataObj[keyObj.prKey]
if (keyObj.enum) {
let vIndex = keyObj.enum.findIndex(ke => ke == gotValue)
gotValue = keyObj.enumNames[vIndex]
}
return gotValue
} else {
return dataRecursionObj(
dataObj[keyObj.prKey],
index + 1,
needData,
lastKeyObj,
nd
)
}
}
}
const getData = (applyDetail, needData) => {
for (let nd in needData) {
if (needData[nd].noProcess) {
continue
}
if (applyDetail.formSchema) {
const { jsonSchema } = JSON.parse(applyDetail.formSchema)
needData[nd].schemaPath = schemaRecursionObj({
jsonSchema: jsonSchema || {},
keyWord: needData[nd]['keyWord'],
targetKeys: needData[nd]['keys'],
schemaPath: [],
require: nd.require
})
if (
needData[nd].schemaPath
&& needData[nd].schemaPath.length
) {
const lastKeyObj = needData[nd]
.schemaPath[needData[nd].schemaPath.length - 1]
if (applyDetail.formData) {
const formData = JSON.parse(applyDetail.formData)
needData[nd].value = dataRecursionObj(
formData, 0, needData, lastKeyObj, nd
)
} else {
console.warn(
`表单数据缺失:[${nd}]`,
applyDetail
);
}
} else {
if (needData[nd].require) {
// 记录错误 关键数据没找到
console.warn(
`数据字段查找错误:${nd.needData[nd]['keyWord'].join('、')}`,
jsonSchema
);
}
}
}
}
}
const existOvertimeCount = await models.Overtime.count()
const existVacateCount = await models.Vacate.count()
const attendanceRes = await clickHouse.pepEmis.query(
`
SELECT
story.id AS historyId,
story.apply_user AS pepUserId,
story.form_data AS formData,
story.submit_form_data AS submitFormData,
fform.form_schema AS formSchema,
fprocess.name AS processName,
procin.state_ AS state,
fform.id AS formId
`
+
`,fversion.id AS versionId` +
`,fgroup.name AS groupName` +
`
FROM
workflow_process_history AS story
INNER JOIN workflow_process_version AS fversion
ON fversion.id = story.version_id
INNER JOIN workflow_process_form AS fform
ON fform.id = fversion.form_id
INNER JOIN workflow_process AS fprocess
ON fprocess.id = fform.process_id
INNER JOIN workflow_group AS fgroup
ON fgroup.id = fprocess.group_id
AND fgroup.name = '假勤管理'
INNER JOIN ${camWorkflow}.act_hi_procinst AS procin
ON procin.id_ = story.procinst_id
` +
`
${existOvertimeCount || existVacateCount ?
`WHERE story.create_at > '${moment().subtract(1, 'month').format('YYYY-MM-DD HH:mm:ss')}'`
: ''}
`
).toPromise()
let insertCount = 0, updateCount = 0, invalidCount = 0, unCompletedCount = 0, unknowCount = 0
for (let a of attendanceRes) {
console.log(`处理 ${a.pepUserId}·s ${a.processName}(form:${a.formId} story:${a.historyId}`);
if (a.processName) {
if ('COMPLETED' == a.state) {
if (a.processName.indexOf('请假') > -1) {
let needData = JSON.parse(JSON.stringify(vacateNeedData))
getData(a, needData)
const { begainTime, endTime, type, hrAffirmType, duration, hrAffirmDuration, reason } = needData
if (
begainTime.value && endTime.value && type.value
&& (duration.value || hrAffirmDuration.value)
) {
let durationSec = 0
if (hrAffirmDuration.value) {
durationSec = parseFloat(hrAffirmDuration.value) * 3600
} else if (duration.value) {
durationSec = parseFloat(duration.value) * 3600
}
if (
typeof durationSec != 'number'
|| isNaN(durationSec)
|| durationSec <= 0
) {
console.warn('请假时长计算结果错误', hrAffirmDuration, duration);
invalidCount++
} else {
// 计算每个工作日请了多少假
let begainTime_ = moment(begainTime.value)
let endTime_ = moment(endTime.value)
let curTime = begainTime_.clone()
let packageDay = []
let durationDayAddDay = 0
let forenoonDuration = 3600 * 3.5
let afternoonDuration = 3600 * 4.5
let lunchBreakDuration = 3600 * 1
while (curTime.isBefore(endTime_) && durationDayAddDay < durationSec) {
let curDayStr = curTime.format('YYYY-MM-DD')
const holidayRes = await judgeHoliday(curDayStr, true)
if (holidayRes && holidayRes.workday) {
// 只有上班的日子才计算
let workdayForenoonStart = moment(curDayStr + ' 08:30:00')
let workdayForenoonEnd = moment(curDayStr + ' 12:00:00')
let workdayAfternoonStart = moment(curDayStr + ' 13:00:00')
let workdayAfternoonEnd = moment(curDayStr + ' 17:30:00')
let calcStartTime = workdayForenoonStart.clone()
if (calcStartTime.isSame(begainTime_, 'day')) {
// 和开始日期是同一天就取开始日期
calcStartTime = curTime.clone()
}
if (calcStartTime.isBefore(workdayForenoonStart)) {
// 在上班开始之前 取上班开始时间
calcStartTime = workdayForenoonStart.clone()
} else if (calcStartTime.isBetween(workdayForenoonEnd, workdayAfternoonStart, null, '()')) {
// 在午休时间内取下午上班时间
calcStartTime = workdayAfternoonStart.clone()
}
let calcEndTime = workdayAfternoonEnd.clone()
if (calcEndTime.isSame(endTime_, 'day')) {
// 和结束日期是同一天就取结束时间
calcEndTime = endTime_.clone()
}
if (calcEndTime.isBefore(workdayForenoonStart)) {
// 在上班开始之前
calcEndTime = null
} else if (calcEndTime.isBetween(workdayForenoonEnd, workdayAfternoonStart, null, '()')) {
// 在午休时间内取上午下班时间
calcEndTime = workdayAfternoonStart.clone()
} else if (calcEndTime.isAfter(workdayAfternoonEnd)) {
// 在下午下班之后 取下午下班时间
calcEndTime = workdayAfternoonEnd.clone()
}
if (calcStartTime && calcEndTime && calcStartTime.isBefore(calcEndTime)) {
let durationDay = calcEndTime.diff(calcStartTime, 'seconds')
if (calcStartTime.isBefore(workdayForenoonEnd) && calcEndTime.isAfter(workdayAfternoonStart)) {
// 跨越了午休时间
durationDay -= lunchBreakDuration
}
durationDayAddDay += durationDay
if (durationDayAddDay > durationSec) {
console.log(`请假每日持续时间之和已超过认定时间:day ${curDayStr} duration ${durationDay}`);
durationDay = durationSec - durationDayAddDay
}
if (durationDay > 0) {
packageDay.push({
day: curDayStr,
duration: durationDay
})
}
}
}
curTime = curTime.add(1, 'day').startOf('day')
}
if (durationDayAddDay < durationSec) {
console.log(`请假每日持续时间之和少于认定时间,补偿之`);
// 每日持续时间比认定时间少
if (packageDay.length) {
packageDay[packageDay.length - 1].duration += durationSec - durationDayAddDay
}
}
let typeStorage = ''
if (hrAffirmType.value) {
typeStorage = hrAffirmType.value
} else {
typeStorage = type.value
}
const existRes =
await models.Vacate.findOne({
where: {
pepProcessStoryId: a.historyId
}
})
let storageD = {
pepUserId: a.pepUserId,
pepProcessStoryId: a.historyId,
startTime: moment(begainTime.value).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime.value).format('YYYY-MM-DD HH:mm:ss'),
duration: durationSec,
type: typeStorage,
wfProcessState: a.state,
reason: reason.value
}
let storagedId = null
if (existRes) {
// 结束且存在 当前条件下没必要更新
// await models.Vacate.update(storageD, {
// where: {
// id: existRes.id
// }
// })
// storagedId = existRes.id
updateCount++
} else {
let res = await models.Vacate.create(storageD)
storagedId = res.id
insertCount++
}
if (storagedId) {
await models.VacateDay.destroy({
where: {
vacateId: storagedId
}
})
if (packageDay.length) {
await models.VacateDay.bulkCreate(packageDay.map(p => {
return {
...p,
vacateId: storagedId
}
}))
}
}
}
} else {
console.warn('必填 value 缺失:', needData,);
console.warn('流程数据:', a);
invalidCount++
}
} else if (a.processName.indexOf('加班') > -1) {
// 深拷贝所需查询数据
let needData = JSON.parse(JSON.stringify(overtimeNeedData))
// 获取表单里的数据并插入 needData
getData(a, needData)
const { begainTime, endTime, duration, compensate, hrAffirmDuration, reason } = needData
if (begainTime.value && endTime.value && hrAffirmDuration.value && compensate.value) {
let durationSec = parseFloat(hrAffirmDuration.value) * 3600
if (typeof durationSec != 'number' || isNaN(durationSec || durationSec <= 0)) {
console.warn('加班时长计算结果错误', duration);
invalidCount++
} else {
// 开始时间
let begainTime_ = moment(begainTime.value)
// 加上人事确定的时间的结束时间
let endTimeWithHrAffirm = begainTime_.clone().add(durationSec, 'seconds')
// 定义需要统计的各类变量
let takeRestWorkday = 0,
takeRestDayoff = 0,
takeRestFestivals = 0,
payWorkday = 0,
payDayoff = 0,
payFestivals = 0
// 判断 结束时间在加班当天的时间节点后 也就是说跨天加班了
if (endTimeWithHrAffirm.isSameOrAfter(begainTime_)) {
let packageSuccess = true
// 考虑加了好多天的流程
let curday = begainTime_.clone()
let packageDay = []
while (curday.isSameOrBefore(endTimeWithHrAffirm)) {
let duration_ = 0
let curDayStr = curday.format('YYYY-MM-DD')
if (curday.isSame(endTimeWithHrAffirm, 'day')) {
// 是同一天
duration_ = endTimeWithHrAffirm.diff(curday, 'seconds')
} else if (curday.isSame(begainTime_, 'day')) {
// 和开始日期是同一天
// 同时也说明和结束日期不是同一天
duration_ = begainTime_.clone()
.endOf('day')
.diff(begainTime_, 'seconds') + 1
} else {
// 跨了好几天
// 不但 curday 加 后一天也加
duration_ = 24 * 3600
}
packageDay.push({
day: curDayStr,
duration: duration_
})
const holidayRes = await judgeHoliday(curDayStr, true)
if (holidayRes) {
if (compensate.value && compensate.value.indexOf('调休') >= 0) {
if (holidayRes.workday) {
takeRestWorkday += duration_
} else if (holidayRes.dayoff) {
takeRestDayoff += duration_
} else if (holidayRes.festivals) {
takeRestFestivals += duration_
} else {
let a = 4
}
} else if (compensate.value && compensate.value.indexOf('补偿') >= 0) {
if (holidayRes.workday) {
payWorkday += duration_
} else if (holidayRes.dayoff) {
payDayoff += duration_
} else if (holidayRes.festivals) {
payFestivals += duration_
}
} else {
console.warn(`加班补偿字段未知:`, compensate.value);
invalidCount++
packageSuccess = false
break
}
} else {
console.warn(`节假日信息获取失败`, curday.format('YYYY-MM-DD'));
invalidCount++
packageSuccess = false
break
}
curday = curday.add(1, 'day').startOf('day')
}
if (packageSuccess) {
const existRes =
await models.Overtime.findOne({
where: {
pepProcessStoryId: a.historyId
}
})
let storageD = {
pepUserId: a.pepUserId,
pepProcessStoryId: a.historyId,
startTime: moment(begainTime.value).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime.value).format('YYYY-MM-DD HH:mm:ss'),
duration: durationSec,
wfProcessState: a.state,
takeRestWorkday,
takeRestDayoff,
takeRestFestivals,
payWorkday,
payDayoff,
payFestivals,
compensate: compensate.value,
reason: reason.value
}
let storagedId = null
if (existRes) {
// 结束且存在 当前条件下没必要更新
// storagedId = existRes.id
// await models.Overtime.update(storageD, {
// where: {
// id: existRes.id
// }
// })
updateCount++
} else {
let res = await models.Overtime.create(storageD)
storagedId = res.id
insertCount++
}
if (storagedId) {
await models.OvertimeDay.destroy({
where: {
overtimeId: storagedId
}
})
if (packageDay.length) {
await models.OvertimeDay.bulkCreate(packageDay.map(p => {
return {
...p,
overtimeId: storagedId
}
}))
}
}
}
} else {
console.warn(`结束时间在开始时间之前(unbelievable)`, '开始' + begainTime.value, '结束' + endTime.value, '人事确认结束' + endTimeWithHrAffirm.format());
invalidCount++
}
}
} else {
console.warn('必填 value 缺失:', needData,);
console.warn('流程数据:', a);
invalidCount++
}
} else {
console.warn('假勤分组内不明流程');
unknowCount++
}
} else {
unCompletedCount++
}
} else {
invalidCount++
}
}
console.log(`
假勤数据更新 用时 ${moment().diff(startTime, 'seconds')} s
`)
console.log(`
共:${attendanceRes.length};
新增:${insertCount};
更新数据:${updateCount};
非完成状态流程:${unCompletedCount};
不明流程:${unknowCount}
无效(warning):${invalidCount};
`);
} catch (error) {
app.fs.logger.error(`sechedule: updateAttendance, error: ${error} `);
}
});
return {
updateAttendance,
}
}