Browse Source

Merge branch 'master' of https://git.2fi-solutions.com/davidhui/TSMS-backend

# Conflicts:
#	src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
tags/Baseline_30082024_BACKEND_UAT
leoho2fi 1 year ago
parent
commit
ba0aaf717b
9 changed files with 301 additions and 115 deletions
  1. +2
    -1
      src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java
  2. +20
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/projections/FinancialStatusReportInfo.kt
  3. +55
    -10
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  4. +1
    -1
      src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt
  5. +7
    -1
      src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt
  6. +1
    -1
      src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt
  7. +1
    -1
      src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt
  8. +212
    -99
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  9. +2
    -1
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt

+ 2
- 1
src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java View File

@@ -5,6 +5,7 @@ import com.ffii.core.support.AbstractRepository;
import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo; import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;


import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -13,7 +14,7 @@ public interface StaffRepository extends AbstractRepository<Staff, Long> {
List<StaffSearchInfo> findStaffSearchInfoByAndDeletedFalse(); List<StaffSearchInfo> findStaffSearchInfoByAndDeletedFalse();
List<StaffSearchInfo> findStaffSearchInfoByAndDeletedFalseAndTeamIdIsNull(); List<StaffSearchInfo> findStaffSearchInfoByAndDeletedFalseAndTeamIdIsNull();


List<StaffSearchInfo> findAllStaffSearchInfoByIdIn(List<Long> ids);
List<StaffSearchInfo> findAllStaffSearchInfoByIdIn(List<Serializable> ids);
Optional<Staff> findByStaffId(@Param("staffId") String staffId); Optional<Staff> findByStaffId(@Param("staffId") String staffId);


Optional<StaffSearchInfo> findStaffSearchInfoById(@Param("id") Long id); Optional<StaffSearchInfo> findStaffSearchInfoById(@Param("id") Long id);


+ 20
- 0
src/main/java/com/ffii/tsms/modules/data/entity/projections/FinancialStatusReportInfo.kt View File

@@ -0,0 +1,20 @@
package com.ffii.tsms.modules.data.entity.projections

import java.math.BigDecimal
import java.time.LocalDate

interface FinancialStatusReportInfo {
val code: String
val description: String
val client: String
val teamLead: String
val planStart: LocalDate
val planEnd: LocalDate
val expectedTotalFee: BigDecimal
val staff: String
val normalConsumed: BigDecimal
val otConsumed: BigDecimal
val hourlyRate: BigDecimal
val sumIssuedAmount: BigDecimal
val sumPaidAmount: BigDecimal
}

+ 55
- 10
src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt View File

@@ -23,6 +23,9 @@ open class DashboardService(


fun CustomerSubsidiary(args: Map<String, Any>): List<Map<String, Any>> { fun CustomerSubsidiary(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("select" val sql = StringBuilder("select"
+ " row_number()OVER ("
+ " ORDER BY c.id"
+ " ) as id,"
+ " c.id as customerId," + " c.id as customerId,"
+ " c.name as customerName," + " c.name as customerName,"
+ " c.code as customerCode," + " c.code as customerCode,"
@@ -30,18 +33,17 @@ open class DashboardService(
+ " c.district as cutomerDistrict," + " c.district as cutomerDistrict,"
+ " c.brNo as customerBrNo," + " c.brNo as customerBrNo,"
+ " c.typeId as customerTypeId," + " c.typeId as customerTypeId,"
+ " s.id as subsidiaryId,"
+ " s.name as subsidiaryName,"
+ " s.code as subsidiaryCode,"
+ " s.address as subsidiaryAddress,"
+ " s.district as subsidiaryDistrict,"
+ " s.brNo as subsidiaryBrNo,"
+ " s.typeId as subsidiaryTypeId,"
+ " coalesce (s.id,'-') as subsidiaryId,"
+ " coalesce (s.name,'-') as subsidiaryName,"
+ " coalesce (s.code,'-') as subsidiaryCode,"
+ " coalesce (s.address,'-') as subsidiaryAddress,"
+ " coalesce (s.district,'-') as subsidiaryDistrict,"
+ " coalesce (s.brNo,'-') as subsidiaryBrNo,"
+ " coalesce (s.typeId,'-') as subsidiaryTypeId,"
+ " count(p.id) as projectNo" + " count(p.id) as projectNo"
+ " from customer c" + " from customer c"
+ " left join customer_subsidiary cs on c.id = cs.customerId"
+ " left join subsidiary s on cs.subsidiaryId = s.id"
+ " left join project p on cs.customerId = p.customerId and cs.subsidiaryId = p.customerSubsidiaryId "
+ " left join project p on c.id = p.customerId"
+ " left join subsidiary s on p.customerSubsidiaryId = s.id"
+ " where c.deleted = 0" + " where c.deleted = 0"
) )
if (args != null) { if (args != null) {
@@ -94,4 +96,47 @@ open class DashboardService(


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

fun searchCustomerNonSubsidiaryProject(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder("select"
+ " p.id as id,"
+ " p.id as projectId,"
+ " p.code as projectCode,"
+ " p.name as projectName,"
+ " te.code as team,"
+ " s.name as teamLead,"
+ " tg.name as expectedStage,"
+ " p.totalManhour as budgetedManhour,"
+ " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour,"
+ " p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) - sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone"
+ " from project p"
+ " left join project_task pt on p.id = pt.project_id"
+ " left join timesheet t on pt.id = t.projectTaskId"
+ " left join team te on p.teamLead = te.teamLead"
+ " left join staff s on te.teamLead = s.id"
+ " left join milestone m on p.id = m.projectId and curdate() >= m.startDate and curdate() <= m.endDate"
+ " left join task_group tg on m.taskGroupId = tg.id"
+ " left join ("
+ " select"
+ " mp.date as comingPaymentMilestone"
+ " from project p"
+ " left join milestone m on p.id = m.projectId"
+ " left join milestone_payment mp on m.id = mp.milestoneId"
+ " where p.customerId = :customerId"
+ " and isNull(p.customerSubsidiaryId)"
+ " and mp.date >= curdate()"
+ " order by date asc"
+ " limit 1"
+ " ) milestonePayment on 1=1"
+ " where p.customerId = :customerId"
+ " and isNull(p.customerSubsidiaryId)"
+ " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone"
)

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



+ 1
- 1
src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt View File

@@ -35,7 +35,7 @@ open class StaffsService(
) : AbstractBaseEntityService<Staff, Long, StaffRepository>(jdbcDao, staffRepository) { ) : AbstractBaseEntityService<Staff, Long, StaffRepository>(jdbcDao, staffRepository) {
open fun getTeamLeads(): List<StaffSearchInfo> { open fun getTeamLeads(): List<StaffSearchInfo> {
// TODO: Replace by actual logic // TODO: Replace by actual logic
val teamIds = teamRepository.findAll().map { team -> team.staff.id }
val teamIds = teamRepository.findAll().filter { team -> team.deleted == false }.map { team -> team.staff.id }
return staffRepository.findAllStaffSearchInfoByIdIn(teamIds) return staffRepository.findAllStaffSearchInfoByIdIn(teamIds)
} }




+ 7
- 1
src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt View File

@@ -54,6 +54,7 @@ class DashboardController(
val customerId = request?.getParameter("customerId") val customerId = request?.getParameter("customerId")
val subsidiaryId = request?.getParameter("subsidiaryId") val subsidiaryId = request?.getParameter("subsidiaryId")
val args = mutableMapOf<String, Any>() val args = mutableMapOf<String, Any>()
var result: List<Map<String, Any>> = emptyList()
if (customerId != null) { if (customerId != null) {
args["customerId"] = customerId args["customerId"] = customerId
} }
@@ -61,6 +62,11 @@ class DashboardController(
args["subsidiaryId"] = subsidiaryId args["subsidiaryId"] = subsidiaryId
} }


return dashboardService.searchCustomerSubsidiaryProject(args)
if (customerId != null && subsidiaryId != null) {
result = dashboardService.searchCustomerSubsidiaryProject(args)
} else if (customerId != null && subsidiaryId == null){
result = dashboardService.searchCustomerNonSubsidiaryProject(args)
}
return result
} }
} }

+ 1
- 1
src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt View File

@@ -15,5 +15,5 @@ interface ProjectRepository : AbstractRepository<Project, Long> {


fun findInvoiceInfoSearchInfoById(id: Long): List<InvoiceInfoSearchInfo> fun findInvoiceInfoSearchInfoById(id: Long): List<InvoiceInfoSearchInfo>


fun findFirstByIsClpProjectAndIdNotOrderByIdDesc(isClpProject: Boolean, id: Serializable?): Project?
fun findFirstByIsClpProjectAndIdIsNotOrderByIdDesc(isClpProject: Boolean, id: Serializable?): Project?
} }

+ 1
- 1
src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt View File

@@ -131,7 +131,7 @@ open class ProjectsService(
val checkIsClpProject = isClpProject != null && isClpProject val checkIsClpProject = isClpProject != null && isClpProject
val prefix = if (checkIsClpProject) "C" else "M" val prefix = if (checkIsClpProject) "C" else "M"


val latestProjectCode = projectRepository.findFirstByIsClpProjectAndIdNotOrderByIdDesc(checkIsClpProject, project.id)
val latestProjectCode = projectRepository.findFirstByIsClpProjectAndIdIsNotOrderByIdDesc(checkIsClpProject, project.id ?: -1)


if (latestProjectCode != null) { if (latestProjectCode != null) {
val lastFix = latestProjectCode.code!!.split("-")[1] //C-0001 -> 0001 val lastFix = latestProjectCode.code!!.split("-")[1] //C-0001 -> 0001


+ 212
- 99
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt View File

@@ -1,12 +1,17 @@
package com.ffii.tsms.modules.report.service package com.ffii.tsms.modules.report.service


import com.ffii.core.support.JdbcDao
import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Salary
import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Invoice
import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Leave
import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.Timesheet
import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.apache.poi.ss.usermodel.* import org.apache.poi.ss.usermodel.*
import org.apache.poi.ss.util.CellAddress
import org.apache.poi.ss.util.CellRangeAddress import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
@@ -16,11 +21,15 @@ import java.io.IOException
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.* import java.util.*
import org.apache.poi.ss.util.CellAddress
import kotlin.time.times


data class DayInfo(val date: String?, val weekday: String?) data class DayInfo(val date: String?, val weekday: String?)

@Service @Service
open class ReportService {
open class ReportService(
private val jdbcDao: JdbcDao,
) {
private val logger: Log = LogFactory.getLog(javaClass)
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER) private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER)


@@ -32,8 +41,8 @@ open class ReportService {


// ==============================|| GENERATE REPORT ||============================== // // ==============================|| GENERATE REPORT ||============================== //


fun genFinancialStatusReport(): ByteArray {
val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT)
fun genFinancialStatusReport(projectId: Long): ByteArray {
val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, projectId)


val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream) workbook.write(outputStream)
@@ -41,8 +50,13 @@ open class ReportService {


return outputStream.toByteArray() return outputStream.toByteArray()
} }

@Throws(IOException::class) @Throws(IOException::class)
fun generateProjectCashFlowReport(project: Project, invoices: List<Invoice>, timesheets: List<Timesheet>): ByteArray {
fun generateProjectCashFlowReport(
project: Project,
invoices: List<Invoice>,
timesheets: List<Timesheet>
): ByteArray {
// Generate the Excel report with query results // Generate the Excel report with query results
val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, PROJECT_CASH_FLOW_REPORT) val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, PROJECT_CASH_FLOW_REPORT)


@@ -55,9 +69,22 @@ open class ReportService {
} }


@Throws(IOException::class) @Throws(IOException::class)
fun generateStaffMonthlyWorkHourAnalysisReport(month: LocalDate, staff: Staff, timesheets: List<Timesheet>, leaves: List<Leave>, projectList: List<String>): ByteArray {
fun generateStaffMonthlyWorkHourAnalysisReport(
month: LocalDate,
staff: Staff,
timesheets: List<Timesheet>,
leaves: List<Leave>,
projectList: List<String>
): ByteArray {
// Generate the Excel report with query results // Generate the Excel report with query results
val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport(month, staff, timesheets, leaves, projectList, MONTHLY_WORK_HOURS_ANALYSIS_REPORT)
val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport(
month,
staff,
timesheets,
leaves,
projectList,
MONTHLY_WORK_HOURS_ANALYSIS_REPORT
)


// Write the workbook to a ByteArrayOutputStream // Write the workbook to a ByteArrayOutputStream
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
@@ -82,7 +109,7 @@ open class ReportService {


@Throws(IOException::class) @Throws(IOException::class)
fun generateLateStartReport(project: Project): ByteArray { fun generateLateStartReport(project: Project): ByteArray {
val workbook: Workbook = createLateStartReport(project,LATE_START_REPORT)
val workbook: Workbook = createLateStartReport(project, LATE_START_REPORT)
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream) workbook.write(outputStream)
workbook.close() workbook.close()
@@ -94,7 +121,8 @@ open class ReportService {
// EX01 Financial Report // EX01 Financial Report
private fun createFinancialStatusReport( private fun createFinancialStatusReport(
templatePath: String, templatePath: String,
) : Workbook {
projectId: Long,
): Workbook {
val resource = ClassPathResource(templatePath) val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream) val workbook: Workbook = XSSFWorkbook(templateInputStream)
@@ -117,42 +145,42 @@ open class ReportService {
val sheet: Sheet = workbook.getSheetAt(0) val sheet: Sheet = workbook.getSheetAt(0)


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


var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
var columnIndex = 2 var columnIndex = 2
sheet.getRow(rowIndex).getCell(columnIndex).apply {
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(FORMATTED_TODAY) setCellValue(FORMATTED_TODAY)
} }


rowIndex = 2 rowIndex = 2
sheet.getRow(rowIndex).getCell(columnIndex).apply {
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(project.code) setCellValue(project.code)
} }


rowIndex = 3 rowIndex = 3
sheet.getRow(rowIndex).getCell(columnIndex).apply {
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(project.name) setCellValue(project.name)
} }


rowIndex = 4 rowIndex = 4
sheet.getRow(rowIndex).getCell(columnIndex).apply {
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(if (project.customer?.name == null) "N/A" else project.customer?.name) setCellValue(if (project.customer?.name == null) "N/A" else project.customer?.name)
} }


rowIndex = 5 rowIndex = 5
sheet.getRow(rowIndex).getCell(columnIndex).apply {
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(if (project.teamLead?.team?.name == null) "N/A" else project.teamLead?.team?.name) setCellValue(if (project.teamLead?.team?.name == null) "N/A" else project.teamLead?.team?.name)
} }


rowIndex = 9 rowIndex = 9
sheet.getRow(rowIndex).apply { sheet.getRow(rowIndex).apply {
getCell(1).apply {
createCell(1).apply {
setCellValue(project.expectedTotalFee!!) setCellValue(project.expectedTotalFee!!)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


getCell(2).apply {
createCell(2).apply {
setCellValue(project.expectedTotalFee!! / 0.8) setCellValue(project.expectedTotalFee!! / 0.8)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
@@ -160,15 +188,17 @@ open class ReportService {


rowIndex = 10 rowIndex = 10
val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! } val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! }
val actualExpenditure = timesheets.sumOf { timesheet -> timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0)) }
val actualExpenditure = timesheets.sumOf { timesheet ->
timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed
?: 0.0))
}
sheet.getRow(rowIndex).apply { sheet.getRow(rowIndex).apply {
getCell(1).apply {
// TODO: Replace by actual expenditure
createCell(1).apply {
setCellValue(actualExpenditure) setCellValue(actualExpenditure)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


getCell(2).apply {
createCell(2).apply {
setCellValue(actualIncome.toDouble()) setCellValue(actualIncome.toDouble())
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
@@ -176,88 +206,137 @@ open class ReportService {


rowIndex = 11 rowIndex = 11
sheet.getRow(rowIndex).apply { sheet.getRow(rowIndex).apply {
getCell(1).apply {
// TODO: Replace by actual expenditure
createCell(1).apply {
cellFormula = "B10-B11" cellFormula = "B10-B11"
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


getCell(2).apply {
createCell(2).apply {
cellFormula = "C10-C11" cellFormula = "C10-C11"
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
} }


// TODO: Add expenditure
rowIndex = 15 rowIndex = 15
val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it }


val dateFormatter = DateTimeFormatter.ofPattern("MMM YYYY") val dateFormatter = DateTimeFormatter.ofPattern("MMM YYYY")
combinedResults.forEach { result: LocalDate ->
val invoice = invoices.find { invoice: Invoice -> invoice.receiptDate == result }
val timesheet = timesheets.find { timesheet: Timesheet -> timesheet.recordDate == result}
val combinedResults =
(invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it }
.map { it.format(dateFormatter) }.distinct()
val groupedTimesheets = timesheets.sortedBy { it.recordDate }
.groupBy { timesheetEntry -> timesheetEntry.recordDate?.format(dateFormatter).toString() }
.mapValues { (_, timesheetEntries) ->
timesheetEntries.map { timesheet ->
if (timesheet.normalConsumed != null) {
timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble())
} else if (timesheet.otConsumed != null) {
timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble())
} else {
0.0
}
}
}


if (invoice != null) {
sheet.getRow(rowIndex++).apply {
val groupedInvoices = invoices.sortedBy { it.receiptDate }.filter { it.paidAmount != null }
.groupBy { invoiceEntry -> invoiceEntry.invoiceDate?.format(dateFormatter).toString() }
.mapValues { (_, invoiceEntries) ->
invoiceEntries.map { invoice ->
mapOf(
"paidAmount" to invoice.paidAmount?.toDouble(),
"description" to invoice.milestonePayment?.description
)
}
}


getCell(0).apply {
setCellValue(result.format(dateFormatter))
}
groupedTimesheets.entries.forEach { (key, value) ->
logger.info("key: $key")
logger.info("value: " + value.sumOf { it })
}


getCell(1).apply {
setCellValue(0.0)
cellStyle.dataFormat = accountingStyle
}
groupedInvoices.entries.forEach { (key, value) ->
logger.info("key: $key")
logger.info("value: " + value)
groupedInvoices[key]!!.forEachIndexed { index, invoice ->
logger.info("index: $index")
logger.info("invoice: $invoice")
invoice.get("paidAmount")
}
}


getCell(2).apply {
setCellValue(invoice.paidAmount!!.toDouble())
cellStyle.dataFormat = accountingStyle
}
combinedResults.forEach { result: String ->


getCell(3).apply {
val lastRow = rowIndex - 1
if (lastRow == 15) {
cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString())
} else {
cellFormula = "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace("{currentRow}", rowIndex.toString()).replace("{lastRow}", lastRow.toString())
if (groupedInvoices.containsKey(result)) {
groupedInvoices[result]!!.forEachIndexed { _, invoice ->
sheet.getRow(rowIndex++).apply {
createCell(0).apply {
setCellValue(result)
} }
cellStyle.dataFormat = accountingStyle
}


getCell(4).apply {
setCellValue(invoice.milestonePayment!!.description!!)
createCell(1).apply {
setCellValue(0.0)
cellStyle.dataFormat = accountingStyle
}

createCell(2).apply {
setCellValue(invoice["paidAmount"] as Double)
cellStyle.dataFormat = accountingStyle
}

createCell(3).apply {
val lastRow = rowIndex - 1
if (lastRow == 15) {
cellFormula =
"C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString())
} else {
cellFormula =
"IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace(
"{currentRow}",
rowIndex.toString()
).replace("{lastRow}", lastRow.toString())
}
cellStyle.dataFormat = accountingStyle
}

createCell(4)?.apply {
setCellValue(invoice["description"].toString())
}
} }
} }
} }


if (timesheet != null) {
if (groupedTimesheets.containsKey(result)) {
sheet.getRow(rowIndex++).apply { sheet.getRow(rowIndex++).apply {


getCell(0).apply {
createCell(0).apply {
setCellValue(result.format(dateFormatter)) setCellValue(result.format(dateFormatter))
} }


getCell(1).apply {
setCellValue(timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0)))
createCell(1).apply {
setCellValue(groupedTimesheets[result]!!.sum())
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


getCell(2).apply {
createCell(2).apply {
setCellValue(0.0) setCellValue(0.0)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


getCell(3).apply {
createCell(3).apply {
val lastRow = rowIndex - 1 val lastRow = rowIndex - 1
if (lastRow == 15) { if (lastRow == 15) {
cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString())
} else { } else {
cellFormula = "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace("{currentRow}", rowIndex.toString()).replace("{lastRow}", lastRow.toString())
cellFormula =
"IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace(
"{currentRow}",
rowIndex.toString()
).replace("{lastRow}", lastRow.toString())
} }
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


getCell(4).apply {
createCell(4).apply {
setCellValue("Monthly Manpower Expenditure") setCellValue("Monthly Manpower Expenditure")
} }
} }
@@ -370,7 +449,7 @@ open class ReportService {
// cellStyle.borderBottom = BorderStyle.DOUBLE // cellStyle.borderBottom = BorderStyle.DOUBLE
cellStyle.alignment = HorizontalAlignment.CENTER cellStyle.alignment = HorizontalAlignment.CENTER
} }
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1))
// //
rowIndex += 1 rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply { sheet.getRow(rowIndex).getCell(0).apply {
@@ -392,7 +471,7 @@ open class ReportService {
cellStyle.alignment = HorizontalAlignment.CENTER cellStyle.alignment = HorizontalAlignment.CENTER
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1))
// //
rowIndex += 1 rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply { sheet.getRow(rowIndex).getCell(0).apply {
@@ -406,7 +485,7 @@ open class ReportService {
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1))


rowIndex += 1 rowIndex += 1
sheet.getRow(rowIndex).getCell(0).apply { sheet.getRow(rowIndex).getCell(0).apply {
@@ -431,12 +510,12 @@ open class ReportService {
// cellStyle.borderBottom = BorderStyle.DOUBLE // cellStyle.borderBottom = BorderStyle.DOUBLE
} }
sheet.getRow(rowIndex).getCell(2).apply { sheet.getRow(rowIndex).getCell(2).apply {
cellFormula = "C${rowIndex-2}+C${rowIndex-1}"
cellFormula = "C${rowIndex - 2}+C${rowIndex - 1}"
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
// cellStyle.borderTop = BorderStyle.THIN // cellStyle.borderTop = BorderStyle.THIN
// cellStyle.borderBottom = BorderStyle.DOUBLE // cellStyle.borderBottom = BorderStyle.DOUBLE
} }
sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1))
sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1))
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
rowIndex = 7 rowIndex = 7
columnIndex = 2 columnIndex = 2
@@ -465,21 +544,21 @@ open class ReportService {
} }
} }
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sheet.getRow(rowIndex).apply {
getCell(columnIndex).setCellValue("Leave Hours")
getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sheet.getRow(rowIndex).apply {
getCell(columnIndex).setCellValue("Leave Hours")
getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER


columnIndex += 1
getCell(columnIndex).setCellValue("Daily Manhour Spent\n" + "(Excluding Leave Hours)")
getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER
columnSize = columnIndex
}
sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex))
columnIndex += 1
getCell(columnIndex).setCellValue("Daily Manhour Spent\n" + "(Excluding Leave Hours)")
getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER
columnSize = columnIndex
}
sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex))
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for (i in 2 until columnIndex) { for (i in 2 until columnIndex) {
for (k in 0 until rowSize) { for (k in 0 until rowSize) {
sheet.getRow(8+k).getCell(i).apply {
sheet.getRow(8 + k).getCell(i).apply {
setCellValue(0.0) setCellValue(0.0)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
@@ -490,14 +569,17 @@ open class ReportService {
if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue != "Leave Hours") { if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue != "Leave Hours") {
// cal daily spent manhour // cal daily spent manhour
for (i in 0 until rowSize) { for (i in 0 until rowSize) {
val cell = sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex)
cell?.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1}:${getColumnAlphabet(columnIndex)}${rowIndex+1})" // should columnIndex - 2
val cell =
sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex)
cell?.cellFormula =
"SUM(${getColumnAlphabet(2)}${rowIndex + 1}:${getColumnAlphabet(columnIndex)}${rowIndex + 1})" // should columnIndex - 2
rowIndex++ rowIndex++
} }
// cal subtotal // cal subtotal
for (i in 0 until columnSize) { // minus last col for (i in 0 until columnSize) { // minus last col
val cell = sheet.getRow(rowIndex)?.getCell(2) ?: sheet.getRow(rowIndex)?.createCell(2) val cell = sheet.getRow(rowIndex)?.getCell(2) ?: sheet.getRow(rowIndex)?.createCell(2)
cell?.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex}:${getColumnAlphabet(columnIndex + i)}${rowIndex})"
cell?.cellFormula =
"SUM(${getColumnAlphabet(2)}${rowIndex}:${getColumnAlphabet(columnIndex + i)}${rowIndex})"
cell?.cellStyle?.dataFormat = accountingStyle cell?.cellStyle?.dataFormat = accountingStyle
cell?.cellStyle?.setFont(boldFont) cell?.cellStyle?.setFont(boldFont)
// cell?.cellStyle?.borderTop = BorderStyle.THIN // cell?.cellStyle?.borderTop = BorderStyle.THIN
@@ -506,7 +588,8 @@ open class ReportService {
} else { // just for preview when no data } else { // just for preview when no data
// cal daily spent manhour // cal daily spent manhour
for (i in 0 until rowSize) { for (i in 0 until rowSize) {
val cell = sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex)
val cell =
sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex)
cell?.setCellValue("daily spent manhour") cell?.setCellValue("daily spent manhour")
rowIndex++ rowIndex++
} }
@@ -526,7 +609,7 @@ open class ReportService {


@Throws(IOException::class) @Throws(IOException::class)
private fun createSalaryList( private fun createSalaryList(
salarys : List<Salary>,
salarys: List<Salary>,
templatePath: String, templatePath: String,
): Workbook { ): Workbook {


@@ -567,29 +650,30 @@ open class ReportService {
project: Project, project: Project,
templatePath: String templatePath: String
):Workbook{ ):Workbook{

project
val resource = ClassPathResource(templatePath) val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream) val workbook: Workbook = XSSFWorkbook(templateInputStream)
val sheet = workbook.getSheetAt(0) val sheet = workbook.getSheetAt(0)
// Formatting the current date to "YYYY/MM/DD" and setting it to cell C2 // Formatting the current date to "YYYY/MM/DD" and setting it to cell C2
val formattedToday = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) val formattedToday = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))
val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2) val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2)
dateCell.setCellValue(formattedToday) dateCell.setCellValue(formattedToday)


// Start populating project data starting at row 7
val startRow = 6 // 0-based index for row 7
val projectDataRow = sheet.createRow(startRow)
// Populate the row with project data
projectDataRow.createCell(1).setCellValue(project.code) // Column B
projectDataRow.createCell(2).setCellValue(project.name) // Column C
projectDataRow.createCell(3).setCellValue(project.teamLead?.name) // Column D
projectDataRow.createCell(4).setCellValue(project.custLeadName) // Column E
projectDataRow.createCell(5).setCellValue(
project.planStart?.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) ?: "N/A" // Column F
)
// // Start populating project data starting at row 7
// val startRow = 6 // 0-based index for row 7
// val projectDataRow = sheet.createRow(startRow)
// // Populate the row with project data
// projectDataRow.createCell(1).setCellValue(project.code) // Column B
// projectDataRow.createCell(2).setCellValue(project.name) // Column C
// projectDataRow.createCell(3).setCellValue(project.teamLead?.name) // Column D
// projectDataRow.createCell(4).setCellValue(project.custLeadName) // Column E
// projectDataRow.createCell(5).setCellValue(
// project.planStart?.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) ?: "N/A" // Column F
// )
// Styling for cell A1: Font size 16 and bold // Styling for cell A1: Font size 16 and bold
val headerFont = workbook.createFont().apply { val headerFont = workbook.createFont().apply {
@@ -602,7 +686,7 @@ open class ReportService {
val headerCell = sheet.getRow(0)?.getCell(0) ?: sheet.getRow(0).createCell(0) val headerCell = sheet.getRow(0)?.getCell(0) ?: sheet.getRow(0).createCell(0)
headerCell.cellStyle = headerCellStyle headerCell.cellStyle = headerCellStyle
headerCell.setCellValue("Report Title") headerCell.setCellValue("Report Title")
// Apply styles from A2 to A4 (bold) // Apply styles from A2 to A4 (bold)
val boldFont = workbook.createFont().apply { bold = true } val boldFont = workbook.createFont().apply { bold = true }
val boldCellStyle = workbook.createCellStyle().apply { setFont(boldFont) } val boldCellStyle = workbook.createCellStyle().apply { setFont(boldFont) }
@@ -611,7 +695,7 @@ open class ReportService {
val cell = row?.getCell(0) ?: row.createCell(0) val cell = row?.getCell(0) ?: row.createCell(0)
cell.cellStyle = boldCellStyle cell.cellStyle = boldCellStyle
} }
// Apply styles from A6 to J6 (bold, bottom border, center alignment) // Apply styles from A6 to J6 (bold, bottom border, center alignment)
val styleA6ToJ6 = workbook.createCellStyle().apply { val styleA6ToJ6 = workbook.createCellStyle().apply {
setFont(boldFont) setFont(boldFont)
@@ -624,7 +708,7 @@ open class ReportService {
val cell = row?.getCell(cellAddress.column) ?: row.createCell(cellAddress.column) val cell = row?.getCell(cellAddress.column) ?: row.createCell(cellAddress.column)
cell.cellStyle = styleA6ToJ6 cell.cellStyle = styleA6ToJ6
} }
// Setting column widths dynamically based on content length (example logic) // Setting column widths dynamically based on content length (example logic)
val maxContentWidths = IntArray(10) { 8 } // Initial widths for A to J val maxContentWidths = IntArray(10) { 8 } // Initial widths for A to J
for (rowIndex in 0..sheet.lastRowNum) { for (rowIndex in 0..sheet.lastRowNum) {
@@ -642,8 +726,37 @@ open class ReportService {
for (colIndex in 0..9) { for (colIndex in 0..9) {
sheet.setColumnWidth(colIndex, (maxContentWidths[colIndex] + 2) * 256) // Set the width for each column sheet.setColumnWidth(colIndex, (maxContentWidths[colIndex] + 2) * 256) // Set the width for each column
} }
return workbook return workbook
} }


open fun getFinancialStatus(projectId: Long?): List<Map<String, Any>> {
val sql = StringBuilder(
"with cte_invoice as (select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount"
+ "from invoice i"
+ "left join project p on p.code = i.projectCode"
+ "group by p.code"
+ ")"
+ " Select p.code, p.description, c.name, t2.name, p.planStart , p.planEnd , p.expectedTotalFee ,"
+ " s.name , IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate,"
+ " cte_i.sumIssuedAmount, cte_i.sumPaidAmount"
+ " from timesheet t"
+ " left join project_task pt on pt.id = t.projectTaskId"
+ " left join project p ON p.id = pt.project_id"
+ " left join staff s on s.id = t.staffId"
+ " left join salary s2 on s.salaryId = s2.salaryPoint"
+ " left join customer c on c.id = p.customerId"
+ " left join team t2 on t2.id = s.teamId"
+ " left join cte_invoice cte_i on cte_i.code = p.code"
)

if (projectId!! > 0) {
sql.append(" where p.id = :projectId ")
}
sql.append(" order by p.code")
val args = mapOf("projectId" to projectId)

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

} }

+ 2
- 1
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt View File

@@ -44,7 +44,8 @@ class ReportController(
@Throws(ServletRequestBindingException::class, IOException::class) @Throws(ServletRequestBindingException::class, IOException::class)
fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity<Resource> { fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity<Resource> {


val reportResult: ByteArray = excelReportService.genFinancialStatusReport()

val reportResult: ByteArray = excelReportService.genFinancialStatusReport(request.projectId)


return ResponseEntity.ok() return ResponseEntity.ok()
.header("filename", "Financial Status Report - " + LocalDate.now() + ".xlsx") .header("filename", "Financial Status Report - " + LocalDate.now() + ".xlsx")


Loading…
Cancel
Save