| @@ -1,12 +1,9 @@ | |||||
| package com.ffii.tsms.modules.report.service | package com.ffii.tsms.modules.report.service | ||||
| import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
| import com.ffii.core.utils.ExcelUtils | |||||
| import com.ffii.tsms.modules.data.entity.Customer | import com.ffii.tsms.modules.data.entity.Customer | ||||
| import com.ffii.tsms.modules.data.entity.Grade | import com.ffii.tsms.modules.data.entity.Grade | ||||
| import com.ffii.tsms.modules.data.entity.Salary | 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.Staff | ||||
| import com.ffii.tsms.modules.data.entity.Team | import com.ffii.tsms.modules.data.entity.Team | ||||
| import com.ffii.tsms.modules.data.service.SalaryEffectiveService | import com.ffii.tsms.modules.data.service.SalaryEffectiveService | ||||
| @@ -25,7 +22,6 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook | |||||
| import org.apache.poi.ss.usermodel.FormulaEvaluator | import org.apache.poi.ss.usermodel.FormulaEvaluator | ||||
| import org.apache.poi.ss.util.CellReference | import org.apache.poi.ss.util.CellReference | ||||
| import org.apache.poi.ss.util.RegionUtil | import org.apache.poi.ss.util.RegionUtil | ||||
| import org.apache.poi.xssf.usermodel.XSSFCellStyle | |||||
| import org.apache.poi.xssf.usermodel.XSSFColor | import org.apache.poi.xssf.usermodel.XSSFColor | ||||
| import org.springframework.core.io.ClassPathResource | import org.springframework.core.io.ClassPathResource | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| @@ -33,7 +29,6 @@ import java.io.ByteArrayOutputStream | |||||
| import java.io.IOException | import java.io.IOException | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.Year | |||||
| import java.time.YearMonth | import java.time.YearMonth | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import java.time.ZoneId | import java.time.ZoneId | ||||
| @@ -41,6 +36,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.time.Year | |||||
| import kotlin.collections.ArrayList | import kotlin.collections.ArrayList | ||||
| @@ -2144,35 +2140,73 @@ open class ReportService( | |||||
| return jdbcDao.queryForList(sql.toString(), args) | return jdbcDao.queryForList(sql.toString(), args) | ||||
| } | } | ||||
| fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>): 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 -----------------------------") | |||||
| // fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>): Double? { | |||||
| // val formatter = DateTimeFormatter.ofPattern("yyyy-MM") | |||||
| // val monthDate = YearMonth.parse(recordDate, formatter) | |||||
| // | |||||
| // var hourlyRate: Double? = null | |||||
| // var prevHourlyRate: Double? = null | |||||
| //// var financialYear = monthDate | |||||
| // | |||||
| // val staffSalaryData = staffSalaryLists.find { it.staffId == staffId }?.salaryData | |||||
| // staffSalaryData?.forEachIndexed { index, 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@forEachIndexed | |||||
| // }else{ | |||||
| // if(index > 0){ | |||||
| // prevHourlyRate = staffSalaryData[index - 1].hourlyRate.toDouble() | |||||
| // return@forEachIndexed | |||||
| // } | // } | ||||
| hourlyRate = salaryData.hourlyRate.toDouble() | |||||
| // financialYear = YearMonth.parse(salaryData.financialYear.toString()) | |||||
| return@forEach | |||||
| // } | |||||
| // } | |||||
| // | |||||
| // return hourlyRate ?: prevHourlyRate | |||||
| // } | |||||
| private fun updateInfo(info: MutableMap<String, Any?>, item: Map<String, Any>) { | |||||
| val simpleFields = listOf("teamLead", "client", "code", "description") | |||||
| val conditionalFields = mapOf( | |||||
| "paidAmount" to "sumPaidAmount", | |||||
| "subsidiary" to "subsidiary", | |||||
| "expectedTotalFee" to "expectedTotalFee" | |||||
| ) | |||||
| // Update simple fields | |||||
| simpleFields.forEach { field -> | |||||
| if (info[field] != item[field]) { | |||||
| info[field] = item.getValue(field) | |||||
| } | |||||
| } | |||||
| // Update conditional fields | |||||
| if (info["code"] == item["code"]) { | |||||
| conditionalFields.forEach { (infoField, itemField) -> | |||||
| if (infoField !in info) { | |||||
| info[infoField] = item.getValue(itemField) | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| return hourlyRate | |||||
| // Update manhourExpenditure separately due to different key names | |||||
| if (info["manhourExpenditure"] != item["sumManhourExpenditure"]) { | |||||
| info["manhourExpenditure"] = item.getValue("sumManhourExpenditure") | |||||
| } | |||||
| } | } | ||||
| open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List<Map<String, Any>> { | open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List<Map<String, Any>> { | ||||
| @@ -2263,39 +2297,16 @@ open class ReportService( | |||||
| "code" to projectsCode["code"], | "code" to projectsCode["code"], | ||||
| "description" to projectsCode["description"] | "description" to projectsCode["description"] | ||||
| ) | ) | ||||
| val financialYears = getFinancialYearDates(queryStartMonth, queryEndMonth, 10) | |||||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 10) | |||||
| val staffInfoList = mutableListOf<Map<String, Any>>() | val staffInfoList = mutableListOf<Map<String, Any>>() | ||||
| val staffSalaryLists = salaryEffectiveService.getStaffSalaryDataByProjectId(projectId) | val staffSalaryLists = salaryEffectiveService.getStaffSalaryDataByProjectId(projectId) | ||||
| for (item in manHoursSpent) { | for (item in manHoursSpent) { | ||||
| if (info["teamLead"] != item.getValue("teamLead")) { | |||||
| info["teamLead"] = item.getValue("teamLead") | |||||
| } | |||||
| if (info["client"] != item.getValue("client")) { | |||||
| info["client"] = item.getValue("client") | |||||
| } | |||||
| if (info["code"] != item.getValue("code")) { | |||||
| info["code"] = item.getValue("code") | |||||
| } | |||||
| if (info["code"] == item["code"] && "paidAmount" !in info) { | |||||
| info["paidAmount"] = item.getValue("sumPaidAmount") | |||||
| } | |||||
| if (info["code"] == item["code"] && "subsidiary" !in info) { | |||||
| info["subsidiary"] = item.getValue("subsidiary") | |||||
| } | |||||
| if (info["code"] == item["code"] && "expectedTotalFee" !in info) { | |||||
| info["expectedTotalFee"] = item.getValue("expectedTotalFee") | |||||
| } | |||||
| if (info["manhourExpenditure"] != item.getValue("sumManhourExpenditure")) { | |||||
| info["manhourExpenditure"] = item.getValue("sumManhourExpenditure") | |||||
| } | |||||
| if (info["description"] != item.getValue("description")) { | |||||
| info["description"] = item.getValue("description") | |||||
| } | |||||
| updateInfo(info, item) | |||||
| val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists) ?: (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||||
| val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists, queryStartMonth, queryEndMonth) ?: (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"] }) { | ||||
| @@ -2358,51 +2369,97 @@ open class ReportService( | |||||
| staffInfoList[updatedIndex] = updatedMap | staffInfoList[updatedIndex] = updatedMap | ||||
| } | } | ||||
| } | } | ||||
| // 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) | |||||
| // println(staffSalaryData) | |||||
| updateStaffFinancialYears(staffInfoList, staffSalaryLists) | |||||
| tempList.add(mapOf("info" to info)) | |||||
| tempList.add(mapOf("staffInfoList" to staffInfoList)) | |||||
| return tempList | |||||
| } | |||||
| fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>, start: YearMonth, end: YearMonth): Double? { | |||||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM") | |||||
| val monthDate = YearMonth.parse(recordDate, formatter) | |||||
| val staffSalaryData = staffSalaryLists.find { it.staffId == staffId }?.salaryData ?: return null | |||||
| return findSalaryForMonth(staffSalaryData, monthDate, start, end) | |||||
| } | |||||
| 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() } | |||||
| } | |||||
| fun updateStaffFinancialYears( | |||||
| staffInfoList: MutableList<Map<String, Any>>, | |||||
| staffSalaryDataList: List<SalaryEffectiveService.StaffSalaryData> | |||||
| ) { | |||||
| staffInfoList.forEachIndexed { index, staffInfo -> | |||||
| val staffId = staffInfo["staffId"] as String | |||||
| val staffSalaryData = staffSalaryDataList.find { it.staffId == staffId } | |||||
| val financialYears = (staff["financialYears"] as List<FinancialYear>).map { year -> | |||||
| val relevantSalaryData = staffSalaryData?.salaryData?.firstOrNull() { | |||||
| year.isYearMonthInFinancialYear(YearMonth.from(it.financialYear)) | |||||
| if (staffSalaryData != null) { | |||||
| val updatedFinancialYears = (staffInfo["financialYears"] as List<FinancialYear>).map { financialYear -> | |||||
| updateFinancialYear(financialYear, staffSalaryData.salaryData) | |||||
| } | |||||
| val updatedStaffInfo = staffInfo.toMutableMap().apply { | |||||
| this["financialYears"] = updatedFinancialYears | |||||
| } | } | ||||
| // println("it.financialYear: ${relevantSalaryData?.financialYear}") | |||||
| // println("year: ${year.start}- ${year.end}") | |||||
| // println("it.salaryPoint: ${relevantSalaryData?.salaryPoint}") | |||||
| // println("it.hourlyRate: ${relevantSalaryData?.hourlyRate?.toDouble()}") | |||||
| // 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 Long).toInt(), | |||||
| hourlyRate = relevantSalaryData?.hourlyRate?.toDouble() ?: (staff["hourlyRate"] as BigDecimal).toDouble() | |||||
| ) | |||||
| } | |||||
| // Create a new map with the updated financialYears | |||||
| val updatedStaff = staff.toMutableMap().apply { | |||||
| this["financialYears"] = financialYears | |||||
| staffInfoList[index] = updatedStaffInfo | |||||
| } | } | ||||
| } | |||||
| } | |||||
| // Update the staffInfoList with the new staff map | |||||
| staffInfoList[index] = updatedStaff | |||||
| fun updateFinancialYear(financialYear: FinancialYear, salaryDataList: List<SalaryEffectiveService.SalaryData>): FinancialYear { | |||||
| val relevantSalaryData = salaryDataList.find { salaryData -> | |||||
| val salaryYearMonth = YearMonth.from(salaryData.financialYear) | |||||
| salaryYearMonth >= financialYear.start && salaryYearMonth <= financialYear.end | |||||
| } | } | ||||
| tempList.add(mapOf("info" to info)) | |||||
| tempList.add(mapOf("staffInfoList" to staffInfoList)) | |||||
| if (relevantSalaryData != null) { | |||||
| return financialYear.copy( | |||||
| hourlyRate = relevantSalaryData.hourlyRate.toDouble(), | |||||
| salaryPoint = relevantSalaryData.salaryPoint | |||||
| ) | |||||
| } else { | |||||
| val previousHourlyRate = findPreviousValue(salaryDataList, financialYear.start) { it.hourlyRate } | |||||
| val previousSalaryPoint = findPreviousValue(salaryDataList, financialYear.start) { it.salaryPoint } | |||||
| // println("Only Staff Info List --------------------- ${tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") | |||||
| // println("tempList----------------- ${tempList.filter { it["staffId"] == "B627" }}") | |||||
| return financialYear.copy( | |||||
| hourlyRate = previousHourlyRate?.toDouble() ?: financialYear.hourlyRate, | |||||
| salaryPoint = previousSalaryPoint ?: financialYear.salaryPoint | |||||
| ) | |||||
| } | |||||
| } | |||||
| return tempList | |||||
| fun <T> findPreviousValue(salaryDataList: List<SalaryEffectiveService.SalaryData>, currentStart: YearMonth, valueSelector: (SalaryEffectiveService.SalaryData) -> T): T? { | |||||
| return salaryDataList | |||||
| .filter { YearMonth.from(it.financialYear) < currentStart } | |||||
| .maxByOrNull { it.financialYear } | |||||
| ?.let(valueSelector) | |||||
| } | } | ||||
| fun genPandLReport(projectId: Long, startMonth: String, endMonth: String): ByteArray { | fun genPandLReport(projectId: Long, startMonth: String, endMonth: String): ByteArray { | ||||
| @@ -2431,6 +2488,51 @@ open class ReportService( | |||||
| } | } | ||||
| } | } | ||||
| // 6 months as a period | |||||
| fun getHalfYearFinancialPeriods( | |||||
| startDate: YearMonth, | |||||
| endDate: YearMonth, | |||||
| periodStartMonth: Int | |||||
| ): List<FinancialYear> { | |||||
| require(periodStartMonth in 1..12) { "periodStartMonth must be between 1 and 12" } | |||||
| val financialPeriods = mutableListOf<FinancialYear>() | |||||
| var currentDate = startDate | |||||
| // Adjust the start date to the beginning of the current or previous period | |||||
| currentDate = if (currentDate.monthValue < periodStartMonth) { | |||||
| YearMonth.of(currentDate.year - 1, periodStartMonth) | |||||
| } else { | |||||
| YearMonth.of(currentDate.year, periodStartMonth) | |||||
| } | |||||
| while (currentDate <= endDate) { | |||||
| val startYear = currentDate.year | |||||
| val startMonth = currentDate.monthValue | |||||
| val endDate = currentDate.plusMonths(5) | |||||
| val endYear = endDate.year | |||||
| val endMonth = endDate.monthValue | |||||
| val financialPeriod = FinancialYear( | |||||
| start = YearMonth.of(startYear, startMonth), | |||||
| end = YearMonth.of(endYear, endMonth), | |||||
| hourlyRate = 0.0, | |||||
| salaryPoint = 0 | |||||
| ) | |||||
| financialPeriods.add(financialPeriod) | |||||
| // Move to the next half-year period | |||||
| currentDate = currentDate.plusMonths(6) | |||||
| } | |||||
| // Remove any periods that start after the specified end date | |||||
| return financialPeriods.filter { it.start <= endDate } | |||||
| } | |||||
| // 12 months as a period | |||||
| fun getFinancialYearDates( | fun getFinancialYearDates( | ||||
| startDate: YearMonth, | startDate: YearMonth, | ||||
| endDate: YearMonth, | endDate: YearMonth, | ||||
| @@ -2478,6 +2580,15 @@ open class ReportService( | |||||
| return financialYearDates | return financialYearDates | ||||
| } | } | ||||
| private fun getOrCreateRow(sheet: Sheet, startRow: Int): Row{ | |||||
| val row: Row = sheet.getRow(startRow) ?: sheet.createRow(startRow) | |||||
| return row | |||||
| } | |||||
| private fun getOrCreateCell(row: Row, columnIndex: Int): Cell{ | |||||
| val cell: Cell = row.getCell(columnIndex) ?: row.createCell(columnIndex) | |||||
| return cell | |||||
| } | |||||
| fun createPandLReportWorkbook( | fun createPandLReportWorkbook( | ||||
| templatePath: String, | templatePath: String, | ||||
| @@ -2512,7 +2623,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 = getFinancialYearDates(queryStartMonth, queryEndMonth, 10) | |||||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 10) | |||||
| var currentDate = startDate | var currentDate = startDate | ||||
| if (currentDate == endDate) { | if (currentDate == endDate) { | ||||
| @@ -2674,20 +2785,6 @@ open class ReportService( | |||||
| } | } | ||||
| } | } | ||||
| // 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++); | sheet.setRowBreak(rowNum++); | ||||
| } | } | ||||