ソースを参照

update

tags/Baseline_30082024_BACKEND_UAT
MSI\derek 1年前
コミット
9c93677640
4個のファイルの変更251行の追加11行の削除
  1. +212
    -7
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  2. +32
    -4
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  3. +7
    -0
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  4. バイナリ
      src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx

+ 212
- 7
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt ファイルの表示

@@ -1,11 +1,12 @@
package com.ffii.tsms.modules.report.service

import com.ffii.tsms.modules.data.entity.Salary
import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.project.entity.Invoice
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.timesheet.entity.Timesheet
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.ss.usermodel.*
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service
@@ -13,13 +14,16 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*

data class DayInfo(val date: String, val weekday: String)
@Service
open class ReportService {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER)

private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx"
private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx"
private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx"

// ==============================|| GENERATE REPORT ||============================== //
@@ -36,6 +40,19 @@ open class ReportService {
return outputStream.toByteArray()
}

@Throws(IOException::class)
fun generateStaffMonthlyWorkHourAnalysisReport(month: LocalDate, staff: Staff, timesheets: List<Timesheet>, projectList: List<String>): ByteArray {
// Generate the Excel report with query results
val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport(month, staff, timesheets, projectList, MONTHLY_WORK_HOURS_ANALYSIS_REPORT)

// Write the workbook to a ByteArrayOutputStream
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()

return outputStream.toByteArray()
}

@Throws(IOException::class)
fun exportSalaryList(salarys: List<Salary>): ByteArray {
// Generate the Excel report with query results
@@ -108,11 +125,10 @@ open class ReportService {

rowIndex = 10
val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! }
val actualExpenditure = timesheets.sumOf { timesheet -> timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0)) }
sheet.getRow(rowIndex).apply {
getCell(1).apply {
// TODO: Replace by actual expenditure
setCellValue(actualExpenditure)
// setCellValue(actualIncome * 0.8)
cellStyle.dataFormat = accountingStyle
}

@@ -137,6 +153,7 @@ open class ReportService {
}

// TODO: Add expenditure
// formula =IF(B17>0,D16-B17,D16+C17)
rowIndex = 15
val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it }

@@ -171,6 +188,18 @@ open class ReportService {
}
cellStyle.dataFormat = accountingStyle
}
getCell(3).apply {
val lastRow = rowIndex - 1
if (lastRow == 15) {
cellFormula = "C{currentRow}".replace("{currentRow}", rowIndex.toString())
} else {
cellFormula = "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace(
"{currentRow}",
rowIndex.toString()
).replace("{lastRow}", lastRow.toString())
}
cellStyle.dataFormat = accountingStyle
}

getCell(4).apply {
setCellValue(invoice.milestonePayment!!.description!!)
@@ -215,6 +244,180 @@ open class ReportService {
return workbook
}

fun getColumnAlphabet(colIndex: Int): String {
val alphabet = StringBuilder()

var index = colIndex
while (index >= 0) {
val remainder = index % 26
val character = ('A'.code + remainder).toChar()
alphabet.insert(0, character)
index = (index / 26) - 1
}

return alphabet.toString()
}

@Throws(IOException::class)
private fun createStaffMonthlyWorkHourAnalysisReport(
month: LocalDate,
staff: Staff,
timesheets: List<Timesheet>,
projectList: List<String>,
templatePath: String,
): Workbook {
// val yearMonth = YearMonth.of(2022, 5) // May 2022
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)

val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val monthStyle = workbook.createDataFormat().getFormat("mmm, yyyy")

val daysOfMonth = (1..month.lengthOfMonth()).map { day ->
val date = month.withDayOfMonth(day)
val formattedDate = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
val weekday = date.format(DateTimeFormatter.ofPattern("EEE", Locale.ENGLISH))
DayInfo(formattedDate, weekday)
}

val sheet: Sheet = workbook.getSheetAt(0)

val boldFont = sheet.workbook.createFont()
boldFont.bold = true
// sheet.forceFormulaRecalculation = true; //Calculate formulas

var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
var columnIndex = 1

var rowSize = 0
var columnSize = 0

// tempCell = tempRow.createCell(columnIndex)
sheet.getRow(rowIndex).getCell(columnIndex).apply {
setCellValue(FORMATTED_TODAY)
}
println(sheet.getRow(1).getCell(2))

rowIndex = 2
sheet.getRow(rowIndex).getCell(columnIndex).apply {
setCellValue(month)
cellStyle.dataFormat = monthStyle
}

rowIndex = 3
sheet.getRow(rowIndex).getCell(columnIndex).apply {
setCellValue(staff.name)
}

rowIndex = 4
sheet.getRow(rowIndex).getCell(columnIndex).apply {
setCellValue(staff.team.name)
}

rowIndex = 5
sheet.getRow(rowIndex).getCell(columnIndex).apply {
setCellValue(staff.grade.code)
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rowIndex = 7
daysOfMonth.forEach { dayInfo ->
rowIndex++
rowSize++
sheet.getRow(rowIndex).getCell(0).apply {
setCellValue(dayInfo.date)
cellStyle.setFont(boldFont)
}
sheet.getRow(rowIndex).getCell(1).apply {
setCellValue(dayInfo.weekday)
cellStyle.setFont(boldFont)
}
}

rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply {
setCellValue("Sub-total")
}
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
//
rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply {
setCellValue("Total Normal Hours [A]")
}
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
//
rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply {
setCellValue("Total Other Hours [B]")
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
}

rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply {
setCellValue("Total Spent Manhours [A+B]")
// cellStyle.borderBottom = BorderStyle.DOUBLE
}
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rowIndex = 7
columnIndex = 2
projectList.forEachIndexed { index, title ->
sheet.getRow(7).getCell(columnIndex + index).apply {
setCellValue(title)
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sheet.getRow(rowIndex).apply {
getCell(columnIndex).setCellValue("Leave Hours")
getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER

columnIndex += 1
getCell(columnIndex).setCellValue("Daily Manhour Spent\n" + "(Excluding Leave Hours)")
getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER
// columnSize = columnIndex
}
sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex))
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for (i in 2 ..columnIndex) {
for (k in 0 until rowSize) {
sheet.getRow(8+k).getCell(i).apply {
setCellValue(" - ")
cellStyle.dataFormat = accountingStyle
}
}
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (timesheets.isNotEmpty()) {
projectList.forEachIndexed { i, _ ->
timesheets.forEach { timesheet ->
val dayInt = timesheet.recordDate!!.dayOfMonth
sheet.getRow(dayInt.plus(7)).getCell(columnIndex + i).apply {
setCellValue(timesheet.normalConsumed!!)
}
}
}
}

rowIndex = 8
println("rowSize is: $rowSize")
if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue == "Leave Hours") {
for (i in 0 until rowSize) {
val cell = sheet.getRow(rowIndex + i)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex)
cell?.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1+i}:${getColumnAlphabet(columnIndex)}${rowIndex+1+i})" // should columnIndex - 2
// cell?.setCellValue("testing")
// rowIndex++
}
// for (i in 0 until columnSize) {
// val cell = sheet.getRow(rowIndex)
// }
}

return workbook
}


@Throws(IOException::class)
private fun createSalaryList(
salarys : List<Salary>,
@@ -235,15 +438,17 @@ open class ReportService {
sheet.getRow(rowIndex++).apply {

getCell(0).setCellValue(salary.salaryPoint.toDouble())
when (index){
when (index) {
0 -> getCell(1).setCellValue(salary.lowerLimit.toDouble())

else -> getCell(1).cellFormula = "(C{previousRow}+1)".replace("{previousRow}", (rowIndex - 1).toString())
else -> getCell(1).cellFormula =
"(C{previousRow}+1)".replace("{previousRow}", (rowIndex - 1).toString())
}
getCell(2).cellFormula = "(B{currentRow}+D{currentRow})-1".replace("{currentRow}", rowIndex.toString())
// getCell(2).cellStyle.dataFormat = accountingStyle
getCell(3).setCellValue(salary.increment.toDouble())
getCell(4).cellFormula = "(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", rowIndex.toString())
getCell(4).cellFormula =
"(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", rowIndex.toString())
// getCell(4).cellStyle.dataFormat = accountingStyle
}
}


+ 32
- 4
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt ファイルの表示

@@ -1,9 +1,12 @@
package com.ffii.tsms.modules.report.web

import com.ffii.tsms.modules.data.entity.StaffRepository
import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo
import com.ffii.tsms.modules.project.entity.*
import com.ffii.tsms.modules.report.service.ReportService
import com.ffii.tsms.modules.project.service.InvoiceService
import com.ffii.tsms.modules.report.web.model.ProjectCashFlowReportRequest
import com.ffii.tsms.modules.report.web.model.StaffMonthlyWorkHourAnalysisReportRequest
import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository
import jakarta.validation.Valid
import org.springframework.core.io.ByteArrayResource
@@ -22,10 +25,15 @@ import java.time.LocalDate

@RestController
@RequestMapping("/reports")
class ReportController(private val invoiceRepository: InvoiceRepository, private val milestonePaymentRepository: MilestonePaymentRepository, private val excelReportService: ReportService, private val projectRepository: ProjectRepository, private val invoiceService: InvoiceService,
private val timesheetRepository: TimesheetRepository,
private val projectTaskRepository: ProjectTaskRepository
) {
class ReportController(
private val invoiceRepository: InvoiceRepository,
private val milestonePaymentRepository: MilestonePaymentRepository,
private val excelReportService: ReportService,
private val projectRepository: ProjectRepository,
private val timesheetRepository: TimesheetRepository,
private val projectTaskRepository: ProjectTaskRepository,
private val staffRepository: StaffRepository,
private val invoiceService: InvoiceService) {

@PostMapping("/ProjectCashFlowReport")
@Throws(ServletRequestBindingException::class, IOException::class)
@@ -44,6 +52,26 @@ class ReportController(private val invoiceRepository: InvoiceRepository, private
.body(ByteArrayResource(reportResult))
}

@PostMapping("/StaffMonthlyWorkHourAnalysisReport")
@Throws(ServletRequestBindingException::class, IOException::class)
fun StaffMonthlyWorkHourAnalysisReport(@RequestBody @Valid request: StaffMonthlyWorkHourAnalysisReportRequest): ResponseEntity<Resource> {
val thisMonth = request.yearMonth.atDay(1)
val nextMonth = request.yearMonth.plusMonths(1).atDay(1)

val staff = staffRepository.findById(request.id).orElseThrow()
val timesheets = timesheetRepository.findByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth)

val projects = timesheetRepository.findDistinctProjectTaskByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth)
val projectList: List<String> = projects.map { p -> "${p.projectTask!!.project!!.code}\n ${p.projectTask!!.project!!.name}" }

val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, projectList)
// val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
return ResponseEntity.ok()
// .contentType(mediaType)
.header("filename", "Monthly Work Hours Analysis Report - " + staff.name + " - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))
}

@GetMapping("/test/{id}")
fun test(@PathVariable id: Long): List<Invoice> {
val project = projectRepository.findById(id).orElseThrow()


+ 7
- 0
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt ファイルの表示

@@ -1,5 +1,12 @@
package com.ffii.tsms.modules.report.web.model

import java.time.YearMonth

data class ProjectCashFlowReportRequest (
val projectId: Long
)

data class StaffMonthlyWorkHourAnalysisReportRequest (
val id: Long,
val yearMonth: YearMonth
)

バイナリ
src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx ファイルの表示


読み込み中…
キャンセル
保存