瀏覽代碼

update cross team report, dashboard excel

tags/Baseline_30082024_BACKEND_UAT
cyril.tsui 1 年之前
父節點
當前提交
26682b1e6c
共有 5 個文件被更改,包括 198 次插入109 次删除
  1. +1
    -1
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  2. +149
    -100
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  3. +11
    -8
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  4. +1
    -0
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  5. +36
    -0
      src/main/resources/db/changelog/changes/20240715_01_cyril/01_update_authority.sql

+ 1
- 1
src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt 查看文件

@@ -2442,7 +2442,7 @@ open class DashboardService(
}

createCell(5).apply {
cellFormula = "IFERROR(IF(L${rowIndex}=0, 0, K${rowIndex}/J${rowIndex}),0)"
cellFormula = "IFERROR(IF(L${rowIndex}=0, 0,L${rowIndex}/K${rowIndex}),0)"
cellStyle.apply {
setFont(normalFont)
dataFormat = accountingStyle


+ 149
- 100
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt 查看文件

@@ -331,10 +331,11 @@ open class ReportService(
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
teamId: String,
): ByteArray {
// Generate the Excel report with query results
val workbook: Workbook =
createCrossTeamChargeReport(month, timesheets, teams, grades, CROSS_TEAM_CHARGE_REPORT)
createCrossTeamChargeReport(month, timesheets, teams, grades, teamId, CROSS_TEAM_CHARGE_REPORT)

// Write the workbook to a ByteArrayOutputStream
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
@@ -2026,51 +2027,52 @@ open class ReportService(
}

open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("with teamNormalConsumed as ("
+ " SELECT"
+ " t.projectId,"
+ " p.teamLead,"
+ " sum(t.normalConsumed) as normalConsumed,"
+ " sum(t.otConsumed) as otConsumed,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as totalBudget"
+ " from timesheet t"
+ " left join project p on p.id = t.projectId"
+ " left join staff s on s.id = p.teamLead"
+ " left join salary sal on sal.salaryPoint = s.salaryId"
+ " group by p.teamLead, t.projectId, sal.hourlyRate"
+ " )"
+ " SELECT"
+ " p.id,"
+ " p.code,"
+ " p.name,"
+ " t.code,"
+ " 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) >= :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'"
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1"
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1"
+ " then 'Overconsumption'"
+ " else 'Within Budget'"
+ " END as status"
+ " from project p"
+ " left join staff s on s.id = p.teamLead"
+ " left join team t on t.id = s.teamId"
+ " left join customer c on c.id = p.customerId"
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id"
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint"
+ " left join teamNormalConsumed tns on tns.projectId = p.id"
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
val sql = StringBuilder(
"with teamNormalConsumed as ("
+ " SELECT"
+ " t.projectId,"
+ " p.teamLead,"
+ " sum(t.normalConsumed) as normalConsumed,"
+ " sum(t.otConsumed) as otConsumed,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed,"
+ " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as totalBudget"
+ " from timesheet t"
+ " left join project p on p.id = t.projectId"
+ " left join staff s on s.id = p.teamLead"
+ " left join salary sal on sal.salaryPoint = s.salaryId"
+ " group by p.teamLead, t.projectId, sal.hourlyRate"
+ " )"
+ " SELECT"
+ " p.id,"
+ " p.code,"
+ " p.name,"
+ " t.code,"
+ " 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) >= :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'"
+ " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1"
+ " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1"
+ " then 'Overconsumption'"
+ " else 'Within Budget'"
+ " END as status"
+ " from project p"
+ " left join staff s on s.id = p.teamLead"
+ " left join team t on t.id = s.teamId"
+ " left join customer c on c.id = p.customerId"
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id"
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint"
+ " left join teamNormalConsumed tns on tns.projectId = p.id"
+ " WHERE p.deleted = false "
+ " and p.status = 'On-going' "
)
if (args != null) {
var statusFilter: String = ""
@@ -3037,6 +3039,7 @@ open class ReportService(
timesheets: List<Timesheet>,
teams: List<Team>,
grades: List<Grade>,
teamId: String,
templatePath: String,
): Workbook {
// please create a new function for each report template
@@ -3044,7 +3047,7 @@ open class ReportService(
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)

val sheet: Sheet = workbook.getSheetAt(0)
var sheet: Sheet = workbook.getSheetAt(0)

// accounting style + comma style
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
@@ -3076,6 +3079,7 @@ open class ReportService(
borderRight = BorderStyle.THIN
}

// -------------------------- sheet 0 (By Grade) -------------------------- //
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)
@@ -3088,9 +3092,12 @@ open class ReportService(
// if (timesheets.isNotEmpty()) {
val combinedTeamCodeColNumber = grades.size * 2
val sortedGrades = grades.sortedBy { it.id }
val sortedTeams = teams.sortedBy { it.id }
var sortedTeams = teams.sortedBy { it.id }.toMutableList()
if (teamId.lowercase() != "all") {
val teamIndex = sortedTeams.indexOfFirst { it.id == teamId.toLong() }
sortedTeams.add(0, sortedTeams.removeAt(teamIndex))
}

logger.info(timesheets.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }.size)
val groupedTimesheets = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
.groupBy { timesheetEntry ->
@@ -3200,75 +3207,84 @@ open class ReportService(
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)
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
if (teamId.lowercase() == "all" || teamId.toLong() == chargedTeam.id || teamId.toLong() == team.id) {
if (team.id != chargedTeam.id) {
endRow++
sheet.createRow(rowIndex++).apply {
columnIndex = 0
createCell(columnIndex++).apply {
setCellValue(chargedTeam.code)
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
}
}
}

var totalSalary = 0.0
sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(
groupedTimesheets[Triple(
var totalSalary = 0.0
sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(
groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("manHour") } ?: 0.0)

totalSalary += groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("manHour") } ?: 0.0)
)]?.sumOf { it.getValue("salary") } ?: 0.0

totalSalary += groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("salary") } ?: 0.0
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
createCell(columnIndex++).apply {
setCellValue(
groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("salary") } ?: 0.0)

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}
}

createCell(columnIndex++).apply {
setCellValue(
groupedTimesheets[Triple(
team.id,
chargedTeam.id,
grade.id
)]?.sumOf { it.getValue("manHour") * it.getValue("salary") } ?: 0.0)
// val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 2)
// cellFormula = "sum(B${this.rowIndex + 1}, ${lastCellLetter}${this.rowIndex + 1})"
var cellFormulaStr = "sum("
for (i in 1..(grades.size * 2) step 2) {
val cellLetter = CellReference.convertNumToColString(i)
cellFormulaStr += "${cellLetter}${this.rowIndex + 1},"
}
cellFormulaStr += ")"
cellFormula = cellFormulaStr

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(normalFontWithBorderStyle)
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}
}

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

val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}

createCell(columnIndex).apply {
setCellValue(totalSalary)
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
createCell(columnIndex).apply {
setCellValue(totalSalary)
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
dataFormat = accountingStyle
}
}
}
}
@@ -3315,6 +3331,39 @@ open class ReportService(
// }

conditionalFormattingNegative(sheet)
// -------------------------- sheet 1 (By Detail) -------------------------- //
// sheet = workbook.getSheetAt(1)
//
// val groupedTimesheets2 = timesheets
// .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }
// .groupBy { timesheetEntry ->
// Pair(
// timesheetEntry.project?.id,
// timesheetEntry.staff?.id
// )
// }
// .mapValues { (_, timesheetEntries) ->
// timesheetEntries.map { timesheet ->
// if (timesheet.normalConsumed != null) {
// mutableMapOf<String, Double>().apply {
// this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
// this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0)
// .times(timesheet.staff!!.salary.hourlyRate.toDouble())
// }
// } else if (timesheet.otConsumed != null) {
// mutableMapOf<String, Double>().apply {
// this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
// this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0)
// .times(timesheet.staff!!.salary.hourlyRate.toDouble())
// }
// } else {
// mutableMapOf<String, Double>().apply {
// this["manHour"] = 0.0
// this["salary"] = 0.0
// }
// }
// }
// }

return workbook
}

+ 11
- 8
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt 查看文件

@@ -317,23 +317,26 @@ class ReportController(

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()
if (authorities.stream().anyMatch { it.authority.equals("GENERATE_CROSS_TEAM_CHARGE_REPORT") }) {
// 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 &&
// 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 timesheets = timesheetRepository.findAll()
.filter { it.deleted == false && it.recordDate != null && it.staff != null && it.staff!!.team != null && it.project != 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)
val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades, request.teamId)
return ResponseEntity.ok()
.header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))


+ 1
- 0
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt 查看文件

@@ -62,4 +62,5 @@ data class ProjectCompletionReport (

data class CrossTeamChargeReportRequest (
val month: String,
val teamId: String
)

+ 36
- 0
src/main/resources/db/changelog/changes/20240715_01_cyril/01_update_authority.sql 查看文件

@@ -0,0 +1,36 @@
-- liquibase formatted sql
-- changeset cyril:authority, user_authority

UPDATE tsmsdb.authority
SET authority='GENERATE_LATE_START_REPORT',name='Generate Late Start Report'
WHERE id=17;

INSERT INTO authority (authority,name)
VALUES ('GENERATE_PROJECT_POTENTIAL_DELAY_REPORT','Generate Project Potential Delay Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_RESOURCE_OVERCONSUMPTION_REPORT','Generate Resource Overconsumption Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_COST_ANT_EXPENSE_REPORT','Generate Cost And Expense Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_PROJECT_COMPLETION_REPORT','Generate Project Completion Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_PROJECT_P&L_REPORT','Generate Project P&L Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_FINANCIAL_STATUS_REPORT','Generate Financial Status Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_PROJECT_CASH_FLOW_REPORT','Generate Project Cash Flow Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT','Generate Staff Monthly Work Hours Analysis Report');
INSERT INTO authority (authority,name)
VALUES ('GENERATE_CROSS_TEAM_CHARGE_REPORT','Generate Cross Team Charge Report');

INSERT INTO `user_authority` VALUES
(1,44),
(1,45),
(1,46),
(1,47),
(1,48),
(1,49),
(1,50),
(1,51),
(1,52);

Loading…
取消
儲存