Kaynağa Gözat

add cross team charge report

tags/Baseline_30082024_BACKEND_UAT
cyril.tsui 1 yıl önce
ebeveyn
işleme
7d7a1f7d0c
5 değiştirilmiş dosya ile 433 ekleme ve 137 silme
  1. +392
    -136
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  2. +35
    -1
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  3. +4
    -0
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  4. +2
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt
  5. BIN
      src/main/resources/templates/report/Cross Team Charge Report.xlsx

+ 392
- 136
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Dosyayı Görüntüle

@@ -2,6 +2,7 @@ package com.ffii.tsms.modules.report.service

import com.ffii.core.support.JdbcDao
import com.ffii.tsms.modules.data.entity.Customer
import com.ffii.tsms.modules.data.entity.Grade
import com.ffii.tsms.modules.data.entity.Salary
import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.data.entity.Team
@@ -17,6 +18,8 @@ import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.ss.util.CellUtil
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.ss.usermodel.FormulaEvaluator
import org.apache.poi.ss.util.CellReference
import org.apache.poi.ss.util.RegionUtil
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream
@@ -54,6 +57,7 @@ open class ReportService(
private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE =
"templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx"
private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx"
private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx"

// ==============================|| GENERATE REPORT ||============================== //
fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex
@@ -299,6 +303,25 @@ open class ReportService(
return outputStream.toByteArray()
}

@Throws(IOException::class)
fun generateCrossTeamChargeReport(
month: String,
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
): ByteArray {
// Generate the Excel report with query results
val workbook: Workbook =
createCrossTeamChargeReport(month, timesheets, teams, grades, CROSS_TEAM_CHARGE_REPORT)

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

return outputStream.toByteArray()
}

// ==============================|| CREATE REPORT ||============================== //

// EX01 Financial Report
@@ -985,10 +1008,15 @@ open class ReportService(
project.milestones.forEach { milestone: Milestone ->

val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0
val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
val resourceUtilization =
manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
// logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate))
// logger.info(daysUntilCurrentStageEnd)
if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0) {
if (ChronoUnit.DAYS.between(
LocalDate.now(),
milestone.endDate
) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0
) {
milestoneCount++
val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex)
rowIndex++
@@ -1010,7 +1038,7 @@ open class ReportService(
//
// val resourceUtilization =
// manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
setCellValue(resourceUtilization)
setCellValue(resourceUtilization)
// } else {
// setCellValue(0.0)
// }
@@ -1480,7 +1508,7 @@ open class ReportService(
setDataAndConditionalFormatting(workbook, sheet, lateStartData, evaluator)

// Automatically adjust column widths to fit content
autoSizeColumns(sheet)
autoSizeColumns(sheet)

return workbook
}
@@ -1626,7 +1654,8 @@ open class ReportService(
// NEW Column F: Subsidiary Name
val subsidiaryNameCell = row.createCell(5)
// subsidiaryNameCell.setCellValue(data["subsidiary_name"] as String)
val subsidiaryName = data["subsidiary_name"] as? String ?: "N/A" // Checks if subsidiary_name is null and replaces it with "N/A"
val subsidiaryName =
data["subsidiary_name"] as? String ?: "N/A" // Checks if subsidiary_name is null and replaces it with "N/A"
subsidiaryNameCell.setCellValue(subsidiaryName)

// Column G: Project Plan Start Date
@@ -1747,112 +1776,118 @@ open class ReportService(
)
return jdbcDao.queryForList(sql.toString(), args)
}

open fun getProjectCompletionReport(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("select"
+ " result.code, "
+ " result.name, "
+ " result.teamCode, "
+ " result.custCode, "
+ " result.subCode, " )
val sql = StringBuilder(
"select"
+ " result.code, "
+ " result.name, "
+ " result.teamCode, "
+ " result.custCode, "
+ " result.subCode, "
)
if (args.get("outstanding") as Boolean) {
sql.append(" result.projectFee - COALESCE(i.paidAmount, 0) as `Receivable Remained`, ")
// sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ")
}
sql.append(
" DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd "
+ " from ( "
+ " SELECT "
+ " pt.project_id, "
+ " min(p.code) as code, "
+ " min(p.name) as name, "
+ " min(t.code) as teamCode, "
+ " concat(min(c.code), ' - ', min(c.name)) as custCode, "
+ " COALESCE(concat(min(ss.code),' - ',min(ss.name)), 'N/A') as subCode, "
+ " min(p.actualEnd) as actualEnd, "
+ " min(p.expectedTotalFee) as projectFee, "
+ " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget "
+ " FROM ( "
+ " SELECT "
+ " t.staffId, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " t.projectTaskId AS taskId "
+ " FROM timesheet t "
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " LEFT JOIN team te on s.teamId = te.id "
+ " GROUP BY t.staffId, t.projectTaskId "
+ " order by t.staffId "
+ " ) AS tns "
+ " right join project_task pt ON tns.taskId = pt.id "
+ " left join project p on p.id = pt.project_id "
+ " left JOIN staff s ON p.teamLead = s.id "
+ " left join salary sal on s.salaryId = sal.salaryPoint "
+ " left JOIN team t ON s.teamId = t.id "
+ " left join customer c on c.id = p.customerId "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " where p.deleted = false ")
if (args.containsKey("teamId")) {
sql.append("t.id = :teamId")
}
sql.append(
+ " from ( "
+ " SELECT "
+ " pt.project_id, "
+ " min(p.code) as code, "
+ " min(p.name) as name, "
+ " min(t.code) as teamCode, "
+ " concat(min(c.code), ' - ', min(c.name)) as custCode, "
+ " COALESCE(concat(min(ss.code),' - ',min(ss.name)), 'N/A') as subCode, "
+ " min(p.actualEnd) as actualEnd, "
+ " min(p.expectedTotalFee) as projectFee, "
+ " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget "
+ " FROM ( "
+ " SELECT "
+ " t.staffId, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " t.projectTaskId AS taskId "
+ " FROM timesheet t "
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " LEFT JOIN team te on s.teamId = te.id "
+ " GROUP BY t.staffId, t.projectTaskId "
+ " order by t.staffId "
+ " ) AS tns "
+ " right join project_task pt ON tns.taskId = pt.id "
+ " left join project p on p.id = pt.project_id "
+ " left JOIN staff s ON p.teamLead = s.id "
+ " left join salary sal on s.salaryId = sal.salaryPoint "
+ " left JOIN team t ON s.teamId = t.id "
+ " left join customer c on c.id = p.customerId "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " where p.deleted = false "
)
if (args.containsKey("teamId")) {
sql.append("t.id = :teamId")
}
sql.append(
" and p.status = 'Completed' "
+ " and p.actualEnd BETWEEN :startDate and :endDate "
+ " group by pt.project_id "
+ " ) as result "
+ " left join invoice i on result.code = i.projectCode "
+ " order by result.actualEnd ")
+ " and p.actualEnd BETWEEN :startDate and :endDate "
+ " group by pt.project_id "
+ " ) as result "
+ " left join invoice i on result.code = i.projectCode "
+ " order by result.actualEnd "
)

return jdbcDao.queryForList(sql.toString(), args)
}

open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("WITH teamNormalConsumed AS ("
+ " SELECT"
+ " tns.projectId,"
+ " SUM(tns.totalConsumed) AS totalConsumed, "
+ " sum(tns.totalBudget) as totalBudget "
+ " FROM ( "
+ " SELECT"
+ " t.staffId,"
+ " t.projectId AS projectId,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget "
+ " FROM timesheet t"
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " left join salary sal on sal.salaryPoint = s.salaryId "
+ " GROUP BY t.staffId, t.projectId"
+ " ) AS tns"
+ " GROUP BY projectId"
+ " ) "
+ " SELECT "
+ " p.code, "
+ " p.name, "
+ " t.code as team, "
+ " concat(c.code, ' - ', c.name) as client, "
+ " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, "
+ " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, "
+ " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, "
+ " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, "
+ " CASE "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 "
+ " then 'Overconsumption' "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 "
+ " then 'Potential Overconsumption' "
+ " else 'Within Budget' "
+ " END as status "
+ " FROM project p "
+ " LEFT JOIN team t ON p.teamLead = t.teamLead "
+ " LEFT JOIN staff s ON p.teamLead = s.id "
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint "
+ " LEFT JOIN customer c ON p.customerId = c.id "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " left join teamNormalConsumed tns on tns.project_id = p.id "
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
val sql = StringBuilder(
"WITH teamNormalConsumed AS ("
+ " SELECT"
+ " tns.projectId,"
+ " SUM(tns.totalConsumed) AS totalConsumed, "
+ " sum(tns.totalBudget) as totalBudget "
+ " FROM ( "
+ " SELECT"
+ " t.staffId,"
+ " t.projectId AS projectId,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, "
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget "
+ " FROM timesheet t"
+ " LEFT JOIN staff s ON t.staffId = s.id "
+ " left join salary sal on sal.salaryPoint = s.salaryId "
+ " GROUP BY t.staffId, t.projectId"
+ " ) AS tns"
+ " GROUP BY projectId"
+ " ) "
+ " SELECT "
+ " p.code, "
+ " p.name, "
+ " t.code as team, "
+ " concat(c.code, ' - ', c.name) as client, "
+ " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, "
+ " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, "
+ " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, "
+ " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, "
+ " CASE "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 "
+ " then 'Overconsumption' "
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 "
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 "
+ " then 'Potential Overconsumption' "
+ " else 'Within Budget' "
+ " END as status "
+ " FROM project p "
+ " LEFT JOIN team t ON p.teamLead = t.teamLead "
+ " LEFT JOIN staff s ON p.teamLead = s.id "
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint "
+ " LEFT JOIN customer c ON p.customerId = c.id "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " left join teamNormalConsumed tns on tns.project_id = p.id "
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
)
if (args != null) {
var statusFilter: String = ""
@@ -1869,6 +1904,7 @@ open class ReportService(
" and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " +
" or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " +
" and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 "

"All" -> " and " +
" (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " +
" or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit "
@@ -2170,9 +2206,9 @@ open class ReportService(
rowNum = 6
val row6: Row = sheet.getRow(rowNum)
val row6Cell = row6.getCell(1)
val clientSubsidiary = if(info.getValue("subsidiary").toString() != "N/A"){
val clientSubsidiary = if (info.getValue("subsidiary").toString() != "N/A") {
info.getValue("subsidiary").toString()
}else{
} else {
info.getValue("client").toString()
}
row6Cell.setCellValue(clientSubsidiary)
@@ -2367,10 +2403,10 @@ open class ReportService(
return workbook
}

fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String,Any?>>{
fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String, Any?>> {
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.salaryPoint, s2.hourlyRate, t.staffId,"
+ " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId,"
+ " t.recordDate"
+ " from timesheet t"
+ " left join project_task pt on pt.id = t.projectTaskId"
@@ -2394,20 +2430,20 @@ open class ReportService(
+ " left join team t2 on t2.id = s.teamId"
+ " where ISNULL(p.code) = False"
)
if(clientId != null){
if(type == "client"){
if (clientId != null) {
if (type == "client") {
sql.append(
" and c.id = :clientId "
)
}
if(type == "subsidiary"){
if (type == "subsidiary") {
sql.append(
" and s2.id = :clientId "
)
}
}

if(teamId != null){
if (teamId != null) {
sql.append(
" and p.teamLead = :teamId "
)
@@ -2426,9 +2462,9 @@ open class ReportService(
val queryList = jdbcDao.queryForList(sql.toString(), args)
val costAndExpenseList = mutableListOf<Map<String, Any?>>()

for(item in queryList){
for (item in queryList) {
val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble()
if(item["code"] !in costAndExpenseList.map { it["code"] }){
if (item["code"] !in costAndExpenseList.map { it["code"] }) {
costAndExpenseList.add(
mapOf(
"code" to item["code"],
@@ -2437,22 +2473,27 @@ open class ReportService(
"subsidiary" to item["subsidiary"],
"teamLead" to item["teamLead"],
"budget" to item["expectedTotalFee"],
"totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double,
"manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double )
+ (hourlyRate * item["otConsumed"] as Double * otFactor)
"totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double,
"manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double)
+ (hourlyRate * item["otConsumed"] as Double * otFactor)
)
)
}else{
} else {
val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!!
costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply {
put("totalManhours", get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double))
put("manhourExpenditure", get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double )
+ (hourlyRate * item["otConsumed"]as Double * otFactor)))
put(
"totalManhours",
get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)
)
put(
"manhourExpenditure",
get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double)
+ (hourlyRate * item["otConsumed"] as Double * otFactor))
)
}
}
}
val result = costAndExpenseList.map {
item ->
val result = costAndExpenseList.map { item ->
val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0
val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0)
val remainingPercent = (budgetRemain / budget)
@@ -2471,7 +2512,7 @@ open class ReportService(
clientId: Long?,
budgetPercentage: Double?,
type: String
): Workbook{
): Workbook {
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)
@@ -2489,9 +2530,9 @@ open class ReportService(
rowNum = 2
val row2: Row = sheet.getRow(rowNum)
val row2Cell = row2.getCell(2)
if(teamId == null){
if (teamId == null) {
row2Cell.setCellValue("All")
}else{
} else {
val sql = StringBuilder(
" select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId "
)
@@ -2502,16 +2543,16 @@ open class ReportService(
rowNum = 3
val row3: Row = sheet.getRow(rowNum)
val row3Cell = row3.getCell(2)
if(clientId == null){
if (clientId == null) {
row3Cell.setCellValue("All")
}else{
val sql= StringBuilder()
if(type == "client"){
} else {
val sql = StringBuilder()
if (type == "client") {
sql.append(
" select c.id, c.name from customer c where c.id = :clientId "
)
}
if(type == "subsidiary"){
if (type == "subsidiary") {
sql.append(
" select s.id, s.name from subsidiary s where s.id = :clientId "
)
@@ -2522,15 +2563,15 @@ open class ReportService(


val filterList: List<Map<String, Any?>>
if(budgetPercentage != null){
if (budgetPercentage != null) {
filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) <= budgetPercentage }
}else{
} else {
filterList = costAndExpenseList
}


rowNum = 6
for(item in filterList){
for (item in filterList) {
val index = filterList.indexOf(item)
val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum)
val cell = row.getCell(0) ?: row.createCell(0)
@@ -2580,13 +2621,13 @@ open class ReportService(

val cell8 = row.getCell(8) ?: row.createCell(8)
cell8.apply {
cellFormula = "G${rowNum+1}-H${rowNum+1}"
cellFormula = "G${rowNum + 1}-H${rowNum + 1}"
}
CellUtil.setCellStyleProperty(cell8, "dataFormat", accountingStyle)

val cell9 = row.getCell(9) ?: row.createCell(9)
cell9.apply {
cellFormula = "I${rowNum+1}/G${rowNum+1}"
cellFormula = "I${rowNum + 1}/G${rowNum + 1}"
}
CellUtil.setCellStyleProperty(cell9, "dataFormat", percentStyle)

@@ -2596,11 +2637,18 @@ open class ReportService(
return workbook
}

fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{
fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray {

val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId, request.type)

val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.teamId, request.clientId, request.budgetPercentage, request.type)
val workbook: Workbook = createCostAndExpenseWorkbook(
COSTANDEXPENSE_REPORT,
costAndExpenseList,
request.teamId,
request.clientId,
request.budgetPercentage,
request.type
)

val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
@@ -2608,8 +2656,16 @@ open class ReportService(

return outputStream.toByteArray()
}
open fun getLateStartDetails(teamId: Long?, clientId: Long?, remainedDate: LocalDate, remainedDateTo: LocalDate, type: String?): List<Map<String, Any>> {
val sql = StringBuilder("""

open fun getLateStartDetails(
teamId: Long?,
clientId: Long?,
remainedDate: LocalDate,
remainedDateTo: LocalDate,
type: String?
): List<Map<String, Any>> {
val sql = StringBuilder(
"""
SELECT
p.code AS project_code,
p.name AS project_name,
@@ -2630,7 +2686,8 @@ open class ReportService(
p.status = 'Pending to Start'
AND p.planStart < CURRENT_DATE
AND m.endDate BETWEEN :remainedDate AND :remainedDateTo
""".trimIndent())
""".trimIndent()
)

if (teamId != null && teamId > 0) {
sql.append(" AND t.id = :teamId")
@@ -2654,4 +2711,203 @@ open class ReportService(
)
return jdbcDao.queryForList(sql.toString(), args)
}
}

@Throws(IOException::class)
private fun createCrossTeamChargeReport(
month: String,
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
templatePath: String,
): Workbook {
// please create a new function for each report template
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)

val sheet: Sheet = workbook.getSheetAt(0)

// accounting style + comma style
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")

// bold font with border style
val boldFont = workbook.createFont().apply {
bold = true
fontName = "Times New Roman"
}

val boldFontWithBorderStyle = workbook.createCellStyle().apply {
setFont(boldFont)
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

// normal font
val normalFont = workbook.createFont().apply {
fontName = "Times New Roman"
}

val normalFontWithBorderStyle = workbook.createCellStyle().apply {
setFont(normalFont)
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
var columnIndex = 2
val monthFormat = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH)
val reportMonth = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM"))
val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month).format(monthFormat)
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(convertReportMonth)
}

if (timesheets.isNotEmpty()) {
val combinedTeamCodeColNumber = grades.size
val sortedGrades = grades.sortedBy { it.id }
val sortedTeams = teams.sortedBy { it.id }

val groupedTimesheets = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
.groupBy { timesheetEntry ->
Triple(
timesheetEntry.project?.teamLead?.team?.id,
timesheetEntry.staff?.team?.id,
timesheetEntry.staff?.grade?.id
)
}
.mapValues { (_, timesheetEntries) ->
timesheetEntries.map { timesheet ->
if (timesheet.normalConsumed != null) {
timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
} else if (timesheet.otConsumed != null) {
timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
} else {
0.0
}
}
}

if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) {
rowIndex = 3
sortedTeams.forEach { team: Team ->

// Team
sheet.createRow(rowIndex++).apply {
createCell(0).apply {
setCellValue("Team to be charged:")
cellStyle = boldFontWithBorderStyle.apply {
alignment = HorizontalAlignment.LEFT
}
}

val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 1 + combinedTeamCodeColNumber)
sheet.addMergedRegion(rangeAddress)
RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet)
RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet)

createCell(1).apply {
setCellValue(team.code)
cellStyle = normalFontWithBorderStyle.apply {
alignment = HorizontalAlignment.CENTER
}
}

}

// Grades
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}


sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(grade.name)
cellStyle = boldFontWithBorderStyle.apply {
alignment = HorizontalAlignment.CENTER
}
}
}

createCell(columnIndex).apply {
setCellValue("Total Manhour by Team")
cellStyle = boldFontWithBorderStyle
}
}

// Team + Manhour
val startRow = rowIndex
var endRow = rowIndex
sortedTeams.forEach { chargedTeam: Team ->
if (team.id != chargedTeam.id) {
endRow++
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue(chargedTeam.code)
cellStyle = normalFontWithBorderStyle
}

sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(
groupedTimesheets[Triple(team.id, chargedTeam.id, grade.id)]?.sum() ?: 0.0
)
cellStyle = normalFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}
}

createCell(columnIndex).apply {
val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1)
cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})"
cellStyle = boldFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}
}
}
}

// Total Manhour by grade
sheet.createRow(rowIndex).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue("Total Manhour by Grade")
cellStyle = boldFontWithBorderStyle
}

sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
val currentCellLetter = CellReference.convertNumToColString(this.columnIndex)
cellFormula = "sum(${currentCellLetter}${startRow}:${currentCellLetter}${endRow})"
cellStyle = normalFontWithBorderStyle.apply {
dataFormat = accountingStyle
}
}
}

createCell(columnIndex).apply {
setCellValue("")
cellStyle = boldFontWithBorderStyle
}
}
rowIndex += 2
}
}
}

return workbook
}
}

+ 35
- 1
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Dosyayı Görüntüle

@@ -38,6 +38,8 @@ import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.data.domain.Example
import org.springframework.data.domain.ExampleMatcher
import java.time.YearMonth
import java.util.*

@RestController
@RequestMapping("/reports")
@@ -57,7 +59,8 @@ class ReportController(
private val customerService: CustomerService,
private val subsidiaryService: SubsidiaryService,
private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository,
private val subsidiaryRepository: SubsidiaryRepository
private val subsidiaryRepository: SubsidiaryRepository, private val staffAllocationRepository: StaffAllocationRepository,
private val gradeRepository: GradeRepository
) {

private val logger: Log = LogFactory.getLog(javaClass)
@@ -304,4 +307,35 @@ class ReportController(
.body(ByteArrayResource(reportResult))
}

@PostMapping("/CrossTeamChargeReport")
@Throws(ServletRequestBindingException::class, IOException::class)
fun getCrossTeamChargeReport(@RequestBody @Valid request: CrossTeamChargeReportRequest): ResponseEntity<Resource> {

val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build()

if (authorities.stream().anyMatch { it.authority.equals("GENERATE_REPORTS") }) {
val crossProjects = staffAllocationRepository.findAll()
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
.map { it.project!! }
.distinct()

val reportMonth = YearMonth.parse(request.month, DateTimeFormatter.ofPattern("yyyy-MM"))
val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month)
val startDate = convertReportMonth.atDay(1)
val endDate = convertReportMonth.atEndOfMonth()
val timesheets = timesheetRepository.findAllByProjectIn(crossProjects)
.filter { it.recordDate != null &&
(it.recordDate!!.isEqual(startDate) || it.recordDate!!.isEqual(endDate) || (it.recordDate!!.isAfter(startDate) && it.recordDate!!.isBefore(endDate)))}
val teams = teamRepository.findAll().filter { it.deleted == false }
val grades = gradeRepository.findAll().filter { it.deleted == false }

val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades)
return ResponseEntity.ok()
.header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))

} else {
return ResponseEntity.noContent().build()
}
}
}

+ 4
- 0
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt Dosyayı Görüntüle

@@ -57,4 +57,8 @@ data class ProjectCompletionReport (
val startDate: LocalDate,
val endDate: LocalDate,
val outstanding: Boolean
)

data class CrossTeamChargeReportRequest (
val month: String,
)

+ 2
- 0
src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt Dosyayı Görüntüle

@@ -14,6 +14,8 @@ interface TimesheetRepository : AbstractRepository<Timesheet, Long> {

fun findAllByProjectTaskIn(projectTasks: List<ProjectTask>): List<Timesheet>

fun findAllByProjectIn(project: List<Project>): List<Timesheet>

fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate)

@Query("SELECT new com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours(IFNULL(SUM(normalConsumed), 0), IFNULL(SUM(otConsumed), 0)) FROM Timesheet t JOIN ProjectTask pt on t.projectTask = pt WHERE pt.project = ?1")


BIN
src/main/resources/templates/report/Cross Team Charge Report.xlsx Dosyayı Görüntüle


Yükleniyor…
İptal
Kaydet