From 4e0d61eb1641d5dc816ede8f0a63fada3b3c2f5f Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 17 Sep 2024 11:01:04 +0800 Subject: [PATCH] Add project expense to PnL Financial status report in progrss: project cash flow report --- .../modules/report/service/ReportService.kt | 225 +++++++++++++----- .../modules/report/web/ReportController.kt | 4 +- .../AR04_Cost and Expense Report v02.xlsx | Bin 12733 -> 12825 bytes .../report/EX01_Financial Status Report.xlsx | Bin 13179 -> 13221 bytes 4 files changed, 164 insertions(+), 65 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 1e0f2a5..33fd49a 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -4,10 +4,7 @@ import com.ffii.core.support.JdbcDao import com.ffii.tsms.modules.data.entity.* import com.ffii.tsms.modules.data.service.SalaryEffectiveService import com.ffii.tsms.modules.data.service.SalaryEffectiveService.MonthlyStaffSalaryData -import com.ffii.tsms.modules.project.entity.Invoice -import com.ffii.tsms.modules.project.entity.Milestone -import com.ffii.tsms.modules.project.entity.Project -import com.ffii.tsms.modules.project.entity.ProjectRepository +import com.ffii.tsms.modules.project.entity.* import com.ffii.tsms.modules.report.web.model.costAndExpenseRequest import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository @@ -158,6 +155,7 @@ open class ReportService( project: Project, invoices: List, timesheets: List, + projectExpenses: List, monthlyStaffSalaryEffective: List, dateType: String ): ByteArray { @@ -167,6 +165,7 @@ open class ReportService( project, invoices, timesheets, + projectExpenses, monthlyStaffSalaryEffective, dateType, PROJECT_CASH_FLOW_REPORT @@ -374,6 +373,7 @@ open class ReportService( sheet.setColumnWidth(3, 20 * 256) sheet.setColumnWidth(4, 45 * 256) sheet.setColumnWidth(5, 15 * 256) + sheet.setColumnWidth(7, 30 * 256) val boldFont = sheet.workbook.createFont() boldFont.bold = true @@ -467,22 +467,35 @@ open class ReportService( } val cumExpenditureCell = row.createCell(9) + cumExpenditureCell.apply { + cellFormula = "K${rowNum} + L${rowNum}" + cellStyle.dataFormat = accountingStyle + } + + val cumManHourSpentCell = row.createCell(10) // val normalConsumed = item["normalConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) // val otConsumed = item["otConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) // val cumExpenditure = normalConsumed.add(otConsumed) - val totalCumulativeExpenditure = item["totalCumulativeExpenditure"]?.let { it as BigDecimal } ?: BigDecimal(0) - cumExpenditureCell.apply { - setCellValue(totalCumulativeExpenditure.toDouble()) + val totalCumulativeManhoursExpenditure = item["totalCumulativeExpenditure"]?.let { it as BigDecimal } ?: BigDecimal(0) + cumManHourSpentCell.apply { + setCellValue(totalCumulativeManhoursExpenditure.toDouble()) + cellStyle.dataFormat = accountingStyle + } + + val projectExpenseCell = row.createCell(11) + val projectExpense = item["projectExpense"]?.let { it as BigDecimal } ?: BigDecimal(0) + projectExpenseCell.apply { + setCellValue(projectExpense.toDouble()) cellStyle.dataFormat = accountingStyle } - val budgetVCell = row.createCell(10) + val budgetVCell = row.createCell(12) budgetVCell.apply { cellFormula = "I${rowNum} - J${rowNum}" cellStyle.dataFormat = accountingStyle } - val issuedCell = row.createCell(11) + val issuedCell = row.createCell(13) val issuedAmount = item["sumIssuedAmount"]?.let { it as BigDecimal } ?: BigDecimal(0) issuedCell.apply { setCellValue(issuedAmount.toDouble()) @@ -490,36 +503,36 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - val uninvoiceCell = row.createCell(12) + val uninvoiceCell = row.createCell(14) uninvoiceCell.apply { cellFormula = - " IF(H${rowNum}-L${rowNum}<0, 0, H${rowNum}-L${rowNum})" + " IF(H${rowNum}-M${rowNum}<0, 0, H${rowNum}-M${rowNum})" // " IF(I${rowNum}<=J${rowNum}, I${rowNum}-L${rowNum}, IF(AND(I${rowNum}>J${rowNum}, J${rowNum}J${rowNum}, J${rowNum}>=L${rowNum}), J${rowNum}-L${rowNum}, 0))) " cellStyle.dataFormat = accountingStyle } - val cpiCell = row.createCell(13) + val cpiCell = row.createCell(15) cpiCell.apply { - cellFormula = "IF(J${rowNum} = 0, 0, L${rowNum}/J${rowNum})" + cellFormula = "IF(J${rowNum} = 0, 0, M${rowNum}/(J${rowNum}+K${rowNum}))" cellStyle = boldFontCellStyle } - val projectedCpiCell = row.createCell(14) + val projectedCpiCell = row.createCell(16) projectedCpiCell.apply { - cellFormula = "IF(J${rowNum} = 0, 0, H${rowNum}/J${rowNum})" + cellFormula = "IF(J${rowNum} = 0, 0, H${rowNum}/(J${rowNum}+K${rowNum}))" cellStyle = boldFontCellStyle } - val receivedAmountCell = row.createCell(15) + val receivedAmountCell = row.createCell(17) val paidAmount = item["sumPaidAmount"]?.let { it as BigDecimal } ?: BigDecimal(0) receivedAmountCell.apply { setCellValue(paidAmount.toDouble()) cellStyle.dataFormat = accountingStyle } - val unsettledAmountCell = row.createCell(16) + val unsettledAmountCell = row.createCell(18) unsettledAmountCell.apply { - cellFormula = "L${rowNum}-P${rowNum}" + cellFormula = "M${rowNum}-Q${rowNum}" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -566,42 +579,58 @@ open class ReportService( CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderBottom", BorderStyle.DOUBLE) - val sumBudgetVCell = row.createCell(10) - sumBudgetVCell.apply { + val sumTotalManHourSpent = row.createCell(10) + sumTotalManHourSpent.apply { cellFormula = "SUM(K15:K${rowNum})" + cellStyle.dataFormat = accountingStyle + } + CellUtil.setCellStyleProperty(sumTotalManHourSpent, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumTotalManHourSpent, "borderBottom", BorderStyle.DOUBLE) + + val sumProjectExpenseCell = row.createCell(11) + sumProjectExpenseCell.apply { + cellFormula = "SUM(L15:L${rowNum})" + cellStyle.dataFormat = accountingStyle + } + CellUtil.setCellStyleProperty(sumProjectExpenseCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumProjectExpenseCell, "borderBottom", BorderStyle.DOUBLE) + + val sumBudgetVCell = row.createCell(12) + sumBudgetVCell.apply { + cellFormula = "SUM(M15:M${rowNum})" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } CellUtil.setCellStyleProperty(sumBudgetVCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(sumBudgetVCell, "borderBottom", BorderStyle.DOUBLE) - val sumIInvoiceCell = row.createCell(11) + val sumIInvoiceCell = row.createCell(13) sumIInvoiceCell.apply { - cellFormula = "SUM(L15:L${rowNum})" + cellFormula = "SUM(N15:N${rowNum})" cellStyle.dataFormat = accountingStyle } CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderBottom", BorderStyle.DOUBLE) - val sumUInvoiceCell = row.createCell(12) + val sumUInvoiceCell = row.createCell(14) sumUInvoiceCell.apply { - cellFormula = "SUM(M15:M${rowNum})" + cellFormula = "SUM(O15:O${rowNum})" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) - val lastCpiCell = row.createCell(13) + val lastCpiCell = row.createCell(15) lastCpiCell.apply { - cellFormula = "IF(J${rowNum+1}=0,0,L${rowNum+1}/J${rowNum+1})" + cellFormula = "IF(J${rowNum+1}=0,0,N${rowNum+1}/J${rowNum+1})" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) - val lastPCpiCell = row.createCell(14) + val lastPCpiCell = row.createCell(16) lastPCpiCell.apply { cellFormula = "IF(J${rowNum+1}=0,0,H${rowNum+1}/J${rowNum+1})" cellStyle = boldFontCellStyle @@ -611,17 +640,17 @@ open class ReportService( CellUtil.setCellStyleProperty(lastPCpiCell, "borderBottom", BorderStyle.DOUBLE) - val sumRAmountCell = row.createCell(15) + val sumRAmountCell = row.createCell(17) sumRAmountCell.apply { - cellFormula = "SUM(P15:P${rowNum})" + cellFormula = "SUM(R15:R${rowNum})" cellStyle.dataFormat = accountingStyle } CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE) - val sumUnSettleCell = row.createCell(16) + val sumUnSettleCell = row.createCell(18) sumUnSettleCell.apply { - cellFormula = "SUM(Q15:Q${rowNum})" + cellFormula = "SUM(S15:S${rowNum})" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -670,7 +699,7 @@ open class ReportService( val row7: Row = sheet.getRow(rowNum) val cell3 = row7.createCell(2) cell3.apply { - cellFormula = "K${lastRowNum}" + cellFormula = "L${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -678,7 +707,7 @@ open class ReportService( val row8: Row = sheet.getRow(rowNum) val cell4 = row8.createCell(2) cell4.apply { - cellFormula = "L${lastRowNum}" + cellFormula = "M${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -686,7 +715,7 @@ open class ReportService( val row9: Row = sheet.getRow(rowNum) val cell5 = row9.createCell(2) cell5.apply { - cellFormula = "M${lastRowNum}" + cellFormula = "N${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -694,7 +723,7 @@ open class ReportService( val row10: Row = sheet.getRow(rowNum) val cell6 = row10.createCell(2) cell6.apply { - cellFormula = "P${lastRowNum}" + cellFormula = "Q${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -702,7 +731,7 @@ open class ReportService( val row11: Row = sheet.getRow(rowNum) val cell7 = row11.createCell(2) cell7.apply { - cellFormula = "Q${lastRowNum}" + cellFormula = "R${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -716,6 +745,7 @@ open class ReportService( project: Project, invoices: List, timesheets: List, + projectExpenses: List, monthlyStaffSalaryEffective: List, dateType: String, templatePath: String, @@ -859,6 +889,18 @@ open class ReportService( } } + val groupedProjectExpense = projectExpenses.sortedBy { it.issueDate } + .groupBy { projectExpenseEntries -> projectExpenseEntries.issueDate?.format(dateFormatter).toString() } + .mapValues { (_, projectExpenses) -> + projectExpenses.map {projectExpense-> + mapOf( + "projectExpense" to (projectExpense.amount ?: 0.0), + "expenseNo" to projectExpense.expenseNo + ) + } + } + println("grouped Project Expense") + println(groupedProjectExpense) // groupedTimesheets.entries.forEach { (key, value) -> // logger.info("key: $key") // logger.info("value: " + value.sumOf { it }) @@ -1943,10 +1985,16 @@ open class ReportService( + " left join project p on p.code = i.projectCode" + " where i.deleted = false " + " group by p.code" - + " )" + + " )," + + " cte_expense as ( " + + " select IFNULL(sum(pe.amount),0) as amount, pe.projectId " + + " from project_expense pe " + + " where pe.deleted = false " + + " group by projectId " + + " ) " + " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee, " + " IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" - + " ,0 as totalCumulativeExpenditure " + + " ,0 as totalCumulativeExpenditure, IFNULL(cte_e.amount,0) as projectExpense " + " from project p" // + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + " left join customer c on c.id = p.customerId" @@ -1954,6 +2002,7 @@ open class ReportService( + " left join subsidiary s2 on s2.id = cs.subsidiaryId " + " left join tsmsdb.team t on t.teamLead = p.teamLead" + " left join cte_invoice cte_i on cte_i.code = p.code" + + " left join cte_expense cte_e on cte_e.projectId = p.id " + " where p.status = \'On-going\'" ) if (teamLeadId!! > 0) { @@ -2269,13 +2318,19 @@ open class ReportService( + " left join project p on p.code = i.projectCode" + " where i.deleted = false " + " group by p.code" - + " )" + + " )," + + " cte_expense as ( " + + " select sum(pe.amount) as amount, pe.projectId " + + " from project_expense pe " + + " where pe.projectId = :projectId " + + " and (DATE_FORMAT(pe.issueDate, '%Y-%m') >= :startMonth and DATE_FORMAT(pe.issueDate, '%Y-%m') <= :endMonth) " + + " ) " + " 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.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," - + " 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, cte_e.amount " + " from project p" + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + " left join cte_timesheet_sum cte_tss on p.code = cte_tss.code" @@ -2287,6 +2342,7 @@ open class ReportService( + " left join team t2 on t2.id = s.teamId" + " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId " + " left join subsidiary s2 on s2.id = cs.subsidiaryId " + + " left join cte_expense cte_e on cte_e.projectId = p.id " + " where p.deleted = false" + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" + " and p.id = :projectId" @@ -2322,8 +2378,18 @@ open class ReportService( "select p.code, p.description from project p where p.deleted = false and p.id = :projectId" ) + val projectExpenseSql = StringBuilder( + "select IFNULL(sum(pe.amount),0) as amount" + + " from project_expense pe" + + " where pe.projectId = :projectId" + + " and (DATE_FORMAT(pe.issueDate, '%Y-%m') >= :startMonth and DATE_FORMAT(pe.issueDate, '%Y-%m') <= :endMonth) " + + " and pe.deleted = false " + ) + val projectsCode = jdbcDao.queryForMap(projectCodeSql.toString(), args).get() + val projectExpense = jdbcDao.queryForMap(projectExpenseSql.toString(), args).get() + val otFactor = BigDecimal(1).toDouble() var tempList = mutableListOf>() @@ -2333,7 +2399,8 @@ open class ReportService( "startMonth" to startMonth, "endMonth" to endMonth, "code" to projectsCode["code"], - "description" to projectsCode["description"] + "description" to projectsCode["description"], + "projectExpense" to projectExpense["amount"] ) val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 1) @@ -2984,8 +3051,21 @@ open class ReportService( // setCellValue(info.getValue("manhourExpenditure") as Double) cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) - CellUtil.setCellStyleProperty(totalManhourECell, "borderBottom", BorderStyle.THIN) + + rowNum += 1 + val projectExpenseRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) + val projectExpenseTitleCell = projectExpenseRow.getCell(0) ?: projectExpenseRow.createCell(0) + projectExpenseTitleCell.apply { + setCellValue("Less: Project Expense (HKD)") + } + val projectExpenseCell = projectExpenseRow.getCell(1) ?: projectExpenseRow.createCell(1) + projectExpenseCell.apply { + setCellValue(info.getValue("projectExpense").toString().toDouble()) + cellStyle.dataFormat = accountingStyle + } + + CellUtil.setCellStyleProperty(projectExpenseTitleCell, "borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(projectExpenseCell, "borderBottom", BorderStyle.THIN) rowNum += 1 val pandlRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) @@ -2995,7 +3075,7 @@ open class ReportService( } val panlCell = pandlRow.getCell(1) ?: pandlRow.createCell(1) panlCell.apply { - cellFormula = "B${rowNum - 1}-B${rowNum}" + cellFormula = "B${rowNum - 2}-B${rowNum - 1}-B${rowNum}" cellStyle.dataFormat = accountingStyle } // CellUtil.setCellStyleProperty(panlCellTitle, "borderBottom", BorderStyle.DOUBLE) @@ -3036,15 +3116,22 @@ open class ReportService( fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List> { val sql = StringBuilder( - " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, " + + " with cte_expense as ( " + + " select IFNULL(sum(pe.amount),0) as amount, pe.projectId " + + " from project_expense pe " + + " where pe.deleted = false " + + " group by projectId " + + " ) " + + " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, " + " concat(t.code, ' - ', t.name) as teamLead, ifnull(p.expectedTotalFee, 0) as expectedTotalFee, " + " ifnull(p.subContractFee, 0) as subContractFee, " + - " 0 as totalCumulativeExpenditure " + + " 0 as totalCumulativeExpenditure, IFNULL(sum(cte_e.amount),0) as projectExpense " + " from project p " + " left join customer c on c.id = p.customerId " + " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId " + " left join subsidiary s2 on s2.id = cs.subsidiaryId " + " left join tsmsdb.team t on t.teamLead = p.teamLead " + + " left join cte_expense cte_e on cte_e.projectId = p.id " + " where ISNULL(p.code) = False " ) @@ -3066,6 +3153,8 @@ open class ReportService( " and p.teamLead = :teamLeadId " ) } + + sql.append(" group by p.code, p.description, c.name, s2.name, t.code, t.name, p.expectedTotalFee, p.subContractFee ") sql.append(" order by p.code ") val args = mapOf( @@ -3083,9 +3172,10 @@ open class ReportService( val updatedList = costAndExpenseList.map { item -> val code = item["code"] as? String val expenditure = projectsExpenditure[code] ?: BigDecimal.ZERO + val projectExpense = item["projectExpense"] as? Double ?: 0.0 val budget = (item["expectedTotalFee"] as Double - item["subContractFee"] as Double).times(0.8) val totalCumulativeExpenditure = (expenditure as? BigDecimal ?: 0.0).toDouble() - val budgetRemain = budget.minus(totalCumulativeExpenditure) + val budgetRemain = budget.minus(totalCumulativeExpenditure).minus(projectExpense) val remainingPercent = (budgetRemain).div(budget) // println("-----------------------------------${item["code"]}-------------------------------") // println("budget: $budget") @@ -3099,20 +3189,20 @@ open class ReportService( } } - val result = updatedList.map { item -> - val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 - val totalCumulativeExpenditure = (item["totalCumulativeExpenditure"] as? BigDecimal ?: 0.0).toDouble() - val budgetRemain = budget.minus(totalCumulativeExpenditure) - val remainingPercent = (budgetRemain).div(budget) -// println("-----------------------------------${item["code"]}-------------------------------") -// println("budget: $budget") -// println("totalCumulativeExpenditure: $totalCumulativeExpenditure") -// println("budgetRemain: $budgetRemain") -// println("remainingPercent: $remainingPercent") - item.toMutableMap().apply { - put("budgetPercentage", remainingPercent) - } - } +// val result = updatedList.map { item -> +// val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 +// val totalCumulativeExpenditure = (item["totalCumulativeExpenditure"] as? BigDecimal ?: 0.0).toDouble() +// val budgetRemain = budget.minus(totalCumulativeExpenditure) +// val remainingPercent = (budgetRemain).div(budget) +//// println("-----------------------------------${item["code"]}-------------------------------") +//// println("budget: $budget") +//// println("totalCumulativeExpenditure: $totalCumulativeExpenditure") +//// println("budgetRemain: $budgetRemain") +//// println("remainingPercent: $remainingPercent") +// item.toMutableMap().apply { +// put("budgetPercentage", remainingPercent) +// } +// } return updatedList } @@ -3232,16 +3322,23 @@ open class ReportService( CellUtil.setCellStyleProperty(cell7, "dataFormat", accountingStyle) val cell8 = row.getCell(8) ?: row.createCell(8) + val projectExpense = (item["projectExpense"] as BigDecimal).toDouble() cell8.apply { - cellFormula = "G${rowNum + 1}-H${rowNum + 1}" + setCellValue(projectExpense) } CellUtil.setCellStyleProperty(cell8, "dataFormat", accountingStyle) val cell9 = row.getCell(9) ?: row.createCell(9) cell9.apply { - cellFormula = "I${rowNum + 1}/G${rowNum + 1}" + cellFormula = "G${rowNum + 1}-H${rowNum + 1}-I${rowNum + 1}" + } + CellUtil.setCellStyleProperty(cell9, "dataFormat", accountingStyle) + + val cell10 = row.getCell(10) ?: row.createCell(10) + cell10.apply { + cellFormula = "J${rowNum + 1}/G${rowNum + 1}" } - CellUtil.setCellStyleProperty(cell9, "dataFormat", percentStyle) + CellUtil.setCellStyleProperty(cell10, "dataFormat", percentStyle) sheet.setRowBreak(rowNum++); } diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index 096fb28..c12c060 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -64,6 +64,7 @@ class ReportController( private val subsidiaryRepository: SubsidiaryRepository, private val staffAllocationRepository: StaffAllocationRepository, private val gradeRepository: GradeRepository, private val salaryEffectiveService: SalaryEffectiveService, + private val projectExpenseRepository: ProjectExpenseRepository, ) { private val logger: Log = LogFactory.getLog(javaClass) @@ -88,9 +89,10 @@ class ReportController( // val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project) val invoices = invoiceRepository.findAllByProjectCodeAndPaidAmountIsNotNullAndDeletedFalse(project.code!!) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) + val projectExpenses = projectExpenseRepository.findAll().filter { it.project == project } val monthlyStaffSalaryEffective = salaryEffectiveService.getMonthlyStaffSalaryData(timesheets.minByOrNull { it.recordDate!! }?.recordDate ?: LocalDate.parse("2012-01-01"), timesheets.maxByOrNull { it.recordDate!! }?.recordDate ?: LocalDate.now()) - val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, monthlyStaffSalaryEffective, request.dateType) + val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, projectExpenses, monthlyStaffSalaryEffective, request.dateType) // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return ResponseEntity.ok() // .contentType(mediaType) diff --git a/src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx b/src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx index 6dea4f8e97cf4db487ef9860577c55da4e429415..934a821d1bab14cd6707d2eee95ef12ad7f576cf 100644 GIT binary patch delta 4641 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 RA03C@7rsXtCh+g(e*n@ahY|n) diff --git a/src/main/resources/templates/report/EX01_Financial Status Report.xlsx b/src/main/resources/templates/report/EX01_Financial Status Report.xlsx index 1279a4c1cb4fa23e225395d6c839b7bdf55dc078..e1dcf08ea19dd3f971ba500187a77754809f3875 100644 GIT binary patch delta 3490 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 TD@rK