@@ -1,11 +1,12 @@
package com.ffii.tsms.modules.report.service
package com.ffii.tsms.modules.report.service
import com.ffii.tsms.modules.data.entity.Salary
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.Invoice
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.timesheet.entity.Timesheet
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.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service
import org.springframework.stereotype.Service
@@ -13,13 +14,16 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.IOException
import java.time.LocalDate
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatter
import java.util.*
data class DayInfo(val date: String, val weekday: String)
@Service
@Service
open class ReportService {
open class ReportService {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER)
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 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"
private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx"
// ==============================|| GENERATE REPORT ||============================== //
// ==============================|| GENERATE REPORT ||============================== //
@@ -36,6 +40,19 @@ open class ReportService {
return outputStream.toByteArray()
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)
@Throws(IOException::class)
fun exportSalaryList(salarys: List<Salary>): ByteArray {
fun exportSalaryList(salarys: List<Salary>): ByteArray {
// Generate the Excel report with query results
// Generate the Excel report with query results
@@ -108,11 +125,10 @@ open class ReportService {
rowIndex = 10
rowIndex = 10
val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! }
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 {
sheet.getRow(rowIndex).apply {
getCell(1).apply {
getCell(1).apply {
// TODO: Replace by actual expenditure
// TODO: Replace by actual expenditure
setCellValue(actualExpenditure )
// setCellValue(actualIncome * 0.8 )
cellStyle.dataFormat = accountingStyle
cellStyle.dataFormat = accountingStyle
}
}
@@ -137,6 +153,7 @@ open class ReportService {
}
}
// TODO: Add expenditure
// TODO: Add expenditure
// formula =IF(B17>0,D16-B17,D16+C17)
rowIndex = 15
rowIndex = 15
val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it }
val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it }
@@ -171,6 +188,18 @@ open class ReportService {
}
}
cellStyle.dataFormat = accountingStyle
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 {
getCell(4).apply {
setCellValue(invoice.milestonePayment!!.description!!)
setCellValue(invoice.milestonePayment!!.description!!)
@@ -215,6 +244,180 @@ open class ReportService {
return workbook
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)
@Throws(IOException::class)
private fun createSalaryList(
private fun createSalaryList(
salarys : List<Salary>,
salarys : List<Salary>,
@@ -235,15 +438,17 @@ open class ReportService {
sheet.getRow(rowIndex++).apply {
sheet.getRow(rowIndex++).apply {
getCell(0).setCellValue(salary.salaryPoint.toDouble())
getCell(0).setCellValue(salary.salaryPoint.toDouble())
when (index){
when (index) {
0 -> getCell(1).setCellValue(salary.lowerLimit.toDouble())
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).cellFormula = "(B{currentRow}+D{currentRow})-1".replace("{currentRow}", rowIndex.toString())
// getCell(2).cellStyle.dataFormat = accountingStyle
// getCell(2).cellStyle.dataFormat = accountingStyle
getCell(3).setCellValue(salary.increment.toDouble())
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
// getCell(4).cellStyle.dataFormat = accountingStyle
}
}
}
}