|
|
@@ -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 |
|
|
|
} |