From cec341d81fdf8345749fdfe67ce309a8b88a14cb Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Fri, 24 May 2024 11:31:33 +0800 Subject: [PATCH 1/2] update --- .../ffii/tsms/modules/report/service/ReportService.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 42c9754..bad39d3 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -1395,14 +1395,17 @@ open class ReportService( + " SELECT " + " s.teamId, " + " pt.project_id, " - + " SUM(tns.totalConsumed) AS totalConsumed " + + " SUM(tns.totalConsumed) AS totalConsumed, " + + " sum(tns.totalBudget) as totalBudget " + " FROM ( " + " SELECT " + " t.staffId, " + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " t.projectTaskId AS taskId " + + " t.projectTaskId AS taskId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " + " FROM timesheet t " + " LEFT JOIN staff s ON t.staffId = s.id " + + " left join salary sal on sal.salaryPoint = s.salaryId " + " GROUP BY t.staffId, t.projectTaskId " + " ) AS tns " + " INNER JOIN project_task pt ON tns.taskId = pt.id " @@ -1416,7 +1419,7 @@ open class ReportService( + " t.code as team, " + " c.code as client, " + " p.expectedTotalFee as plannedBudget, " - + " COALESCE((tns.totalConsumed * sa.hourlyRate), 0) as actualConsumedBudget, " + + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " + " COALESCE(p.totalManhour, 0) as plannedManhour, " + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " From 03fc7e8c2f1e93d9efb5b6d6076862b48f45210e Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 24 May 2024 12:27:40 +0800 Subject: [PATCH 2/2] update report & fix project --- .../project/entity/ProjectRepository.kt | 7 +- .../project/entity/ProjectTaskRepository.kt | 1 + .../modules/report/service/ReportService.kt | 681 +++++++++++------- .../modules/report/web/ReportController.kt | 37 +- .../modules/report/web/model/ReportRequest.kt | 5 + .../report/AR02_Delay Report v02.xlsx | Bin 15065 -> 12399 bytes 6 files changed, 477 insertions(+), 254 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt index 57d1720..8f367a8 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt @@ -1,10 +1,13 @@ package com.ffii.tsms.modules.project.entity; import com.ffii.core.support.AbstractRepository +import com.ffii.tsms.modules.data.entity.Customer +import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import org.springframework.data.jpa.repository.Query +import org.springframework.lang.Nullable import java.io.Serializable import java.time.LocalDate @@ -25,8 +28,10 @@ interface ProjectRepository : AbstractRepository { "") fun getLatestCodeNumberByMainProject(isClpProject: Boolean, id: Serializable?): Long? - @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like ?1 and p.id != ?2") + @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like %?1% and p.id != ?2") fun getLatestCodeNumberBySubProject(code: String, id: Serializable?): Long? fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List + + fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt index b1b57e0..085ed7e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt @@ -5,5 +5,6 @@ import com.ffii.core.support.AbstractRepository interface ProjectTaskRepository : AbstractRepository { fun findAllByProject(project: Project): List + fun findAllByProjectIn(projects: List): List fun findByProjectAndTask(project: Project, task: Task): ProjectTask? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index bad39d3..9ea3ebd 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -6,6 +6,7 @@ import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.project.entity.Invoice +import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Timesheet @@ -47,6 +48,7 @@ open class ReportService( private val PandL_REPORT = "templates/report/AR07_Project P&L Report v02.xlsx" private val FINANCIAL_STATUS_REPORT = "templates/report/EX01_Financial Status Report.xlsx" private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx" + private val PROJECT_POTENTIAL_DELAY_REPORT = "templates/report/AR02_Delay Report v02.xlsx" private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx" private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" @@ -64,33 +66,35 @@ open class ReportService( val tempList = mutableListOf>() - for (item in financialStatus){ - val normalConsumed = item.getValue("normalConsumed") as Double - val hourlyRate = item.getValue("hourlyRate") as BigDecimal + for (item in financialStatus) { + val normalConsumed = item.getValue("normalConsumed") as Double + val hourlyRate = item.getValue("hourlyRate") as BigDecimal // println("normalConsumed------------- $normalConsumed") // println("hourlyRate------------- $hourlyRate") val manHourRate = normalConsumed.toBigDecimal().multiply(hourlyRate) // println("manHourRate------------ $manHourRate") - val otConsumed = item.getValue("otConsumed") as Double + val otConsumed = item.getValue("otConsumed") as Double val manOtHourRate = otConsumed.toBigDecimal().multiply(hourlyRate).multiply(otFactor) - if(!tempList.any{ it.containsValue(item.getValue("code"))}){ - - tempList.add(mapOf( - "code" to item.getValue("code"), - "description" to item.getValue("description"), - "client" to item.getValue("client"), - "teamLead" to item.getValue("teamLead"), - "planStart" to item.getValue("planStart"), - "planEnd" to item.getValue("planEnd"), - "expectedTotalFee" to item.getValue("expectedTotalFee"), - "normalConsumed" to manHourRate, - "otConsumed" to manOtHourRate, - "issuedAmount" to item.getValue("sumIssuedAmount"), - "paidAmount" to item.getValue("sumPaidAmount"), - )) - }else{ + if (!tempList.any { it.containsValue(item.getValue("code")) }) { + + tempList.add( + mapOf( + "code" to item.getValue("code"), + "description" to item.getValue("description"), + "client" to item.getValue("client"), + "teamLead" to item.getValue("teamLead"), + "planStart" to item.getValue("planStart"), + "planEnd" to item.getValue("planEnd"), + "expectedTotalFee" to item.getValue("expectedTotalFee"), + "normalConsumed" to manHourRate, + "otConsumed" to manOtHourRate, + "issuedAmount" to item.getValue("sumIssuedAmount"), + "paidAmount" to item.getValue("sumPaidAmount"), + ) + ) + } else { // Find the existing Map in the tempList that has the same "code" value val existingMap = tempList.find { it.containsValue(item.getValue("code")) }!! @@ -121,7 +125,32 @@ open class ReportService( dateType: String ): ByteArray { // Generate the Excel report with query results - val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) + val workbook: Workbook = + createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) + + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } + + @Throws(IOException::class) + fun generateProjectPotentialDelayReport( + searchedTeam: String, + searchedClient: String, + projects: List, + timesheets: List, + ): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = createProjectPotentialDelayReport( + searchedTeam, + searchedClient, + projects, + timesheets, + PROJECT_POTENTIAL_DELAY_REPORT + ) // Write the workbook to a ByteArrayOutputStream val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() @@ -223,7 +252,8 @@ open class ReportService( team, customer, project, - LATE_START_REPORT) + LATE_START_REPORT + ) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) workbook.close() @@ -237,20 +267,20 @@ open class ReportService( templatePath: String, projects: List>, teamLeadId: Long - ) : Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) - val accountingStyle = workbook.createDataFormat() .getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") val sheet = workbook.getSheetAt(0) //Set Column 2, 3, 4 to auto width - sheet.setColumnWidth(2, 20*256) - sheet.setColumnWidth(3, 45*256) - sheet.setColumnWidth(4, 15*256) + sheet.setColumnWidth(2, 20 * 256) + sheet.setColumnWidth(3, 45 * 256) + sheet.setColumnWidth(4, 15 * 256) val boldFont = sheet.workbook.createFont() boldFont.bold = true @@ -259,7 +289,7 @@ open class ReportService( boldFontCellStyle.setFont(boldFont) var rowNum = 14 - if (projects.isEmpty()){ + if (projects.isEmpty()) { // Fill the cell in Row 2-12 with thr calculated sum rowNum = 1 val row1: Row = sheet.getRow(rowNum) @@ -269,12 +299,13 @@ open class ReportService( rowNum = 2 val row2: Row = sheet.getRow(rowNum) val row2Cell = row2.createCell(2) - val sql = StringBuilder("select" - + " t.teamLead as id," - + " t.name, t.code " - + " from team t" - + " where t.deleted = false " - + " and t.teamLead = :teamLead " + val sql = StringBuilder( + "select" + + " t.teamLead as id," + + " t.name, t.code " + + " from team t" + + " where t.deleted = false " + + " and t.teamLead = :teamLead " ) val args = mapOf("teamLead" to teamLeadId) val team = jdbcDao.queryForMap(sql.toString(), args).get() @@ -292,14 +323,16 @@ open class ReportService( return workbook } - for(item in projects){ + for (item in projects) { val row: Row = sheet.createRow(rowNum++) val codeCell = row.createCell(0) codeCell.setCellValue(if (item["code"] != null) item.getValue("code").toString() else "N/A") val descriptionCell = row.createCell(1) - descriptionCell.setCellValue(if (item["description"] != null) item.getValue("description").toString() else "N/A") + descriptionCell.setCellValue( + if (item["description"] != null) item.getValue("description").toString() else "N/A" + ) val clientCell = row.createCell(2) clientCell.apply { @@ -333,7 +366,7 @@ open class ReportService( val normalConsumed = item["normalConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) val otConsumed = item["otConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) val cumExpenditure = normalConsumed.add(otConsumed) - cumExpenditureCell.apply{ + cumExpenditureCell.apply { setCellValue(cumExpenditure.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -354,7 +387,8 @@ open class ReportService( val uninvoiceCell = row.createCell(11) uninvoiceCell.apply { - cellFormula = " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) " + cellFormula = + " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) " cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -366,7 +400,7 @@ open class ReportService( val receivedAmountCell = row.createCell(13) val paidAmount = item["paidAmount"]?.let { it as BigDecimal } ?: BigDecimal(0) - receivedAmountCell.apply{ + receivedAmountCell.apply { setCellValue(paidAmount.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -382,18 +416,18 @@ open class ReportService( val lastRowNum = rowNum + 1 val row: Row = sheet.createRow(rowNum) - for(i in 0..4){ + for (i in 0..4) { val cell = row.createCell(i) - CellUtil.setCellStyleProperty(cell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(cell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(cell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(cell, "borderBottom", BorderStyle.DOUBLE) } val subTotalCell = row.createCell(5) subTotalCell.apply { setCellValue("Sub-total:") } - CellUtil.setCellStyleProperty(subTotalCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(subTotalCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(subTotalCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(subTotalCell, "borderBottom", BorderStyle.DOUBLE) val sumTotalFeeCell = row.createCell(6) @@ -401,24 +435,24 @@ open class ReportService( cellFormula = "SUM(G15:G${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumTotalFeeCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumTotalFeeCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumTotalFeeCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumTotalFeeCell, "borderBottom", BorderStyle.DOUBLE) val sumBudgetCell = row.createCell(7) sumBudgetCell.apply { cellFormula = "SUM(H15:H${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumBudgetCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumBudgetCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumBudgetCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumBudgetCell, "borderBottom", BorderStyle.DOUBLE) val sumCumExpenditureCell = row.createCell(8) sumCumExpenditureCell.apply { cellFormula = "SUM(I15:I${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumCumExpenditureCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumCumExpenditureCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderBottom", BorderStyle.DOUBLE) val sumBudgetVCell = row.createCell(9) sumBudgetVCell.apply { @@ -426,16 +460,16 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumBudgetVCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumBudgetVCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumBudgetVCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumBudgetVCell, "borderBottom", BorderStyle.DOUBLE) val sumIInvoiceCell = row.createCell(10) sumIInvoiceCell.apply { cellFormula = "SUM(K15:K${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumIInvoiceCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumIInvoiceCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val sumUInvoiceCell = row.createCell(11) sumUInvoiceCell.apply { @@ -443,20 +477,20 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumUInvoiceCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumUInvoiceCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val lastCpiCell = row.createCell(12) - CellUtil.setCellStyleProperty(lastCpiCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(lastCpiCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) val sumRAmountCell = row.createCell(13) sumRAmountCell.apply { cellFormula = "SUM(N15:N${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumRAmountCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumRAmountCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE) val sumUnSettleCell = row.createCell(14) sumUnSettleCell.apply { @@ -464,8 +498,8 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumUnSettleCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumUnSettleCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumUnSettleCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumUnSettleCell, "borderBottom", BorderStyle.DOUBLE) // Fill the cell in Row 2-12 with thr calculated sum rowNum = 1 @@ -478,7 +512,7 @@ open class ReportService( val row2Cell = row2.createCell(2) if (teamLeadId < 0) { row2Cell.setCellValue("All") - }else{ + } else { row2Cell.apply { cellFormula = "D15" } @@ -639,7 +673,8 @@ open class ReportService( rowIndex = 15 - val dateFormatter = if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") + val dateFormatter = + if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } .map { it.format(dateFormatter) }.distinct() @@ -648,9 +683,11 @@ open class ReportService( .mapValues { (_, timesheetEntries) -> timesheetEntries.map { timesheet -> if (timesheet.normalConsumed != null) { - timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble()) + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) } else if (timesheet.otConsumed != null) { - timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble()) + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) } else { 0.0 } @@ -765,6 +802,136 @@ open class ReportService( return workbook } + @Throws(IOException::class) + private fun createProjectPotentialDelayReport( + searchedTeam: String, + searchedClient: String, + projects: List, + timesheets: List, + templatePath: String, + ): Workbook { + // please create a new function for each report template + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val sheet: Sheet = workbook.getSheetAt(0) + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(FORMATTED_TODAY) + } + + rowIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(searchedTeam) + } + + rowIndex = 3 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(searchedClient) + } + + val groupedTimesheets = timesheets + .groupBy { timesheetEntry -> + Pair( + timesheetEntry.projectTask?.project?.id, + timesheetEntry.projectTask?.milestone?.id + ) + } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + } else if (timesheet.otConsumed != null) { + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + } else { + 0.0 + } + } + } + +// groupedTimesheets.entries.forEach { (key, value) -> +// logger.info("key: $key") +// logger.info(key == Pair(1L, 1L)) +// logger.info("value: " + value.sumOf { it }) +// } + + rowIndex = 6 + projects.forEach { project: Project -> + + sheet.createRow(rowIndex).apply { + createCell(0).apply { + setCellValue((rowIndex - 5).toString()) + } + + createCell(1).apply { + setCellValue(project.code) + } + + createCell(2).apply { + setCellValue(project.name) + } + + createCell(3).apply { + val currentTeam = project.teamLead?.team + setCellValue(currentTeam?.code + " - " + currentTeam?.name) + } + + createCell(4).apply { + val currentClient = project.customer + setCellValue(currentClient?.code + " - " + currentClient?.name) + } + + createCell(5).apply { + setCellValue(project.actualStart?.format(DATE_FORMATTER)) + } + + createCell(6).apply { + setCellValue(project.planEnd?.format(DATE_FORMATTER)) + } + } + + project.milestones.forEach { milestone: Milestone -> + logger.info(milestone.id) + + val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) + rowIndex++ + + tempRow.apply { + createCell(7).apply { + setCellValue(milestone.taskGroup?.name ?: "N/A") + } + + createCell(8).apply { + setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") + } + + createCell(9).apply { + cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") + + if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { + val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]!!.sum() + + logger.info("manHoursSpent: $manHoursSpent") + logger.info("milestone.stagePercentAllocation: " + milestone.stagePercentAllocation) + logger.info("project.totalManhour: " + project.totalManhour) + + val resourceUtilization = + manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) + setCellValue(resourceUtilization ?: 0.0) + } else { + setCellValue(0.0) + } + } + } + } + } + + return workbook + } + fun getColumnAlphabet(colIndex: Int): String { val alphabet = StringBuilder() @@ -796,14 +963,16 @@ open class ReportService( // result = timesheet record mapped var result: Map = mapOf() if (timesheets.isNotEmpty()) { - projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList().distinct() + projectList = timesheets.map { "${it["code"]}\n ${it["name"]}" }.toList().distinct() result = timesheets.groupBy( { it["id"].toString() }, - { mapOf( - "date" to it["recordDate"], - "normalConsumed" to it["normalConsumed"], - "otConsumed" to it["otConsumed"], - ) } + { + mapOf( + "date" to it["recordDate"], + "normalConsumed" to it["normalConsumed"], + "otConsumed" to it["otConsumed"], + ) + } ) } println("---result---") @@ -887,7 +1056,7 @@ open class ReportService( rowIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(0) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) tempCell.setCellValue("Sub-total") tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) @@ -915,7 +1084,7 @@ open class ReportService( tempCell.setCellValue(normalConsumed) tempCell.cellStyle.dataFormat = accountingStyle - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // rowIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(0) @@ -935,7 +1104,7 @@ open class ReportService( tempCell.setCellValue("Total Leave Hours") tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // cal total leave hour if (leaves.isNotEmpty()) { leaves.forEach { l -> @@ -958,11 +1127,11 @@ open class ReportService( CellUtil.setCellStyleProperties(tempCell, DoubleBorderBottom) tempCell = sheet.getRow(rowIndex).createCell(2) - tempCell.cellFormula = "C${rowIndex-2}+C${rowIndex-1}" + tempCell.cellFormula = "C${rowIndex - 2}+C${rowIndex - 1}" tempCell.cellStyle.dataFormat = accountingStyle CellUtil.setCellStyleProperties(tempCell, DoubleBorderBottom) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 7 columnIndex = 2 @@ -986,13 +1155,13 @@ open class ReportService( tempCell.cellStyle.dataFormat = accountingStyle } } - result.forEach{ (id, list) -> + result.forEach { (id, list) -> val temp: List> = list as List> - temp.forEachIndexed { index, _ -> - dayInt = temp[index]["date"].toString().toInt() - tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) - tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) - } + temp.forEachIndexed { index, _ -> + dayInt = temp[index]["date"].toString().toInt() + tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) + tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) + } columnIndex++ } } @@ -1009,34 +1178,35 @@ open class ReportService( tempCell.setCellValue(leave["leaveHours"] as Double) } } - ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue("Leave Hours") - tempCell.cellStyle = boldStyle - CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) - CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) - CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) + ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// + tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue("Leave Hours") + tempCell.cellStyle = boldStyle + CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) + CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) + CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) columnIndex += 1 - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") - tempCell.cellStyle = boldStyle - CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) - CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) + tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") + tempCell.cellStyle = boldStyle + CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) + CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) - sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex)) + sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 8 if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue != "Leave Hours") { // cal daily spent manhour for (i in 0 until rowSize) { tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1}:${getColumnAlphabet(columnIndex - 2)}${rowIndex+1})" // should columnIndex - 2 + tempCell.cellFormula = + "SUM(${getColumnAlphabet(2)}${rowIndex + 1}:${getColumnAlphabet(columnIndex - 2)}${rowIndex + 1})" // should columnIndex - 2 rowIndex++ } // cal subtotal println(rowIndex) - for (i in 0 ..columnSize ) { // minus last col + for (i in 0..columnSize) { // minus last col println(sheet.getRow(rowIndex).getCell(2 + i)) tempCell = sheet.getRow(rowIndex).getCell(2 + i) ?: sheet.getRow(rowIndex).createCell(2 + i) tempCell.cellFormula = "SUM(${getColumnAlphabet(2 + i)}9:${getColumnAlphabet(2 + i)}${rowIndex})" @@ -1136,7 +1306,7 @@ open class ReportService( result: List>, lowerLimit: Double, templatePath: String - ):Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1169,10 +1339,11 @@ open class ReportService( tempCell.setCellValue((index + 1).toDouble()) val keys = obj.keys.toList() keys.forEachIndexed { keyIndex, key -> - tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) + tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex) + .createCell(columnIndex + keyIndex + 1) when (obj[key]) { is Double -> tempCell.setCellValue(obj[key] as Double) - else -> tempCell.setCellValue(obj[key] as String ) + else -> tempCell.setCellValue(obj[key] as String) } } rowIndex++ @@ -1190,20 +1361,20 @@ open class ReportService( fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) val cfRules = arrayOf(rule1, rule2) - val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex+1}")) + val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex + 1}")) sheetCF.addConditionalFormatting(regions, cfRules); return workbook } -//createLateStartReport + //createLateStartReport private fun createLateStartReport( team: Team, customer: Customer, project: List, templatePath: String - ):Workbook{ - + ): Workbook { + val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1214,9 +1385,9 @@ open class ReportService( val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2) dateCell.setCellValue(formattedToday) - // Start populating project data starting at row 7 - val startRow = 6 // 0-based index for row 7 - val projectDataRow = sheet.createRow(startRow) + // Start populating project data starting at row 7 + val startRow = 6 // 0-based index for row 7 + val projectDataRow = sheet.createRow(startRow) // Styling for cell A1: Font size 16 and bold val headerFont = workbook.createFont().apply { @@ -1276,29 +1447,29 @@ open class ReportService( open fun getFinancialStatus(teamLeadId: Long?): List> { val sql = StringBuilder( " with cte_timesheet as (" - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" - + " from timesheet t" - + " left join project_task pt on pt.id = t.projectTaskId" - + " left join project p ON p.id = pt.project_id" - + " left join staff s on s.id = t.staffId" - + " left join salary s2 on s.salaryId = s2.salaryPoint" - + " left join team t2 on t2.id = s.teamId" - + " )," - + " cte_invoice as (" - + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" - + " from invoice i" - + " left join project p on p.code = i.projectCode" - + " group by p.code" - + " )" - + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," - + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" - + " from project p" - + " left join cte_timesheet cte_ts on p.code = cte_ts.code" - + " left join customer c on c.id = p.customerId" - + " left join tsmsdb.team t on t.teamLead = p.teamLead" - + " left join cte_invoice cte_i on cte_i.code = p.code" - + " where p.status = \'On-going\'" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" + + " from timesheet t" + + " left join project_task pt on pt.id = t.projectTaskId" + + " left join project p ON p.id = pt.project_id" + + " left join staff s on s.id = t.staffId" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " left join team t2 on t2.id = s.teamId" + + " )," + + " cte_invoice as (" + + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " group by p.code" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " where p.status = \'On-going\'" ) if (teamLeadId!! > 0) { sql.append(" and p.teamLead = :teamLeadId ") @@ -1312,32 +1483,33 @@ open class ReportService( open fun getTimesheet(args: Map): List> { val sql = StringBuilder( "SELECT" - + " p.id," - + " p.name," - + " p.code," - + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," - + " sum(t.normalConsumed) as normalConsumed," - + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" - + " from timesheet t" - + " left join project_task pt on t.projectTaskId = pt.id" - + " left join project p on p.id = pt.project_id" - + " where t.staffId = :staffId" - + " group by p.id, t.recordDate" - + " order by p.id, t.recordDate" - + " and t.recordDate BETWEEN :startDate and :endDate" + + " p.id," + + " p.name," + + " p.code," + + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," + + " sum(t.normalConsumed) as normalConsumed," + + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" + + " from timesheet t" + + " left join project_task pt on t.projectTaskId = pt.id" + + " left join project p on p.id = pt.project_id" + + " where t.staffId = :staffId" + + " group by p.id, t.recordDate" + + " order by p.id, t.recordDate" + + " and t.recordDate BETWEEN :startDate and :endDate" ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getLeaves(args: Map): List> { val sql = StringBuilder( " SELECT " - + " sum(leaveHours) as leaveHours, " - + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " - + " from `leave` " - + " where staffId = :staffId " - + " and recordDate BETWEEN :startDate and :endDate " - + " group by recordDate " - + " order by recordDate " + + " sum(leaveHours) as leaveHours, " + + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " + + " from `leave` " + + " where staffId = :staffId " + + " and recordDate BETWEEN :startDate and :endDate " + + " group by recordDate " + + " order by recordDate " ) return jdbcDao.queryForList(sql.toString(), args) } @@ -1449,60 +1621,64 @@ open class ReportService( if (args.containsKey("custId")) sql.append("and c.id = :custId") if (args.containsKey("status")) - statusFilter = when (args.get("status")) { - "Potential Overconsumption" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + - " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + - " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + - " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" - "Overconsumption" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + - " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " - "Within Budget" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + - " and " + - " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " - else -> "" - } - sql.append(statusFilter) + statusFilter = when (args.get("status")) { + "Potential Overconsumption" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + + " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + + " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" + + "Overconsumption" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " + + "Within Budget" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + + " and " + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " + + else -> "" + } + sql.append(statusFilter) } return jdbcDao.queryForList(sql.toString(), args) } - open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List>{ + + open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List> { val sql = StringBuilder( - " with cte_timesheet as (" - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," - + " t.recordDate" - + " from timesheet t" - + " left join project_task pt on pt.id = t.projectTaskId" - + " left join project p ON p.id = pt.project_id" - + " left join staff s on s.id = t.staffId" - + " left join salary s2 on s.salaryId = s2.salaryPoint" - + " left join team t2 on t2.id = s.teamId" - + " )," - + " cte_invoice as (" - + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" - + " from invoice i" - + " left join project p on p.code = i.projectCode" - + " group by p.code" - + " )" - + " select p.code, p.description, c.name as client, concat(t.code, \" - \", t.name) as teamLead," - + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " - + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," - + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" - + " from project p" - + " left join cte_timesheet cte_ts on p.code = cte_ts.code" - + " left join customer c on c.id = p.customerId" - + " left join tsmsdb.team t on t.teamLead = p.teamLead" - + " left join cte_invoice cte_i on cte_i.code = p.code" - + " left join staff s on s.id = cte_ts.staffId" - + " left join grade g on g.id = s.gradeId" - + " left join team t2 on t2.id = s.teamId" - + " where p.deleted = false" - + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" - + " and p.id = :projectId" - + " order by g.code, s.name, cte_ts.recordDate" + " with cte_timesheet as (" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + + " t.recordDate" + + " from timesheet t" + + " left join project_task pt on pt.id = t.projectTaskId" + + " left join project p ON p.id = pt.project_id" + + " left join staff s on s.id = t.staffId" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " left join team t2 on t2.id = s.teamId" + + " )," + + " cte_invoice as (" + + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " group by p.code" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \" - \", t.name) as teamLead," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " + + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," + + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " left join staff s on s.id = cte_ts.staffId" + + " left join grade g on g.id = s.gradeId" + + " left join team t2 on t2.id = s.teamId" + + " where p.deleted = false" + + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" + + " and p.id = :projectId" + + " order by g.code, s.name, cte_ts.recordDate" ) val args = mapOf( @@ -1537,24 +1713,24 @@ open class ReportService( val staffInfoList = mutableListOf>() println("manHoursSpent------- ${manHoursSpent}") - for(item in manHoursSpent){ - if(info["teamLead"] != item.getValue("teamLead")){ + for (item in manHoursSpent) { + if (info["teamLead"] != item.getValue("teamLead")) { info["teamLead"] = item.getValue("teamLead") } - if(info["client"] != item.getValue("client")){ + if (info["client"] != item.getValue("client")) { info["client"] = item.getValue("client") } - if(info["code"] != item.getValue("code")){ + if (info["code"] != item.getValue("code")) { info["code"] = item.getValue("code") } - if(info["code"] == item["code"] && "paidAmount" !in info){ + if (info["code"] == item["code"] && "paidAmount" !in info) { info["paidAmount"] = item.getValue("sumPaidAmount") } - if(info["description"] != item.getValue("description")){ + if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"]}){ + if (!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"] }) { staffInfoList.add( mapOf( "staffId" to item.getValue("staffId"), @@ -1569,13 +1745,13 @@ open class ReportService( "normalConsumed" to item.getValue("normalConsumed"), "otConsumed" to item.getValue("otConsumed"), "totalManHours" to item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double, - "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double ) - + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double) + + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) ) ) ) ) - }else{ + } else { val existingMap = staffInfoList.find { it.containsValue(item.getValue("staffId")) }!! val updatedMap = existingMap.toMutableMap() val hourlySpentList = updatedMap["hourlySpent"] as MutableList> @@ -1583,10 +1759,18 @@ open class ReportService( if (existingRecord != null) { val existingIndex = hourlySpentList.indexOf(existingRecord) val updatedRecord = existingRecord.toMutableMap() - updatedRecord["normalConsumed"] = (updatedRecord["normalConsumed"] as Double) + item.getValue("normalConsumed") as Double - updatedRecord["otConsumed"] = (updatedRecord["otConsumed"] as Double) + item.getValue("otConsumed") as Double - updatedRecord["totalManHours"] = (updatedRecord["totalManHours"] as Double) + item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double - updatedRecord["manhourExpenditure"] = (updatedRecord["manhourExpenditure"] as Double) + (hourlyRate * item.getValue("normalConsumed") as Double) + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + updatedRecord["normalConsumed"] = + (updatedRecord["normalConsumed"] as Double) + item.getValue("normalConsumed") as Double + updatedRecord["otConsumed"] = + (updatedRecord["otConsumed"] as Double) + item.getValue("otConsumed") as Double + updatedRecord["totalManHours"] = + (updatedRecord["totalManHours"] as Double) + item.getValue("normalConsumed") as Double + item.getValue( + "otConsumed" + ) as Double + updatedRecord["manhourExpenditure"] = + (updatedRecord["manhourExpenditure"] as Double) + (hourlyRate * item.getValue("normalConsumed") as Double) + (hourlyRate * item.getValue( + "otConsumed" + ) as Double * otFactor) hourlySpentList[existingIndex] = updatedRecord } else { hourlySpentList.add( @@ -1596,7 +1780,7 @@ open class ReportService( "otConsumed" to item.getValue("otConsumed"), "totalManHours" to item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double, "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double) - + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) ) ) } @@ -1611,7 +1795,7 @@ open class ReportService( tempList.add(mapOf("info" to info)) tempList.add(mapOf("staffInfoList" to staffInfoList)) - println("Only Staff Info List --------------------- ${ tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") + println("Only Staff Info List --------------------- ${tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") println("tempList----------------- $tempList") return tempList @@ -1634,7 +1818,7 @@ open class ReportService( manhoursSpent: List>, startMonth: String, endMonth: String - ): Workbook{ + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1660,10 +1844,11 @@ open class ReportService( val monthFormat = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH) - val info:Map = manhoursSpent.first() { it.containsKey("info") }["info"] as Map - val staffInfoList: List> = manhoursSpent.first() { it.containsKey("staffInfoList") }["staffInfoList"] as List> + val info: Map = manhoursSpent.first() { it.containsKey("info") }["info"] as Map + val staffInfoList: List> = + manhoursSpent.first() { it.containsKey("staffInfoList") }["staffInfoList"] as List> - if (staffInfoList.isEmpty()){ + if (staffInfoList.isEmpty()) { val sheet = workbook.getSheetAt(0) var rowNum = 0 rowNum = 1 @@ -1737,7 +1922,7 @@ open class ReportService( row9Cell.setCellValue("${convertStartMonth.format(monthFormat)} - ${convertEndMonth.format(monthFormat)}") rowNum = 10 - for(staff in staffInfoList){ + for (staff in staffInfoList) { // val row: Row = sheet.getRow(rowNum++) ?: sheet.createRow(rowNum++) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val staffCell = row.getCell(0) ?: row.createCell(0) @@ -1769,7 +1954,7 @@ open class ReportService( } rowNum += 2 val titleRow = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 0, monthRange.size+3)) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 0, monthRange.size + 3)) val titleCell = titleRow.getCell(0) ?: titleRow.createCell(0) titleCell.apply { setCellValue("Manhours Spent per Month") @@ -1786,7 +1971,7 @@ open class ReportService( } CellUtil.setAlignment(staffCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(staffCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(staffCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(staffCell, "borderBottom", BorderStyle.THIN) val teamCell = manhoursSpentRow.getCell(1) ?: manhoursSpentRow.createCell(1) teamCell.apply { @@ -1794,44 +1979,44 @@ open class ReportService( } CellUtil.setAlignment(teamCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(teamCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(teamCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(teamCell, "borderBottom", BorderStyle.THIN) - for ((column, month) in monthRange.withIndex()){ + for ((column, month) in monthRange.withIndex()) { val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val monthCell = row.getCell(2+column) ?: row.createCell(2+column) + val monthCell = row.getCell(2 + column) ?: row.createCell(2 + column) monthCell.apply { setCellValue(month.getValue("display").toString()) } CellUtil.setAlignment(monthCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(monthCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(monthCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(monthCell, "borderBottom", BorderStyle.THIN) } val monthColumnEnd = monthRange.size val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val manhourCell = row.getCell(2+monthColumnEnd) ?: row.createCell(2+monthColumnEnd) + val manhourCell = row.getCell(2 + monthColumnEnd) ?: row.createCell(2 + monthColumnEnd) manhourCell.apply { setCellValue("Manhour Sub-total") } CellUtil.setAlignment(manhourCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(manhourCell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourCell, CellUtil.WRAP_TEXT, true) - CellUtil.setCellStyleProperty(manhourCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(manhourCell, "borderBottom", BorderStyle.THIN) - val manhourECell = row.getCell(2+monthColumnEnd+1) ?: row.createCell(2+monthColumnEnd+1) + val manhourECell = row.getCell(2 + monthColumnEnd + 1) ?: row.createCell(2 + monthColumnEnd + 1) manhourECell.apply { setCellValue("Manhour Expenditure (HKD)") } CellUtil.setAlignment(manhourECell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(manhourECell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourECell, CellUtil.WRAP_TEXT, true) - CellUtil.setCellStyleProperty(manhourECell,"borderBottom", BorderStyle.THIN) - sheet.setColumnWidth(2+monthColumnEnd+1, 20*256) + CellUtil.setCellStyleProperty(manhourECell, "borderBottom", BorderStyle.THIN) + sheet.setColumnWidth(2 + monthColumnEnd + 1, 20 * 256) // Manhour Spent LOOP println("monthRange--------------- ${monthRange}\n") rowNum += 1 - for(staff in staffInfoList){ + for (staff in staffInfoList) { val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val staffCell = row.getCell(0) ?: row.createCell(0) var manhourExpenditure = 0.0 @@ -1845,10 +2030,10 @@ open class ReportService( } CellUtil.setCellStyleProperty(teamCell, CellUtil.WRAP_TEXT, true) - for(i in 0 until monthRange.size){ - val cell = row.getCell(i+2) ?: row.createCell(i+2) + for (i in 0 until monthRange.size) { + val cell = row.getCell(i + 2) ?: row.createCell(i + 2) cell.setCellValue(0.0) - for(hourlySpent in staff.getValue("hourlySpent") as List>){ + for (hourlySpent in staff.getValue("hourlySpent") as List>) { cell.apply { if (hourlySpent.getValue("recordDate") == monthRange[i].getValue("date").toString()) { setCellValue(hourlySpent.getValue("totalManHours") as Double) @@ -1861,13 +2046,13 @@ open class ReportService( CellUtil.setVerticalAlignment(cell, VerticalAlignment.CENTER); } - val manhourCell = row.getCell(2+monthRange.size) ?: row.createCell(2+monthRange.size) + val manhourCell = row.getCell(2 + monthRange.size) ?: row.createCell(2 + monthRange.size) manhourCell.apply { - cellFormula="SUM(C${rowNum+1}:${getColumnAlphabet(1+monthRange.size)}${rowNum+1})" + cellFormula = "SUM(C${rowNum + 1}:${getColumnAlphabet(1 + monthRange.size)}${rowNum + 1})" cellStyle.dataFormat = accountingStyle } - val manhourECell = row.getCell(3+monthRange.size) ?: row.createCell(3+monthRange.size) + val manhourECell = row.getCell(3 + monthRange.size) ?: row.createCell(3 + monthRange.size) manhourECell.apply { setCellValue(manhourExpenditure) cellStyle.dataFormat = accountingStyle @@ -1889,19 +2074,19 @@ open class ReportService( rowNum += 1 val totalManhourERow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val startRow = 15+staffInfoList.size - val lastColumnIndex = getColumnAlphabet(3+monthRange.size) + val startRow = 15 + staffInfoList.size + val lastColumnIndex = getColumnAlphabet(3 + monthRange.size) val totalManhourETitleCell = totalManhourERow.getCell(0) ?: totalManhourERow.createCell(0) totalManhourETitleCell.apply { setCellValue("Total Manhour Expenditure (HKD)") } val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) totalManhourECell.apply { - cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow+staffInfoList.size})" + cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(totalManhourETitleCell,"borderBottom", BorderStyle.THIN) - CellUtil.setCellStyleProperty(totalManhourECell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(totalManhourECell, "borderBottom", BorderStyle.THIN) rowNum += 1 val pandlRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) @@ -1911,11 +2096,11 @@ open class ReportService( } val panlCell = pandlRow.getCell(1) ?: pandlRow.createCell(1) panlCell.apply { - cellFormula = "B${rowNum-1}-B${rowNum}" + cellFormula = "B${rowNum - 1}-B${rowNum}" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(panlCellTitle,"borderBottom", BorderStyle.DOUBLE) - CellUtil.setCellStyleProperty(panlCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(panlCellTitle, "borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(panlCell, "borderBottom", BorderStyle.DOUBLE) return workbook } diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index b6be334..8221e96 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -2,11 +2,9 @@ package com.ffii.tsms.modules.report.web import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.ffii.tsms.modules.data.entity.Customer -import com.ffii.tsms.modules.data.entity.StaffRepository +import com.ffii.tsms.modules.data.entity.* //import com.ffii.tsms.modules.data.entity.projections.FinancialStatusReportInfo import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo -import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.data.service.CustomerService import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.project.entity.* @@ -35,11 +33,12 @@ import java.time.LocalDate import java.net.URLEncoder import java.time.format.DateTimeFormatter import org.springframework.stereotype.Controller -import com.ffii.tsms.modules.data.entity.TeamRepository -import com.ffii.tsms.modules.data.entity.CustomerRepository import org.springframework.data.jpa.repository.JpaRepository import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.report.web.model.* +import com.ffii.tsms.modules.timesheet.entity.Timesheet +import org.springframework.data.domain.Example +import org.springframework.data.domain.ExampleMatcher @RestController @RequestMapping("/reports") @@ -87,6 +86,34 @@ class ReportController( .body(ByteArrayResource(reportResult)) } + @PostMapping("/ProjectPotentialDelayReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getProjectPotentialDelayReport(@RequestBody @Valid request: ProjectPotentialDelayReportRequest): ResponseEntity { + + val team = if (request.teamId.lowercase() == "all") null else teamRepository.findById(request.teamId.toLong()).orElse(null) + val searchedTeam = if (team == null) "All" else team.code + " - " +team.name + val client = if (request.clientId.lowercase() == "all") null else customerRepository.findById(request.clientId.toLong()).orElse(null) + val searchedClient = if (client == null) "All" else client.code + " - " +client.name + + val matcher = ExampleMatcher.matching().withIgnoreNullValues() + val exampleQuery = Example.of(Project().apply { + // org.springframework.dao.InvalidDataAccessApiUsageException: Path 'teamLead.team.staff' from root Project must not span a cyclic property reference + // [{ com.ffii.tsms.modules.project.entity.Project@6847e037 }] -teamLead-> [{ com.ffii.tsms.modules.data.entity.Staff@2a4c488b }] -team-> [{ com.ffii.tsms.modules.data.entity.Team@a09acb5 }] -staff-> [{ com.ffii.tsms.modules.data.entity.Staff@2a4c488b }] + // teamLead = team?.staff + customer = client + status = "On-going" + }, matcher) + + val projects = if (team == null) projectRepository.findAll(exampleQuery) else projectRepository.findAll(exampleQuery).filter { it.teamLead == team.staff } + + val projectTasks = projectTaskRepository.findAllByProjectIn(projects) + val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) + val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets) + return ResponseEntity.ok() + .header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } + @PostMapping("/StaffMonthlyWorkHourAnalysisReport") @Throws(ServletRequestBindingException::class, IOException::class) fun StaffMonthlyWorkHourAnalysisReport(@RequestBody @Valid request: StaffMonthlyWorkHourAnalysisReportRequest): ResponseEntity { diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index 70671a7..f908bf3 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -18,6 +18,11 @@ data class ProjectCashFlowReportRequest ( val dateType: String, // "Date", "Month" ) +data class ProjectPotentialDelayReportRequest ( + val teamId: String, + val clientId: String, +) + data class StaffMonthlyWorkHourAnalysisReportRequest ( val id: Long, val yearMonth: YearMonth diff --git a/src/main/resources/templates/report/AR02_Delay Report v02.xlsx b/src/main/resources/templates/report/AR02_Delay Report v02.xlsx index fe0d49339c350e5bcd8b6d0daa9d46d9b5e7b3f8..1d844349d56aafd889458b6a688a8caec9490764 100644 GIT binary patch delta 5173 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

Z7c4IVO z^(KvZ(p+E~)(O1lTq0iDMNeF_H_Kz=j-;dADSHn_B3yI24=_ zdunHD@fcIj6`sZu&*)OcM^A%XHoFyyx9QH{D%hB@Yj@zac=gTUL)noGOTryxhr^P4 z{QlF8yqLByHjIq&jbIew>M(E+B}(K(k~HsMSAP&3UseaJ4=ec-=G)yz+w0kt zCi5l9j=@ik>DeMLS6AuQ{38D)=C`SvI0t3urfm4?&>Hdb8vI0>(>bVzyhVRxx5?}! zTIm_wIj+PxU5R*qnrPjCHGl7Vu1YH^B^dMslzt?M^o!1}B4x{xv5q!V9a!zn!oT~E zj{*tIby+doTUdP{T9*}u3Qd#O8HAH6U#P8E+s)S~#*kK~J?7%y*P~pGF#0Gf#uB@Dg?0R*4Up?GWxy*&nnk9K> zn5VltzT1(&ag4Lr!Q+*|Rk2FR%}R?}|F3tGTo5xO^>xX|Wp!jrC{b@R<&+sNZ6F9k zhW{ffJo%?l+>)}IZQZA=Pt`t<_>DZnnEEZzOq7bJ2ZWz0k=-rtH<))(kI;3vWT27J zfKC@_mjuaTwu*ZXZuXha5jK)|&9Az)3a?NVc$38{G%_EX)5}uyh_2i(c&K7uB83Xx z*<_oPB&mS%e?B%1!%R{Vgv=-1?kcuYQ(U#qy^+@vHH{T~`M<6+oD^~g8%8PohM2cj-2OF ze5#}eNLWy)RE&L+;NGSX;ipB2oCgWdMk?>!oO5hvpDg#G--=Ol_T%N3@@-2hs(uq4 zUOZuaJfYH46xvp&J)CQqjXj@;dWZcvI+eY)PYBgdH%JekWhJvnjZ9Gs6SgNIrBfuN z3F{T@HJ^K zR?uPCZ}t+MDh<(n8vq~$mqz8_uE>`>F=R0e8 zw7;WvxouGcn_4y5;X+Ew^sIBha?(~v;FXg4TCVAcq}mdm<;iyOXK)u`#@R@kEM1R9 z*alz8d4_+#;#kP&jY`-JQvkP;niI1~!cqGcv5h&=OKt3$Nuo4qZa=je6_-!?(Ws6V zyq5llt)KO)6TX=C5%wGDSq%Fy6+Bp=zx8yYoalSSND{_o#qMeAJMUC#R>?SDnyQ$z zzc&_+wh_=M#zY0_P=y$(33+a>IpNcSkj?%yrGfOLQXdCr{FlqC?c(+z;Mk#t--@ng zPtVO9sl7DTcI}!~2VicGMCJt?v{2uT)O6di`uK2Z$~TNByHA&&A;+iXhRdE3WXY+u zDLpjyO-0R19v5ez{d*8|KNlmpms9`51R4jyLi>*`1OQ-08sx6uR_Nol!01gXn80ldK*DT`?t&i0EqvUNlJ|-lVGI#htL4` zm;UcCr$cK=$N{VA(a93Jz*+|Mx`YJp-_3mgTC)Gn27KiN0PH+%^}Id3d<1Phz3tH( i2nxEt|LgtOiS%Fjt~}^^Nf-nS00TJgK@tAH(EkBp`Cyp< delta 7918 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