diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt b/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt index a828556..857b0f3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt @@ -5,11 +5,14 @@ import org.springframework.beans.factory.annotation.Value import java.time.LocalDate interface SalaryEffectiveInfo { + val id: Long? val date: LocalDate val salary: Salary @get:Value("#{target.staff?.name}") val name: String? + @get:Value("#{target.staff?.id}") + val idInStaff: Long @get:Value("#{target.staff?.staffId}") val staffId: String? @get:Value("#{target.staff?.grade?.name}") diff --git a/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt b/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt index f78fd42..4ab6e32 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt @@ -2,11 +2,13 @@ package com.ffii.tsms.modules.data.service import com.ffii.core.support.AbstractIdEntityService import com.ffii.core.support.JdbcDao +import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.SalaryEffective import com.ffii.tsms.modules.data.entity.SalaryEffectiveRepository import com.ffii.tsms.modules.data.entity.SalaryRepository import com.ffii.tsms.modules.data.entity.StaffRepository import org.springframework.stereotype.Service +import java.math.BigDecimal import java.time.LocalDate @Service @@ -14,7 +16,7 @@ open class SalaryEffectiveService( private val salaryEffectiveRepository: SalaryEffectiveRepository, private val staffRepository: StaffRepository, private val salaryRepository: SalaryRepository, - private val jdbcDao: JdbcDao, + jdbcDao: JdbcDao, ) : AbstractIdEntityService(jdbcDao, salaryEffectiveRepository) { // open fun combo(args: Map): List> { @@ -63,4 +65,27 @@ open class SalaryEffectiveService( return salaryEffective } + + data class SalaryData(val idInStaff: Long, val staffId: String, val financialYear: LocalDate, val hourlyRate: BigDecimal, val salaryPoint: Int) + data class StaffSalaryData(val staffId: String, val salaryData: List) + + open fun getStaffSalaryDataByProjectId(projectId: Long): List { + + val sql = StringBuilder( + " select distinct t.staffId from timesheet t where t.projectId = :projectId " + ) + val staffIdList = jdbcDao.queryForList(sql.toString(), mapOf("projectId" to projectId)) + + val temp = staffIdList.mapNotNull{ + it["staffId"].toString().toLong() + } + + val salaryEffectiveLists = salaryEffectiveRepository.findSalaryEffectiveInfoByStaffIdInOrderByStaffId(temp) + .map { SalaryData(it.idInStaff, it.staffId!!, it.date, it.salary.hourlyRate, it.salary.salaryPoint) } + .groupBy { it.staffId } + .map { (staffId, salaryData) -> StaffSalaryData(staffId, salaryData.sortedBy { it.financialYear }) } + +// println(salaryEffectiveLists) + return salaryEffectiveLists + } } \ 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 356575c..9010c7b 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 @@ -5,8 +5,11 @@ import com.ffii.core.utils.ExcelUtils import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.Grade import com.ffii.tsms.modules.data.entity.Salary +import com.ffii.tsms.modules.data.entity.SalaryEffective +import com.ffii.tsms.modules.data.entity.SalaryEffectiveRepository import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team +import com.ffii.tsms.modules.data.service.SalaryEffectiveService import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project @@ -37,6 +40,7 @@ import java.time.ZoneId import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit import java.util.* +import kotlin.collections.ArrayList data class DayInfo(val date: String?, val weekday: String?) @@ -45,7 +49,8 @@ data class DayInfo(val date: String?, val weekday: String?) @Service open class ReportService( private val jdbcDao: JdbcDao, - private val projectRepository: ProjectRepository + private val projectRepository: ProjectRepository, + private val salaryEffectiveService: SalaryEffectiveService ) { private val logger: Log = LogFactory.getLog(javaClass) private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") @@ -2121,6 +2126,37 @@ open class ReportService( return jdbcDao.queryForList(sql.toString(), args) } + fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List): Double? { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM") + val monthDate = YearMonth.parse(recordDate, formatter) + + var hourlyRate: Double? = null +// var financialYear = monthDate + + val staffSalaryData = staffSalaryLists.find { it.staffId == staffId } + staffSalaryData?.salaryData?.forEach { salaryData -> + val financialYearStart = YearMonth.of(salaryData.financialYear.year, 10) + val financialYearEnd = YearMonth.of(salaryData.financialYear.year + 1, 9) + + if ((monthDate.isAfter(financialYearStart) || monthDate.equals(financialYearStart)) && (monthDate.isBefore(financialYearEnd) || monthDate.equals(financialYearEnd))) { +// if(staffId == "B627"){ +// println("----------------- Start -------------------------") +// println("financialYearStart: ${financialYearStart}") +// println("financialYearEnd: ${financialYearEnd}") +// println("monthDate: ${monthDate}") +// println("Result: ${(monthDate.isAfter(financialYearStart) || monthDate.equals(financialYearStart)) && monthDate.isBefore(financialYearEnd)}") +// println("hourlyRate: ${salaryData.hourlyRate.toDouble()}") +// println("---------------------- End -----------------------------") +// } + hourlyRate = salaryData.hourlyRate.toDouble() +// financialYear = YearMonth.parse(salaryData.financialYear.toString()) + return@forEach + } + } + + return hourlyRate + } + open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List> { val sql = StringBuilder( " with cte_timesheet as (" @@ -2189,16 +2225,14 @@ open class ReportService( "endMonth" to queryEndMonth.toString(), ) + // All recorded man hours of the project val manHoursSpent = jdbcDao.queryForList(sql.toString(), args) + val projectCodeSql = StringBuilder( "select p.code, p.description from project p where p.deleted = false and p.id = :projectId" ) - val args2 = mapOf( - "projectId" to projectId, - ) - val projectsCode = jdbcDao.queryForMap(projectCodeSql.toString(), args).get() val otFactor = BigDecimal(1).toDouble() @@ -2212,16 +2246,11 @@ open class ReportService( "code" to projectsCode["code"], "description" to projectsCode["description"] ) - val financialYears = getFinancialYearDates(queryStartMonth, queryEndMonth, 10) val staffInfoList = mutableListOf>() -// println("manHoursSpent------- ${manHoursSpent}") - for (financialYear in financialYears) { -// println("${financialYear.start.year}-${financialYear.start.monthValue} - ${financialYear.end.year}-${financialYear.end.monthValue}") - println("financialYear--------- ${financialYear.start} - ${financialYear.end}") - } + val staffSalaryLists = salaryEffectiveService.getStaffSalaryDataByProjectId(projectId) for (item in manHoursSpent) { if (info["teamLead"] != item.getValue("teamLead")) { @@ -2248,8 +2277,18 @@ open class ReportService( if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } - val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - val recordDate = item.getValue("recordDate") as String + +// val stadd = item.getValue("staffId") as String +// println("StaffId: ${stadd}") +// println( +// "HourlyRate: " + +// "${ +// getSalaryForMonth( +// item.getValue("recordDate") as String, +// item.getValue("staffId") as String, +// staffSalaryLists +// )}") + 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"] }) { @@ -2312,14 +2351,47 @@ open class ReportService( staffInfoList[updatedIndex] = updatedMap } } -// println("staffInfoList----------------- $staffInfoList") +// println("staffInfoList----------------- ${staffInfoList.filter { it["staffId"] == "B627" }}") // println("info----------------- $info") + staffInfoList.forEachIndexed { index, staff -> + val staffId = staff["staffId"] + val staffSalaryData = staffSalaryLists.find { it.staffId == staffId } +// println("----------------------- $staffId ---------------------------") +// println(staff) + + val financialYears = (staff["financialYears"] as List).map { year -> + val relevantSalaryData = staffSalaryData?.salaryData?.lastOrNull { + year.isYearMonthInFinancialYear(YearMonth.from(it.financialYear)) + } +// println("it.financialYear: ${relevantSalaryData?.financialYear}") +// println("year: ${year.start}- ${year.end}") +// println("it.salaryPoint: ${relevantSalaryData?.salaryPoint}") +// println("it.staffId: ${relevantSalaryData?.staffId}") + + // Create a new FinancialYear object with the updated salaryPoint + year.copy( + start = year.start, + end = year.end, + salaryPoint = relevantSalaryData?.salaryPoint ?: staff["salaryPoint"] as Int, + hourlyRate = relevantSalaryData?.hourlyRate?.toDouble() ?: staff["hourlyRate"] as Double + ) + } + + // Create a new map with the updated financialYears + val updatedStaff = staff.toMutableMap().apply { + this["financialYears"] = financialYears + } + + // Update the staffInfoList with the new staff map + staffInfoList[index] = updatedStaff + } + tempList.add(mapOf("info" to info)) tempList.add(mapOf("staffInfoList" to staffInfoList)) // println("Only Staff Info List --------------------- ${tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") - println("tempList----------------- $tempList") +// println("tempList----------------- ${tempList.filter { it["staffId"] == "B627" }}") return tempList } @@ -2340,13 +2412,21 @@ open class ReportService( } // For Calculating the Financial Year - data class FinancialYear(val start: YearMonth, val end: YearMonth, val hourlyRate: Double) + data class FinancialYear(val start: YearMonth, val end: YearMonth, var hourlyRate: Double, var salaryPoint: Int){ + fun isYearMonthInFinancialYear(salaryEffectiveMonth: YearMonth) :Boolean{ + if ((salaryEffectiveMonth.isAfter(start) || salaryEffectiveMonth.equals(start)) && + (salaryEffectiveMonth.isBefore(end) || salaryEffectiveMonth.equals(end))){ + return true + } + return false + } + } fun getFinancialYearDates( startDate: YearMonth, endDate: YearMonth, financialYearStartMonth: Int - ): Array { + ): List { val financialYearDates = mutableListOf() var currentYear = startDate.year @@ -2375,7 +2455,8 @@ open class ReportService( val financialYear = FinancialYear( start = YearMonth.of(startYear, startMonth), end = YearMonth.of(endYear, endMonth), - hourlyRate = 0.0 + hourlyRate = 0.0, + salaryPoint = 0 ) financialYearDates.add(financialYear) @@ -2386,9 +2467,10 @@ open class ReportService( } } - return financialYearDates.toTypedArray() + return financialYearDates } + fun createPandLReportWorkbook( templatePath: String, manhoursSpent: List>, @@ -2422,6 +2504,7 @@ open class ReportService( val convertEndMonth = YearMonth.of(endDate.year, endDate.month) val monthRange: MutableList> = ArrayList() + val financialYears = getFinancialYearDates(queryStartMonth, queryEndMonth, 10) var currentDate = startDate if (currentDate == endDate) { @@ -2520,12 +2603,35 @@ open class ReportService( } row6Cell.setCellValue(clientSubsidiary) + // Average Hourly Rate by Pay Scale Point + rowNum = 8 + val row8: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 2, (financialYears.size+1)*2-1)) + val row8Cell = row8.getCell(2) ?: row8.createCell(2) + row8Cell.apply { + setCellValue("Average Hourly Rate by Pay Scale Point") + } + CellUtil.setAlignment(row8Cell, HorizontalAlignment.CENTER); + CellUtil.setVerticalAlignment(row8Cell, VerticalAlignment.CENTER); + CellUtil.setCellStyleProperty(row8Cell, CellUtil.WRAP_TEXT, true) + + // Need to be updated to financial year // Base on the searching criteria rowNum = 9 - val row9: Row = sheet.getRow(rowNum) - val row9Cell = row9.getCell(3) - row9Cell.setCellValue("${convertStartMonth.format(monthFormat)} - ${convertEndMonth.format(monthFormat)}") + val row9: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) + var column = 2 + financialYears.indices.forEach { i -> + val row9Cell = row9.getCell(column) ?: row9.createCell(column) + val row9Cell2 = row9.getCell(column+1) ?: row9.createCell(column+1) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, column, column + 1)) + row9Cell.setCellValue("${financialYears[i].start.format(monthFormat)} - ${financialYears[i].end.format(monthFormat)}") + CellUtil.setCellStyleProperty(row9Cell, "borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(row9Cell2, "borderBottom", BorderStyle.THIN) + column = column.plus(2) + } + + rowNum = 10 for (staff in staffInfoList) { @@ -2542,19 +2648,37 @@ open class ReportService( } CellUtil.setAlignment(gradeCell, HorizontalAlignment.CENTER); - val salaryPointCell = row.getCell(2) ?: row.createCell(2) - salaryPointCell.apply { - setCellValue((staff.getValue("salaryPoint") as Long).toDouble()) - } - CellUtil.setAlignment(salaryPointCell, HorizontalAlignment.CENTER); + // Get the array of FinancialYear instances + val financialYears = staff["financialYears"] as List + - sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 3, 4)) - val hourlyRateCell = row.getCell(3) ?: row.createCell(3) - hourlyRateCell.apply { - setCellValue((staff.getValue("hourlyRate") as Double)) + // Access individual FinancialYear instances + financialYears.let { years -> + var salaryColumn = 2 + for (year in years) { + val salaryPointCell = row.getCell(salaryColumn) ?: row.createCell(salaryColumn) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, salaryColumn, salaryColumn+1)) + salaryPointCell.apply { + setCellValue("${year.salaryPoint} (${year.hourlyRate})") + } + CellUtil.setAlignment(salaryPointCell, HorizontalAlignment.CENTER); + salaryColumn = salaryColumn.plus(2) + } } - CellUtil.setAlignment(hourlyRateCell, HorizontalAlignment.CENTER); - CellUtil.setVerticalAlignment(hourlyRateCell, VerticalAlignment.CENTER); + +// val salaryPointCell = row.getCell(2) ?: row.createCell(2) +// salaryPointCell.apply { +// setCellValue((staff.getValue("salaryPoint") as Long).toDouble()) +// } +// CellUtil.setAlignment(salaryPointCell, HorizontalAlignment.CENTER); +// +// sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 3, 4)) +// val hourlyRateCell = row.getCell(3) ?: row.createCell(3) +// hourlyRateCell.apply { +// setCellValue((staff.getValue("hourlyRate") as Double)) +// } +// CellUtil.setAlignment(hourlyRateCell, HorizontalAlignment.CENTER); +// CellUtil.setVerticalAlignment(hourlyRateCell, VerticalAlignment.CENTER); sheet.setRowBreak(rowNum++); } @@ -2608,6 +2732,7 @@ open class ReportService( CellUtil.setVerticalAlignment(manhourCell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourCell, CellUtil.WRAP_TEXT, true) CellUtil.setCellStyleProperty(manhourCell, "borderBottom", BorderStyle.THIN) + sheet.setColumnWidth(2 + monthColumnEnd, 15 * 256) val manhourECell = row.getCell(2 + monthColumnEnd + 1) ?: row.createCell(2 + monthColumnEnd + 1) manhourECell.apply { diff --git a/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx b/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx index 8acd339..ba5e1b1 100644 Binary files a/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx and b/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx differ