@@ -10,6 +10,7 @@ import com.ffii.tsms.modules.data.entity.Team
import com.ffii.tsms.modules.project.entity.Invoice
import com.ffii.tsms.modules.project.entity.Invoice
import com.ffii.tsms.modules.project.entity.Milestone
import com.ffii.tsms.modules.project.entity.Milestone
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.entity.ProjectRepository
import com.ffii.tsms.modules.report.web.model.costAndExpenseRequest
import com.ffii.tsms.modules.report.web.model.costAndExpenseRequest
import com.ffii.tsms.modules.timesheet.entity.Timesheet
import com.ffii.tsms.modules.timesheet.entity.Timesheet
import org.apache.commons.logging.Log
import org.apache.commons.logging.Log
@@ -22,12 +23,14 @@ 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.XSSFCellStyle
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
import java.io.ByteArrayOutputStream
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
@@ -42,6 +45,7 @@ data class DayInfo(val date: String?, val weekday: String?)
@Service
@Service
open class ReportService(
open class ReportService(
private val jdbcDao: JdbcDao,
private val jdbcDao: JdbcDao,
private val projectRepository: ProjectRepository
) {
) {
private val logger: Log = LogFactory.getLog(javaClass)
private val logger: Log = LogFactory.getLog(javaClass)
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
@@ -487,16 +491,22 @@ open class ReportService(
cellStyle = boldFontCellStyle
cellStyle = boldFontCellStyle
}
}
val receivedAmountCell = row.createCell(14)
val projectedCpiCell = row.createCell(14)
projectedCpiCell.apply {
cellFormula = "(H${rowNum}/J${rowNum})"
cellStyle = boldFontCellStyle
}
val receivedAmountCell = row.createCell(15)
val paidAmount = item["paidAmount"]?.let { it as BigDecimal } ?: BigDecimal(0)
val paidAmount = item["paidAmount"]?.let { it as BigDecimal } ?: BigDecimal(0)
receivedAmountCell.apply {
receivedAmountCell.apply {
setCellValue(paidAmount.toDouble())
setCellValue(paidAmount.toDouble())
cellStyle.dataFormat = accountingStyle
cellStyle.dataFormat = accountingStyle
}
}
val unsettledAmountCell = row.createCell(15 )
val unsettledAmountCell = row.createCell(16 )
unsettledAmountCell.apply {
unsettledAmountCell.apply {
cellFormula = "L${rowNum}-O ${rowNum}"
cellFormula = "L${rowNum}-P ${rowNum}"
cellStyle = boldFontCellStyle
cellStyle = boldFontCellStyle
cellStyle.dataFormat = accountingStyle
cellStyle.dataFormat = accountingStyle
}
}
@@ -578,17 +588,27 @@ open class ReportService(
CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE)
CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE)
val sumRAmoun tCell = row.createCell(14)
sumRAmoun tCell.apply {
val la stPCpi Cell = row.createCell(14)
la stPCpi Cell.apply {
cellFormula = "SUM(O15:O${rowNum})"
cellFormula = "SUM(O15:O${rowNum})"
cellStyle = boldFontCellStyle
cellStyle.dataFormat = accountingStyle
}
CellUtil.setCellStyleProperty(lastPCpiCell, "borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(lastPCpiCell, "borderBottom", BorderStyle.DOUBLE)
val sumRAmountCell = row.createCell(15)
sumRAmountCell.apply {
cellFormula = "SUM(P15:P${rowNum})"
cellStyle.dataFormat = accountingStyle
cellStyle.dataFormat = accountingStyle
}
}
CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE)
CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE)
val sumUnSettleCell = row.createCell(15)
val sumUnSettleCell = row.createCell(16 )
sumUnSettleCell.apply {
sumUnSettleCell.apply {
cellFormula = "SUM(P15:P${rowNum})"
cellFormula = "SUM(Q15:Q ${rowNum})"
cellStyle = boldFontCellStyle
cellStyle = boldFontCellStyle
cellStyle.dataFormat = accountingStyle
cellStyle.dataFormat = accountingStyle
}
}
@@ -1434,6 +1454,13 @@ open class ReportService(
val workbook: Workbook = XSSFWorkbook(templateInputStream)
val workbook: Workbook = XSSFWorkbook(templateInputStream)
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val defaultStyle = workbook.createDataFormat().getFormat("###,##0")
// Fill Orange Style
val fillOrange = workbook.createCellStyle()
val customColor = XSSFColor(byteArrayOf(255.toByte(), 192.toByte(), 0.toByte()))
fillOrange.setFillForegroundColor(customColor);
fillOrange.setFillPattern(FillPatternType.SOLID_FOREGROUND)
val sheet: Sheet = workbook.getSheetAt(0)
val sheet: Sheet = workbook.getSheetAt(0)
var rowIndex = 2
var rowIndex = 2
@@ -1446,30 +1473,46 @@ open class ReportService(
val cell = getCell(0) ?: createCell(0)
val cell = getCell(0) ?: createCell(0)
cell.setCellValue(salary.salaryPoint.toDouble())
cell.setCellValue(salary.salaryPoint.toDouble())
CellUtil.setAlignment(cell, HorizontalAlignment.CENTER)
cell.cellStyle.dataFormat = defaultStyle
when (index) {
when (index) {
0 -> {
0 -> {
val cell1 = getCell(1) ?: createCell(1)
val cell1 = getCell(1) ?: createCell(1)
cell1.setCellValue(salary.lowerLimit.toDouble())
cell1.setCellValue(salary.lowerLimit.toDouble())
CellUtil.setAlignment(cell1, HorizontalAlignment.CENTER)
cell1.cellStyle.dataFormat = defaultStyle
}
}
else -> {
else -> {
val cell1 = getCell(1) ?: createCell(1)
val cell1 = getCell(1) ?: createCell(1)
cell1.cellFormula =
cell1.cellFormula =
"(C{previousRow}+1)".replace("{previousRow}", (rowIndex).toString())
"(C{previousRow}+1)".replace("{previousRow}", (rowIndex).toString())
CellUtil.setAlignment(cell1, HorizontalAlignment.CENTER)
cell1.cellStyle.dataFormat = defaultStyle
}
}
}
}
val cell2 = getCell(2) ?: createCell(2)
val cell2 = getCell(2) ?: createCell(2)
cell2.cellFormula = "(B{currentRow}+D{currentRow})-1".replace("{currentRow}", (rowIndex+1).toString())
cell2.cellFormula = "(B{currentRow}+D{currentRow})-1".replace("{currentRow}", (rowIndex+1).toString())
CellUtil.setAlignment(cell2, HorizontalAlignment.CENTER)
cell2.cellStyle.dataFormat = defaultStyle
// getCell(2).cellStyle.dataFormat = accountingStyle
// getCell(2).cellStyle.dataFormat = accountingStyle
val cell3 = getCell(3)?:createCell(3)
val cell3 = getCell(3)?:createCell(3)
cell3.setCellValue(salary.increment.toDouble())
cell3.setCellValue(salary.increment.toDouble())
cell3.cellStyle = fillOrange
cell3.cellStyle.dataFormat = defaultStyle
CellUtil.setAlignment(cell3, HorizontalAlignment.CENTER)
val cell4 = getCell(4)?:createCell(4)
val cell4 = getCell(4)?:createCell(4)
cell4.cellFormula =
cell4.cellFormula =
"(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", (rowIndex+1).toString())
"(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", (rowIndex+1).toString())
// getCell(4).cellStyle.dataFormat = accountingStyle
// getCell(4).cellStyle.dataFormat = accountingStyle
cell4.cellStyle.dataFormat = accountingStyle
cell4.cellStyle.dataFormat = accountingStyle
CellUtil.setAlignment(cell4, HorizontalAlignment.CENTER)
}
}
rowIndex++;
rowIndex++;
}
}
@@ -2042,6 +2085,7 @@ open class ReportService(
+ " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \" - \", t.name) as teamLead,"
+ " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \" - \", t.name) as teamLead,"
+ " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, "
+ " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, "
+ " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, "
+ " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, "
+ " (p.expectedTotalFee - IFNULL(cte_i.sumIssuedAmount, 0)) as expectedTotalFee, "
+ " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount, IFNULL(cte_tss.sumManhourExpenditure, 0) as sumManhourExpenditure,"
+ " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount, IFNULL(cte_tss.sumManhourExpenditure, 0) as sumManhourExpenditure,"
+ " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName"
+ " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName"
+ " from project p"
+ " from project p"
@@ -2060,11 +2104,23 @@ open class ReportService(
+ " and p.id = :projectId"
+ " and p.id = :projectId"
+ " order by g.code, s.name, cte_ts.recordDate"
+ " order by g.code, s.name, cte_ts.recordDate"
)
)
val projectPlanStartEndMonth = projectRepository.findProjectPlanStartEndByIdAndDeletedFalse(projectId)
val compareStartMonth= YearMonth.parse(startMonth)
val compareEndMonth= YearMonth.parse(endMonth)
val actualStartMonth: YearMonth
(if (projectPlanStartEndMonth.actualStart == null){
YearMonth.now().plusMonths(1)
}else{
YearMonth.from(projectPlanStartEndMonth.actualStart)
}).also { actualStartMonth = it }
val queryStartMonth = if(compareStartMonth.isAfter(actualStartMonth)) compareStartMonth else actualStartMonth
val queryEndMonth = if(compareEndMonth.isAfter(YearMonth.now())) YearMonth.now() else compareEndMonth
val args = mapOf(
val args = mapOf(
"projectId" to projectId,
"projectId" to projectId,
"startMonth" to startMonth,
"endMonth" to endMonth,
"startMonth" to queryStartMonth.toString() ,
"endMonth" to qu eryE ndMonth.toString() ,
)
)
val manHoursSpent = jdbcDao.queryForList(sql.toString(), args)
val manHoursSpent = jdbcDao.queryForList(sql.toString(), args)
@@ -2088,7 +2144,7 @@ open class ReportService(
"startMonth" to startMonth,
"startMonth" to startMonth,
"endMonth" to endMonth,
"endMonth" to endMonth,
"code" to projectsCode["code"],
"code" to projectsCode["code"],
"description" to projectsCode["description"],
"description" to projectsCode["description"]
)
)
val staffInfoList = mutableListOf<Map<String, Any>>()
val staffInfoList = mutableListOf<Map<String, Any>>()
@@ -2109,6 +2165,9 @@ open class ReportService(
if (info["code"] == item["code"] && "subsidiary" !in info) {
if (info["code"] == item["code"] && "subsidiary" !in info) {
info["subsidiary"] = item.getValue("subsidiary")
info["subsidiary"] = item.getValue("subsidiary")
}
}
if (info["code"] == item["code"] && "expectedTotalFee" !in info) {
info["expectedTotalFee"] = item.getValue("expectedTotalFee")
}
if (info["manhourExpenditure"] != item.getValue("sumManhourExpenditure")) {
if (info["manhourExpenditure"] != item.getValue("sumManhourExpenditure")) {
info["manhourExpenditure"] = item.getValue("sumManhourExpenditure")
info["manhourExpenditure"] = item.getValue("sumManhourExpenditure")
}
}
@@ -2188,9 +2247,11 @@ open class ReportService(
}
}
fun genPandLReport(projectId: Long, startMonth: String, endMonth: String): ByteArray {
fun genPandLReport(projectId: Long, startMonth: String, endMonth: String): ByteArray {
val manhoursSpentList = getManhoursSpent(projectId, startMonth, endMonth)
val manhoursSpentList = getManhoursSpent(projectId, startMonth, endMonth)
val workbook: Workbook = createPandLReportWorkbook(PandL_REPORT, manhoursSpentList, startMonth, endMonth)
val workbook: Workbook = createPandLReportWorkbook(PandL_REPORT, manhoursSpentList, startMonth, endMonth, projectId )
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.write(outputStream)
@@ -2203,7 +2264,8 @@ open class ReportService(
templatePath: String,
templatePath: String,
manhoursSpent: List<Map<String, Any>>,
manhoursSpent: List<Map<String, Any>>,
startMonth: String,
startMonth: String,
endMonth: String
endMonth: String,
projectId: Long
): Workbook {
): Workbook {
val resource = ClassPathResource(templatePath)
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val templateInputStream = resource.inputStream
@@ -2211,14 +2273,35 @@ open class ReportService(
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val projectPlanStartEndMonth = projectRepository.findProjectPlanStartEndByIdAndDeletedFalse(projectId)
val compareStartMonth= YearMonth.parse(startMonth)
val compareEndMonth= YearMonth.parse(endMonth)
val actualStartMonth: YearMonth
(if (projectPlanStartEndMonth.actualStart == null){
YearMonth.now().plusMonths(1)
}else{
YearMonth.from(projectPlanStartEndMonth.actualStart)
}).also { actualStartMonth = it }
val queryStartMonth = if(compareStartMonth.isAfter(actualStartMonth)) compareStartMonth else actualStartMonth
val queryEndMonth = if(compareEndMonth.isAfter(YearMonth.now())) YearMonth.now() else compareEndMonth
val monthFormat = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH)
val monthFormat = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH)
val startDate = YearMonth.parse(startMonth, DateTimeFormatter.ofPattern("yyyy-MM"))
val startDate = YearMonth.parse(queryStartMonth.toString() , DateTimeFormatter.ofPattern("yyyy-MM"))
val convertStartMonth = YearMonth.of(startDate.year, startDate.month)
val convertStartMonth = YearMonth.of(startDate.year, startDate.month)
val endDate = YearMonth.parse(endMonth, DateTimeFormatter.ofPattern("yyyy-MM"))
val endDate = YearMonth.parse(qu eryE ndMonth.toString() , DateTimeFormatter.ofPattern("yyyy-MM"))
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()
var currentDate = startDate
var currentDate = startDate
if (currentDate == endDate){
monthRange.add(
mapOf(
"display" to YearMonth.of(currentDate.year, currentDate.month).format(monthFormat),
"date" to currentDate
)
)
}
while (!currentDate.isAfter(endDate)) {
while (!currentDate.isAfter(endDate)) {
monthRange.add(
monthRange.add(
mapOf(
mapOf(
@@ -2468,7 +2551,7 @@ open class ReportService(
val lastColumnIndex = getColumnAlphabet(3 + monthRange.size)
val lastColumnIndex = getColumnAlphabet(3 + monthRange.size)
val totalManhourETitleCell = totalManhourERow.getCell(0) ?: totalManhourERow.createCell(0)
val totalManhourETitleCell = totalManhourERow.getCell(0) ?: totalManhourERow.createCell(0)
totalManhourETitleCell.apply {
totalManhourETitleCell.apply {
setCellValue("Total Manhour Expenditure (HKD)")
setCellValue("Less: Total Manhour Expenditure (HKD)")
}
}
val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1)
val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1)
totalManhourECell.apply {
totalManhourECell.apply {
@@ -2490,8 +2573,36 @@ open class ReportService(
cellFormula = "B${rowNum - 1}-B${rowNum}"
cellFormula = "B${rowNum - 1}-B${rowNum}"
cellStyle.dataFormat = accountingStyle
cellStyle.dataFormat = accountingStyle
}
}
CellUtil.setCellStyleProperty(panlCellTitle, "borderBottom", BorderStyle.DOUBLE)
CellUtil.setCellStyleProperty(panlCell, "borderBottom", BorderStyle.DOUBLE)
// CellUtil.setCellStyleProperty(panlCellTitle, "borderBottom", BorderStyle.DOUBLE)
// CellUtil.setCellStyleProperty(panlCell, "borderBottom", BorderStyle.DOUBLE)
val profitAndLossRowNum = rowNum+1
rowNum += 1
val uninvoicedAmountRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum)
val uninvoicedAmountTitleCell = uninvoicedAmountRow.getCell(0) ?: uninvoicedAmountRow.createCell(0)
uninvoicedAmountTitleCell.apply {
setCellValue("Add: Uninvoice Amount (HKD)")
}
val uninvoicedAmountCell = uninvoicedAmountRow.getCell(1) ?: uninvoicedAmountRow.createCell(1)
uninvoicedAmountCell.apply {
setCellValue(if ((info.getValue("expectedTotalFee") as Double) < 0.0 ) 0.0 else info.getValue("expectedTotalFee") as Double)
cellStyle.dataFormat = accountingStyle
}
rowNum += 1
val projectedPnLRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum)
val projectedPnLTitleCell = projectedPnLRow.getCell(0) ?: projectedPnLRow.createCell(0)
projectedPnLTitleCell.apply {
setCellValue("Projected Profit / (Loss)")
}
val projectedPnLCell = projectedPnLRow.getCell(1) ?: projectedPnLRow.createCell(1)
projectedPnLCell.apply {
cellFormula = "B${profitAndLossRowNum}+B${profitAndLossRowNum+1}"
cellStyle.dataFormat = accountingStyle
}
CellUtil.setCellStyleProperty(projectedPnLTitleCell, "borderBottom", BorderStyle.DOUBLE)
CellUtil.setCellStyleProperty(projectedPnLCell, "borderBottom", BorderStyle.DOUBLE)
conditionalFormattingNegative(sheet)
conditionalFormattingNegative(sheet)