| @@ -63,6 +63,30 @@ open class ReportService( | |||
| private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" | |||
| // ==============================|| GENERATE REPORT ||============================== // | |||
| fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex | |||
| sheet: Sheet, | |||
| result: List<Map<String, Any>>, | |||
| startRow: Int, | |||
| startColumn: Int | |||
| ): Int { | |||
| var rowIndex = startRow | |||
| var columnIndex = startColumn | |||
| result.forEachIndexed { index, obj -> | |||
| var tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||
| 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) | |||
| when (obj[key]) { | |||
| is Double -> tempCell.setCellValue(obj[key] as Double) | |||
| else -> tempCell.setCellValue(obj[key] as String) | |||
| } | |||
| } | |||
| rowIndex++ | |||
| } | |||
| return rowIndex | |||
| } | |||
| fun genFinancialStatusReport(teamLeadId: Long): ByteArray { | |||
| @@ -1298,20 +1322,8 @@ open class ReportService( | |||
| rowIndex = 5 | |||
| columnIndex = 0 | |||
| result.forEachIndexed { index, obj -> | |||
| tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||
| 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) | |||
| when (obj[key]) { | |||
| is Double -> tempCell.setCellValue(obj[key] as Double) | |||
| else -> tempCell.setCellValue(obj[key] as String) | |||
| } | |||
| } | |||
| rowIndex++ | |||
| } | |||
| generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) | |||
| return workbook | |||
| } | |||
| @@ -1350,20 +1362,9 @@ open class ReportService( | |||
| rowIndex = 6 | |||
| columnIndex = 0 | |||
| result.forEachIndexed { index, obj -> | |||
| tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||
| 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) | |||
| when (obj[key]) { | |||
| is Double -> tempCell.setCellValue(obj[key] as Double) | |||
| else -> tempCell.setCellValue(obj[key] as String) | |||
| } | |||
| } | |||
| rowIndex++ | |||
| } | |||
| // val currRow = generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) | |||
| generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) | |||
| val sheetCF = sheet.sheetConditionalFormatting | |||
| val rule1 = sheetCF.createConditionalFormattingRule("AND(J7 >= $lowerLimit, J7 <= 1)") | |||
| @@ -1529,111 +1530,107 @@ open class ReportService( | |||
| ) | |||
| return jdbcDao.queryForList(sql.toString(), args) | |||
| } | |||
| open fun getProjectCompletionReport(args: Map<String, Any>): List<Map<String, Any>> { | |||
| val sql = StringBuilder( | |||
| "select" | |||
| + " result.code, " | |||
| + " result.name, " | |||
| + " result.teamCode, " | |||
| + " result.custCode, " | |||
| ) | |||
| val sql = StringBuilder("select" | |||
| + " result.code, " | |||
| + " result.name, " | |||
| + " result.teamCode, " | |||
| + " result.custCode, " ) | |||
| if (args.get("outstanding") as Boolean) { | |||
| sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ") | |||
| } | |||
| sql.append( | |||
| " DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd " | |||
| + " from ( " | |||
| + " SELECT " | |||
| + " pt.project_id, " | |||
| + " min(p.code) as code, " | |||
| + " min(p.name) as name, " | |||
| + " min(t.code) as teamCode, " | |||
| + " min(c.code) as custCode, " | |||
| + " min(p.actualEnd) as actualEnd, " | |||
| + " min(p.expectedTotalFee) as projectFee, " | |||
| + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " | |||
| + " FROM ( " | |||
| + " SELECT " | |||
| + " t.staffId, " | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " | |||
| + " t.projectTaskId AS taskId " | |||
| + " FROM timesheet t " | |||
| + " LEFT JOIN staff s ON t.staffId = s.id " | |||
| + " LEFT JOIN team te on s.teamId = te.id " | |||
| + " GROUP BY t.staffId, t.projectTaskId " | |||
| + " order by t.staffId " | |||
| + " ) AS tns " | |||
| + " right join project_task pt ON tns.taskId = pt.id " | |||
| + " left join project p on p.id = pt.project_id " | |||
| + " left JOIN staff s ON p.teamLead = s.id " | |||
| + " left join salary sal on s.salaryId = sal.salaryPoint " | |||
| + " left JOIN team t ON s.teamId = t.id " | |||
| + " left join customer c on c.id = p.customerId " | |||
| + " where p.deleted = false " | |||
| + " and p.status = 'Completed' " | |||
| + " and p.actualEnd BETWEEN :startDate and :endDate " | |||
| + " group by pt.project_id " | |||
| + " ) as result " | |||
| + " left join invoice i on result.code = i.projectCode " | |||
| + " order by result.actualEnd " | |||
| + " from ( " | |||
| + " SELECT " | |||
| + " pt.project_id, " | |||
| + " min(p.code) as code, " | |||
| + " min(p.name) as name, " | |||
| + " min(t.code) as teamCode, " | |||
| + " min(c.code) as custCode, " | |||
| + " min(p.actualEnd) as actualEnd, " | |||
| + " min(p.expectedTotalFee) as projectFee, " | |||
| + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " | |||
| + " FROM ( " | |||
| + " SELECT " | |||
| + " t.staffId, " | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " | |||
| + " t.projectTaskId AS taskId " | |||
| + " FROM timesheet t " | |||
| + " LEFT JOIN staff s ON t.staffId = s.id " | |||
| + " LEFT JOIN team te on s.teamId = te.id " | |||
| + " GROUP BY t.staffId, t.projectTaskId " | |||
| + " order by t.staffId " | |||
| + " ) AS tns " | |||
| + " right join project_task pt ON tns.taskId = pt.id " | |||
| + " left join project p on p.id = pt.project_id " | |||
| + " left JOIN staff s ON p.teamLead = s.id " | |||
| + " left join salary sal on s.salaryId = sal.salaryPoint " | |||
| + " left JOIN team t ON s.teamId = t.id " | |||
| + " left join customer c on c.id = p.customerId " | |||
| + " where p.deleted = false " | |||
| + " and p.status = 'Completed' " | |||
| + " and p.actualEnd BETWEEN :startDate and :endDate " | |||
| + " group by pt.project_id " | |||
| + " ) as result " | |||
| + " left join invoice i on result.code = i.projectCode " | |||
| + " order by result.actualEnd " | |||
| ) | |||
| return jdbcDao.queryForList(sql.toString(), args) | |||
| } | |||
| open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> { | |||
| val sql = StringBuilder( | |||
| "WITH teamNormalConsumed AS (" | |||
| + " SELECT " | |||
| + " s.teamId, " | |||
| + " pt.project_id, " | |||
| + " 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, " | |||
| + " 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 " | |||
| + " JOIN staff s ON tns.staffId = s.id " | |||
| + " JOIN team t ON s.teamId = t.id " | |||
| + " GROUP BY teamId, project_id " | |||
| + " ) " | |||
| + " SELECT " | |||
| + " p.code, " | |||
| + " p.name, " | |||
| + " t.code as team, " | |||
| + " c.code as client, " | |||
| + " p.expectedTotalFee * 0.8 as plannedBudget, " | |||
| + " 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, " | |||
| + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " | |||
| + " CASE " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " | |||
| + " then 'Potential Overconsumption' " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " | |||
| + " then 'Overconsumption' " | |||
| + " else 'Within Budget' " | |||
| + " END as status " | |||
| + " FROM project p " | |||
| + " LEFT JOIN team t ON p.teamLead = t.teamLead " | |||
| + " LEFT JOIN staff s ON p.teamLead = s.id " | |||
| + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " | |||
| + " LEFT JOIN customer c ON p.customerId = c.id " | |||
| + " left join teamNormalConsumed tns on tns.project_id = p.id " | |||
| + " WHERE p.deleted = false " | |||
| + " and p.status = 'On-going' " | |||
| val sql = StringBuilder("WITH teamNormalConsumed AS (" | |||
| + " SELECT " | |||
| + " s.teamId, " | |||
| + " pt.project_id, " | |||
| + " 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, " | |||
| + " 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 " | |||
| + " JOIN staff s ON tns.staffId = s.id " | |||
| + " JOIN team t ON s.teamId = t.id " | |||
| + " GROUP BY teamId, project_id " | |||
| + " ) " | |||
| + " SELECT " | |||
| + " p.code, " | |||
| + " p.name, " | |||
| + " t.code as team, " | |||
| + " c.code as client, " | |||
| + " p.expectedTotalFee * 0.8 as plannedBudget, " | |||
| + " 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, " | |||
| + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " | |||
| + " CASE " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " | |||
| + " then 'Potential Overconsumption' " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " | |||
| + " then 'Overconsumption' " | |||
| + " else 'Within Budget' " | |||
| + " END as status " | |||
| + " FROM project p " | |||
| + " LEFT JOIN team t ON p.teamLead = t.teamLead " | |||
| + " LEFT JOIN staff s ON p.teamLead = s.id " | |||
| + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " | |||
| + " LEFT JOIN customer c ON p.customerId = c.id " | |||
| + " left join teamNormalConsumed tns on tns.project_id = p.id " | |||
| + " WHERE p.deleted = false " | |||
| + " and p.status = 'On-going' " | |||
| ) | |||
| if (args != null) { | |||
| var statusFilter: String = "" | |||
| @@ -2126,10 +2123,10 @@ open class ReportService( | |||
| return workbook | |||
| } | |||
| fun getCostAndExpense(clientId: Long?, teamId: Long?): List<Map<String, Any?>> { | |||
| fun getCostAndExpense(clientId: Long?, teamId: Long?): List<Map<String,Any?>>{ | |||
| 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," | |||
| + " 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" | |||
| @@ -2151,13 +2148,13 @@ open class ReportService( | |||
| + " left join team t2 on t2.id = s.teamId" | |||
| + " where ISNULL(p.code) = False" | |||
| ) | |||
| if (clientId != null) { | |||
| if(clientId != null){ | |||
| sql.append( | |||
| " and c.id = :clientId " | |||
| ) | |||
| } | |||
| if (teamId != null) { | |||
| if(teamId != null){ | |||
| sql.append( | |||
| " and p.teamLead = :teamId " | |||
| ) | |||
| @@ -2176,9 +2173,9 @@ open class ReportService( | |||
| val queryList = jdbcDao.queryForList(sql.toString(), args) | |||
| val costAndExpenseList = mutableListOf<Map<String, Any?>>() | |||
| for (item in queryList) { | |||
| for(item in queryList){ | |||
| val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||
| if (item["code"] !in costAndExpenseList) { | |||
| if(item["code"] !in costAndExpenseList){ | |||
| costAndExpenseList.add( | |||
| mapOf( | |||
| "code" to item["code"], | |||
| @@ -2186,27 +2183,22 @@ open class ReportService( | |||
| "client" to item["client"], | |||
| "teamLead" to item["teamLead"], | |||
| "budget" to item["expectedTotalFee"], | |||
| "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, | |||
| "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double) | |||
| + (hourlyRate * item["otConsumed"] as Double * otFactor) | |||
| "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, | |||
| "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double ) | |||
| + (hourlyRate * item["otConsumed"]as Double * otFactor) | |||
| ) | |||
| ) | |||
| } else { | |||
| }else{ | |||
| val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! | |||
| costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { | |||
| put( | |||
| "totalManhours", | |||
| get("manhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double) | |||
| ) | |||
| put( | |||
| "manhourExpenditure", | |||
| get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double) | |||
| + (hourlyRate * item["otConsumed"] as Double * otFactor)) | |||
| ) | |||
| put("totalManhours", get("manhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)) | |||
| put("manhourExpenditure", get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double ) | |||
| + (hourlyRate * item["otConsumed"]as Double * otFactor))) | |||
| } | |||
| } | |||
| } | |||
| val result = costAndExpenseList.map { item -> | |||
| val result = costAndExpenseList.map { | |||
| item -> | |||
| val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 | |||
| val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) | |||
| val remainingPercent = (budgetRemain / budget) | |||
| @@ -2224,7 +2216,7 @@ open class ReportService( | |||
| teamId: Long?, | |||
| clientId: Long?, | |||
| budgetPercentage: Double? | |||
| ): Workbook { | |||
| ): Workbook{ | |||
| val resource = ClassPathResource(templatePath) | |||
| val templateInputStream = resource.inputStream | |||
| val workbook: Workbook = XSSFWorkbook(templateInputStream) | |||
| @@ -2242,9 +2234,9 @@ open class ReportService( | |||
| rowNum = 2 | |||
| val row2: Row = sheet.getRow(rowNum) | |||
| val row2Cell = row2.getCell(2) | |||
| if (teamId == null) { | |||
| if(teamId == null){ | |||
| row2Cell.setCellValue("All") | |||
| } else { | |||
| }else{ | |||
| val sql = StringBuilder( | |||
| " select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId " | |||
| ) | |||
| @@ -2255,9 +2247,9 @@ open class ReportService( | |||
| rowNum = 3 | |||
| val row3: Row = sheet.getRow(rowNum) | |||
| val row3Cell = row3.getCell(2) | |||
| if (clientId == null) { | |||
| if(clientId == null){ | |||
| row3Cell.setCellValue("All") | |||
| } else { | |||
| }else{ | |||
| val sql = StringBuilder( | |||
| " select c.id, c.name from customer c where c.id = :clientId " | |||
| ) | |||
| @@ -2267,15 +2259,15 @@ open class ReportService( | |||
| val filterList: List<Map<String, Any?>> | |||
| if (budgetPercentage != null) { | |||
| if(budgetPercentage != null){ | |||
| filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) > budgetPercentage } | |||
| } else { | |||
| }else{ | |||
| filterList = costAndExpenseList | |||
| } | |||
| rowNum = 6 | |||
| for (item in filterList) { | |||
| for(item in filterList){ | |||
| val index = filterList.indexOf(item) | |||
| val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) | |||
| val cell = row.getCell(0) ?: row.createCell(0) | |||
| @@ -2320,13 +2312,13 @@ open class ReportService( | |||
| val cell7 = row.getCell(7) ?: row.createCell(7) | |||
| cell7.apply { | |||
| cellFormula = "F${rowNum + 1}-G${rowNum + 1}" | |||
| cellFormula = "F${rowNum+1}-G${rowNum+1}" | |||
| } | |||
| CellUtil.setCellStyleProperty(cell7, "dataFormat", accountingStyle) | |||
| val cell8 = row.getCell(8) ?: row.createCell(8) | |||
| cell8.apply { | |||
| cellFormula = "H${rowNum + 1}/F${rowNum + 1}" | |||
| cellFormula = "H${rowNum+1}/F${rowNum+1}" | |||
| } | |||
| CellUtil.setCellStyleProperty(cell8, "dataFormat", percentStyle) | |||
| @@ -2336,17 +2328,11 @@ open class ReportService( | |||
| return workbook | |||
| } | |||
| fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray { | |||
| fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{ | |||
| val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId) | |||
| val workbook: Workbook = createCostAndExpenseWorkbook( | |||
| COSTANDEXPENSE_REPORT, | |||
| costAndExpenseList, | |||
| request.teamId, | |||
| request.clientId, | |||
| request.budgetPercentage | |||
| ) | |||
| val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.teamId, request.clientId, request.budgetPercentage) | |||
| val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | |||
| workbook.write(outputStream) | |||