Переглянути джерело

update report & project

tags/Baseline_30082024_BACKEND_UAT
cyril.tsui 1 рік тому
джерело
коміт
693ad90f7a
5 змінених файлів з 320 додано та 98 видалено
  1. +3
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt
  2. +2
    -2
      src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt
  3. +309
    -90
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  4. +6
    -6
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  5. BIN
      src/main/resources/templates/report/Cross Team Charge Report.xlsx

+ 3
- 0
src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt Переглянути файл

@@ -38,5 +38,8 @@ interface ProjectRepository : AbstractRepository<Project, Long> {

fun findByCode(code: String): Project?

@Query("SELECT p.id FROM Project p WHERE substring_index(substring_index(p.code, '-', 2), '-', -1) like concat('%', substring_index(substring_index(?1, '-', 2), '-', -1), '%')")
fun checkMainProjectByCodeLike(code: String): List<Long?>?

fun findProjectPlanStartEndByIdAndDeletedFalse(id: Long): ProjectPlanStartEnd
}

+ 2
- 2
src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt Переглянути файл

@@ -200,10 +200,10 @@ open class ProjectsService(
.orElseThrow() else Project()

val duplicateProject =
if (request.projectCode != null) projectRepository.findByCode(request.projectCode) else null
if (request.projectCode != null && request.mainProjectId == null) projectRepository.checkMainProjectByCodeLike(request.projectCode) else null

//check duplicate project
if (duplicateProject != null && !duplicateProject.deleted && duplicateProject.id?.equals(request.projectId) == false) {
if (!duplicateProject.isNullOrEmpty() && !duplicateProject.contains(request.projectId)) {
return NewProjectResponse(
id = request.projectId,
code = request.projectCode,


+ 309
- 90
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Переглянути файл

@@ -684,7 +684,7 @@ open class ReportService(
val row10: Row = sheet.getRow(rowNum)
val cell6 = row10.createCell(2)
cell6.apply {
cellFormula = "O${lastRowNum}"
cellFormula = "P${lastRowNum}"
cellStyle.dataFormat = accountingStyle
}

@@ -692,7 +692,7 @@ open class ReportService(
val row11: Row = sheet.getRow(rowNum)
val cell7 = row11.createCell(2)
cell7.apply {
cellFormula = "P${lastRowNum}"
cellFormula = "Q${lastRowNum}"
cellStyle.dataFormat = accountingStyle
}

@@ -1182,7 +1182,7 @@ open class ReportService(
println("----leaves-----")
println(leaves)
// result = timesheet record mapped
var result: Map<String, Any> = mapOf()
var result: Map<String, Any> = mapOf()
if (timesheets.isNotEmpty()) {
projectList = timesheets.map { "${it["code"]}\n ${it["name"]}" }.toList().distinct()
result = timesheets.groupBy(
@@ -1333,7 +1333,7 @@ open class ReportService(
// }
// }
if (totalConsumed.isNotEmpty()) {
totalConsumed.forEach{ t ->
totalConsumed.forEach { t ->
val total = t["totalConsumed"] as Double
if (total > 8.0) {
normalConsumed += 8.0
@@ -1964,16 +1964,18 @@ open class ReportService(
)
return jdbcDao.queryForList(sql.toString(), args)
}

open fun getTotalConsumed(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("SELECT"
+ " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate, "
+ " sum(t.normalConsumed) + sum(t.otConsumed) as totalConsumed "
+ " from timesheet t "
+ " left join project p on p.id = t.projectId "
+ " where t.staffId = :staffId "
+ " and t.recordDate BETWEEN :startDate and :endDate "
+ " group by t.recordDate "
+ " order by t.recordDate; "
val sql = StringBuilder(
"SELECT"
+ " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate, "
+ " sum(t.normalConsumed) + sum(t.otConsumed) as totalConsumed "
+ " from timesheet t "
+ " left join project p on p.id = t.projectId "
+ " where t.staffId = :staffId "
+ " and t.recordDate BETWEEN :startDate and :endDate "
+ " group by t.recordDate "
+ " order by t.recordDate; "
)
return jdbcDao.queryForList(sql.toString(), args)
}
@@ -2054,42 +2056,43 @@ open class ReportService(
}

open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("SELECT"
+ " p.code, "
+ " p.name, "
+ " tm.code as team, "
+ " concat(c.code, ' - ',c.name) as client, "
+ " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " sum(t.consumedBudget) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour, "
+ " sum(t.consumedBudget) / p.expectedTotalFee * 0.8 as budgetConsumptionRate, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate, "
+ " case "
+ " when (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit and (sum(t.consumedBudget) / p.expectedTotalFee) <= 1 "
+ " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 "
+ " then 'Potential Overconsumption' "
+ " when (sum(t.consumedBudget) / p.expectedTotalFee) >= 1 "
+ " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= 1 "
+ " then 'Overconsumption' "
+ " else 'Within Budget' "
+ " END as status "
+ " from "
+ " (SELECT "
+ " t.*, "
+ " (t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as consumedBudget "
+ " from timesheet t "
+ " left join staff s on s.id = t.staffId "
+ " left join salary sal on sal.salaryPoint = s.salaryId ) t "
+ " left join project p on p.id = t.projectId "
+ " left join team tm on p.teamLead = tm.teamLead "
+ " left join customer c on c.id = p.customerId "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
val sql = StringBuilder(
"SELECT"
+ " p.code, "
+ " p.name, "
+ " tm.code as team, "
+ " concat(c.code, ' - ',c.name) as client, "
+ " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " sum(t.consumedBudget) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour, "
+ " sum(t.consumedBudget) / p.expectedTotalFee * 0.8 as budgetConsumptionRate, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate, "
+ " case "
+ " when (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit and (sum(t.consumedBudget) / p.expectedTotalFee) <= 1 "
+ " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 "
+ " then 'Potential Overconsumption' "
+ " when (sum(t.consumedBudget) / p.expectedTotalFee) >= 1 "
+ " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= 1 "
+ " then 'Overconsumption' "
+ " else 'Within Budget' "
+ " END as status "
+ " from "
+ " (SELECT "
+ " t.*, "
+ " (t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as consumedBudget "
+ " from timesheet t "
+ " left join staff s on s.id = t.staffId "
+ " left join salary sal on sal.salaryPoint = s.salaryId ) t "
+ " left join project p on p.id = t.projectId "
+ " left join team tm on p.teamLead = tm.teamLead "
+ " left join customer c on c.id = p.customerId "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
)
var statusFilter: String = ""
var statusFilter: String = ""
if (args != null) {
if (args.containsKey("teamId"))
sql.append(" and t.id = :teamId")
@@ -2104,14 +2107,16 @@ open class ReportService(
" and (sum(t.consumedBudget) / p.expectedTotalFee) <= 1 " +
" or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " +
" and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 "

"All" -> " having " +
" (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit " +
" or (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit "

else -> ""
}
}
sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name,p.expectedTotalFee, p.totalManhour, p.expectedTotalFee ")
sql.append(statusFilter)
}
sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name,p.expectedTotalFee, p.totalManhour, p.expectedTotalFee ")
sql.append(statusFilter)
return jdbcDao.queryForList(sql.toString(), args)
}

@@ -2212,7 +2217,7 @@ open class ReportService(
val staffInfoList = mutableListOf<Map<String, Any>>()
// println("manHoursSpent------- ${manHoursSpent}")

for (financialYear in financialYears){
for (financialYear in financialYears) {
// println("${financialYear.start.year}-${financialYear.start.monthValue} - ${financialYear.end.year}-${financialYear.end.monthValue}")
println("financialYear--------- ${financialYear.start} - ${financialYear.end}")
}
@@ -2336,7 +2341,11 @@ open class ReportService(
// For Calculating the Financial Year
data class FinancialYear(val start: YearMonth, val end: YearMonth, val hourlyRate: Double)

fun getFinancialYearDates(startDate: YearMonth, endDate: YearMonth, financialYearStartMonth: Int): Array<FinancialYear> {
fun getFinancialYearDates(
startDate: YearMonth,
endDate: YearMonth,
financialYearStartMonth: Int
): Array<FinancialYear> {
val financialYearDates = mutableListOf<FinancialYear>()

var currentYear = startDate.year
@@ -3101,9 +3110,9 @@ open class ReportService(
}

// if (timesheets.isNotEmpty()) {
val combinedTeamCodeColNumber = grades.size * 2
var combinedTeamCodeColNumber = grades.size * 2
val sortedGrades = grades.sortedBy { it.id }
var sortedTeams = teams.sortedBy { it.id }.toMutableList()
val sortedTeams = teams.sortedBy { it.id }.toMutableList()
if (teamId.lowercase() != "all") {
val teamIndex = sortedTeams.indexOfFirst { it.id == teamId.toLong() }
sortedTeams.add(0, sortedTeams.removeAt(teamIndex))
@@ -3205,12 +3214,20 @@ open class ReportService(

createCell(columnIndex++).apply {
setCellValue("Total Manhour by Team")
cellStyle = boldFontWithBorderStyle
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}
}

createCell(columnIndex).apply {
setCellValue("Total Cost Adjusted by Salary Point by Team")
cellStyle = boldFontWithBorderStyle
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}
}
}

@@ -3342,39 +3359,241 @@ open class ReportService(
// }

conditionalFormattingNegative(sheet)
// -------------------------- sheet 1 (By Detail) -------------------------- //
// sheet = workbook.getSheetAt(1)
//
// val groupedTimesheets2 = timesheets
// .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
// .groupBy { timesheetEntry ->
// Pair(
// timesheetEntry.project?.id,
// timesheetEntry.staff?.id
// )
// }
// .mapValues { (_, timesheetEntries) ->
// timesheetEntries.map { timesheet ->
// if (timesheet.normalConsumed != null) {
// mutableMapOf<String, Double>().apply {
// this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
// this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
// .times(timesheet.staff!!.salary.hourlyRate.toDouble())
// }
// } else if (timesheet.otConsumed != null) {
// mutableMapOf<String, Double>().apply {
// this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
// this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
// .times(timesheet.staff!!.salary.hourlyRate.toDouble())
// }
// } else {
// mutableMapOf<String, Double>().apply {
// this["manHour"] = 0.0
// this["salary"] = 0.0
// }
// }
// }
// }
// -------------------------- sheet 1 (Individual) -------------------------- //
sheet = workbook.getSheetAt(1)

rowIndex = 1
columnIndex = 2
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(convertReportMonth)
}

val groupedTimesheetsIndividual = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
.groupBy { timesheetEntry ->
Pair(
timesheetEntry.project?.id,
timesheetEntry.staff?.id
)
}
.mapValues { (_, timesheetEntries) ->
timesheetEntries.map { timesheet ->
if (timesheet.normalConsumed != null) {
mutableMapOf<String, Double>().apply {
this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
.times(timesheet.staff!!.salary.hourlyRate.toDouble())
}
} else if (timesheet.otConsumed != null) {
mutableMapOf<String, Double>().apply {
this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
.times(timesheet.staff!!.salary.hourlyRate.toDouble())
}
} else {
mutableMapOf<String, Double>().apply {
this["manHour"] = 0.0
this["salary"] = 0.0
}
}
}
}

if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) {
rowIndex = 3
sortedTeams.forEach { team: Team ->
// not his/her team staffs
val staffs = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id && (it.project?.teamLead?.id != team.staff.id || it.staff?.team?.id != team.id) }
.mapNotNull { it.staff }
.sortedBy { it.staffId }
.distinct()

// his/her team projects
val projects = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id && it.project?.teamLead?.id == team.staff.id }
.mapNotNull { it.project }
.sortedByDescending { it.code }
.distinct()

// Team
if (!projects.isNullOrEmpty()) {
sheet.createRow(rowIndex++).apply {
createCell(0).apply {
setCellValue("Team to be charged:")
cellStyle = boldFontWithBorderStyle
CellUtil.setAlignment(this, HorizontalAlignment.LEFT)
}

val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 2 + staffs.size)
sheet.addMergedRegion(rangeAddress)
RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet)

createCell(1).apply {
setCellValue(team.code)

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}
}

}

// Staffs
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}


staffs.forEach { staff: Staff ->
createCell(columnIndex++).apply {
setCellValue("${staff.staffId} - ${staff.name} (${staff.team.code})")
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}

sheet.setColumnWidth(this.columnIndex, 50 * 256)
}
}

createCell(columnIndex++).apply {
setCellValue("Total Manhour by Project")
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}
sheet.setColumnWidth(this.columnIndex, 50 * 256)
}

createCell(columnIndex).apply {
setCellValue("Total Cost Adjusted by Salary Point by Project")
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}
sheet.setColumnWidth(this.columnIndex, 50 * 256)
}
}

// Project + Manhour
val startRow = rowIndex + 1
var endRow = rowIndex
projects.forEach { project: Project ->
if (teamId.lowercase() == "all" || teamId.toLong() == project.teamLead?.team?.id || teamId.toLong() == team.id) {
if (team.id == project.teamLead?.team?.id) {
endRow++
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("${project.code}: ${project.name}")
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.LEFT
}
}

var totalSalary = 0.0
staffs.forEach { staff: Staff ->
logger.info("Staff: ${staff.staffId}")
createCell(columnIndex++).apply {
setCellValue(
groupedTimesheetsIndividual[Pair(
project.id,
staff.id,
)]?.sumOf { it.getValue("manHour") } ?: 0.0)

totalSalary += groupedTimesheetsIndividual[Pair(
project.id,
staff.id
)]?.sumOf { it.getValue("salary") } ?: 0.0

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}
}

createCell(columnIndex++).apply {
val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1)
cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})"

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}

createCell(columnIndex).apply {
setCellValue(totalSalary)
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}
}
}
}
}

// Total Manhour & Cost
sheet.createRow(rowIndex).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}

staffs.forEach { staff: Staff ->
createCell(columnIndex++).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}
}

createCell(columnIndex++).apply {
val lastCellLetter = CellReference.convertNumToColString(this.columnIndex)
cellFormula = "sum(${lastCellLetter}${startRow}:${lastCellLetter}${endRow})"
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}

createCell(columnIndex).apply {
val lastCellLetter = CellReference.convertNumToColString(this.columnIndex)
cellFormula = "sum(${lastCellLetter}${startRow}:${lastCellLetter}${endRow})"
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}
}

rowIndex += 2
}
}
}

conditionalFormattingNegative(sheet)

return workbook
}

+ 6
- 6
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Переглянути файл

@@ -316,9 +316,9 @@ class ReportController(
@Throws(ServletRequestBindingException::class, IOException::class)
fun getCrossTeamChargeReport(@RequestBody @Valid request: CrossTeamChargeReportRequest): ResponseEntity<Resource> {

val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build()
if (authorities.stream().anyMatch { it.authority.equals("GENERATE_CROSS_TEAM_CHARGE_REPORT") }) {
// val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build()
//
// if (authorities.stream().anyMatch { it.authority.equals("G_CROSS_TEAM_CHARGE_REPORT") }) {
// val crossProjects = staffAllocationRepository.findAll()
// .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
// .map { it.project!! }
@@ -342,8 +342,8 @@ class ReportController(
.header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))

} else {
return ResponseEntity.noContent().build()
}
// } else {
// return ResponseEntity.noContent().build()
// }
}
}

BIN
src/main/resources/templates/report/Cross Team Charge Report.xlsx Переглянути файл


Завантаження…
Відмінити
Зберегти