From 6cdf5b65194aa19624d9af8c9aca165e9968bbaf Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 13 May 2025 18:46:44 +0800 Subject: [PATCH] Update export Staff Info, Update Financial status report --- .../modules/data/service/StaffsService.kt | 14 ++ .../tsms/modules/data/web/StaffsController.kt | 5 + .../modules/report/service/ReportService.kt | 126 +++++++++++++----- .../modules/report/web/ReportController.kt | 16 ++- .../modules/report/web/model/ReportRequest.kt | 4 +- .../report/EX01_Financial Status Report.xlsx | Bin 13221 -> 13611 bytes 6 files changed, 127 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt index 0013733..039e152 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt @@ -22,6 +22,7 @@ import java.util.* import kotlin.jvm.optionals.getOrNull + @Service open class StaffsService( private val staffRepository: StaffRepository, @@ -426,4 +427,17 @@ open class StaffsService( return jdbcDao.queryForList(sql.toString(), mapOf("ids" to args)) } + + open fun manuelUpdateSalaryEffectiveDate() { + val staffIds = findAll().get().filter { it.departDate == null || it.departDate < LocalDate.now() }.map { it.id } + println(staffIds) + val salaryEffectiveInfo = salaryEffectiveRepository.findSalaryEffectiveInfoByStaffIdInOrderByStaffId(staffIds) + staffIds.forEach { id -> + val filteredSalaryEffective = salaryEffectiveInfo.filter { it.idInStaff == id } + val updatedSalaryEffectiveInfo = filteredSalaryEffective.map { it -> SalaryEffectiveInfo(it.idInStaff, it.salary.salaryPoint.toLong(), it.date ) } + val delSalaryEffectiveInfo = filteredSalaryEffective.map { it.id!! } + salaryEffectiveService.updateSalaryEffective(id!!, updatedSalaryEffectiveInfo.sortedBy { it.date }, delSalaryEffectiveInfo) + } + + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/web/StaffsController.kt b/src/main/java/com/ffii/tsms/modules/data/web/StaffsController.kt index b115d5f..36b3f71 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/StaffsController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/StaffsController.kt @@ -78,4 +78,9 @@ class StaffsController(private val staffsService: StaffsService) { fun saveStaff(@Valid @RequestBody newStaff: NewStaffRequest): NewStaffResponse { return staffsService.saveOrUpdate(newStaff) } + + @GetMapping("/manuelUpdate") + fun manualUpdate(){ + staffsService.manuelUpdateSalaryEffectiveDate() + } } \ 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 2f5428f..5a7689f 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 @@ -193,10 +193,10 @@ open class ReportService( return rowIndex } - fun genFinancialStatusReport(teamLeadId: Long): ByteArray { + fun genFinancialStatusReport(teamLeadId: Long, startMonth: String, endMonth: String): ByteArray { - val financialStatus: List> = getFinancialStatus(teamLeadId) - val manhoursSpent = getManHoursSpentByTeam(teamLeadId) + val financialStatus: List> = getFinancialStatus(teamLeadId, startMonth, endMonth) + val manhoursSpent = getManHoursSpentByTeam(teamLeadId, startMonth, endMonth) val salaryEffectiveMap = getSalaryEffectiveByTeamLead(teamLeadId) val updatedTimesheetData = updateTimesheetDataWithEffectiveSalary(manhoursSpent, salaryEffectiveMap) val projectsExpenditure = calculateProjectExpenditures(updatedTimesheetData) @@ -213,7 +213,7 @@ open class ReportService( } } - val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, updatedList, teamLeadId) + val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, updatedList, teamLeadId, startMonth, endMonth) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) @@ -444,7 +444,9 @@ open class ReportService( private fun createFinancialStatusReport( templatePath: String, projects: List>, - teamLeadId: Long + teamLeadId: Long, + startMonth: String, + endMonth: String, ): Workbook { val resource = ClassPathResource(templatePath) @@ -468,7 +470,7 @@ open class ReportService( val boldFontCellStyle = workbook.createCellStyle() boldFontCellStyle.setFont(boldFont) - var rowNum = 14 + var rowNum = 16 if (projects.isEmpty()) { // Fill the cell in Row 2-12 with thr calculated sum @@ -496,10 +498,15 @@ open class ReportService( setCellValue("$code - $name") } - rowNum = 4 - val row4: Row = sheet.getRow(rowNum) - val row4Cell = row4.createCell(2) - row4Cell.setCellValue(projects.size.toString()) + rowNum = 3 + val row3: Row = sheet.getRow(rowNum) + val row3Cell = row3.createCell(2) + row3Cell.setCellValue("$startMonth - $endMonth") + + rowNum = 4+1 + val row5: Row = sheet.getRow(rowNum) + val row5Cell = row5.createCell(2) + row5Cell.setCellValue(projects.size.toString()) return workbook } @@ -543,9 +550,11 @@ open class ReportService( setCellValue(totalFee) cellStyle.dataFormat = accountingStyle } +// subContractFee is count in fee +// val fee = (item["expectedTotalFee"]?.let { it as Double } ?: 0.0) - (item["subContractFee"]?.let { it as Double } +// ?: 0.0) - val fee = (item["expectedTotalFee"]?.let { it as Double } ?: 0.0) - (item["subContractFee"]?.let { it as Double } - ?: 0.0) + val fee = (item["expectedTotalFee"]?.let { it as Double } ?: 0.0) val budgetCell = row.createCell(8) budgetCell.apply { @@ -759,16 +768,21 @@ open class ReportService( row2Cell.setCellValue("All") } else { row2Cell.apply { - cellFormula = "E15" + cellFormula = "E16" } } - rowNum = 4 + rowNum = 3 + val row3: Row = sheet.getRow(rowNum) + val row3Cell = row3.createCell(2) + row3Cell.setCellValue("$startMonth - $endMonth") + + rowNum = 4+1 val row4: Row = sheet.getRow(rowNum) val row4Cell = row4.createCell(2) row4Cell.setCellValue(projects.size.toString()) - rowNum = 5 + rowNum = 5+1 val row5: Row = sheet.getRow(rowNum) val cell1 = row5.createCell(2) cell1.apply { @@ -776,7 +790,7 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - rowNum = 6 + rowNum = 6+1 val row6: Row = sheet.getRow(rowNum) val cell2 = row6.createCell(2) cell2.apply { @@ -784,7 +798,7 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - rowNum = 7 + rowNum = 7+1 val row7: Row = sheet.getRow(rowNum) val cell3 = row7.createCell(2) cell3.apply { @@ -792,7 +806,7 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - rowNum = 8 + rowNum = 8+1 val row8: Row = sheet.getRow(rowNum) val cell4 = row8.createCell(2) cell4.apply { @@ -800,7 +814,7 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - rowNum = 9 + rowNum = 9+1 val row9: Row = sheet.getRow(rowNum) val cell5 = row9.createCell(2) cell5.apply { @@ -808,7 +822,7 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - rowNum = 10 + rowNum = 10+1 val row10: Row = sheet.getRow(rowNum) val cell6 = row10.createCell(2) cell6.apply { @@ -816,7 +830,7 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - rowNum = 11 + rowNum = 11+1 val row11: Row = sheet.getRow(rowNum) val cell7 = row11.createCell(2) cell7.apply { @@ -2179,7 +2193,16 @@ open class ReportService( } } - open fun getFinancialStatus(teamLeadId: Long?): List> { + open fun getFinancialStatus(teamLeadId: Long?, startMonth: String, endMonth: String): List> { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startYearMonth = YearMonth.parse(startMonth) + val startDate = startYearMonth.atDay(1) + val startDateString = startDate.format(formatter) + + val endYearMonth = YearMonth.parse(endMonth) + val endDate = endYearMonth.atEndOfMonth() + val endDateString = endDate.format(formatter) + 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" @@ -2191,21 +2214,46 @@ open class ReportService( // + " 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 " + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " where i.deleted = false " + + " and p.deleted = false " + + " and i.invoiceDate >= :startMonth and i.invoiceDate <= :endMonth " + + " group by p.code" + + " )," + + " cte_rinvoice as (" + + " select p.code, sum(i.paidAmount) as sumPaidAmount" + " from invoice i" + " left join project p on p.code = i.projectCode" + " where i.deleted = false " + " and p.deleted = false " + + " and i.receiptDate >= :startMonth and i.receiptDate <= :endMonth " + " group by p.code" + " )," + " cte_expense as ( " + " select IFNULL(sum(pe.amount),0) as amount, pe.projectId " + " from project_expense pe " + " where pe.deleted = false " + + " and pe.issueDate >= :startMonth and pe.issueDate <= :endMonth " + " group by projectId " + + " ), " + + " cte_fee as ( " + + " select IFNULL(sum(mp.amount),0) as expectedTotalFee, p.id as projectId " + + " from milestone_payment mp " + + " left join milestone m on mp.milestoneId = m.id " + + " left join project p on p.id = m.projectId " + + " where p.status = 'On-going' " + + " and p.deleted = false " + + " and coalesce(p.actualStart, p.planStart) <= :endMonth " + + " and mp.`date` >= :startMonth and mp.`date` <= :endMonth " + + " and p.teamLead = :teamLeadId" + + " group by p.id" + " ) " - + " select p.code, p.name, 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_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" + + " select p.code, p.name, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \' - \', t.name) as teamLead, " + + " p.planStart , p.planEnd ," + + " IFNULL(cte_f.expectedTotalFee, 0) as expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee, " + + " IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_ri.sumPaidAmount, 0) as sumPaidAmount" + " ,0 as totalCumulativeExpenditure, IFNULL(cte_e.amount,0) as projectExpense " + " from project p" // + " left join cte_timesheet cte_ts on p.code = cte_ts.code" @@ -2214,15 +2262,18 @@ open class ReportService( + " left join subsidiary s2 on s2.id = cs.subsidiaryId " + " left join tsmsdb.team t on t.teamLead = p.teamLead" + " left join cte_invoice cte_i on cte_i.code = p.code" + + " left join cte_rinvoice cte_ri on cte_ri.code = p.code" + " left join cte_expense cte_e on cte_e.projectId = p.id " + + " left join cte_fee cte_f on cte_f.projectId = p.id " + " where p.status = \'On-going\'" + " and p.deleted = false " + + " and coalesce(p.actualStart, p.planStart) <= :endMonth" ) if (teamLeadId!! > 0) { sql.append(" and p.teamLead = :teamLeadId ") } sql.append(" order by p.code") - val args = mapOf("teamLeadId" to teamLeadId) + val args = mapOf("teamLeadId" to teamLeadId, "startMonth" to startDateString, "endMonth" to endDateString ) return jdbcDao.queryForList(sql.toString(), args) } @@ -3437,7 +3488,9 @@ open class ReportService( "clientId" to clientId, "teamLeadId" to teamId ) - val manhoursSpent = getManHoursSpentByTeam(teamId) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM") + + val manhoursSpent = getManHoursSpentByTeam(teamId, "1970-01", LocalDate.now().format(formatter)) val salaryEffectiveMap = getSalaryEffectiveByTeamLead(teamId) val updatedTimesheetData = updateTimesheetDataWithEffectiveSalary(manhoursSpent, salaryEffectiveMap) val projectsExpenditure = calculateProjectExpenditures(updatedTimesheetData) @@ -5223,7 +5276,16 @@ open class ReportService( } // Get all the timesheet data by Team Lead - fun getManHoursSpentByTeam(teamLeadId: Long?): List{ + fun getManHoursSpentByTeam(teamLeadId: Long?, startMonth: String, endMonth: String): List{ + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startYearMonth = YearMonth.parse(startMonth) + val startDate = startYearMonth.atDay(1) + val startDateString = startDate.format(formatter) + + val endYearMonth = YearMonth.parse(endMonth) + val endDate = endYearMonth.atEndOfMonth() + val endDateString = endDate.format(formatter) + 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" // For later calculating cross team charge @@ -5236,7 +5298,8 @@ open class ReportService( + " 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" + + " where t.recordDate >= :startMonth and t.recordDate <= :endMonth " + + " and t.projectId in" + " (" + " select p.id from project p" + " where p.status = 'On-going'" @@ -5248,7 +5311,7 @@ open class ReportService( sql.append(") order by code, recordDate, staffId; ") - val results = jdbcDao.queryForList(sql.toString(), mapOf("teamLeadId" to teamLeadId)).map { + val results = jdbcDao.queryForList(sql.toString(), mapOf("teamLeadId" to teamLeadId, "startMonth" to startDateString, "endMonth" to endDateString )).map { result -> TimesheetData( result["normalConsumed"] as Double, @@ -5312,7 +5375,8 @@ open class ReportService( // 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) } +// .filter { YearMonth.from(it.effectiveDate) <= YearMonth.from(timesheetData.recordDate) } + .filter{ it.effectiveDate <= timesheetData.recordDate } .maxByOrNull { it.effectiveDate } if (nearestEffectiveSalary != null) { 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 d8c62e8..2ea4633 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 @@ -74,8 +74,12 @@ class ReportController( @Throws(ServletRequestBindingException::class, IOException::class) fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity { - println(request.teamLeadId) - val reportResult: ByteArray = excelReportService.genFinancialStatusReport(request.teamLeadId) + println("----- start downloading financial status report -----") + println("teamLeadId: ${request.teamLeadId}") + println("startMonth: ${request.startMonth}") + println("endMonth: ${request.endMonth}") + + val reportResult: ByteArray = excelReportService.genFinancialStatusReport(request.teamLeadId, request.startMonth, request.endMonth) return ResponseEntity.ok() .header("filename", "Financial Status Report - " + LocalDate.now() + ".xlsx") @@ -279,10 +283,10 @@ class ReportController( } // For API TESTING - @GetMapping("/financialReport/{id}") - fun getFinancialReport(@PathVariable id: Long): List> { - return excelReportService.getFinancialStatus(id) - } +// @GetMapping("/financialReport/{id}") +// fun getFinancialReport(@PathVariable id: Long): List> { +// return excelReportService.getFinancialStatus(id) +// } // For API TESTING @GetMapping("/manHoursSpent/{id}") 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 a379967..f7a687e 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 @@ -4,7 +4,9 @@ import java.time.LocalDate import java.time.YearMonth data class FinancialStatusReportRequest ( - val teamLeadId: Long + val teamLeadId: Long, + val startMonth: String, + val endMonth: String, ) data class PandLReportRequest ( diff --git a/src/main/resources/templates/report/EX01_Financial Status Report.xlsx b/src/main/resources/templates/report/EX01_Financial Status Report.xlsx index e1dcf08ea19dd3f971ba500187a77754809f3875..962ef19e2e5cf5ebc2fb91e5a167b74758c5a46d 100644 GIT binary patch delta 5279 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