From 9a49dcf847762151417cee820951c83a31d3339a Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Thu, 11 Jul 2024 18:10:52 +0800 Subject: [PATCH] Update Salary export Update PNL Report, show started Project till now Update Fincal Status, add projected CPI Update Invoice List API --- .../project/entity/InvoiceRepository.kt | 5 +- .../project/entity/ProjectRepository.kt | 3 + .../entity/projections/ImportInvoiceInfo.kt | 21 +++ .../project/entity/projections/InvoiceInfo.kt | 1 + .../entity/projections/ProjectPlanStartEnd.kt | 12 ++ .../modules/project/service/InvoiceService.kt | 160 +++++++++++++++++- .../modules/project/web/InvoiceController.kt | 25 +++ .../modules/project/web/ProjectsController.kt | 6 + .../modules/report/service/ReportService.kt | 145 ++++++++++++++-- .../report/EX01_Financial Status Report.xlsx | Bin 13158 -> 13179 bytes .../templates/report/Salary Template.xlsx | Bin 12675 -> 12671 bytes 11 files changed, 357 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/ffii/tsms/modules/project/entity/projections/ImportInvoiceInfo.kt create mode 100644 src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectPlanStartEnd.kt diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt index 940f2d2..a08be63 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt @@ -1,13 +1,16 @@ package com.ffii.tsms.modules.project.entity import com.ffii.core.support.AbstractRepository +import com.ffii.tsms.modules.project.entity.projections.ImportInvoiceInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceInfo interface InvoiceRepository : AbstractRepository { fun findAllByPaidAmountIsNotNullAndMilestonePaymentIn(milestonePayment: List): List - fun findInvoiceInfoBy(): List + fun findInvoiceInfoByAndDeletedFalse(): List + + fun findImportInvoiceInfoByAndDeletedFalse(): List fun findInvoiceInfoByPaidAmountIsNotNull(): List diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt index abc61dd..df7cd45 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt @@ -5,6 +5,7 @@ import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo +import com.ffii.tsms.modules.project.entity.projections.ProjectPlanStartEnd import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import org.springframework.data.jpa.repository.Query import org.springframework.lang.Nullable @@ -36,4 +37,6 @@ interface ProjectRepository : AbstractRepository { fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List fun findByCode(code: String): Project? + + fun findProjectPlanStartEndByIdAndDeletedFalse(id: Long): ProjectPlanStartEnd } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/projections/ImportInvoiceInfo.kt b/src/main/java/com/ffii/tsms/modules/project/entity/projections/ImportInvoiceInfo.kt new file mode 100644 index 0000000..ee9f8e2 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/project/entity/projections/ImportInvoiceInfo.kt @@ -0,0 +1,21 @@ +package com.ffii.tsms.modules.project.entity.projections + +import org.springframework.beans.factory.annotation.Value +import java.math.BigDecimal +import java.time.LocalDate + +interface ImportInvoiceInfo { + val invoiceNo: String? + + val projectCode: String? + + val invoiceDate: LocalDate? + + val receiptDate: LocalDate? + + @get:Value("#{target.paidAmount}") + val receivedAmount: BigDecimal? + + @get:Value("#{target.issueAmount}") + val issuedAmount: BigDecimal? +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/projections/InvoiceInfo.kt b/src/main/java/com/ffii/tsms/modules/project/entity/projections/InvoiceInfo.kt index 5c76060..b208c47 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/projections/InvoiceInfo.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/projections/InvoiceInfo.kt @@ -3,6 +3,7 @@ package com.ffii.tsms.modules.project.entity.projections import com.ffii.tsms.modules.project.entity.MilestonePayment import org.springframework.beans.factory.annotation.Value import org.springframework.cglib.core.Local +import org.springframework.data.jpa.repository.Query import java.math.BigDecimal import java.time.LocalDate diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectPlanStartEnd.kt b/src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectPlanStartEnd.kt new file mode 100644 index 0000000..367e8e5 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectPlanStartEnd.kt @@ -0,0 +1,12 @@ +package com.ffii.tsms.modules.project.entity.projections + +import java.time.LocalDate +import java.time.YearMonth + +interface ProjectPlanStartEnd { + val id: Long? + val planStart: LocalDate? + val planEnd: LocalDate? + val actualStart: LocalDate? + val actualEnd: LocalDate? +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt b/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt index 950ab7e..fde8f71 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt @@ -12,16 +12,15 @@ import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import com.ffii.tsms.modules.project.web.models.InvoiceResponse import net.sf.jasperreports.engine.JasperCompileManager import net.sf.jasperreports.engine.JasperReport -import org.apache.poi.ss.usermodel.Cell import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.Sheet import org.apache.poi.ss.usermodel.Workbook import org.springframework.core.io.ClassPathResource -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.io.InputStream import java.math.BigDecimal +import java.time.LocalDate import java.time.ZoneId @@ -325,7 +324,20 @@ open class InvoiceService( } open fun allInvoice(): List{ - return invoiceRepository.findInvoiceInfoBy() + return invoiceRepository.findInvoiceInfoByAndDeletedFalse() + } + + open fun allInvoiceV3(): List>{ + val sql = StringBuilder( + "select i.id, i.invoiceNo, i.projectCode, " + + "p.name as projectName, t.code as team, i.invoiceDate, " + + "i.receiptDate, i.issueAmount , i.paidAmount " + + "from invoice i " + + "left join project p on i.projectCode = p.code " + + "left join team t on t.id = p.teamLead " + + "order by i.invoiceDate " + ) + return jdbcDao.queryForList(sql.toString()); } open fun allInvoicePaid(): List{ @@ -497,4 +509,146 @@ open class InvoiceService( return InvoiceResponse(true, "OK", newProjectCodes, emptyRowList, invoicesResult, duplicateItemsInInvoice, ArrayList()) } + + @Transactional(rollbackFor = [Exception::class]) + open fun importInvoices(workbook: Workbook?): InvoiceResponse { + if (workbook == null) { + return InvoiceResponse(false, "No Excel import", ArrayList(), ArrayList(), ArrayList(), ArrayList(), ArrayList()) // if workbook is null + } + val invoiceRecords = repository.findImportInvoiceInfoByAndDeletedFalse() + + val importInvoices: MutableList> = mutableListOf(); + + val sheet: Sheet = workbook.getSheetAt(1) + println(sheet.lastRowNum) + for(i in 3 until sheet.lastRowNum){ + val issueYear = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("B")).numericCellValue + val issueMonth = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("C")).numericCellValue + val issueDay = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("D")).numericCellValue + val adjustedYear = if (issueYear < 100) 2000 + issueYear else issueYear + val issueDate = convertToLocalDate(adjustedYear.toInt(), issueMonth.toInt(), issueDay.toInt()) + + val projectPrefix = getCellValue(sheet, i, convertAlphaToNumber("E") ) + val projectNo = getCellValue(sheet, i, convertAlphaToNumber("F") ) + val projectCode = "${projectPrefix}-${projectNo}" + println("${i}: ${projectCode}") + + val invoicePrefix1 = getCellValueWithBracket(sheet, i, convertAlphaToNumber("G") ) + val invoicePrefix2 = getCellValueWithBracket(sheet, i, convertAlphaToNumber("H") ) + val invoicePrefix3 = getCellValue(sheet, i, convertAlphaToNumber("I") ) + val invoicePrefix4 = getCellValue(sheet, i, convertAlphaToNumber("J") ) + // Put the cell into list, if cell is empty not join with "-" + val invoiceNo = listOf(projectCode, invoicePrefix1, invoicePrefix2, invoicePrefix3, invoicePrefix4) + .filter { it.isNotEmpty() } + .joinToString("-") + + val issuedAmount = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("M")).numericCellValue + val canceledAmount = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("N")).numericCellValue + val settleYear = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("Q")).numericCellValue + val settleMonth = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("R")).numericCellValue + val settleDay = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("S")).numericCellValue + val adjustedSettleYear = if (settleYear < 100) 2000 + settleYear else settleYear + val settleDate = convertToLocalDate(adjustedSettleYear.toInt(), settleMonth.toInt(), settleDay.toInt()) + val receivedAmount = ExcelUtils.getCell(sheet, i, convertAlphaToNumber("T")).numericCellValue + + val importInvoice: Map = mapOf( + "invoiceNo" to invoiceNo, + "projectCode" to projectCode, + "issuedAmount" to issuedAmount, + "canceledAmount" to canceledAmount, + "invoiceDate" to issueDate, + "receiptDate" to settleDate, + "receivedAmount" to receivedAmount, + ) + importInvoices.add(importInvoice) + } + + println("invoiceRecords: $invoiceRecords") + val (intersect, notIntersect) = importInvoices.partition { item -> + invoiceRecords.any { invoice -> + invoice.invoiceNo == item["invoiceNo"] && + invoice.invoiceDate == item["invoiceDate"] && + invoice.receiptDate == item["receiptDate"] && + invoice.receivedAmount == item["receivedAmount"] && + invoice.issuedAmount == item["issuedAmount"] + } + } + + println("interect $intersect") + println("Not Intersect $notIntersect") + + + return InvoiceResponse(true, "OK", ArrayList(), ArrayList(), ArrayList(), ArrayList(), importInvoices) + } + + fun getCellValue(sheet: Sheet, rowIndex: Int, columnIndex: Int): String { + val row = sheet.getRow(rowIndex) + val cell = row.getCell(columnIndex) + + return when (cell.cellType) { + CellType.NUMERIC -> cell.numericCellValue.toInt().toString() + CellType.STRING -> cell.stringCellValue + // CellType.BOOLEAN -> cell.booleanCellValue + // CellType.FORMULA -> cell.setCellFormula(cell.cellFormula) + // CellType.BLANK -> null + else -> cell.toString() + } + } + + fun getCellValueWithBracket(sheet: Sheet, rowIndex: Int, columnIndex: Int): String { + val row = sheet.getRow(rowIndex) + val cell = row.getCell(columnIndex) + + return when (cell.cellType) { + CellType.NUMERIC -> "(${cell.numericCellValue.toInt()})" + CellType.STRING -> cell.stringCellValue + // CellType.BOOLEAN -> cell.booleanCellValue + // CellType.FORMULA -> cell.setCellFormula(cell.cellFormula) + // CellType.BLANK -> null + else -> cell.toString() + } + } + + fun convertToLocalDate(year: Int, month: Int, day: Int): String { + // Check if the values are valid + if (year < 0 || month < 1 || month > 12 || day < 1 || day > 31) { +// throw IllegalArgumentException("Invalid date values: year=$year, month=$month, day=$day") + return "-" + } + + // Adjust the year to be 2023 if the input year is 23 + val adjustedYear = if (year < 100) 2000 + year else year + + // Create and return the LocalDate object + return LocalDate.of(adjustedYear, month, day).toString() + } + + fun convertAlphaToNumber(input: String): Int { + // Convert the input string to uppercase + val uppercaseInput = input.uppercase() + + // Initialize the result to 0 + var result = 0 + + // Iterate through each character in the input string + for (i in uppercaseInput.indices) { + // Get the current character + val c = uppercaseInput[i] + + // Check if the character is a letter + if (c.isLetter()) { + // Calculate the numeric value of the letter + val value = c - 'A' + + // Add the value to the result + result = result * 26 + value + } else { + // If the character is not a letter, throw an exception + throw IllegalArgumentException("Input string must contain only alphabetic characters: $c") + } + } + + return result + } + } diff --git a/src/main/java/com/ffii/tsms/modules/project/web/InvoiceController.kt b/src/main/java/com/ffii/tsms/modules/project/web/InvoiceController.kt index a8c7abd..c143adf 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/InvoiceController.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/InvoiceController.kt @@ -75,6 +75,11 @@ class InvoiceController( return invoiceService.allInvoice() } + @GetMapping("/v3/allInvoices") + fun allInvoice_v3(): List> { + return invoiceService.allInvoiceV3() + } + @GetMapping("/v2/allInvoices/paid") fun allInvoice_paid_v2(): List { return invoiceService.allInvoicePaid() @@ -113,4 +118,24 @@ class InvoiceController( println("--------- OK -------------") return ResponseEntity.ok(invoiceService.importReceivedInvoice(workbook)) } + + /** + * Import the combined Invoice Summary (issued and received) + */ + @PostMapping("/import/v2") + fun importInvoices(request: HttpServletRequest): ResponseEntity<*> { + var workbook: Workbook? = null + + try { + val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList") + if (multipartFile != null) { + workbook = XSSFWorkbook(multipartFile.inputStream) + } + } catch (e: Exception) { + println("Excel Wrong") + println(e) + } +// println("--------- OK -------------") + return ResponseEntity.ok(invoiceService.importInvoices(workbook)) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt b/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt index 61c070e..f1b172a 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt @@ -4,6 +4,7 @@ import com.ffii.core.exception.NotFoundException import com.ffii.tsms.modules.data.entity.* import com.ffii.tsms.modules.project.entity.ProjectCategory import com.ffii.tsms.modules.project.entity.ProjectRepository +import com.ffii.tsms.modules.project.entity.projections.ProjectPlanStartEnd import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import com.ffii.tsms.modules.project.service.ProjectsService import com.ffii.tsms.modules.project.web.models.* @@ -107,4 +108,9 @@ class ProjectsController(private val projectsService: ProjectsService, private v return ResponseEntity.ok(projectsService.importFile(workbook)) } + + @GetMapping("/test") + fun test(@RequestParam id: Long): ProjectPlanStartEnd{ + return projectRepository.findProjectPlanStartEndByIdAndDeletedFalse(id); + } } \ No newline at end of file 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 86a957c..5762e73 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 @@ -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.Milestone 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.timesheet.entity.Timesheet 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.RegionUtil import org.apache.poi.xssf.usermodel.XSSFCellStyle +import org.apache.poi.xssf.usermodel.XSSFColor import org.springframework.core.io.ClassPathResource import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream import java.io.IOException import java.math.BigDecimal import java.time.LocalDate +import java.time.Year import java.time.YearMonth import java.time.format.DateTimeFormatter import java.time.ZoneId @@ -42,6 +45,7 @@ data class DayInfo(val date: String?, val weekday: String?) @Service open class ReportService( private val jdbcDao: JdbcDao, + private val projectRepository: ProjectRepository ) { private val logger: Log = LogFactory.getLog(javaClass) private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") @@ -487,16 +491,22 @@ open class ReportService( 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) receivedAmountCell.apply { setCellValue(paidAmount.toDouble()) cellStyle.dataFormat = accountingStyle } - val unsettledAmountCell = row.createCell(15) + val unsettledAmountCell = row.createCell(16) unsettledAmountCell.apply { - cellFormula = "L${rowNum}-O${rowNum}" + cellFormula = "L${rowNum}-P${rowNum}" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -578,17 +588,27 @@ open class ReportService( CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) - val sumRAmountCell = row.createCell(14) - sumRAmountCell.apply { + val lastPCpiCell = row.createCell(14) + lastPCpiCell.apply { 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 } CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE) - val sumUnSettleCell = row.createCell(15) + val sumUnSettleCell = row.createCell(16) sumUnSettleCell.apply { - cellFormula = "SUM(P15:P${rowNum})" + cellFormula = "SUM(Q15:Q${rowNum})" cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -1434,6 +1454,13 @@ open class ReportService( val workbook: Workbook = XSSFWorkbook(templateInputStream) 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) var rowIndex = 2 @@ -1446,30 +1473,46 @@ open class ReportService( val cell = getCell(0) ?: createCell(0) cell.setCellValue(salary.salaryPoint.toDouble()) + CellUtil.setAlignment(cell, HorizontalAlignment.CENTER) + cell.cellStyle.dataFormat = defaultStyle + when (index) { 0 -> { val cell1 = getCell(1) ?: createCell(1) cell1.setCellValue(salary.lowerLimit.toDouble()) + CellUtil.setAlignment(cell1, HorizontalAlignment.CENTER) + cell1.cellStyle.dataFormat = defaultStyle } else -> { val cell1 = getCell(1) ?: createCell(1) cell1.cellFormula = "(C{previousRow}+1)".replace("{previousRow}", (rowIndex).toString()) + CellUtil.setAlignment(cell1, HorizontalAlignment.CENTER) + cell1.cellStyle.dataFormat = defaultStyle } + } + val cell2 = getCell(2) ?: createCell(2) 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 val cell3 = getCell(3)?:createCell(3) cell3.setCellValue(salary.increment.toDouble()) + cell3.cellStyle = fillOrange + cell3.cellStyle.dataFormat = defaultStyle + CellUtil.setAlignment(cell3, HorizontalAlignment.CENTER) + val cell4 = getCell(4)?:createCell(4) cell4.cellFormula = "(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", (rowIndex+1).toString()) // getCell(4).cellStyle.dataFormat = accountingStyle cell4.cellStyle.dataFormat = accountingStyle + CellUtil.setAlignment(cell4, HorizontalAlignment.CENTER) } 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," + " 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" + " from project p" @@ -2060,11 +2104,23 @@ open class ReportService( + " and p.id = :projectId" + " 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( "projectId" to projectId, - "startMonth" to startMonth, - "endMonth" to endMonth, + "startMonth" to queryStartMonth.toString(), + "endMonth" to queryEndMonth.toString(), ) val manHoursSpent = jdbcDao.queryForList(sql.toString(), args) @@ -2088,7 +2144,7 @@ open class ReportService( "startMonth" to startMonth, "endMonth" to endMonth, "code" to projectsCode["code"], - "description" to projectsCode["description"], + "description" to projectsCode["description"] ) val staffInfoList = mutableListOf>() @@ -2109,6 +2165,9 @@ open class ReportService( if (info["code"] == item["code"] && "subsidiary" !in info) { info["subsidiary"] = item.getValue("subsidiary") } + if (info["code"] == item["code"] && "expectedTotalFee" !in info) { + info["expectedTotalFee"] = item.getValue("expectedTotalFee") + } if (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 { + + 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() workbook.write(outputStream) @@ -2203,7 +2264,8 @@ open class ReportService( templatePath: String, manhoursSpent: List>, startMonth: String, - endMonth: String + endMonth: String, + projectId: Long ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream @@ -2211,14 +2273,35 @@ open class ReportService( 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 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 endDate = YearMonth.parse(endMonth, DateTimeFormatter.ofPattern("yyyy-MM")) + val endDate = YearMonth.parse(queryEndMonth.toString(), DateTimeFormatter.ofPattern("yyyy-MM")) val convertEndMonth = YearMonth.of(endDate.year, endDate.month) val monthRange: MutableList> = ArrayList() 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)) { monthRange.add( mapOf( @@ -2468,7 +2551,7 @@ open class ReportService( val lastColumnIndex = getColumnAlphabet(3 + monthRange.size) val totalManhourETitleCell = totalManhourERow.getCell(0) ?: totalManhourERow.createCell(0) totalManhourETitleCell.apply { - setCellValue("Total Manhour Expenditure (HKD)") + setCellValue("Less: Total Manhour Expenditure (HKD)") } val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) totalManhourECell.apply { @@ -2490,8 +2573,36 @@ open class ReportService( cellFormula = "B${rowNum - 1}-B${rowNum}" 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) 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 b269cbb871e289314c1786c47f805f07dece6d7c..1279a4c1cb4fa23e225395d6c839b7bdf55dc078 100644 GIT binary patch delta 3377 zcmV-14bJlBX8UHa+6D#AE-e3rlivm$e{FB0I1v7R()|a-cM2xFB&g{s1khD#R#lQ~ zccn;`Yn);gFlU=4)oTCyj!8(`&1seE9!hNNF^`{_dB(o{w5>|xg$l`dJvPyWV;Zz3 zJZJUO*!+8$Swqv13fDOUWE0yM_t3Us^+;H*y$oaDY0JR*Og;Gsq+maNtf5P&D zH?#)Jnu`i67>XxbHiF_@7L=;0v|Yyu>_6cQIY8SspbMdmf-dl`NmejFMvOCpmF=1z;v=z z#fadNuhj)W+dI-H{v*dm=v@f5f2T1x56QCyePKF>2Ua5ZAgkbj6&%5k^V?_$q}6F9 zg5Z4+%|CE+&8y2bE9u=%x(05V&$!Z=DovxrQl&XlG#{HG4Ecs0C%`MwHgU^Ju)1OB z2Bv*=kn^p80pxyCDk^HM=#UDUlgaPl@$qnRUfnOjqkpFyNxYrPRG6E7YGD0H1L{tnKFAR-*Ms@Nx8Lyt`lGJIm1;cah0 zPSmpeNaLHce=Obq)bzIR+F^aX-7&h6M|tR>vAt(?&NrZ*VK4-*Ui-0yTo`UT>pja= zF*XOoz&p(SMcGpUl#vfUY1Xc$Ha6c;N_Lbocr4w#qh!C~)SaZjf353{x>JAp#X&~K zYFrbBOJkw1Y9%O5>j4gOefH|u!{H?&y|cK^QJ32ujNp>o3PX2Yl19jJM|uU_svD`g z9<<4TH1eED=!`teNe8~=4M(mu^aidq^%6Jr!!$`_U#Csq57GZ|N_Hg@^%sw3Szw`- z0!jj|t2tfASc1-We+~iPw`5~~7&`-?=w%2rhDMGR$AM@0NjC69luT1UJCfDzuRnN3 z4ejo03XzA})OFpBBi&~E*~8>|H?hy&yONCNiH`gJ>t8KgfcGUmb8&We=ECXc*>ZN~ z+M9IwaG#yKcruHV$@%Li^ZDfKGX2tTVgGAD?JnWE?PO~A1Twk$0h6Hv6tjy8GXe>! zY!#8&1polHlgA7pe_L3MIRUG2oY|(prhWGv z8xkr}Ayx^n6CeN1|J)eg>_p|gg3?^p6B79$@t|h1;Pq-ko}cEuA@Q`KbwMjx!-VWX zlbh-1PvfmrFM17NJS5b5Le|DKqae_14T5T4Hc;b9DV3lGe;?H<(2W9GxJ#l6;xNnt zL3vFMgps<2A9Rrs**$8bmC4#m(M9i6_|T z+p3m|E-Q?C7p0VWJB6PZze(%Hy+89LI8#!~()dUY4w}!y&x1UmtRtPre-$Q50|l?# z7Vw0MKPD;ae|N0-1WP^$%Q~1%ml|z&F(H4_EQ#;>$$c-(2E$%@I}3Yv89wI8mo!i2 z!|-+%|0UD0>yw3=jvKmyM=;OLLU|=OPjZ1TuvbYi9S5CN!LcE15hbXls z&f*7$t$wrj3|&4#1bGMR`@Q&e^IbazLfICUjK%Mq1ENCSx1x^ z{tEX{q4^@mIr-&W~FSaV*YM8sRz<>%U>83LGviivjv!0OLkJ2!U`=mX3klU}o zS8Ics`u+e9T+fY){R}k2AwXF^+M`bA1{5y^ewFQO7A!$3QLvvLgW3 zHW_%~bae^Z%(+eS#aISM46ZFE_6JdzC$=(IDZzhK7YeQ|B7HZdS4zUpJCykiaL{$p zRg$8Pftyy;eo*|1vrn_9n;k0%AB;(#UJ&gGi;ut{una`;7lYzEJB|;N zuL=f>FNpSp#YbQeSO%i_G`I(>zO&=_FfXcLu=s*#Pgr~e27zTDichccfcVaif8)ah zse-}c3!*(?@evpVmVqd~nheT+(&GZ~(>*J!0*Q1$tOh}hAeNK347VNev%oh8fUuvj z5p)3`u?h+=Lm(Y2UWS)?VuWKkiOU$adn&)L(G$B_lw%n|;bjPGK|XEUp!V0@=qOi+>KLt(pU-KGl$6hWHq zGW5VoOrpV}Jc$aDzn|=+hsLR~9YvAv@sW@D)z;QxgFYBXg*+RLr9e_n8(J0eX)*co zO^U!k8fr&a$Q=fGRet=ChrlA$QYhq_xw*`;pw?(1j9i11ewNN#;PmvXe=IZ}L5DRW z+d4ZRkH2OX3`wC}BrfE6Vu@t_M63>hGg;=rlsT6(?|!3VQ8|ry#$}$h@%`Td!5+t+ zC?Io15 z+Bl=y9tOY`9qlxPm=dS^e~Q6^o2XaF;wN|mE%|_$D%w~Bn+=M)ZCZ#n9Q|O$0ek>+ zg6{!=$Pg$oI{Kwjfp)`M!Jd>nxYz@|N{+eT8Zxo|`KIVIjGB3es1Af>2H0yLu zY}Cw)r6Zo|LS9J0yC&TjxysSuCS~@<@CT{MF9S~eZ|p1`bNLsOp#v1Nogg|214f!|3k@rm!vxq+7489Ar1s9igzLTwiTNLN#?eHZ`y5jL-8_s z-#mHpk$k>aP4WfK*G3nJ4hTv>3nOd2F3@YWNM|VVfosVdqoF_t@MvD%-sHkEVH`X= zW5IBKl>hTUms~Ve9S%0W>7;wfmf{%p?Y*!2;%>Ja z?1r62^r!fJ`Sj8~HLZ0!D*>p?rC`DV55|>S4=%?S8}0BK?!z+9c~yhFJCu*y8=X`} z<@k3-7-60Jkq9!0HtgtrPTq$157h#d8KL8pOj0tg=#0@R%g9H2_tp8cc{IQk0AyX9*s&ddFa>{%JT_*%h=h@N*fCidHQl@Fw$xG!vAPZAct3| zNZ@JkTR^dqh%%|=??+}61zBiy>%=@0d;@2LC{O(UkjWcIQDa4imK%?*mW)f&*KLAT zMaKv%-|tl3;B1n#C=U{R#-R?3$U3kLa1n#i){5@Ge+7mN^9b^X5RTj#-K@kgwM7u2 zAqNUiFCRM(v0tjE4pbP@7L|^KRaSv^Nw1|Xps7_PVhOH!vwuew4?|-JA%As9ePX<; zcn|+6G~w-R8c737G+W=aERZs0k4Eh*#q7SaODjpyC`sE(Y)Gvby-_&x#@Zn z(fd!q`IPriaUeo&`>UHZ4=eS7u)9+Il04aFYCGfzH+fmyIKZ$M@Ox8-b@ zjniY`26r5AcjFDO@=WYvg?0N{E|K7v_^aoOZ7O^f!A%?CKo?;560;eUxdbj}=O2*Y zx&Gb@`bI+1+Z`QN<@T{C966&Mq5&D!vX_>hz)+TR!@7{3M4M}2&^AZMz-*F*6uKE$ zT2|aVDQBHuCZst(P8I8MgqifX`gsMxbLR5pz8V>+b<6t~Vcd@1 zO+ML9Sigjd&9Wu6qzvFcBu(qki&2n17Su?nM-u~?(G=unM+t&mOekVP-)G}AII&xl*E_Yu&s&*z_&(Smyn_?q=gjt!QZoYN_M$ zr&&DXZik&SPi2rZj~;w1yYaS%*R$20h|t=-V1=ZCwb`__gFTYde)(+oIVq~eqg!`& zczVkb=z9djmH^osQyFek(sv%>g2t%lMve4>$41d{EoyHT)jgg3Clt!{7{;y z=I%AC*{#n|W^>H)2L)=St8aw*kH_JS*RoE!Gru+vS&!9L>hp$U zNL73=7%@z3#Z_%1=3&DV#m~1fp5jGtOtTT}`yGdVplXd@lR^bIb##Thc;c~Bj~i67 zoDJRERccAyVwlRl{p@ATb*3oyEp7 z$GlwAohmhwk22p+&To$u)*qiZf1RCtLn<*W;hJhYE=He<{4QX5z9@vP0P@PoHC&LE zc64pg8Oh1=IfJ;|$q-x|7C#>Ox z$KC!in_|t4_@JC_2TrtBL$C^ym-B?JJa?;##mk|0LQ=?B$yU^uR>DSBMZw@{LOZ?Mo@oNQDMc#y|WnZm+JS{UGmvI6q4=yfyOh)yX?xJtT z8RB(2GWS2(bkg>WZ}^g`MO3e#?Tt;Nf3R1|T<^qrweB>1s;VJ2L%w>-2$scUIU47_ zs*c?2uJ+6P0i~aMnfAF(4tk1{BOckUL*djo{$=cZS2j;ds2dfDk93SNeR zbGVC1vUacysMrQ*&{5o$3c@ z+j+1KwMw=d8U2&}15<${X_#qs*NC^eYMeXA_*9Bu5-N>4)#|4fEsq4y007|f@=wal z+0t8xyZ*GIE@DM%XOMgEodQ26R5Hm&N`ICaHAo!T;qXtH@?K`xtoOGHxU@bvKWR|iuL%h zfw{BVjAGmo17k|DW-+Tg{~&$}X^-3LE}RWqrAUb{)P1ea$ZXs>;V!H(KOzKwIZ^==IL=~L;oK}6zE&+7%je4RGe@e$hIf7s>BEF~C z!+{49{Aw9!Qf?(4$MxN}V=rejV6OM)=U)$kNV_fvj!>MSZ zodlEX`eXNt4&MR71I8Gt5W--VDkO_Y;;QJ)9*l^}4>ah+zOY_oozmtp2fIx?Ldm?1 z>DEy%-GDD!jw~t^0w=8}%~{A3BeFm zCjaYLBTS&Rb_D0V&L;Rzyhbl}-+_M;w;u{h>d7g*ir+nV4-tBu zjQ5=8eJ|ps2Sa4DWft!Yvik+lm-c;`|B`FDBySt-YjJvinQW1j#w9PplukW|b*i`N z`f)%J3aRCYqg3n_NI?smouzMD){U|rVQjtsZp>%9#e|Mj;192&008QM008@MOoCCj z{9NpPog9t*eO*1B{cZ+(pi+%>yspWzbcpN;XsQsRp%0fz+=8kaJOou=zmQ*zfxbHy zm9KAlI)vuJKk=L8ca@g zyJs)Sj0W}T9{n_76;Z1#H&2%K3zK%5w(^JQg{NeSxC<04q2e*A9tfu;&J1gjx{9f^ zNl6{bCjH*>cRxi1GtRMi6~&4dkQX%!wVZ@Y%~6A}*rzM6({Yy|s}kR}4vFN!FU8ST zb3OOXUud=(k6r1`_ag7iGuz#(`uhB%+?STx1oTMmo3REiP^eEC>sLo_IEpYyw3<7T zp?tgHx+Ysu0EL^Ta)xU?iDiO6#J?61U^SM=;pq!Z@a`OAl+88<)j*@u?1;Oz0EmCY z95!0HFIF1Xzz{BLbeA(}Y~Z9}k9IYlcgOj7L1KXvLTK;G3FlB=W3wvy)*);^2;7~1 zaNE{a2^W0;cX!U-|K-tEf=Ue3r53e@I*q#!aSAk2r$+`9zY}v= zvNA93F*iLPSlkc`WvRYCue$FcrJ$uH*Lof{6<7sM9$1MuSQ)zC&}Z`xfeU2inpFxF zz7FVn^Kx4Je%-2E8!|fJ0lld^wW3m#M#|E@@bZ<_&s~iZS3qXbSMF(94CnKC;M}kG zXL}*#cz?w2FIw^r71*Nlu6Tkj9eKOxh5T+1WC?sdU>TIAu#zyfK*lI>8iw#@y7QL- zaS58dr%#H`I35E=$hi&=gLy71O1CqmPg(xnYOm>kJp0x#|CnbU-oH~-nRD^VzP!c4 z2caPjU8Q+GRWV~B!(E8-@2ScSkb@&^F>uRWBx{r;9Bl(x)fYV07b3Oi zC(jamlak|l8`D3i$w@8q^4Lzrh5H8;;dVCTuY|~-<~)CN)Y~N5ocX~ePcKmdF%edD znYhIJ=Vd*mt$_b`a}Z^WC?c>zveJD2|JnHs59l{P{|253JIcg^?UQA}X3FwVyv5eZ zA}B(!yRw43{|nu}rUtB(9Fj6k5ZfszhQ-Q3C^Lkx1R+W6x*QAdpP~Ln)rJxPVExzf WyBfk?yRAZ@gEhJ>LIaWcZ|gtrGFWv0 diff --git a/src/main/resources/templates/report/Salary Template.xlsx b/src/main/resources/templates/report/Salary Template.xlsx index 882cca1fdca08b1b13946c8ed854f092d7fb9f93..c6bf84d3f4dc2c68e78e19ebcb7cc1d7faf28589 100644 GIT binary patch delta 4339 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