| @@ -2,6 +2,7 @@ package com.ffii.tsms.modules.report.service | |||||
| import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
| import com.ffii.tsms.modules.data.entity.* | import com.ffii.tsms.modules.data.entity.* | ||||
| import com.ffii.tsms.modules.data.entity.projections.SalaryEffectiveInfo | |||||
| import com.ffii.tsms.modules.data.service.SalaryEffectiveService | import com.ffii.tsms.modules.data.service.SalaryEffectiveService | ||||
| import com.ffii.tsms.modules.project.entity.Invoice | import com.ffii.tsms.modules.project.entity.Invoice | ||||
| import com.ffii.tsms.modules.project.entity.Milestone | import com.ffii.tsms.modules.project.entity.Milestone | ||||
| @@ -32,6 +33,7 @@ import java.time.format.DateTimeParseException | |||||
| import java.time.temporal.ChronoUnit | import java.time.temporal.ChronoUnit | ||||
| import java.util.* | import java.util.* | ||||
| import java.awt.Color | import java.awt.Color | ||||
| import java.math.RoundingMode | |||||
| import java.time.Year | import java.time.Year | ||||
| import kotlin.collections.ArrayList | import kotlin.collections.ArrayList | ||||
| @@ -123,56 +125,24 @@ open class ReportService( | |||||
| fun genFinancialStatusReport(teamLeadId: Long): ByteArray { | fun genFinancialStatusReport(teamLeadId: Long): ByteArray { | ||||
| val financialStatus: List<Map<String, Any>> = getFinancialStatus(teamLeadId) | val financialStatus: List<Map<String, Any>> = getFinancialStatus(teamLeadId) | ||||
| val otFactor = BigDecimal(1) | |||||
| val tempList = mutableListOf<Map<String, Any>>() | |||||
| 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 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"), | |||||
| "subsidiary" to item.getValue("subsidiary"), | |||||
| "teamLead" to item.getValue("teamLead"), | |||||
| "planStart" to item.getValue("planStart"), | |||||
| "planEnd" to item.getValue("planEnd"), | |||||
| "expectedTotalFee" to item.getValue("expectedTotalFee"), | |||||
| "subContractFee" to item.getValue("subContractFee"), | |||||
| "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")) }!! | |||||
| // Update the existing Map with the new manHourRate and manOtHourRate values | |||||
| tempList[tempList.indexOf(existingMap)] = existingMap.toMutableMap().apply { | |||||
| put("normalConsumed", (get("normalConsumed") as BigDecimal).add(manHourRate)) | |||||
| put("otConsumed", (get("otConsumed") as BigDecimal).add(manOtHourRate)) | |||||
| val manhoursSpent = getManHoursSpentByTeam(teamLeadId) | |||||
| val salaryEffectiveMap = getSalaryEffectiveByTeamLead(teamLeadId) | |||||
| val updatedTimesheetData = updateTimesheetDataWithEffectiveSalary(manhoursSpent, salaryEffectiveMap) | |||||
| val projectsExpenditure = calculateProjectExpenditures(updatedTimesheetData) | |||||
| val updatedList = financialStatus.map { item -> | |||||
| val code = item["code"] as? String | |||||
| val expenditure = projectsExpenditure[code] | |||||
| item.toMutableMap().apply { | |||||
| if (code != null && expenditure != null) { | |||||
| this["totalCumulativeExpenditure"] = expenditure | |||||
| }else{ | |||||
| this["totalCumulativeExpenditure"] = BigDecimal.ZERO | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| // println("tempList---------------------- $tempList") | |||||
| val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, tempList, teamLeadId) | |||||
| val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, updatedList, teamLeadId) | |||||
| val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | ||||
| workbook.write(outputStream) | workbook.write(outputStream) | ||||
| @@ -474,11 +444,12 @@ open class ReportService( | |||||
| } | } | ||||
| val cumExpenditureCell = row.createCell(9) | val cumExpenditureCell = row.createCell(9) | ||||
| 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) | |||||
| // 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) | |||||
| val totalCumulativeExpenditure = item["totalCumulativeExpenditure"]?.let { it as BigDecimal } ?: BigDecimal(0) | |||||
| cumExpenditureCell.apply { | cumExpenditureCell.apply { | ||||
| setCellValue(cumExpenditure.toDouble()) | |||||
| setCellValue(totalCumulativeExpenditure.toDouble()) | |||||
| cellStyle.dataFormat = accountingStyle | cellStyle.dataFormat = accountingStyle | ||||
| } | } | ||||
| @@ -512,7 +483,7 @@ open class ReportService( | |||||
| val projectedCpiCell = row.createCell(14) | val projectedCpiCell = row.createCell(14) | ||||
| projectedCpiCell.apply { | projectedCpiCell.apply { | ||||
| cellFormula = "(H${rowNum}/J${rowNum})" | |||||
| cellFormula = "IF(J${rowNum} = 0, 0, H${rowNum}/J${rowNum})" | |||||
| cellStyle = boldFontCellStyle | cellStyle = boldFontCellStyle | ||||
| } | } | ||||
| @@ -1925,26 +1896,27 @@ open class ReportService( | |||||
| open fun getFinancialStatus(teamLeadId: Long?): List<Map<String, Any>> { | open fun getFinancialStatus(teamLeadId: Long?): List<Map<String, Any>> { | ||||
| val sql = StringBuilder( | 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 (" | |||||
| // " 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" | |||||
| // + " )," | |||||
| " With cte_invoice as (" | |||||
| + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" | + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" | ||||
| + " from invoice i" | + " from invoice i" | ||||
| + " left join project p on p.code = i.projectCode" | + " left join project p on p.code = i.projectCode" | ||||
| + " group by p.code" | + " group by p.code" | ||||
| + " )" | + " )" | ||||
| + " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee, " | + " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee, " | ||||
| + " 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" | |||||
| // + " 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" | |||||
| + " ,0 as totalCumulativeExpenditure " | |||||
| + " from project p" | + " from project p" | ||||
| + " left join cte_timesheet cte_ts on p.code = cte_ts.code" | |||||
| // + " left join cte_timesheet cte_ts on p.code = cte_ts.code" | |||||
| + " left join customer c on c.id = p.customerId" | + " left join customer c on c.id = p.customerId" | ||||
| + " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId" | + " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId" | ||||
| + " left join subsidiary s2 on s2.id = cs.subsidiaryId " | + " left join subsidiary s2 on s2.id = cs.subsidiaryId " | ||||
| @@ -2293,7 +2265,7 @@ open class ReportService( | |||||
| "code" to projectsCode["code"], | "code" to projectsCode["code"], | ||||
| "description" to projectsCode["description"] | "description" to projectsCode["description"] | ||||
| ) | ) | ||||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 10) | |||||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 1) | |||||
| val staffInfoList = mutableListOf<Map<String, Any>>() | val staffInfoList = mutableListOf<Map<String, Any>>() | ||||
| @@ -2302,7 +2274,7 @@ open class ReportService( | |||||
| for (item in manHoursSpent) { | for (item in manHoursSpent) { | ||||
| updateInfo(info, item) | updateInfo(info, item) | ||||
| val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists, queryStartMonth, queryEndMonth) ?: (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||||
| val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists) ?: (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"] }) { | ||||
| @@ -2374,37 +2346,24 @@ open class ReportService( | |||||
| return tempList | return tempList | ||||
| } | } | ||||
| fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>, start: YearMonth, end: YearMonth): Double? { | |||||
| fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>): Double? { | |||||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM") | val formatter = DateTimeFormatter.ofPattern("yyyy-MM") | ||||
| val monthDate = YearMonth.parse(recordDate, formatter) | val monthDate = YearMonth.parse(recordDate, formatter) | ||||
| val staffSalaryData = staffSalaryLists.find { it.staffId == staffId }?.salaryData ?: return null | val staffSalaryData = staffSalaryLists.find { it.staffId == staffId }?.salaryData ?: return null | ||||
| return findSalaryForMonth(staffSalaryData, monthDate, start, end) | |||||
| return findSalaryForMonth(staffSalaryData, monthDate) | |||||
| } | } | ||||
| private fun findSalaryForMonth(salaryDataList: List<SalaryEffectiveService.SalaryData>, targetMonth: YearMonth, start: YearMonth, end: YearMonth): Double? { | |||||
| if (salaryDataList.isEmpty()) return null | |||||
| val periodStartMonth = 10 | |||||
| val financialPeriods = getHalfYearFinancialPeriods(start, end, periodStartMonth) | |||||
| // First, try to find a salary in the current financial period | |||||
| val currentFinancialPeriod = financialPeriods.find { targetMonth in it.start..it.end } | |||||
| val currentSalary = currentFinancialPeriod?.let { period -> | |||||
| salaryDataList.find { salaryData -> | |||||
| val salaryYearMonth = YearMonth.from(salaryData.financialYear) | |||||
| salaryYearMonth in period.start..period.end | |||||
| }?.hourlyRate?.toDouble() | |||||
| } | |||||
| if (currentSalary != null) { | |||||
| return currentSalary | |||||
| } | |||||
| // If not found, find the most recent previous salary | |||||
| return findPreviousValue(salaryDataList, targetMonth) { it.hourlyRate.toDouble() } | |||||
| private fun findSalaryForMonth(salaryDataList: List<SalaryEffectiveService.SalaryData>, targetMonth: YearMonth): Double? { | |||||
| return salaryDataList | |||||
| .filter { salaryData -> | |||||
| val effectiveDate = YearMonth.from(salaryData.financialYear) | |||||
| effectiveDate <= targetMonth | |||||
| } | |||||
| .maxByOrNull { it.financialYear } | |||||
| ?.hourlyRate | |||||
| ?.toDouble() | |||||
| } | } | ||||
| fun updateStaffFinancialYears( | fun updateStaffFinancialYears( | ||||
| @@ -2430,7 +2389,7 @@ open class ReportService( | |||||
| } | } | ||||
| fun updateFinancialYear(financialYear: FinancialYear, salaryDataList: List<SalaryEffectiveService.SalaryData>?, staffInfo: Map<String, Any>): FinancialYear { | fun updateFinancialYear(financialYear: FinancialYear, salaryDataList: List<SalaryEffectiveService.SalaryData>?, staffInfo: Map<String, Any>): FinancialYear { | ||||
| println("====================== staffInfo: $staffInfo ===============================") | |||||
| // println("====================== staffInfo: $staffInfo ===============================") | |||||
| if(salaryDataList == null){ | if(salaryDataList == null){ | ||||
| return financialYear.copy( | return financialYear.copy( | ||||
| @@ -2444,7 +2403,7 @@ open class ReportService( | |||||
| salaryYearMonth >= financialYear.start && salaryYearMonth <= financialYear.end | salaryYearMonth >= financialYear.start && salaryYearMonth <= financialYear.end | ||||
| } | } | ||||
| println("====================== relevantSalaryData: $relevantSalaryData ===============================") | |||||
| // println("====================== relevantSalaryData: $relevantSalaryData ===============================") | |||||
| if (relevantSalaryData != null) { | if (relevantSalaryData != null) { | ||||
| return financialYear.copy( | return financialYear.copy( | ||||
| @@ -2455,7 +2414,7 @@ open class ReportService( | |||||
| val previousHourlyRate = findPreviousValue(salaryDataList, financialYear.start) { it.hourlyRate } | val previousHourlyRate = findPreviousValue(salaryDataList, financialYear.start) { it.hourlyRate } | ||||
| val previousSalaryPoint = findPreviousValue(salaryDataList, financialYear.start) { it.salaryPoint } | val previousSalaryPoint = findPreviousValue(salaryDataList, financialYear.start) { it.salaryPoint } | ||||
| println("====================== staffInfo: $staffInfo ===============================") | |||||
| // println("====================== staffInfo: $staffInfo ===============================") | |||||
| return financialYear.copy( | return financialYear.copy( | ||||
| hourlyRate = previousHourlyRate?.toDouble() ?: financialYear.hourlyRate, | hourlyRate = previousHourlyRate?.toDouble() ?: financialYear.hourlyRate, | ||||
| salaryPoint = previousSalaryPoint ?: financialYear.salaryPoint | salaryPoint = previousSalaryPoint ?: financialYear.salaryPoint | ||||
| @@ -2497,6 +2456,7 @@ open class ReportService( | |||||
| } | } | ||||
| // 6 months as a period | // 6 months as a period | ||||
| // PeriodStartMonth can be changed depends on the client, now we will be using Jan as Start Month | |||||
| fun getHalfYearFinancialPeriods( | fun getHalfYearFinancialPeriods( | ||||
| startDate: YearMonth, | startDate: YearMonth, | ||||
| endDate: YearMonth, | endDate: YearMonth, | ||||
| @@ -2631,7 +2591,7 @@ open class ReportService( | |||||
| val convertEndMonth = YearMonth.of(endDate.year, endDate.month) | val convertEndMonth = YearMonth.of(endDate.year, endDate.month) | ||||
| val monthRange: MutableList<Map<String, Any>> = ArrayList() | val monthRange: MutableList<Map<String, Any>> = ArrayList() | ||||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 10) | |||||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 1) | |||||
| var currentDate = startDate | var currentDate = startDate | ||||
| if (currentDate == endDate) { | if (currentDate == endDate) { | ||||
| @@ -2929,8 +2889,8 @@ open class ReportService( | |||||
| } | } | ||||
| val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) | val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) | ||||
| totalManhourECell.apply { | totalManhourECell.apply { | ||||
| // cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" | |||||
| setCellValue(info.getValue("manhourExpenditure") as Double) | |||||
| cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" | |||||
| // setCellValue(info.getValue("manhourExpenditure") as Double) | |||||
| cellStyle.dataFormat = accountingStyle | cellStyle.dataFormat = accountingStyle | ||||
| } | } | ||||
| CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) | CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) | ||||
| @@ -2985,31 +2945,19 @@ open class ReportService( | |||||
| fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String, Any?>> { | fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String, Any?>> { | ||||
| val sql = StringBuilder( | 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" | |||||
| + " )" | |||||
| + " select p.code, p.description, c.name as client, IFNULL(s2.name, \'NA\') as subsidiary, concat(t.code, \' - \', t.name) as teamLead, p.expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee," | |||||
| + " SUM(IFNULL(cte_ts.normalConsumed, 0)) as normalConsumed," | |||||
| + " SUM(IFNULL(cte_ts.otConsumed, 0)) as otConsumed," | |||||
| + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate" | |||||
| + " 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 customer_subsidiary cs on cs.id = p.customerSubsidiaryId" | |||||
| + " left join subsidiary s2 on s2.id = cs.subsidiaryId " | |||||
| + " left join tsmsdb.team t on t.teamLead = p.teamLead" | |||||
| + " 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 ISNULL(p.code) = False" | |||||
| " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, " + | |||||
| " concat(t.code, ' - ', t.name) as teamLead, ifnull(p.expectedTotalFee, 0) as expectedTotalFee, " + | |||||
| " ifnull(p.subContractFee, 0) as subContractFee, " + | |||||
| " 0 as totalCumulativeExpenditure " + | |||||
| " from project p " + | |||||
| " left join customer c on c.id = p.customerId " + | |||||
| " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId " + | |||||
| " left join subsidiary s2 on s2.id = cs.subsidiaryId " + | |||||
| " left join tsmsdb.team t on t.teamLead = p.teamLead " + | |||||
| " where ISNULL(p.code) = False " + | |||||
| " order by p.code" | |||||
| ) | ) | ||||
| if (clientId != null) { | if (clientId != null) { | ||||
| if (type == "client") { | if (type == "client") { | ||||
| sql.append( | sql.append( | ||||
| @@ -3025,57 +2973,39 @@ open class ReportService( | |||||
| if (teamId != null) { | if (teamId != null) { | ||||
| sql.append( | sql.append( | ||||
| " and p.teamLead = :teamId " | |||||
| " and p.teamLead = :teamLeadId " | |||||
| ) | ) | ||||
| } | } | ||||
| sql.append( | |||||
| " group by p.code, p.description , c.name, teamLead, p.expectedTotalFee, p.subContractFee , hourlyRate, s2.name order by p.code" | |||||
| ) | |||||
| val args = mapOf( | val args = mapOf( | ||||
| "clientId" to clientId, | "clientId" to clientId, | ||||
| "teamId" to teamId | |||||
| "teamLeadId" to teamId | |||||
| ) | ) | ||||
| val manhoursSpent = getManHoursSpentByTeam(teamId) | |||||
| val salaryEffectiveMap = getSalaryEffectiveByTeamLead(teamId) | |||||
| val updatedTimesheetData = updateTimesheetDataWithEffectiveSalary(manhoursSpent, salaryEffectiveMap) | |||||
| val projectsExpenditure = calculateProjectExpenditures(updatedTimesheetData) | |||||
| val otFactor = BigDecimal(1).toDouble() | |||||
| val queryList = jdbcDao.queryForList(sql.toString(), args) | |||||
| val costAndExpenseList = mutableListOf<Map<String, Any?>>() | |||||
| val costAndExpenseList = jdbcDao.queryForList(sql.toString(), args) | |||||
| for (item in queryList) { | |||||
| val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||||
| if (item["code"] !in costAndExpenseList.map { it["code"] }) { | |||||
| costAndExpenseList.add( | |||||
| mapOf( | |||||
| "code" to item["code"], | |||||
| "description" to item["description"], | |||||
| "client" to item["client"], | |||||
| "subsidiary" to item["subsidiary"], | |||||
| "teamLead" to item["teamLead"], | |||||
| "budget" to item["expectedTotalFee"] as Double - item["subContractFee"] as Double, | |||||
| "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, | |||||
| "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double) | |||||
| + (hourlyRate * item["otConsumed"] as Double * otFactor) | |||||
| ) | |||||
| ) | |||||
| } else { | |||||
| val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! | |||||
| costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { | |||||
| put( | |||||
| "totalManhours", | |||||
| get("totalManhours") 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 updatedList = costAndExpenseList.map { item -> | |||||
| val code = item["code"] as? String | |||||
| val expenditure = projectsExpenditure[code] | |||||
| item.toMutableMap().apply { | |||||
| if (code != null && expenditure != null) { | |||||
| this["totalCumulativeExpenditure"] = expenditure | |||||
| } else { | |||||
| this["totalCumulativeExpenditure"] = BigDecimal.ZERO | |||||
| } | } | ||||
| put("budget", item["expectedTotalFee"] as Double - item["subContractFee"] as Double) | |||||
| } | } | ||||
| } | } | ||||
| val result = costAndExpenseList.map { item -> | |||||
| val result = updatedList.map { item -> | |||||
| val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 | val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 | ||||
| val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) | |||||
| val budgetRemain = budget - (item["totalCumulativeExpenditure"] as? Double ?: 0.0) | |||||
| val remainingPercent = (budgetRemain / budget) | val remainingPercent = (budgetRemain / budget) | ||||
| item.toMutableMap().apply { | item.toMutableMap().apply { | ||||
| put("budgetPercentage", remainingPercent) | put("budgetPercentage", remainingPercent) | ||||
| @@ -3193,7 +3123,7 @@ open class ReportService( | |||||
| CellUtil.setCellStyleProperty(cell6, "dataFormat", accountingStyle) | CellUtil.setCellStyleProperty(cell6, "dataFormat", accountingStyle) | ||||
| val cell7 = row.getCell(7) ?: row.createCell(7) | val cell7 = row.getCell(7) ?: row.createCell(7) | ||||
| val manHoutsSpentCost = item["manhourExpenditure"] as Double | |||||
| val manHoutsSpentCost = (item["totalCumulativeExpenditure"] as BigDecimal).toDouble() | |||||
| cell7.apply { | cell7.apply { | ||||
| setCellValue(manHoutsSpentCost) | setCellValue(manHoutsSpentCost) | ||||
| } | } | ||||
| @@ -3842,4 +3772,239 @@ open class ReportService( | |||||
| return workbook | return workbook | ||||
| } | } | ||||
| // Use to Calculate cummunlative expenditure | |||||
| data class TimesheetData( | |||||
| val normalConsumed: Double, | |||||
| val otConsumed: Double, | |||||
| val recordDate: LocalDate, | |||||
| val staffId: Long, | |||||
| val hourlyRate: BigDecimal, | |||||
| val salaryPoint: Int, | |||||
| val projectCode: String, | |||||
| val planStart: LocalDate, | |||||
| val planEnd: LocalDate | |||||
| ) | |||||
| data class SalaryEffectiveInfo( | |||||
| val staffId: Long, | |||||
| val effectiveDate: LocalDate, | |||||
| val hourlyRate: BigDecimal, | |||||
| val salaryPoint: Int | |||||
| ) | |||||
| data class ProjectSummary( | |||||
| val staffData: Map<Long, StaffSummary>, | |||||
| val projectCumulativeExpenditure: BigDecimal | |||||
| ) | |||||
| data class StaffSummary( | |||||
| val monthlyData: Map<YearMonth, MonthSummary>, | |||||
| val staffCumulativeExpenditure: BigDecimal | |||||
| ) | |||||
| data class MonthSummary( | |||||
| val hourlyRate: BigDecimal, | |||||
| val salaryPoint: Int, | |||||
| val totalNormalHours: Double, | |||||
| val totalOTHours: Double | |||||
| ) { | |||||
| val otFactor = BigDecimal(1.0) | |||||
| val totalHours: Double = totalNormalHours + totalOTHours | |||||
| val totalCost: BigDecimal = (hourlyRate * BigDecimal(totalNormalHours) + BigDecimal(totalOTHours) * otFactor * hourlyRate).setScale(2, RoundingMode.HALF_UP) | |||||
| } | |||||
| // Get all the timesheet data by Team Lead | |||||
| fun getManHoursSpentByTeam(teamLeadId: Long?): List<TimesheetData>{ | |||||
| val sql = StringBuilder( | |||||
| "select coalesce(t.normalConsumed, 0) as normalConsumed, coalesce(t.otConsumed, 0) as otConsumed, t.recordDate, t.staffId, s2.hourlyRate, s2.salaryPoint, p.code, p.planStart, p.planEnd" | |||||
| + " from timesheet t" | |||||
| + " left join project p on t.projectId = p.id" | |||||
| + " left join staff s on t.staffId = s.id" | |||||
| + " left join salary s2 on s.salaryId = s2.salaryPoint" | |||||
| + " where t.projectId in" | |||||
| + " (" | |||||
| + " select p.id from project p" | |||||
| + " where p.status = 'On-going'" | |||||
| ) | |||||
| if (teamLeadId != null){ | |||||
| sql.append( "and p.teamLead = :teamLeadId " ) | |||||
| } | |||||
| sql.append(" ) and t.recordDate >= p.actualStart ") | |||||
| sql.append(" order by code, recordDate, staffId; ") | |||||
| val results = jdbcDao.queryForList(sql.toString(), mapOf("teamLeadId" to teamLeadId)).map { | |||||
| result -> | |||||
| TimesheetData( | |||||
| result["normalConsumed"] as Double, | |||||
| result["otConsumed"] as Double, | |||||
| (result["recordDate"] as java.sql.Date).toLocalDate(), | |||||
| (result["staffId"] as Int).toLong(), | |||||
| result["hourlyRate"] as BigDecimal, | |||||
| result["salaryPoint"] as Int, | |||||
| result["code"] as String, | |||||
| (result["planStart"] as java.sql.Date).toLocalDate(), | |||||
| (result["planEnd"] as java.sql.Date).toLocalDate() | |||||
| ) | |||||
| } | |||||
| return results | |||||
| } | |||||
| // Get corresponding Salary Effective Data by Team Lead | |||||
| fun getSalaryEffectiveByTeamLead(teamLeadId: Long?): Map<Long, List<SalaryEffectiveInfo>> { | |||||
| val sql = StringBuilder( | |||||
| " select se.*, s.hourlyRate, s.salaryPoint" | |||||
| + " from salary_effective se" | |||||
| + " left join salary s on s.salaryPoint = se.salaryId" | |||||
| + " where se.staffId in" | |||||
| + " (" | |||||
| + " select distinct t.staffId" | |||||
| + " from timesheet t" | |||||
| + " left join project p on t.projectId = p.id" | |||||
| + " where t.projectId in" | |||||
| + " (" | |||||
| + " select p.id from project p" | |||||
| + " where p.status = 'On-going'" | |||||
| ) | |||||
| if(teamLeadId != null){ | |||||
| sql.append(" and p.teamLead = :teamLeadId ") | |||||
| } | |||||
| sql.append(" )) order by staffId, salaryId, date ") | |||||
| val results = jdbcDao.queryForList(sql.toString(), mapOf("teamLeadId" to teamLeadId)).map { | |||||
| result -> | |||||
| SalaryEffectiveInfo( | |||||
| (result["staffId"] as Int).toLong(), | |||||
| (result["date"] as java.sql.Date).toLocalDate(), | |||||
| result["hourlyRate"] as BigDecimal, | |||||
| result["salaryPoint"] as Int, | |||||
| ) | |||||
| }.groupBy { it.staffId } | |||||
| return results | |||||
| } | |||||
| // Update corresponding hourly rate and salary point if there is salary modification during the project period | |||||
| fun updateTimesheetDataWithEffectiveSalary( | |||||
| timesheetDataList: List<TimesheetData>, | |||||
| salaryEffectiveMap: Map<Long, List<SalaryEffectiveInfo>> | |||||
| ): List<TimesheetData> { | |||||
| return timesheetDataList.map { timesheetData -> | |||||
| // Check if the staffId exists in the salaryEffectiveMap | |||||
| if (salaryEffectiveMap.containsKey(timesheetData.staffId)) { | |||||
| val effectiveSalaryList = salaryEffectiveMap[timesheetData.staffId]!! | |||||
| // Find the nearest effective date that is less than or equal to the record date | |||||
| val nearestEffectiveSalary = effectiveSalaryList | |||||
| .filter { YearMonth.from(it.effectiveDate) <= YearMonth.from(timesheetData.recordDate) } | |||||
| .maxByOrNull { it.effectiveDate } | |||||
| if (nearestEffectiveSalary != null) { | |||||
| timesheetData.copy( | |||||
| hourlyRate = nearestEffectiveSalary.hourlyRate, | |||||
| salaryPoint = nearestEffectiveSalary.salaryPoint | |||||
| ) | |||||
| } else { | |||||
| timesheetData | |||||
| } | |||||
| } else { | |||||
| // If the staffId is not in the salaryEffectiveMap, return the original timesheetData | |||||
| timesheetData | |||||
| } | |||||
| } | |||||
| } | |||||
| // Calculate the project expenditure, and group by project Code | |||||
| // { | |||||
| // "M-0534": { | |||||
| // "3": 294953.13, | |||||
| // "184": 4564389.06, | |||||
| // "47": 148769.82, | |||||
| // "36": 592376.60, | |||||
| // "179": 127758.41, | |||||
| // "124": 4478.13, | |||||
| // "123": 287.50, | |||||
| // "116": 1575.00 | |||||
| // }, | |||||
| // "M-0553": { | |||||
| // "184": 1932268.75, | |||||
| // "46": 269365.11, | |||||
| // "47": 717492.84, | |||||
| // "18": 64716.51, | |||||
| // "3": 2956.25, | |||||
| // "124": 9084.38, | |||||
| // "123": 59.38 | |||||
| // }, | |||||
| // ... | |||||
| // } | |||||
| fun calculateProjectExpenditures(timesheetDataList: List<TimesheetData>): Map<String, BigDecimal> { | |||||
| val otFactor = BigDecimal(1.0) | |||||
| return timesheetDataList | |||||
| .groupBy { it.projectCode } | |||||
| .mapValues { (_, projectTimesheets) -> | |||||
| projectTimesheets.fold(BigDecimal.ZERO) { acc, timesheet -> | |||||
| val normalExpenditure = timesheet.hourlyRate * BigDecimal(timesheet.normalConsumed) | |||||
| val otExpenditure = timesheet.hourlyRate * BigDecimal(timesheet.otConsumed) * otFactor | |||||
| acc + normalExpenditure + otExpenditure | |||||
| }.setScale(2, RoundingMode.HALF_UP) | |||||
| } | |||||
| } | |||||
| // Update timesheet data with salary effective data, then group by project code, group by staff Id and group by Year Month | |||||
| // Data foramt: | |||||
| // "M-0976": { | |||||
| // "184": { | |||||
| // "2021-02": { | |||||
| // "hourlyRate": 256.25, | |||||
| // "salaryPoint": 18, | |||||
| // "totalNormalHours": 36.0, | |||||
| // "totalOTHours": 0.0, | |||||
| // "totalHours": 36.0, | |||||
| // "totalCost": 9225.00 | |||||
| // }, | |||||
| // "2021-03": { | |||||
| // "hourlyRate": 256.25, | |||||
| // "salaryPoint": 18, | |||||
| // "totalNormalHours": 33.0, | |||||
| // "totalOTHours": 0.0, | |||||
| // "totalHours": 33.0, | |||||
| // "totalCost": 8456.25 | |||||
| // }, | |||||
| // ... | |||||
| // } | |||||
| fun sumTimesheetDataByMonth(timesheetDataList: List<TimesheetData>): Map<String, ProjectSummary> { | |||||
| return timesheetDataList | |||||
| .groupBy { it.projectCode } | |||||
| .mapValues { (_, projectTimesheets) -> | |||||
| val staffData = projectTimesheets.groupBy { it.staffId } | |||||
| .mapValues { (_, staffTimesheets) -> | |||||
| val monthlyData = staffTimesheets.groupBy { timesheet -> | |||||
| YearMonth.from(timesheet.recordDate) | |||||
| }.mapValues { (_, monthTimesheets) -> | |||||
| MonthSummary( | |||||
| hourlyRate = monthTimesheets.maxByOrNull { it.recordDate }?.hourlyRate ?: BigDecimal.ZERO, | |||||
| salaryPoint = monthTimesheets.maxByOrNull { it.recordDate }?.salaryPoint ?: 0, | |||||
| totalNormalHours = monthTimesheets.sumOf { it.normalConsumed }, | |||||
| totalOTHours = monthTimesheets.sumOf { it.otConsumed } | |||||
| ) | |||||
| } | |||||
| val staffCumulativeExpenditure = monthlyData.values | |||||
| .sumOf { it.totalCost } | |||||
| .setScale(2, RoundingMode.HALF_UP) | |||||
| StaffSummary(monthlyData, staffCumulativeExpenditure) | |||||
| } | |||||
| val projectCumulativeExpenditure = staffData.values | |||||
| .sumOf { it.staffCumulativeExpenditure } | |||||
| .setScale(2, RoundingMode.HALF_UP) | |||||
| ProjectSummary(staffData, projectCumulativeExpenditure) | |||||
| } | |||||
| } | |||||
| } | } | ||||