From 9ddfcf6aab466ebe691b9537b712b45eefea1224 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 19 Jun 2024 15:01:35 +0800 Subject: [PATCH 01/22] add project import function --- .../data/entity/BuildingTypeRepository.kt | 1 + .../data/entity/CustomerRepository.java | 4 + .../modules/data/entity/LocationRepository.kt | 2 + .../modules/data/entity/StaffRepository.java | 1 + .../tsms/modules/data/entity/Subsidiary.java | 1 + .../data/entity/SubsidiaryRepository.java | 6 + .../modules/data/entity/TeamRepository.java | 2 + .../data/entity/WorkNatureRepository.kt | 1 + .../modules/data/service/CustomerService.kt | 12 + .../modules/data/service/SubsidiaryService.kt | 11 + .../project/entity/ProjectRepository.kt | 2 + .../project/service/ProjectsService.kt | 294 ++++++++++++++++-- .../modules/project/web/ProjectsController.kt | 26 +- .../project/web/models/NewProjectRequest.kt | 6 +- .../20240617_01_cyril/01_update_task.sql | 9 + .../20240617_01_cyril/02_update_project.sql | 5 + 16 files changed, 355 insertions(+), 28 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql create mode 100644 src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt index 1a4443b..336dc1c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface BuildingTypeRepository : AbstractRepository { + fun findByName(name: String): BuildingType? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java index d075a18..58355b6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java @@ -3,6 +3,7 @@ package com.ffii.tsms.modules.data.entity; import java.util.Optional; import java.util.List; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import com.ffii.core.support.AbstractRepository; @@ -11,4 +12,7 @@ public interface CustomerRepository extends AbstractRepository { List findAllByDeletedFalse(); Optional findByCode(@Param("code") String code); Optional findByName(@Param("name") String name); + + @Query("SELECT max(cast(substring_index(c.code, '-', -1) as long)) FROM Customer c") + Long getLatestCodeNumber(); } diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt index 278bd35..6ad53c3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt @@ -3,4 +3,6 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface LocationRepository : AbstractRepository { + + fun findByName(name: String): Location } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java index b9fc459..f6e8a46 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java @@ -25,4 +25,5 @@ public interface StaffRepository extends AbstractRepository { Optional> findAllByDeletedFalse(); Optional findIdAndNameByUserIdAndDeletedFalse(Long id); + } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java b/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java index 980a8ba..725b475 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java @@ -10,6 +10,7 @@ import java.util.List; import static jakarta.persistence.CascadeType.ALL; + @Entity @Table(name = "subsidiary") public class Subsidiary extends BaseEntity { diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java index d94b9b9..9edc95c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java @@ -1,6 +1,7 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; @@ -12,4 +13,9 @@ public interface SubsidiaryRepository extends AbstractRepository findAllByDeletedFalseAndIdIn(List id); Optional findByCode(@Param("code") String code); + + Optional findByName(@Param("name") String name); + + @Query("SELECT max(cast(substring_index(s.code, '-', -1) as long)) FROM Subsidiary s") + Long getLatestCodeNumber(); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java index 3a3cf41..a788348 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java @@ -9,4 +9,6 @@ public interface TeamRepository extends AbstractRepository { List findByDeletedFalse(); Team findByStaff(Staff staff); + + Team findByCode(String code); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt index 24f88c7..20144a5 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface WorkNatureRepository : AbstractRepository { + fun findByName(name: String): WorkNature? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt index 4abb66e..9202f2d 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt @@ -37,6 +37,18 @@ open class CustomerService( return customerRepository.findByCode(code); } + open fun createClientCode(): String { + val prefix = "CT" + + val latestClientCode = customerRepository.getLatestCodeNumber() + + if (latestClientCode != null) { + return "$prefix-" + String.format("%03d", latestClientCode + 1L) + } else { + return "$prefix-001" + } + } + open fun saveCustomer(saveCustomer: SaveCustomerRequest): SaveCustomerResponse { val duplicateCustomer = findCustomerByCode(saveCustomer.code) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt b/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt index ea55431..23f53c6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt @@ -38,6 +38,17 @@ open class SubsidiaryService( return subsidiaryRepository.findByCode(code); } + open fun createSubsidiaryCode(): String { + val prefix = "SY" + + val latestSubsidiaryCode = subsidiaryRepository.getLatestCodeNumber() + + if (latestSubsidiaryCode != null) { + return "$prefix-" + String.format("%03d", latestSubsidiaryCode + 1L) + } else { + return "$prefix-001" + } + } open fun saveSubsidiary(saveSubsidiary: SaveSubsidiaryRequest): SaveSubsidiaryResponse { val duplicateSubsidiary = findSubsidiaryByCode(saveSubsidiary.code) 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 8f367a8..4fdf2dc 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 @@ -34,4 +34,6 @@ interface ProjectRepository : AbstractRepository { fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List + + fun findByCode(code: String): Project? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index dfb9bbe..1b69735 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -1,26 +1,26 @@ package com.ffii.tsms.modules.project.service +import com.ffii.core.utils.ExcelUtils import com.ffii.tsms.modules.common.SecurityUtils import com.ffii.tsms.modules.data.entity.* -import com.ffii.tsms.modules.data.service.CustomerContactService -import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo -import com.ffii.tsms.modules.data.service.CustomerService -import com.ffii.tsms.modules.data.service.GradeService -import com.ffii.tsms.modules.data.service.SubsidiaryContactService +import com.ffii.tsms.modules.data.service.* import com.ffii.tsms.modules.project.entity.* import com.ffii.tsms.modules.project.entity.Milestone 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.ProjectSearchInfo import com.ffii.tsms.modules.project.web.models.* import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository +import org.apache.commons.logging.LogFactory +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate import java.time.format.DateTimeFormatter -import java.util.Optional -import kotlin.jvm.optionals.getOrElse import kotlin.jvm.optionals.getOrNull + @Service open class ProjectsService( private val projectRepository: ProjectRepository, @@ -46,7 +46,11 @@ open class ProjectsService( private val timesheetRepository: TimesheetRepository, private val taskTemplateRepository: TaskTemplateRepository, private val subsidiaryContactService: SubsidiaryContactService, - private val subsidiaryContactRepository: SubsidiaryContactRepository + private val subsidiaryContactRepository: SubsidiaryContactRepository, + private val teamRepository: TeamRepository, + private val customerRepository: CustomerRepository, + private val subsidiaryRepository: SubsidiaryRepository, + private val customerSubsidiaryService: CustomerSubsidiaryService, ) { open fun allProjects(): List { return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() @@ -164,10 +168,15 @@ open class ProjectsService( .orElseThrow() else null val teamLead = staffRepository.findById(request.projectLeadId).orElseThrow() val customer = customerService.findCustomer(request.clientId) - val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null) - val clientContact = customerContactService.findByContactId(request.clientContactId) + val subsidiaryContact = + if (request.clientContactId != null) subsidiaryContactRepository.findById(request.clientContactId) + .orElse(null) else null + val clientContact = + if (request.clientContactId != null) customerContactService.findByContactId(request.clientContactId) else null val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } - val mainProject = if (request.mainProjectId != null && request.mainProjectId > 0) projectRepository.findById(request.mainProjectId).orElse(null) else null + val mainProject = + if (request.mainProjectId != null && request.mainProjectId > 0) projectRepository.findById(request.mainProjectId) + .orElse(null) else null val allTasksMap = tasksService.allTasks().associateBy { it.id } val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } @@ -179,15 +188,23 @@ open class ProjectsService( project.apply { name = request.projectName description = request.projectDescription - code = if (this.code.isNullOrEmpty() && request.mainProjectId == null) createProjectCode(request.isClpProject, project) else if (this.code.isNullOrEmpty() && request.mainProjectId != null && mainProject != null) createSubProjectCode(mainProject, project) else this.code + code = request.projectCode + ?: if (this.code.isNullOrEmpty() && request.mainProjectId == null) createProjectCode( + request.isClpProject, + project + ) else if (this.code.isNullOrEmpty() && request.mainProjectId != null && mainProject != null) createSubProjectCode( + mainProject, + project + ) else this.code expectedTotalFee = request.expectedProjectFee totalManhour = request.totalManhour actualStart = request.projectActualStart actualEnd = request.projectActualEnd - status = if (this.status == "Deleted" || this.deleted == true) "Deleted" - else if (this.actualStart != null && this.actualEnd != null) "Completed" - else if (this.actualStart != null) "On-going" - else "Pending To Start" + status = request.projectStatus + ?: if (this.status == "Deleted" || this.deleted == true) "Deleted" + else if (this.actualStart != null && this.actualEnd != null) "Completed" + else if (this.actualStart != null) "On-going" + else "Pending To Start" isClpProject = request.isClpProject this.mainProject = mainProject @@ -203,11 +220,11 @@ open class ProjectsService( this.teamLead = teamLead this.customer = customer custLeadName = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.name else subsidiaryContact?.name custLeadEmail = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.email else subsidiaryContact?.email custLeadPhone = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.phone else subsidiaryContact?.phone this.customerSubsidiary = customerSubsidiary this.customerContact = if (customerSubsidiary == null) clientContact else null this.subsidiaryContact = if (customerSubsidiary != null) subsidiaryContact else null @@ -279,11 +296,11 @@ open class ProjectsService( if (milestones.isNotEmpty()) { project.apply { planStart = milestones.mapNotNull { it.startDate }.minOrNull() - planEnd = milestones.mapNotNull { it.endDate }.maxOrNull() + planEnd = request.projectPlanEnd ?: milestones.mapNotNull { it.endDate }.maxOrNull() } } - val savedProject = projectRepository.save(project) + val savedProject = projectRepository.saveAndFlush(project) val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet()) val tasksToDelete = projectTaskRepository.findAllByProject(project).subtract(tasksToSave.toSet()) @@ -422,8 +439,9 @@ open class ProjectsService( val subsidiaryContact = project.customerSubsidiary?.id?.let { subsidiaryId -> subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) } ?: emptyList() - val customerContact = project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } - ?: emptyList() + val customerContact = + project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } + ?: emptyList() MainProjectDetails( projectId = project.id, @@ -441,10 +459,238 @@ open class ProjectsService( buildingTypeIds = project.buildingTypes.mapNotNull { buildingType -> buildingType.id }, workNatureIds = project.workNatures.mapNotNull { workNature -> workNature.id }, clientId = project.customer?.id, - clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id, + clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id + ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id, clientSubsidiaryId = project.customerSubsidiary?.id, expectedProjectFee = project.expectedTotalFee ) } } + + @Transactional(rollbackFor = [Exception::class]) + open fun importFile(workbook: Workbook?): String { + val logger = LogFactory.getLog(javaClass) + + if (workbook == null) { + return "No Excel import" // if workbook is null + } + + val sheet: Sheet = workbook.getSheetAt(0) + + for (i in 2..() + for (j in 18.. 0, + manhourPercentageByGrade = taskTemplate.gradeAllocations.associateBy( + keySelector = { it.grade!!.id!! }, + valueTransform = { it.percentage!! } + ), + projectActualEnd = null, + taskTemplateId = taskTemplate.id, + taskGroups = mapOf(Pair(5, TaskGroupAllocation(mutableListOf(41), 100.0))), + milestones = mapOf( + Pair( + 5, com.ffii.tsms.modules.project.web.models.Milestone( + startDate = ExcelUtils.getDateValue( + row.getCell(2), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).format(formatter), + endDate = ExcelUtils.getDateValue( + row.getCell(3), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).format(formatter), + payments = mutableListOf( + PaymentInputs( + -1, "Manhour Import", ExcelUtils.getDateValue( + row.getCell(2), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).toString(), row.getCell(9).numericCellValue + ) + ) + ) + ) + )) + ) + } + } + + return if (sheet.lastRowNum > 0) "Import Excel success" else "Import Excel failure" + } } 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 447a50b..61c070e 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 @@ -2,15 +2,21 @@ package com.ffii.tsms.modules.project.web import com.ffii.core.exception.NotFoundException import com.ffii.tsms.modules.data.entity.* -import com.ffii.tsms.modules.project.entity.Project -import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import com.ffii.tsms.modules.project.entity.ProjectCategory import com.ffii.tsms.modules.project.entity.ProjectRepository +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.* +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartHttpServletRequest + @RestController @RequestMapping("/projects") @@ -85,4 +91,20 @@ class ProjectsController(private val projectsService: ProjectsService, private v fun projectWorkNatures(): List { return projectsService.allWorkNatures() } + + @PostMapping("/import") + @Throws(ServletRequestBindingException::class) + fun importFile(request: HttpServletRequest): ResponseEntity<*> { + var workbook: Workbook? = null + + try { + val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList") + workbook = XSSFWorkbook(multipartFile?.inputStream) + } catch (e: Exception) { + println("Excel Wrong") + println(e) + } + + return ResponseEntity.ok(projectsService.importFile(workbook)) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt index 1f59b33..aab4473 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt @@ -7,7 +7,7 @@ import java.time.LocalDate data class NewProjectRequest( // Project details // @field:NotBlank(message = "project code cannot be empty") -// val projectCode: String, + val projectCode: String?, @field:NotBlank(message = "project name cannot be empty") val projectName: String, val projectCategoryId: Long, @@ -16,8 +16,10 @@ data class NewProjectRequest( val projectId: Long?, val projectActualStart: LocalDate?, val projectActualEnd: LocalDate?, + val projectPlanEnd: LocalDate?, val isClpProject: Boolean?, val mainProjectId: Long?, + val projectStatus: String?, val serviceTypeId: Long, val fundingTypeId: Long, @@ -30,7 +32,7 @@ data class NewProjectRequest( // Client details val clientId: Long, - val clientContactId: Long, + val clientContactId: Long?, val clientSubsidiaryId: Long?, // Allocation diff --git a/src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql b/src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql new file mode 100644 index 0000000..5c6920d --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql @@ -0,0 +1,9 @@ +-- liquibase formatted sql +-- changeset cyril:task + +INSERT +INTO + task +(name, taskGroupId) +VALUES + ('5.8 Manhour Import', 5); \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql b/src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql new file mode 100644 index 0000000..880b7c1 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:task + +ALTER TABLE `project` + CHANGE COLUMN `name` `name` VARCHAR(255) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' NOT NULL ; \ No newline at end of file From d6c932308edbceebe70964c00255c1956b7f114f Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 21 Jun 2024 18:35:08 +0800 Subject: [PATCH 02/22] update --- .../com/ffii/tsms/modules/project/service/ProjectsService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index 1b69735..f668a71 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -515,9 +515,11 @@ open class ProjectsService( if (projectCode.contains('(')) { val splitProjectCode = projectCode.split('(') + val splitMainProjectCode = splitProjectCode[0].split('-') + logger.info("splitProjectCode: " + splitProjectCode[1].split(')')[0]) projectCode = - splitProjectCode[0] + '-' + String.format("%04d", splitProjectCode[1].split(')')[0].toInt()) + splitMainProjectCode[0] + '-' + String.format("%04d", splitMainProjectCode[1].toInt()) + '-' + String.format("%03d", splitProjectCode[1].split(')')[0].toInt()) val mainProject = projectRepository.findByCode(splitProjectCode[0]) ?: projectRepository.saveAndFlush( From c760a08a49905be7b13ae954e023a222c42098f4 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 25 Jun 2024 14:14:00 +0800 Subject: [PATCH 03/22] add timesheet import --- .../project/service/ProjectsService.kt | 4 +- .../timesheet/service/TimesheetsService.kt | 84 ++++++++++++++++++- .../timesheet/web/TimesheetsController.kt | 22 +++++ 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index f668a71..d659eb1 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -480,7 +480,7 @@ open class ProjectsService( for (i in 2.. 0, + isSubsidiaryContact = subsidiary?.id != null && subsidiary.id!! > 0, manhourPercentageByGrade = taskTemplate.gradeAllocations.associateBy( keySelector = { it.grade!!.id!! }, valueTransform = { it.percentage!! } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index a387127..3f87ff9 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -1,17 +1,22 @@ package com.ffii.tsms.modules.timesheet.service import com.ffii.core.exception.BadRequestException +import com.ffii.core.utils.ExcelUtils +import com.ffii.tsms.modules.data.entity.BuildingType import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.StaffRepository +import com.ffii.tsms.modules.data.entity.WorkNature import com.ffii.tsms.modules.data.service.StaffsService import com.ffii.tsms.modules.data.service.TeamService -import com.ffii.tsms.modules.project.entity.ProjectRepository -import com.ffii.tsms.modules.project.entity.ProjectTaskRepository -import com.ffii.tsms.modules.project.entity.TaskRepository +import com.ffii.tsms.modules.project.entity.* +import com.ffii.tsms.modules.project.web.models.* import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries import com.ffii.tsms.modules.timesheet.web.models.TimeEntry +import org.apache.commons.logging.LogFactory +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -159,4 +164,77 @@ open class TimesheetsService( ) } } } + + @Transactional(rollbackFor = [Exception::class]) + open fun importFile(workbook: Workbook?): String { + val logger = LogFactory.getLog(javaClass) + + if (workbook == null) { + return "No Excel import" // if workbook is null + } + + val sheet: Sheet = workbook.getSheetAt(0) + + val timesheetList = mutableListOf().toMutableList(); + for (i in 1.. task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } + + // process record date + logger.info("---------record date-------") + val formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy") + val recordDate = LocalDate.parse(row.getCell(6).toString(), formatter); + + // normal hour + ot hour + logger.info("---------normal hour + ot hour-------") + val hours: Double = row.getCell(7).numericCellValue + val normalHours = if (hours > 8.0) 8.0 else hours + val otHours = if (hours > 8.0) hours - 8.0 else 0.0 + + timesheetList += Timesheet().apply { + this.staff = staff + this.recordDate = recordDate + this.normalConsumed = normalHours + this.otConsumed = otHours + this.projectTask = projectTask + this.project = project + } + } + } + + timesheetRepository.saveAll(timesheetList) + logger.info("---------end-------") + + return if (sheet.lastRowNum > 0) "Import Excel success" else "Import Excel failure" + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt index 6f2d4ce..29d1c28 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt @@ -5,12 +5,18 @@ import com.ffii.tsms.modules.timesheet.entity.LeaveType import com.ffii.tsms.modules.timesheet.service.LeaveService import com.ffii.tsms.modules.timesheet.service.TimesheetsService import com.ffii.tsms.modules.timesheet.web.models.* +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartHttpServletRequest import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -85,4 +91,20 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri fun leaveTypes(): List { return leaveService.getLeaveTypes() } + + @PostMapping("/import") + @Throws(ServletRequestBindingException::class) + fun importFile(request: HttpServletRequest): ResponseEntity<*> { + var workbook: Workbook? = null + + try { + val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList") + workbook = XSSFWorkbook(multipartFile?.inputStream) + } catch (e: Exception) { + println("Excel Wrong") + println(e) + } + + return ResponseEntity.ok(timesheetsService.importFile(workbook)) + } } \ No newline at end of file From b0eea4b574c33daf1884857ecd47bbd83f0b252c Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 26 Jun 2024 14:58:19 +0800 Subject: [PATCH 04/22] update --- .../modules/project/service/InvoiceService.kt | 8 +- .../project/service/ProjectsService.kt | 7 +- .../modules/report/service/ReportService.kt | 4 +- .../timesheet/service/TimesheetsService.kt | 93 ++++++++++++------- 4 files changed, 67 insertions(+), 45 deletions(-) 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 bb087eb..950ab7e 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 @@ -362,7 +362,7 @@ open class InvoiceService( // Check the import invoice with the data in DB for (i in 2..sheet.lastRowNum){ - val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).stringCellValue + val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString() val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue checkInvoiceNo(sheetInvoice, invoices, invoicesResult, true) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -398,7 +398,7 @@ open class InvoiceService( // val paymentMilestoneId = getMilestonePaymentId(ExcelUtils.getCell(sheet, i, 1).stringCellValue, ExcelUtils.getCell(sheet, i, 5).stringCellValue) // val milestonePayment = milestonePaymentRepository.findById(paymentMilestoneId).orElseThrow() val invoice = Invoice().apply { - invoiceNo = ExcelUtils.getCell(sheet, i, 0).stringCellValue + invoiceNo = ExcelUtils.getCell(sheet, i, 0).toString() projectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue projectName = ExcelUtils.getCell(sheet, i, 2).stringCellValue team = ExcelUtils.getCell(sheet, i, 3).stringCellValue @@ -447,7 +447,7 @@ open class InvoiceService( val duplicateItemsInInvoice = checkDuplicateItemInImportedInvoice(sheet,2,0) for (i in 2..sheet.lastRowNum){ - val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).stringCellValue + val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString() val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue checkInvoiceNo(sheetInvoice, invoices, invoicesResult, false) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -489,7 +489,7 @@ open class InvoiceService( } for (i in 2..sheet.lastRowNum){ - val invoice = getInvoiceByInvoiceNo(ExcelUtils.getCell(sheet, i, 0).stringCellValue) + val invoice = getInvoiceByInvoiceNo(ExcelUtils.getCell(sheet, i, 0).toString()) invoice.paidAmount = ExcelUtils.getCell(sheet, i, 5).numericCellValue.toBigDecimal() invoice.receiptDate = ExcelUtils.getCell(sheet, i, 4).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() saveAndFlush(invoice) diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index d659eb1..2748323 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -200,11 +200,10 @@ open class ProjectsService( totalManhour = request.totalManhour actualStart = request.projectActualStart actualEnd = request.projectActualEnd - status = request.projectStatus - ?: if (this.status == "Deleted" || this.deleted == true) "Deleted" + status = if (this.status == "Deleted" || this.deleted == true) "Deleted" else if (this.actualStart != null && this.actualEnd != null) "Completed" else if (this.actualStart != null) "On-going" - else "Pending To Start" + else request.projectStatus ?: "Pending To Start" isClpProject = request.isClpProject this.mainProject = mainProject @@ -526,7 +525,7 @@ open class ProjectsService( Project().apply { name = row.getCell(1).stringCellValue description = row.getCell(1).stringCellValue - code = splitProjectCode[0] + code = splitMainProjectCode[0] + '-' + String.format("%04d", splitMainProjectCode[1].toInt()) status = "Completed" projectCategory = projectCategoryRepository.findById(1).orElseThrow() customer = currentClient 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 d148010..f7796cb 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 @@ -691,12 +691,12 @@ open class ReportService( rowIndex = 10 sheet.getRow(rowIndex).apply { createCell(1).apply { - setCellValue(project.expectedTotalFee!! * 0.8) + setCellValue(if (project.expectedTotalFee != null) project.expectedTotalFee!! * 0.8 else 0.0) cellStyle.dataFormat = accountingStyle } createCell(2).apply { - setCellValue(project.expectedTotalFee!!) + setCellValue(project.expectedTotalFee ?: 0.0) cellStyle.dataFormat = accountingStyle } } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index 3f87ff9..0b4bf12 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -45,14 +45,15 @@ open class TimesheetsService( mergeTimeEntriesByProjectAndTask(timeEntries).map { timeEntry -> val task = timeEntry.taskId?.let { taskRepository.findById(it).getOrNull() } val project = timeEntry.projectId?.let { projectRepository.findById(it).getOrNull() } - val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } + val projectTask = + project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } Timesheet().apply { this.staff = currentStaff this.recordDate = entryDate this.normalConsumed = timeEntry.inputHours this.otConsumed = timeEntry.otHours - this.projectTask = projectTask + this.projectTask = projectTask this.project = project this.remark = timeEntry.remark } @@ -65,7 +66,11 @@ open class TimesheetsService( } @Transactional - open fun saveMemberTimeEntry(staffId: Long, entry: TimeEntry, recordDate: LocalDate?): Map> { + open fun saveMemberTimeEntry( + staffId: Long, + entry: TimeEntry, + recordDate: LocalDate? + ): Map> { val authorities = staffsService.currentAuthorities() ?: throw BadRequestException() if (!authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET") }) { @@ -80,11 +85,11 @@ open class TimesheetsService( val timesheet = timesheetRepository.findById(entry.id).getOrDefault(Timesheet()).apply { val task = entry.taskId?.let { taskRepository.findById(it).getOrNull() } val project = entry.projectId?.let { projectRepository.findById(it).getOrNull() } - val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } + val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } this.normalConsumed = entry.inputHours this.otConsumed = entry.otHours - this.projectTask = projectTask + this.projectTask = projectTask this.project = project this.remark = entry.remark this.recordDate = this.recordDate ?: recordDate @@ -114,7 +119,7 @@ open class TimesheetsService( open fun getTeamMemberTimesheet(): Map { val authorities = staffsService.currentAuthorities() ?: return emptyMap() - if (authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET")}) { + if (authorities.stream().anyMatch { it.authority.equals("MAINTAIN_TIMESHEET") }) { val currentStaff = staffsService.currentStaff() // Get team where current staff is team lead @@ -141,27 +146,31 @@ open class TimesheetsService( private fun transformToTimeEntryMap(timesheets: List): Map> { return timesheets .groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) } - .mapValues { (_, timesheets) -> timesheets.map { timesheet -> - TimeEntry( - id = timesheet.id!!, - projectId = timesheet.projectTask?.project?.id ?: timesheet.project?.id, - taskId = timesheet.projectTask?.task?.id, - taskGroupId = timesheet.projectTask?.task?.taskGroup?.id, - inputHours = timesheet.normalConsumed ?: 0.0, - otHours = timesheet.otConsumed ?: 0.0, - remark = timesheet.remark - ) - } } + .mapValues { (_, timesheets) -> + timesheets.map { timesheet -> + TimeEntry( + id = timesheet.id!!, + projectId = timesheet.projectTask?.project?.id ?: timesheet.project?.id, + taskId = timesheet.projectTask?.task?.id, + taskGroupId = timesheet.projectTask?.task?.taskGroup?.id, + inputHours = timesheet.normalConsumed ?: 0.0, + otHours = timesheet.otConsumed ?: 0.0, + remark = timesheet.remark + ) + } + } } private fun mergeTimeEntriesByProjectAndTask(entries: List): List { return entries .groupBy { timeEntry -> Pair(timeEntry.projectId, timeEntry.taskId) } .values.map { timeEntries -> - timeEntries.reduce { acc, timeEntry -> acc.copy( - inputHours = (acc.inputHours ?: 0.0) + (timeEntry.inputHours ?: 0.0), - otHours = (acc.otHours ?: 0.0) + (timeEntry.otHours ?: 0.0) - ) } + timeEntries.reduce { acc, timeEntry -> + acc.copy( + inputHours = (acc.inputHours ?: 0.0) + (timeEntry.inputHours ?: 0.0), + otHours = (acc.otHours ?: 0.0) + (timeEntry.otHours ?: 0.0) + ) + } } } @@ -173,13 +182,18 @@ open class TimesheetsService( return "No Excel import" // if workbook is null } + val notExistProjectList = mutableListOf() val sheet: Sheet = workbook.getSheetAt(0) + logger.info("---------Start Import Timesheets-------") val timesheetList = mutableListOf().toMutableList(); for (i in 1.. task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } + val projectTask = + project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } // process record date logger.info("---------record date-------") @@ -221,20 +239,25 @@ open class TimesheetsService( val normalHours = if (hours > 8.0) 8.0 else hours val otHours = if (hours > 8.0) hours - 8.0 else 0.0 - timesheetList += Timesheet().apply { - this.staff = staff - this.recordDate = recordDate - this.normalConsumed = normalHours - this.otConsumed = otHours - this.projectTask = projectTask - this.project = project + if (project != null) { + timesheetList += Timesheet().apply { + this.staff = staff + this.recordDate = recordDate + this.normalConsumed = normalHours + this.otConsumed = otHours + this.projectTask = projectTask + this.project = project + } + } else { + notExistProjectList += projectCode } } } timesheetRepository.saveAll(timesheetList) logger.info("---------end-------") + logger.info("Not Exist Project List: "+ notExistProjectList.joinToString(", ")) - return if (sheet.lastRowNum > 0) "Import Excel success" else "Import Excel failure" + return if (sheet.lastRowNum > 0) "Import Excel success btw " + notExistProjectList.joinToString(", ") else "Import Excel failure" } } \ No newline at end of file From b89945212d7bdbb29b41763d31c39121c4270edc Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 26 Jun 2024 17:31:12 +0800 Subject: [PATCH 05/22] update report --- .../modules/project/service/InvoiceService.kt | 8 +++---- .../modules/report/service/ReportService.kt | 20 +++++++++--------- .../timesheet/service/TimesheetsService.kt | 2 +- .../report/AR03_Resource Overconsumption.xlsx | Bin 13354 -> 13044 bytes .../AR05_Project Completion Report.xlsx | Bin 12562 -> 12234 bytes ...h Outstanding Accounts Receivable v02.xlsx | Bin 12739 -> 12499 bytes 6 files changed, 15 insertions(+), 15 deletions(-) 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..21d3759 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 @@ -362,7 +362,7 @@ open class InvoiceService( // Check the import invoice with the data in DB for (i in 2..sheet.lastRowNum){ - val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString() + val sheetInvoice = BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString() val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue checkInvoiceNo(sheetInvoice, invoices, invoicesResult, true) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -398,7 +398,7 @@ open class InvoiceService( // val paymentMilestoneId = getMilestonePaymentId(ExcelUtils.getCell(sheet, i, 1).stringCellValue, ExcelUtils.getCell(sheet, i, 5).stringCellValue) // val milestonePayment = milestonePaymentRepository.findById(paymentMilestoneId).orElseThrow() val invoice = Invoice().apply { - invoiceNo = ExcelUtils.getCell(sheet, i, 0).toString() + invoiceNo = BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString() projectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue projectName = ExcelUtils.getCell(sheet, i, 2).stringCellValue team = ExcelUtils.getCell(sheet, i, 3).stringCellValue @@ -447,7 +447,7 @@ open class InvoiceService( val duplicateItemsInInvoice = checkDuplicateItemInImportedInvoice(sheet,2,0) for (i in 2..sheet.lastRowNum){ - val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString() + val sheetInvoice = BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString() val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue checkInvoiceNo(sheetInvoice, invoices, invoicesResult, false) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -489,7 +489,7 @@ open class InvoiceService( } for (i in 2..sheet.lastRowNum){ - val invoice = getInvoiceByInvoiceNo(ExcelUtils.getCell(sheet, i, 0).toString()) + val invoice = getInvoiceByInvoiceNo(BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString()) invoice.paidAmount = ExcelUtils.getCell(sheet, i, 5).numericCellValue.toBigDecimal() invoice.receiptDate = ExcelUtils.getCell(sheet, i, 4).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() saveAndFlush(invoice) 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 f7796cb..82cf016 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 @@ -65,7 +65,7 @@ open class ReportService( var rowIndex = startRow var columnIndex = startColumn result.forEachIndexed { index, obj -> - var tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + var tempCell = (sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex)).createCell(columnIndex) tempCell.setCellValue((index + 1).toDouble()) val keys = obj.keys.toList() keys.forEachIndexed { keyIndex, key -> @@ -1846,22 +1846,22 @@ open class ReportService( if (args != null) { var statusFilter: String = "" if (args.containsKey("teamId")) - sql.append("and t.id = :teamId") + sql.append(" and t.id = :teamId") if (args.containsKey("custId")) - sql.append("and c.id = :custId") + sql.append(" and c.id = :custId") if (args.containsKey("subsidiaryId")) - sql.append("and ss.id = :subsidiaryId") + sql.append(" and ss.id = :subsidiaryId") if (args.containsKey("status")) statusFilter = when (args.get("status")) { - "Potential Overconsumption" -> "and " + + "Potential Overconsumption" -> " and " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " + " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " + " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " - "All" -> "and " + + "All" -> " and " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " -// "Overconsumption" -> "and " + +// "Overconsumption" -> " and " + // " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + // " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " @@ -2403,7 +2403,7 @@ open class ReportService( for(item in queryList){ val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(item["code"] !in costAndExpenseList){ + if(item["code"] !in costAndExpenseList.map { it["code"] }){ costAndExpenseList.add( mapOf( "code" to item["code"], @@ -2414,13 +2414,13 @@ open class ReportService( "budget" to item["expectedTotalFee"], "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double ) - + (hourlyRate * item["otConsumed"]as Double * otFactor) + + (hourlyRate * item["otConsumed"] as Double * otFactor) ) ) }else{ val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { - put("totalManhours", get("manhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)) + put("totalManhours", get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)) put("manhourExpenditure", get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double ) + (hourlyRate * item["otConsumed"]as Double * otFactor))) } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index 0b4bf12..1b4e2c2 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -256,7 +256,7 @@ open class TimesheetsService( timesheetRepository.saveAll(timesheetList) logger.info("---------end-------") - logger.info("Not Exist Project List: "+ notExistProjectList.joinToString(", ")) + logger.info("Not Exist Project List: "+ notExistProjectList.distinct().joinToString(", ")) return if (sheet.lastRowNum > 0) "Import Excel success btw " + notExistProjectList.joinToString(", ") else "Import Excel failure" } diff --git a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx index fb9418177b8978fff9da6be57a7ac8d1e8afeef5..20503009bc676f8310a165299bd5749961ca166a 100644 GIT binary patch delta 5037 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 z zIl5$r+BIO6n#NP=JJqj7s@qD{;l(+l~DQ*R}h zJt_~6wM%^2u8~)3>SJwADlv&rt-Lzrg-Sc7$GX~16;uJ1{W4Fs>yY!zHs7vJ^1+%2kpI$MhF zg;#KyQEPhy-wS>L@7cM*wl|rg%SO|ejeYVu>j+V7SVkgQ8+qx`oSLH7?r=mrS>La5 zDU3P?MdVLw_)VF)fQHTrKr(sXGX536NQs7xx~KP-I@eOtx6)Xz*?vQ~*>I;M^B#O%OVSlwsGIpidRfc zA2b15^^+;&EuPI*G)R*R$yr*IsTv-J7wlq=^=%k34J(uBSS(2e)Xy@f14QbSfrUK+ z8m=hGJaA>x%OOJiZ>U||=i7FqA=_U<4~%LpQwlLx+7Gw? zG?{;HqrTs`I#Fmn&8yl+=^GZatg>)~Fqw!G@?35*{2YA$_BD-xVoF>!&Z7Q`6Nk$i zObb)?xklr}Qvc7FRg^dc4~rfZ7iRux>M|PR@uf6$ha+X+UKE212RP z_7XhI|KHU8o1n~qE|6ej{zsdv|FDATk1MAAH~Qz4$BJHJqx%buC&`E}$c|=_e26c^ zfqu#h_cS&+F|1abIV1xcEVM7RwFU1LL{%1-8{0E4I BH@E-* delta 5317 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 delta 4073 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 Date: Thu, 27 Jun 2024 16:08:04 +0800 Subject: [PATCH 06/22] update report --- .../project/entity/InvoiceRepository.kt | 2 + .../modules/report/service/ReportService.kt | 46 +++++++++++++----- .../modules/report/web/ReportController.kt | 6 ++- .../AR04_Cost and Expense Report v02.xlsx | Bin 12808 -> 12733 bytes ...08_Monthly Work Hours Analysis Report.xlsx | Bin 14716 -> 14715 bytes 5 files changed, 42 insertions(+), 12 deletions(-) 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 cdedfc4..940f2d2 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 @@ -12,4 +12,6 @@ interface InvoiceRepository : AbstractRepository { fun findInvoiceInfoByPaidAmountIsNotNull(): List fun findByInvoiceNo(invoiceNo: String): Invoice + + fun findAllByProjectCodeAndPaidAmountIsNotNull(projectCode: String): List } \ 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 82cf016..406c52e 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 @@ -435,7 +435,8 @@ open class ReportService( val uninvoiceCell = row.createCell(12) uninvoiceCell.apply { cellFormula = - " 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))) " + " IF(I${rowNum}-L${rowNum}<0, 0, I${rowNum}-L${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 } @@ -1080,6 +1081,13 @@ open class ReportService( val boldFont = workbook.createFont() boldFont.bold = true boldStyle.setFont(boldFont) + + val projectsStyle = workbook.createCellStyle() + val projectsFont = workbook.createFont() + projectsFont.bold = true + projectsFont.fontName = "Times New Roman" + projectsStyle.setFont(projectsFont) + val daysOfMonth = (1..month.lengthOfMonth()).map { day -> val date = month.withDayOfMonth(day) val formattedDate = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) @@ -1228,7 +1236,7 @@ open class ReportService( projectList.forEachIndexed { index, title -> tempCell = sheet.getRow(7).createCell(columnIndex + index) tempCell.setCellValue(title) - tempCell.cellStyle = boldStyle + tempCell.cellStyle = projectsStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) @@ -1271,7 +1279,7 @@ open class ReportService( ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// tempCell = sheet.getRow(rowIndex).createCell(columnIndex) tempCell.setCellValue("Leave Hours") - tempCell.cellStyle = boldStyle + tempCell.cellStyle = projectsStyle CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) @@ -1279,8 +1287,10 @@ open class ReportService( columnIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(columnIndex) tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") - tempCell.cellStyle = boldStyle - CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) + tempCell.cellStyle = projectsStyle +// CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) + CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) + CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) @@ -1375,7 +1385,7 @@ open class ReportService( rowIndex = 5 columnIndex = 0 - generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) + generalCreateReportIndexed(sheet, result.distinct(), rowIndex, columnIndex) return workbook } @@ -1825,12 +1835,12 @@ open class ReportService( + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " + " CASE " - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " - + " then 'Potential Overconsumption' " + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " + " then 'Overconsumption' " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " + + " then 'Potential Overconsumption' " + " else 'Within Budget' " + " END as status " + " FROM project p " @@ -1884,6 +1894,15 @@ open class ReportService( + " left join salary s2 on s.salaryId = s2.salaryPoint" + " left join team t2 on t2.id = s.teamId" + " )," + + " cte_timesheet_sum as (" + + " Select p.code, sum((IFNULL(t.normalConsumed, 0) + IFNULL(t.otConsumed , 0)) * s2.hourlyRate) as sumManhourExpenditure" + + " 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" + + " group by p.code" + + " )," + " cte_invoice as (" + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + " from invoice i" @@ -1893,10 +1912,11 @@ 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, " - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," + + " 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" + " 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" + " left join customer c on c.id = p.customerId" + " left join tsmsdb.team t on t.teamLead = p.teamLead" + " left join cte_invoice cte_i on cte_i.code = p.code" @@ -1959,6 +1979,9 @@ open class ReportService( if (info["code"] == item["code"] && "subsidiary" !in info) { info["subsidiary"] = item.getValue("subsidiary") } + if (info["manhourExpenditure"] != item.getValue("sumManhourExpenditure")) { + info["manhourExpenditure"] = item.getValue("sumManhourExpenditure") + } if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } @@ -2319,7 +2342,8 @@ open class ReportService( } val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) totalManhourECell.apply { - cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" +// cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" + setCellValue(info.getValue("manhourExpenditure") as Double) cellStyle.dataFormat = accountingStyle } CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) 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 f84307e..1c312f0 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 @@ -34,6 +34,8 @@ import java.time.format.DateTimeFormatter import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.service.SubsidiaryService import com.ffii.tsms.modules.report.web.model.* +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory import org.springframework.data.domain.Example import org.springframework.data.domain.ExampleMatcher @@ -58,6 +60,7 @@ class ReportController( private val subsidiaryRepository: SubsidiaryRepository ) { + private val logger: Log = LogFactory.getLog(javaClass) @PostMapping("/fetchProjectsFinancialStatusReport") @Throws(ServletRequestBindingException::class, IOException::class) fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity { @@ -76,7 +79,8 @@ class ReportController( val project = projectRepository.findById(request.projectId).orElseThrow() val projectTasks = projectTaskRepository.findAllByProject(project) - val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project) +// val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project) + val invoices = invoiceRepository.findAllByProjectCodeAndPaidAmountIsNotNull(project.code!!) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, request.dateType) 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 8f971d5d354e78f2f97e801e1a5f0ab30751ad09..6dea4f8e97cf4db487ef9860577c55da4e429415 100644 GIT binary patch delta 4611 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 delta 4664 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 GbNPR*&#>?S diff --git a/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx b/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx index c471a6a4b6f33e637715ceb29ed99533993071f6..29dbffd5e6763981f01b55acf9964d08585a239d 100644 GIT binary patch delta 5743 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 From f36efde136dcc5d84ea13fe57096fda9b95b2024 Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Thu, 27 Jun 2024 16:45:31 +0800 Subject: [PATCH 07/22] update api --- .../modules/data/service/DashboardService.kt | 195 ++++++++++++++---- 1 file changed, 155 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 69f4567..64c181a 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -68,14 +68,14 @@ open class DashboardService( fun searchCustomerSubsidiaryProject(args: Map): List> { val sql = StringBuilder("select" - + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + " 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," + + " GROUP_CONCAT(DISTINCT tg.name ORDER BY 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," @@ -116,20 +116,20 @@ open class DashboardService( } } - sql.append(" group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone") + sql.append(" group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone") return jdbcDao.queryForList(sql.toString(), args) } fun searchCustomerNonSubsidiaryProject(args: Map): List> { val sql = StringBuilder("select" - + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + " 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," + + " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage," + " p.totalManhour as budgetedManhour," + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," @@ -160,7 +160,7 @@ open class DashboardService( + " and isNull(p.customerSubsidiaryId)" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " and (tg.name != '5. Miscellaneous' or tg.name is null)" - + " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone" + + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) return jdbcDao.queryForList(sql.toString(), args) @@ -276,14 +276,14 @@ open class DashboardService( fun searchTeamProject(args: Map): List> { val sql = StringBuilder( "select" - + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + " 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," + + " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage," + " p.totalManhour as budgetedManhour," + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," @@ -313,7 +313,7 @@ open class DashboardService( + " where p.teamLead = :teamLeadId" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " and (tg.name != '5. Miscellaneous' or tg.name is null)" - + " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone" + + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) return jdbcDao.queryForList(sql.toString(), args) @@ -445,16 +445,103 @@ open class DashboardService( return jdbcDao.queryForList(sql.toString(), args) } +// " case" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)" +// + " end as totalUninvoiced" + fun searchFinancialSummaryByClient(args: Map): List> { - val sql = StringBuilder( "select" - + " ROW_NUMBER() OVER (ORDER BY t.id, c.id) AS id," +// val sql = StringBuilder( "select" +// + " ROW_NUMBER() OVER (ORDER BY t.id, c.id) AS id," +// + " t.id as teamId," +// + " c.id as cid," +// + " c.code as customerCode," +// + " c.name as customerName," +// + " count(p.name) as projectNo," +// + " sum(p.expectedTotalFee) as totalFee," +// + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget," +// + " COALESCE(round(expenditure.cumulativeExpenditure,2),0) as cumulativeExpenditure," +// + " coalesce(sum(i.issueAmount),0) as totalInvoiced," +// + " coalesce(sum(i.paidAmount),0) as totalReceived," +// + " case" +// + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'" +// + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" +// + " end as cashFlowStatus," +// + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," +// + " case" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)" +// + " end as totalUninvoiced" +// + " from team t" +// + " left join project p on t.teamLead = p.teamLead" +// + " left join customer c on p.customerId = c.id" +// + " left join (" +// + " select" +// + " t3.id as tid," +// + " c3.id as cid," +// + " sum(i3.issueAmount) as issueAmount," +// + " sum(i3.paidAmount) as paidAmount" +// + " from team t3" +// + " left join project p3 on t3.teamLead = p3.teamLead" +// + " left join customer c3 on p3.customerId = c3.id" +// + " left join invoice i3 on p3.code = i3.projectCode" +// + " where t3.deleted = 0" +// + " and p3.status = 'On-going'" +// +// ) +// +// if (args != null) { +// if (args.containsKey("teamId")) +// sql.append(" AND t3.id = :teamId"); +// } +// sql.append( " group by c3.id, t3.id" +// + " ) as i on i.cid = c.id and i.tid = t.id" +// + " left join (" +// + " select" +// + " r.teamId as teamId," +// + " r.customerId as customerId," +// + " sum(r.cumulativeExpenditure) as cumulativeExpenditure" +// + " from (" +// + " select" +// + " t1.id as teamId," +// + " c2.id as customerId," +// + " (coalesce(sum(t2.normalConsumed),0) * s2.hourlyRate) + (coalesce(sum(t2.otConsumed),0) * s2.hourlyRate * 1.0) as cumulativeExpenditure," +// + " s2.hourlyRate as hourlyRate," +// + " sum(t2.normalConsumed) as normalConsumed," +// + " sum(t2.otConsumed) as otConsumed" +// + " from team t1" +// + " left join project p2 on t1.teamLead = p2.teamLead" +// + " left join customer c2 on p2.customerId = c2 .id" +// + " left join project_task pt ON p2.id = pt.project_id" +// + " left join timesheet t2 on pt.id = t2.projectTaskId" +// + " left join staff s on t2.staffId = s.id" +// + " left join salary s2 on s.salaryId = s2.salaryPoint" +// + " where p2.status = 'On-going'" +// + " and t2.id is not null" +// + " group by s2.hourlyRate,t1.id,c2.id" +// + " ) as r" +// + " group by r.teamId, r.customerId" +// + " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = c.id" +// + " where t.deleted = 0" +// + " and p.status = 'On-going'") +// +// if (args != null) { +// if (args.containsKey("teamId")) +// sql.append(" AND t.id = :teamId"); +// } +// sql.append(" group by t.id, c.id, c.code, c.name") + val sql = StringBuilder( + "select" + + " ROW_NUMBER() OVER (ORDER BY t.id, i.cid) AS id," + " t.id as teamId," - + " c.id as cid," - + " c.code as customerCode," - + " c.name as customerName," - + " count(p.name) as projectNo," - + " sum(p.expectedTotalFee) as totalFee," - + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget," + + " i.cid as cid," + + " i.customerCode as customerCode," + + " i.customerName as customerName," + + " p.projectNo as projectNo," + + " p.totalFee as totalFee," + + " p.totalBudget as totalBudget," + " COALESCE(round(expenditure.cumulativeExpenditure,2),0) as cumulativeExpenditure," + " coalesce(sum(i.issueAmount),0) as totalInvoiced," + " coalesce(sum(i.paidAmount),0) as totalReceived," @@ -464,17 +551,17 @@ open class DashboardService( + " end as cashFlowStatus," + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," + " case" - + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" - + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" - + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)" + + " when p.totalFee - sum(i.issueAmount) >= 0 then coalesce(round(p.totalFee - sum(i.issueAmount),2),0)" + + " when p.totalFee - sum(i.issueAmount) < 0 then 0" + + " when p.totalFee - sum(i.issueAmount) is null then 0" + " end as totalUninvoiced" + " from team t" - + " left join project p on t.teamLead = p.teamLead" - + " left join customer c on p.customerId = c.id" + " left join (" + " select" + " t3.id as tid," + " c3.id as cid," + + " c3.code as customerCode," + + " c3.name as customerName," + " sum(i3.issueAmount) as issueAmount," + " sum(i3.paidAmount) as paidAmount" + " from team t3" @@ -483,15 +570,34 @@ open class DashboardService( + " left join invoice i3 on p3.code = i3.projectCode" + " where t3.deleted = 0" + " and p3.status = 'On-going'" - - ) - - if (args != null) { - if (args.containsKey("teamId")) + ) + if (args != null) { + if (args.containsKey("teamId")) sql.append(" AND t3.id = :teamId"); - } - sql.append( " group by c3.id, t3.id" - + " ) as i on i.cid = c.id and i.tid = t.id" + } + sql.append( + " group by c3.id, t3.id, customerCode, customerName" + + " ) as i on i.tid = t.id" + + " left join (" + + " select" + + " t.id as tid," + + " c.id as cid," + + " count(p.id) as projectNo," + + " sum(p.expectedTotalFee) as totalFee," + + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget" + + " from team t" + + " left join project p on t.teamLead = p.teamLead" + + " left join customer c on p.customerId = c.id" + + " where t.deleted = 0" + + " and p.status = 'On-going'" + ) + if (args != null) { + if (args.containsKey("teamId")) + sql.append(" AND t.id = :teamId"); + } + sql.append( + " group by t.id, c.id" + + " ) as p on p.tid = t.id and p.cid = i.cid" + " left join (" + " select" + " r.teamId as teamId," @@ -517,15 +623,15 @@ open class DashboardService( + " group by s2.hourlyRate,t1.id,c2.id" + " ) as r" + " group by r.teamId, r.customerId" - + " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = c.id" + + " ) as expenditure on expenditure.teamId = t.id and expenditure.customerId = i.cid" + " where t.deleted = 0" - + " and p.status = 'On-going'") - - if (args != null) { - if (args.containsKey("teamId")) + + " and i.cid is not null" + ) + if (args != null) { + if (args.containsKey("teamId")) sql.append(" AND t.id = :teamId"); - } - sql.append(" group by t.id, c.id, c.code, c.name") + } + sql.append( " group by t.id, i.cid, i.customerCode, i.customerName, p.projectNo, p.totalFee, p.totalBudget") return jdbcDao.queryForList(sql.toString(), args) } @@ -547,10 +653,15 @@ open class DashboardService( + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + " end as cashFlowStatus," + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," +// + " case" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" +// + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)" +// + " end as totalUninvoiced" + " case" - + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" - + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" - + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) >= coalesce(sum(i.issueAmount),0) then COALESCE(expenditure.cumulativeExpenditure,0) - coalesce(sum(i.issueAmount),0)" + + " when p.expectedTotalFee - sum(i.issueAmount) >= 0 then coalesce(round(p.expectedTotalFee - sum(i.issueAmount),2),0)" + + " when p.expectedTotalFee - sum(i.issueAmount) < 0 then 0" + + " when p.expectedTotalFee - sum(i.issueAmount) is null then 0" + " end as totalUninvoiced" + " from team t" + " left join project p on t.teamLead = p.teamLead" @@ -1346,6 +1457,7 @@ open class DashboardService( + " AND dates.missing_date = ts.recordDate" + " WHERE" + " st.teamId = :teamId" + + " and st.deleted = 0" + " AND ts.recordDate IS NULL" + " GROUP BY st.id, st.name" + " ORDER BY" @@ -1595,6 +1707,7 @@ open class DashboardService( + " group by p.id, p.name" + " ) as result on result.pid = p2.id" + " where s2.id = :staffId" + + " and result.manhours > 0" + " group by p2.id, p2.name, result.manhours" ) @@ -1626,6 +1739,7 @@ open class DashboardService( + " group by p.id, p.name" + " ) as result on result.pid = p2.id" + " where s2.id = :staffId" + + " and result.manhours > 0" + " group by p2.id, p2.name, result.manhours" ) @@ -1656,6 +1770,7 @@ open class DashboardService( + " group by p.id, p.name" + " ) as result on result.pid = p2.id" + " where s2.id = :staffId" + + " and result.manhours > 0" + " group by p2.id, p2.name, result.manhours" ) From b0874a4669ae966d8ad4ec39b2ad7b30605335fb Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 27 Jun 2024 18:38:54 +0800 Subject: [PATCH 08/22] update --- .../modules/project/service/InvoiceService.kt | 8 ++-- .../modules/report/service/ReportService.kt | 45 ++++++++++--------- 2 files changed, 27 insertions(+), 26 deletions(-) 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 21d3759..950ab7e 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 @@ -362,7 +362,7 @@ open class InvoiceService( // Check the import invoice with the data in DB for (i in 2..sheet.lastRowNum){ - val sheetInvoice = BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString() + val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString() val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue checkInvoiceNo(sheetInvoice, invoices, invoicesResult, true) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -398,7 +398,7 @@ open class InvoiceService( // val paymentMilestoneId = getMilestonePaymentId(ExcelUtils.getCell(sheet, i, 1).stringCellValue, ExcelUtils.getCell(sheet, i, 5).stringCellValue) // val milestonePayment = milestonePaymentRepository.findById(paymentMilestoneId).orElseThrow() val invoice = Invoice().apply { - invoiceNo = BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString() + invoiceNo = ExcelUtils.getCell(sheet, i, 0).toString() projectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue projectName = ExcelUtils.getCell(sheet, i, 2).stringCellValue team = ExcelUtils.getCell(sheet, i, 3).stringCellValue @@ -447,7 +447,7 @@ open class InvoiceService( val duplicateItemsInInvoice = checkDuplicateItemInImportedInvoice(sheet,2,0) for (i in 2..sheet.lastRowNum){ - val sheetInvoice = BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString() + val sheetInvoice = ExcelUtils.getCell(sheet, i, 0).toString() val sheetProjectCode = ExcelUtils.getCell(sheet, i, 1).stringCellValue checkInvoiceNo(sheetInvoice, invoices, invoicesResult, false) checkProjectCode(sheetProjectCode, projects, newProjectCodes) @@ -489,7 +489,7 @@ open class InvoiceService( } for (i in 2..sheet.lastRowNum){ - val invoice = getInvoiceByInvoiceNo(BigDecimal(ExcelUtils.getCell(sheet, i, 0).toString()).toInt().toString()) + val invoice = getInvoiceByInvoiceNo(ExcelUtils.getCell(sheet, i, 0).toString()) invoice.paidAmount = ExcelUtils.getCell(sheet, i, 5).numericCellValue.toBigDecimal() invoice.receiptDate = ExcelUtils.getCell(sheet, i, 4).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() saveAndFlush(invoice) 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 406c52e..d5c38a1 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 @@ -435,7 +435,7 @@ open class ReportService( val uninvoiceCell = row.createCell(12) uninvoiceCell.apply { cellFormula = - " IF(I${rowNum}-L${rowNum}<0, 0, I${rowNum}-L${rowNum})" + " IF(H${rowNum}-L${rowNum}<0, 0, H${rowNum}-L${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 } @@ -529,6 +529,11 @@ open class ReportService( CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val lastCpiCell = row.createCell(13) + lastCpiCell.apply { + cellFormula = "SUM(N15:N${rowNum})" + cellStyle = boldFontCellStyle + cellStyle.dataFormat = accountingStyle + } CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) @@ -607,7 +612,7 @@ open class ReportService( val row9: Row = sheet.getRow(rowNum) val cell5 = row9.createCell(2) cell5.apply { - cellFormula = "N${lastRowNum}" + cellFormula = "M${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -1801,26 +1806,22 @@ open class ReportService( open fun getProjectResourceOverconsumptionReport(args: Map): List> { val sql = StringBuilder("WITH teamNormalConsumed AS (" - + " SELECT " - + " s.teamId, " - + " pt.project_id, " - + " SUM(tns.totalConsumed) AS totalConsumed, " - + " sum(tns.totalBudget) as totalBudget " - + " FROM ( " - + " SELECT " - + " t.staffId, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " t.projectTaskId AS taskId, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " - + " FROM timesheet t " - + " LEFT JOIN staff s ON t.staffId = s.id " - + " left join salary sal on sal.salaryPoint = s.salaryId " - + " GROUP BY t.staffId, t.projectTaskId " - + " ) AS tns " - + " INNER JOIN project_task pt ON tns.taskId = pt.id " - + " JOIN staff s ON tns.staffId = s.id " - + " JOIN team t ON s.teamId = t.id " - + " GROUP BY teamId, project_id " + + " SELECT" + + " tns.projectId," + + " SUM(tns.totalConsumed) AS totalConsumed, " + + " sum(tns.totalBudget) as totalBudget " + + " FROM ( " + + " SELECT" + + " t.staffId," + + " t.projectId AS projectId," + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " + + " FROM timesheet t" + + " LEFT JOIN staff s ON t.staffId = s.id " + + " left join salary sal on sal.salaryPoint = s.salaryId " + + " GROUP BY t.staffId, t.projectId" + + " ) AS tns" + + " GROUP BY projectId" + " ) " + " SELECT " + " p.code, " From a5a5f3053f446129d208d4b8bbaa3db53bbe5a4c Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Fri, 28 Jun 2024 11:03:40 +0800 Subject: [PATCH 09/22] update api remove miscellaneous filter --- .../com/ffii/tsms/modules/data/service/DashboardService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 64c181a..36c86f0 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -105,7 +105,7 @@ open class DashboardService( + " ) milestonePayment on milestonePayment.pid = p.id" + " where p.customerId = :customerId" + " and p.customerSubsidiaryId = :subsidiaryId" - + " and (tg.name != '5. Miscellaneous' or tg.name is null)" +// + " and (tg.name != '5. Miscellaneous' or tg.name is null)" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" ) @@ -159,7 +159,7 @@ open class DashboardService( + " where p.customerId = :customerId" + " and isNull(p.customerSubsidiaryId)" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" - + " and (tg.name != '5. Miscellaneous' or tg.name is null)" +// + " and (tg.name != '5. Miscellaneous' or tg.name is null)" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) @@ -312,7 +312,7 @@ open class DashboardService( + " ) milestonePayment on milestonePayment.pid = p.id" + " where p.teamLead = :teamLeadId" + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" - + " and (tg.name != '5. Miscellaneous' or tg.name is null)" +// + " and (tg.name != '5. Miscellaneous' or tg.name is null)" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) From 8fc27f033a61ca0064428ccf00af16c6de6e857b Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Fri, 28 Jun 2024 11:55:48 +0800 Subject: [PATCH 10/22] update --- .../java/com/ffii/tsms/modules/data/service/DashboardService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 36c86f0..f29b702 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -1324,6 +1324,7 @@ open class DashboardService( + " LEFT JOIN timesheet ts" + " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id" + " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')" + + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" + " GROUP BY ds.month, st.teamId" + " ORDER BY ds.month" ) @@ -1381,6 +1382,7 @@ open class DashboardService( + " LEFT JOIN timesheet ts" + " ON DATE_FORMAT(ts.recordDate, '%m-%d') = DATE_FORMAT(ds.day, '%m-%d') and ts.staffID = st.id" + " WHERE ds.day BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" + + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" + " GROUP BY ds.day, st.teamId" + " ORDER BY ds.day" ) From 11c111767a7a0a0c917f844de0b956574dc6794c Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Fri, 28 Jun 2024 13:59:07 +0800 Subject: [PATCH 11/22] fix --- .../modules/data/service/DashboardService.kt | 136 ++++++++++++------ 1 file changed, 96 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index f29b702..47a7b4b 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -1307,26 +1307,54 @@ open class DashboardService( } fun monthlyActualTeamTotalManhoursSpent(args: Map): List> { val sql = StringBuilder( - " WITH RECURSIVE date_series AS (" - + " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month" - + " UNION ALL" - + " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')" - + " FROM date_series" - + " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')" - + " )" - + " SELECT" - + " ds.month AS yearMonth," - + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," - + " :teamId AS teamId" - + " FROM date_series ds" - + " LEFT JOIN staff st" - + " on st.teamId = :teamId" - + " LEFT JOIN timesheet ts" - + " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id" - + " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')" - + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" - + " GROUP BY ds.month, st.teamId" - + " ORDER BY ds.month" +// " WITH RECURSIVE date_series AS (" +// + " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month" +// + " UNION ALL" +// + " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')" +// + " FROM date_series" +// + " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')" +// + " )" +// + " SELECT" +// + " ds.month AS yearMonth," +// + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," +// + " :teamId AS teamId" +// + " FROM date_series ds" +// + " LEFT JOIN staff st" +// + " on st.teamId = :teamId" +// + " LEFT JOIN timesheet ts" +// + " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id" +// + " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')" +// + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')" +// + " GROUP BY ds.month, st.teamId" +// + " ORDER BY ds.month" + "WITH RECURSIVE date_series AS (" + + " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month" + + " UNION ALL" + + " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')" + + " FROM date_series" + + " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')" + + " )" + + " SELECT" + + " ds.month AS yearMonth," + + " COALESCE(SUM(COALESCE(ts.normalConsumed, 0) + COALESCE(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," + + " :teamId AS teamId" + + " FROM date_series ds" + + " LEFT JOIN (" + + " SELECT" + + " DATE_FORMAT(ts.recordDate, '%Y-%m-01') AS recordMonth," + + " ts.staffID," + + " SUM(ts.normalConsumed) AS normalConsumed," + + " SUM(ts.otConsumed) AS otConsumed" + + " FROM staff st" + + " LEFT JOIN timesheet ts" + + " on ts.staffID = st.id" + + " WHERE ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND LAST_DAY(DATE_FORMAT(:enddate, '%Y-%m-%d'))" + + " and st.teamId = :teamId" + + " GROUP BY DATE_FORMAT(ts.recordDate, '%Y-%m-01'), staffID" + + " ) ts ON ds.month = ts.recordMonth" + + " GROUP BY ds.month" + + " ORDER BY ds.month;" + ) return jdbcDao.queryForList(sql.toString(), args) @@ -1365,26 +1393,54 @@ open class DashboardService( } fun weeklyActualTeamTotalManhoursSpent(args: Map): List> { val sql = StringBuilder( - "WITH RECURSIVE date_series AS (" - + " SELECT DATE_FORMAT(:startdate, '%Y-%m-%d') AS day" - + " UNION ALL" - + " SELECT DATE_FORMAT(DATE_ADD(day, INTERVAL 1 DAY), '%Y-%m-%d')" - + " FROM date_series" - + " WHERE day < DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" - + " )" - + " SELECT" - + " ds.day AS yearMonth," - + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," - + " :teamId AS teamId" - + " FROM date_series ds" - + " LEFT JOIN staff st" - + " on st.teamId = :teamId" - + " LEFT JOIN timesheet ts" - + " ON DATE_FORMAT(ts.recordDate, '%m-%d') = DATE_FORMAT(ds.day, '%m-%d') and ts.staffID = st.id" - + " WHERE ds.day BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" - + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" - + " GROUP BY ds.day, st.teamId" - + " ORDER BY ds.day" +// "WITH RECURSIVE date_series AS (" +// + " SELECT DATE_FORMAT(:startdate, '%Y-%m-%d') AS day" +// + " UNION ALL" +// + " SELECT DATE_FORMAT(DATE_ADD(day, INTERVAL 1 DAY), '%Y-%m-%d')" +// + " FROM date_series" +// + " WHERE day < DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" +// + " )" +// + " SELECT" +// + " ds.day AS yearMonth," +// + " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," +// + " :teamId AS teamId" +// + " FROM date_series ds" +// + " LEFT JOIN staff st" +// + " on st.teamId = :teamId" +// + " LEFT JOIN timesheet ts" +// + " ON DATE_FORMAT(ts.recordDate, '%m-%d') = DATE_FORMAT(ds.day, '%m-%d') and ts.staffID = st.id" +// + " WHERE ds.day BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" +// + " and ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" +// + " GROUP BY ds.day, st.teamId" +// + " ORDER BY ds.day" + "WITH RECURSIVE date_series AS (" + + " SELECT DATE_FORMAT(:startdate, '%Y-%m-%d') AS day" + + " UNION ALL" + + " SELECT DATE_FORMAT(DATE_ADD(day, INTERVAL 1 DAY), '%Y-%m-%d')" + + " FROM date_series" + + " WHERE day < DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" + + " )" + + " SELECT" + + " ds.day AS yearMonth," + + " COALESCE(SUM(COALESCE(ts.normalConsumed, 0) + COALESCE(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed'," + + " :teamId AS teamId" + + " FROM date_series ds" + + " LEFT JOIN (" + + " SELECT" + + " DATE_FORMAT(ts.recordDate, '%Y-%m-%d') AS recordDate," + + " ts.staffID," + + " SUM(ts.normalConsumed) AS normalConsumed," + + " SUM(ts.otConsumed) AS otConsumed" + + " FROM staff st" + + " LEFT JOIN timesheet ts" + + " on ts.staffID = st.id" + + " WHERE ts.recordDate BETWEEN DATE_FORMAT(:startdate, '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(:startdate, INTERVAL 6 DAY), '%Y-%m-%d')" + + " and st.teamId = :teamId" + + " GROUP BY DATE_FORMAT(ts.recordDate, '%Y-%m-%d'), ts.staffID" + + " ) ts ON ds.day = ts.recordDate" + + " GROUP BY ds.day" + + " ORDER BY ds.day;" + ) return jdbcDao.queryForList(sql.toString(), args) From c8b596d64ab95a5ebeccb407bfb9b225cb64dcbf Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Fri, 28 Jun 2024 16:31:39 +0800 Subject: [PATCH 12/22] new add sub stage order fix --- .../java/com/ffii/tsms/modules/data/service/DashboardService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 47a7b4b..d5decd7 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -1300,7 +1300,7 @@ open class DashboardService( + " left join staff s on s.id = ts.staffId" + " where p.id = :projectId" + " group by p.id, t.id, t.name, g.name" - + " order by p.id;" + + " order by t.id asc, p.id;" ) return jdbcDao.queryForList(sql.toString(), args) From 40ec16b3467d8b3bf5a61ec7b94d108b01f44bf7 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 2 Jul 2024 17:52:31 +0800 Subject: [PATCH 13/22] update master data authority --- .../20240702_01_cyril/01_update_authority.sql | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/main/resources/db/changelog/changes/20240702_01_cyril/01_update_authority.sql diff --git a/src/main/resources/db/changelog/changes/20240702_01_cyril/01_update_authority.sql b/src/main/resources/db/changelog/changes/20240702_01_cyril/01_update_authority.sql new file mode 100644 index 0000000..393ffc0 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240702_01_cyril/01_update_authority.sql @@ -0,0 +1,81 @@ +-- liquibase formatted sql +-- changeset cyril:authority, user_authority + +UPDATE authority +SET name='Maintain User in Master Data' +WHERE id=1; +UPDATE authority +SET name='Maintain User Group in Master Data' +WHERE id=2; +UPDATE authority +SET name='View User in Master Data' +WHERE id=3; +UPDATE authority +SET name='View User Group in Master Data' +WHERE id=4; +DELETE FROM authority +WHERE id=5; +DELETE FROM authority +WHERE id=6; +INSERT INTO authority (authority,name) +VALUES ('VIEW_CLIENT','View Client in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_SUBSIDIARY','View Subsidiary in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_STAFF','View Staff in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_COMPANY','View Company in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_SKILL','View Skill in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_DEPARTMENT','View Department in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_POSITION','View Position in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_SALARY','View Salary in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_TEAM','View Team in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('VIEW_HOLIDAY','View Holiday in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_CLIENT','Maintain Client in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_SUBSIDIARY','Maintain Subsidiary in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_STAFF','Maintain Staff in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_COMPANY','Maintain Company in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_SKILL','Maintain Skill in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_DEPARTMENT','Maintain Department in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_POSITION','Maintain Position in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_SALARY','Maintain Salary in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_TEAM','Maintain Team in Master Data'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_HOLIDAY','Maintain Holiday in Master Data'); + +INSERT INTO `user_authority` VALUES +(1,21), +(1,22), +(1,23), +(1,24), +(1,25), +(1,26), +(1,27), +(1,28), +(1,29), +(1,30), +(1,31), +(1,32), +(1,33), +(1,34), +(1,35), +(1,36), +(1,37), +(1,38), +(1,39), +(1,40); \ No newline at end of file From 6fbff4fdabb29f88fa4c46f15946c90af2ee4230 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 2 Jul 2024 18:17:59 +0800 Subject: [PATCH 14/22] update project & user --- .../modules/project/service/ProjectsService.kt | 2 +- .../tsms/modules/user/service/UserService.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index 2748323..bc3fbd4 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -111,7 +111,7 @@ open class ProjectsService( code = project.code!!, name = project.name!!, status = project.status, - tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, + tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }.sortedBy { it.taskGroup?.id }, milestones = milestoneRepository.findAllByProject(project) .filter { milestone -> milestone.taskGroup?.id != null } .associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> diff --git a/src/main/java/com/ffii/tsms/modules/user/service/UserService.java b/src/main/java/com/ffii/tsms/modules/user/service/UserService.java index 089fb00..1e713d2 100644 --- a/src/main/java/com/ffii/tsms/modules/user/service/UserService.java +++ b/src/main/java/com/ffii/tsms/modules/user/service/UserService.java @@ -229,6 +229,13 @@ public class UserService extends AbstractBaseEntityService> authBatchDeleteValues = req.getRemoveAuthIds().stream() .map(authId -> Map.of("userId", (int)id, "authId", authId)) .collect(Collectors.toList()); + if (!authBatchDeleteValues.isEmpty()) { + jdbcDao.batchUpdate( + "DELETE FROM user_authority" + + " WHERE userId = :userId ", +// + "AND authId = :authId", + authBatchDeleteValues); + } if (!authBatchInsertValues.isEmpty()) { jdbcDao.batchUpdate( "INSERT IGNORE INTO user_authority (userId, authId)" @@ -236,13 +243,6 @@ public class UserService extends AbstractBaseEntityService Date: Wed, 3 Jul 2024 18:39:52 +0800 Subject: [PATCH 15/22] add cross team charge report --- .../modules/report/service/ReportService.kt | 528 +++++++++++++----- .../modules/report/web/ReportController.kt | 36 +- .../modules/report/web/model/ReportRequest.kt | 4 + .../timesheet/entity/TimesheetRepository.kt | 2 + .../report/Cross Team Charge Report.xlsx | Bin 0 -> 18610 bytes 5 files changed, 433 insertions(+), 137 deletions(-) create mode 100644 src/main/resources/templates/report/Cross Team Charge Report.xlsx 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 d5c38a1..de32af4 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 @@ -2,6 +2,7 @@ package com.ffii.tsms.modules.report.service import com.ffii.core.support.JdbcDao import com.ffii.tsms.modules.data.entity.Customer +import com.ffii.tsms.modules.data.entity.Grade import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team @@ -17,6 +18,8 @@ import org.apache.poi.ss.util.CellRangeAddress import org.apache.poi.ss.util.CellUtil import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.ss.usermodel.FormulaEvaluator +import org.apache.poi.ss.util.CellReference +import org.apache.poi.ss.util.RegionUtil import org.springframework.core.io.ClassPathResource import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream @@ -54,6 +57,7 @@ open class ReportService( private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE = "templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx" private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" + private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" // ==============================|| GENERATE REPORT ||============================== // fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex @@ -299,6 +303,25 @@ open class ReportService( return outputStream.toByteArray() } + @Throws(IOException::class) + fun generateCrossTeamChargeReport( + month: String, + timesheets: List, + teams: List, + grades: List, + ): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = + createCrossTeamChargeReport(month, timesheets, teams, grades, CROSS_TEAM_CHARGE_REPORT) + + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } + // ==============================|| CREATE REPORT ||============================== // // EX01 Financial Report @@ -985,10 +1008,15 @@ open class ReportService( project.milestones.forEach { milestone: Milestone -> val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0 - val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) + val resourceUtilization = + manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) // logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate)) // logger.info(daysUntilCurrentStageEnd) - if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0) { + if (ChronoUnit.DAYS.between( + LocalDate.now(), + milestone.endDate + ) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0 + ) { milestoneCount++ val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) rowIndex++ @@ -1010,7 +1038,7 @@ open class ReportService( // // val resourceUtilization = // manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) - setCellValue(resourceUtilization) + setCellValue(resourceUtilization) // } else { // setCellValue(0.0) // } @@ -1480,7 +1508,7 @@ open class ReportService( setDataAndConditionalFormatting(workbook, sheet, lateStartData, evaluator) // Automatically adjust column widths to fit content - autoSizeColumns(sheet) + autoSizeColumns(sheet) return workbook } @@ -1626,7 +1654,8 @@ open class ReportService( // NEW Column F: Subsidiary Name val subsidiaryNameCell = row.createCell(5) // subsidiaryNameCell.setCellValue(data["subsidiary_name"] as String) - val subsidiaryName = data["subsidiary_name"] as? String ?: "N/A" // Checks if subsidiary_name is null and replaces it with "N/A" + val subsidiaryName = + data["subsidiary_name"] as? String ?: "N/A" // Checks if subsidiary_name is null and replaces it with "N/A" subsidiaryNameCell.setCellValue(subsidiaryName) // Column G: Project Plan Start Date @@ -1747,112 +1776,118 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getProjectCompletionReport(args: Map): List> { - val sql = StringBuilder("select" - + " result.code, " - + " result.name, " - + " result.teamCode, " - + " result.custCode, " - + " result.subCode, " ) + val sql = StringBuilder( + "select" + + " result.code, " + + " result.name, " + + " result.teamCode, " + + " result.custCode, " + + " result.subCode, " + ) if (args.get("outstanding") as Boolean) { sql.append(" result.projectFee - COALESCE(i.paidAmount, 0) as `Receivable Remained`, ") // sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ") } sql.append( " DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd " - + " from ( " - + " SELECT " - + " pt.project_id, " - + " min(p.code) as code, " - + " min(p.name) as name, " - + " min(t.code) as teamCode, " - + " concat(min(c.code), ' - ', min(c.name)) as custCode, " - + " COALESCE(concat(min(ss.code),' - ',min(ss.name)), 'N/A') as subCode, " - + " min(p.actualEnd) as actualEnd, " - + " min(p.expectedTotalFee) as projectFee, " - + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " - + " FROM ( " - + " SELECT " - + " t.staffId, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " t.projectTaskId AS taskId " - + " FROM timesheet t " - + " LEFT JOIN staff s ON t.staffId = s.id " - + " LEFT JOIN team te on s.teamId = te.id " - + " GROUP BY t.staffId, t.projectTaskId " - + " order by t.staffId " - + " ) AS tns " - + " right join project_task pt ON tns.taskId = pt.id " - + " left join project p on p.id = pt.project_id " - + " left JOIN staff s ON p.teamLead = s.id " - + " left join salary sal on s.salaryId = sal.salaryPoint " - + " left JOIN team t ON s.teamId = t.id " - + " left join customer c on c.id = p.customerId " - + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " - + " where p.deleted = false ") - if (args.containsKey("teamId")) { - sql.append("t.id = :teamId") - } - sql.append( + + " from ( " + + " SELECT " + + " pt.project_id, " + + " min(p.code) as code, " + + " min(p.name) as name, " + + " min(t.code) as teamCode, " + + " concat(min(c.code), ' - ', min(c.name)) as custCode, " + + " COALESCE(concat(min(ss.code),' - ',min(ss.name)), 'N/A') as subCode, " + + " min(p.actualEnd) as actualEnd, " + + " min(p.expectedTotalFee) as projectFee, " + + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " + + " FROM ( " + + " SELECT " + + " t.staffId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + + " t.projectTaskId AS taskId " + + " FROM timesheet t " + + " LEFT JOIN staff s ON t.staffId = s.id " + + " LEFT JOIN team te on s.teamId = te.id " + + " GROUP BY t.staffId, t.projectTaskId " + + " order by t.staffId " + + " ) AS tns " + + " right join project_task pt ON tns.taskId = pt.id " + + " left join project p on p.id = pt.project_id " + + " left JOIN staff s ON p.teamLead = s.id " + + " left join salary sal on s.salaryId = sal.salaryPoint " + + " left JOIN team t ON s.teamId = t.id " + + " left join customer c on c.id = p.customerId " + + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " + + " where p.deleted = false " + ) + if (args.containsKey("teamId")) { + sql.append("t.id = :teamId") + } + sql.append( " and p.status = 'Completed' " - + " and p.actualEnd BETWEEN :startDate and :endDate " - + " group by pt.project_id " - + " ) as result " - + " left join invoice i on result.code = i.projectCode " - + " order by result.actualEnd ") + + " and p.actualEnd BETWEEN :startDate and :endDate " + + " group by pt.project_id " + + " ) as result " + + " left join invoice i on result.code = i.projectCode " + + " order by result.actualEnd " + ) return jdbcDao.queryForList(sql.toString(), args) } open fun getProjectResourceOverconsumptionReport(args: Map): List> { - val sql = StringBuilder("WITH teamNormalConsumed AS (" - + " SELECT" - + " tns.projectId," - + " SUM(tns.totalConsumed) AS totalConsumed, " - + " sum(tns.totalBudget) as totalBudget " - + " FROM ( " - + " SELECT" - + " t.staffId," - + " t.projectId AS projectId," - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " - + " FROM timesheet t" - + " LEFT JOIN staff s ON t.staffId = s.id " - + " left join salary sal on sal.salaryPoint = s.salaryId " - + " GROUP BY t.staffId, t.projectId" - + " ) AS tns" - + " GROUP BY projectId" - + " ) " - + " SELECT " - + " p.code, " - + " p.name, " - + " t.code as team, " - + " concat(c.code, ' - ', c.name) as client, " - + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " - + " p.expectedTotalFee * 0.8 as plannedBudget, " - + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " - + " COALESCE(p.totalManhour, 0) as plannedManhour, " - + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " - + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " - + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " - + " CASE " - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " - + " then 'Overconsumption' " - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " - + " then 'Potential Overconsumption' " - + " else 'Within Budget' " - + " END as status " - + " FROM project p " - + " LEFT JOIN team t ON p.teamLead = t.teamLead " - + " LEFT JOIN staff s ON p.teamLead = s.id " - + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " - + " LEFT JOIN customer c ON p.customerId = c.id " - + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " - + " left join teamNormalConsumed tns on tns.project_id = p.id " - + " WHERE p.deleted = false " - + " and p.status = 'On-going' " + val sql = StringBuilder( + "WITH teamNormalConsumed AS (" + + " SELECT" + + " tns.projectId," + + " SUM(tns.totalConsumed) AS totalConsumed, " + + " sum(tns.totalBudget) as totalBudget " + + " FROM ( " + + " SELECT" + + " t.staffId," + + " t.projectId AS projectId," + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " + + " FROM timesheet t" + + " LEFT JOIN staff s ON t.staffId = s.id " + + " left join salary sal on sal.salaryPoint = s.salaryId " + + " GROUP BY t.staffId, t.projectId" + + " ) AS tns" + + " GROUP BY projectId" + + " ) " + + " SELECT " + + " p.code, " + + " p.name, " + + " t.code as team, " + + " concat(c.code, ' - ', c.name) as client, " + + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " + + " p.expectedTotalFee * 0.8 as plannedBudget, " + + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " + + " COALESCE(p.totalManhour, 0) as plannedManhour, " + + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " + + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " + + " CASE " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " + + " then 'Overconsumption' " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " + + " then 'Potential Overconsumption' " + + " else 'Within Budget' " + + " END as status " + + " FROM project p " + + " LEFT JOIN team t ON p.teamLead = t.teamLead " + + " LEFT JOIN staff s ON p.teamLead = s.id " + + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " + + " LEFT JOIN customer c ON p.customerId = c.id " + + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " + + " left join teamNormalConsumed tns on tns.project_id = p.id " + + " WHERE p.deleted = false " + + " and p.status = 'On-going' " ) if (args != null) { var statusFilter: String = "" @@ -1869,6 +1904,7 @@ open class ReportService( " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " + " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " + "All" -> " and " + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit " + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " @@ -2170,9 +2206,9 @@ open class ReportService( rowNum = 6 val row6: Row = sheet.getRow(rowNum) val row6Cell = row6.getCell(1) - val clientSubsidiary = if(info.getValue("subsidiary").toString() != "N/A"){ + val clientSubsidiary = if (info.getValue("subsidiary").toString() != "N/A") { info.getValue("subsidiary").toString() - }else{ + } else { info.getValue("client").toString() } row6Cell.setCellValue(clientSubsidiary) @@ -2367,10 +2403,10 @@ open class ReportService( return workbook } - fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List>{ + fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List> { val sql = StringBuilder( " with cte_timesheet as ( " - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + " t.recordDate" + " from timesheet t" + " left join project_task pt on pt.id = t.projectTaskId" @@ -2394,20 +2430,20 @@ open class ReportService( + " left join team t2 on t2.id = s.teamId" + " where ISNULL(p.code) = False" ) - if(clientId != null){ - if(type == "client"){ + if (clientId != null) { + if (type == "client") { sql.append( " and c.id = :clientId " ) } - if(type == "subsidiary"){ + if (type == "subsidiary") { sql.append( " and s2.id = :clientId " ) } } - if(teamId != null){ + if (teamId != null) { sql.append( " and p.teamLead = :teamId " ) @@ -2426,9 +2462,9 @@ open class ReportService( val queryList = jdbcDao.queryForList(sql.toString(), args) val costAndExpenseList = mutableListOf>() - for(item in queryList){ + for (item in queryList) { val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(item["code"] !in costAndExpenseList.map { it["code"] }){ + if (item["code"] !in costAndExpenseList.map { it["code"] }) { costAndExpenseList.add( mapOf( "code" to item["code"], @@ -2437,22 +2473,27 @@ open class ReportService( "subsidiary" to item["subsidiary"], "teamLead" to item["teamLead"], "budget" to item["expectedTotalFee"], - "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, - "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double ) - + (hourlyRate * item["otConsumed"] as Double * otFactor) + "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, + "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double) + + (hourlyRate * item["otConsumed"] as Double * otFactor) ) ) - }else{ + } else { val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { - put("totalManhours", get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)) - put("manhourExpenditure", get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double ) - + (hourlyRate * item["otConsumed"]as Double * otFactor))) + put( + "totalManhours", + get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double) + ) + put( + "manhourExpenditure", + get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double) + + (hourlyRate * item["otConsumed"] as Double * otFactor)) + ) } } } - val result = costAndExpenseList.map { - item -> + val result = costAndExpenseList.map { item -> val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) val remainingPercent = (budgetRemain / budget) @@ -2471,7 +2512,7 @@ open class ReportService( clientId: Long?, budgetPercentage: Double?, type: String - ): Workbook{ + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -2489,9 +2530,9 @@ open class ReportService( rowNum = 2 val row2: Row = sheet.getRow(rowNum) val row2Cell = row2.getCell(2) - if(teamId == null){ + if (teamId == null) { row2Cell.setCellValue("All") - }else{ + } else { val sql = StringBuilder( " select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId " ) @@ -2502,16 +2543,16 @@ open class ReportService( rowNum = 3 val row3: Row = sheet.getRow(rowNum) val row3Cell = row3.getCell(2) - if(clientId == null){ + if (clientId == null) { row3Cell.setCellValue("All") - }else{ - val sql= StringBuilder() - if(type == "client"){ + } else { + val sql = StringBuilder() + if (type == "client") { sql.append( " select c.id, c.name from customer c where c.id = :clientId " ) } - if(type == "subsidiary"){ + if (type == "subsidiary") { sql.append( " select s.id, s.name from subsidiary s where s.id = :clientId " ) @@ -2522,15 +2563,15 @@ open class ReportService( val filterList: List> - if(budgetPercentage != null){ + if (budgetPercentage != null) { filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) <= budgetPercentage } - }else{ + } else { filterList = costAndExpenseList } rowNum = 6 - for(item in filterList){ + for (item in filterList) { val index = filterList.indexOf(item) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val cell = row.getCell(0) ?: row.createCell(0) @@ -2580,13 +2621,13 @@ open class ReportService( val cell8 = row.getCell(8) ?: row.createCell(8) cell8.apply { - cellFormula = "G${rowNum+1}-H${rowNum+1}" + cellFormula = "G${rowNum + 1}-H${rowNum + 1}" } CellUtil.setCellStyleProperty(cell8, "dataFormat", accountingStyle) val cell9 = row.getCell(9) ?: row.createCell(9) cell9.apply { - cellFormula = "I${rowNum+1}/G${rowNum+1}" + cellFormula = "I${rowNum + 1}/G${rowNum + 1}" } CellUtil.setCellStyleProperty(cell9, "dataFormat", percentStyle) @@ -2596,11 +2637,18 @@ open class ReportService( return workbook } - fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{ + fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray { val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId, request.type) - val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.teamId, request.clientId, request.budgetPercentage, request.type) + val workbook: Workbook = createCostAndExpenseWorkbook( + COSTANDEXPENSE_REPORT, + costAndExpenseList, + request.teamId, + request.clientId, + request.budgetPercentage, + request.type + ) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) @@ -2608,8 +2656,16 @@ open class ReportService( return outputStream.toByteArray() } - open fun getLateStartDetails(teamId: Long?, clientId: Long?, remainedDate: LocalDate, remainedDateTo: LocalDate, type: String?): List> { - val sql = StringBuilder(""" + + open fun getLateStartDetails( + teamId: Long?, + clientId: Long?, + remainedDate: LocalDate, + remainedDateTo: LocalDate, + type: String? + ): List> { + val sql = StringBuilder( + """ SELECT p.code AS project_code, p.name AS project_name, @@ -2630,7 +2686,8 @@ open class ReportService( p.status = 'Pending to Start' AND p.planStart < CURRENT_DATE AND m.endDate BETWEEN :remainedDate AND :remainedDateTo - """.trimIndent()) + """.trimIndent() + ) if (teamId != null && teamId > 0) { sql.append(" AND t.id = :teamId") @@ -2654,4 +2711,203 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } -} + + @Throws(IOException::class) + private fun createCrossTeamChargeReport( + month: String, + timesheets: List, + teams: List, + grades: List, + templatePath: String, + ): Workbook { + // please create a new function for each report template + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val sheet: Sheet = workbook.getSheetAt(0) + + // accounting style + comma style + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + + // bold font with border style + val boldFont = workbook.createFont().apply { + bold = true + fontName = "Times New Roman" + } + + val boldFontWithBorderStyle = workbook.createCellStyle().apply { + setFont(boldFont) + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + // normal font + val normalFont = workbook.createFont().apply { + fontName = "Times New Roman" + } + + val normalFontWithBorderStyle = workbook.createCellStyle().apply { + setFont(normalFont) + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 2 + val monthFormat = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH) + val reportMonth = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")) + val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month).format(monthFormat) + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(convertReportMonth) + } + + if (timesheets.isNotEmpty()) { + val combinedTeamCodeColNumber = grades.size + val sortedGrades = grades.sortedBy { it.id } + val sortedTeams = teams.sortedBy { it.id } + + val groupedTimesheets = timesheets + .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } + .groupBy { timesheetEntry -> + Triple( + timesheetEntry.project?.teamLead?.team?.id, + timesheetEntry.staff?.team?.id, + timesheetEntry.staff?.grade?.id + ) + } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + } else if (timesheet.otConsumed != null) { + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + } else { + 0.0 + } + } + } + + if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) { + rowIndex = 3 + sortedTeams.forEach { team: Team -> + + // Team + sheet.createRow(rowIndex++).apply { + createCell(0).apply { + setCellValue("Team to be charged:") + cellStyle = boldFontWithBorderStyle.apply { + alignment = HorizontalAlignment.LEFT + } + } + + val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 1 + combinedTeamCodeColNumber) + sheet.addMergedRegion(rangeAddress) + RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet) + + createCell(1).apply { + setCellValue(team.code) + cellStyle = normalFontWithBorderStyle.apply { + alignment = HorizontalAlignment.CENTER + } + } + + } + + // Grades + sheet.createRow(rowIndex++).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle + } + + + sortedGrades.forEach { grade: Grade -> + createCell(columnIndex++).apply { + setCellValue(grade.name) + cellStyle = boldFontWithBorderStyle.apply { + alignment = HorizontalAlignment.CENTER + } + } + } + + createCell(columnIndex).apply { + setCellValue("Total Manhour by Team") + cellStyle = boldFontWithBorderStyle + } + } + + // Team + Manhour + val startRow = rowIndex + var endRow = rowIndex + sortedTeams.forEach { chargedTeam: Team -> + if (team.id != chargedTeam.id) { + endRow++ + sheet.createRow(rowIndex++).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue(chargedTeam.code) + cellStyle = normalFontWithBorderStyle + } + + sortedGrades.forEach { grade: Grade -> + createCell(columnIndex++).apply { + setCellValue( + groupedTimesheets[Triple(team.id, chargedTeam.id, grade.id)]?.sum() ?: 0.0 + ) + cellStyle = normalFontWithBorderStyle.apply { + dataFormat = accountingStyle + } + } + } + + createCell(columnIndex).apply { + val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1) + cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})" + cellStyle = boldFontWithBorderStyle.apply { + dataFormat = accountingStyle + } + } + } + } + } + + // Total Manhour by grade + sheet.createRow(rowIndex).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("Total Manhour by Grade") + cellStyle = boldFontWithBorderStyle + } + + sortedGrades.forEach { grade: Grade -> + createCell(columnIndex++).apply { + val currentCellLetter = CellReference.convertNumToColString(this.columnIndex) + cellFormula = "sum(${currentCellLetter}${startRow}:${currentCellLetter}${endRow})" + cellStyle = normalFontWithBorderStyle.apply { + dataFormat = accountingStyle + } + } + } + + createCell(columnIndex).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle + } + } + rowIndex += 2 + } + } + } + + return workbook + } +} \ No newline at end of file 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 1c312f0..fd6b347 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 @@ -38,6 +38,8 @@ import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory import org.springframework.data.domain.Example import org.springframework.data.domain.ExampleMatcher +import java.time.YearMonth +import java.util.* @RestController @RequestMapping("/reports") @@ -57,7 +59,8 @@ class ReportController( private val customerService: CustomerService, private val subsidiaryService: SubsidiaryService, private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository, - private val subsidiaryRepository: SubsidiaryRepository + private val subsidiaryRepository: SubsidiaryRepository, private val staffAllocationRepository: StaffAllocationRepository, + private val gradeRepository: GradeRepository ) { private val logger: Log = LogFactory.getLog(javaClass) @@ -304,4 +307,35 @@ class ReportController( .body(ByteArrayResource(reportResult)) } + @PostMapping("/CrossTeamChargeReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getCrossTeamChargeReport(@RequestBody @Valid request: CrossTeamChargeReportRequest): ResponseEntity { + + val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build() + + if (authorities.stream().anyMatch { it.authority.equals("GENERATE_REPORTS") }) { + val crossProjects = staffAllocationRepository.findAll() + .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } + .map { it.project!! } + .distinct() + + val reportMonth = YearMonth.parse(request.month, DateTimeFormatter.ofPattern("yyyy-MM")) + val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month) + val startDate = convertReportMonth.atDay(1) + val endDate = convertReportMonth.atEndOfMonth() + val timesheets = timesheetRepository.findAllByProjectIn(crossProjects) + .filter { it.recordDate != null && + (it.recordDate!!.isEqual(startDate) || it.recordDate!!.isEqual(endDate) || (it.recordDate!!.isAfter(startDate) && it.recordDate!!.isBefore(endDate)))} + val teams = teamRepository.findAll().filter { it.deleted == false } + val grades = gradeRepository.findAll().filter { it.deleted == false } + + val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades) + return ResponseEntity.ok() + .header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + + } else { + return ResponseEntity.noContent().build() + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index 1233753..1c8b509 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -57,4 +57,8 @@ data class ProjectCompletionReport ( val startDate: LocalDate, val endDate: LocalDate, val outstanding: Boolean +) + +data class CrossTeamChargeReportRequest ( + val month: String, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt index 3defcf1..2826b9f 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt @@ -14,6 +14,8 @@ interface TimesheetRepository : AbstractRepository { fun findAllByProjectTaskIn(projectTasks: List): List + fun findAllByProjectIn(project: List): List + fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate) @Query("SELECT new com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours(IFNULL(SUM(normalConsumed), 0), IFNULL(SUM(otConsumed), 0)) FROM Timesheet t JOIN ProjectTask pt on t.projectTask = pt WHERE pt.project = ?1") diff --git a/src/main/resources/templates/report/Cross Team Charge Report.xlsx b/src/main/resources/templates/report/Cross Team Charge Report.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..26e201fe936564e94d206545a128e17f0ac0eab3 GIT binary patch literal 18610 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 a2!65>pzq%LGucG|kOg_)+(hW-yZ;00kHZN7 literal 0 HcmV?d00001 From 2c4e77c7d7cd4088f4a2f5a87b3e50e6b8f23ad9 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 4 Jul 2024 14:01:47 +0800 Subject: [PATCH 16/22] update cross team charge report --- .../modules/report/service/ReportService.kt | 247 ++++++++++-------- .../report/Cross Team Charge Report.xlsx | Bin 18610 -> 18521 bytes 2 files changed, 142 insertions(+), 105 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 de32af4..6c492fd 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 @@ -2766,147 +2766,184 @@ open class ReportService( setCellValue(convertReportMonth) } - if (timesheets.isNotEmpty()) { - val combinedTeamCodeColNumber = grades.size - val sortedGrades = grades.sortedBy { it.id } - val sortedTeams = teams.sortedBy { it.id } - - val groupedTimesheets = timesheets - .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } - .groupBy { timesheetEntry -> - Triple( - timesheetEntry.project?.teamLead?.team?.id, - timesheetEntry.staff?.team?.id, - timesheetEntry.staff?.grade?.id - ) - } - .mapValues { (_, timesheetEntries) -> - timesheetEntries.map { timesheet -> - if (timesheet.normalConsumed != null) { - timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) - } else if (timesheet.otConsumed != null) { - timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) - } else { - 0.0 +// if (timesheets.isNotEmpty()) { + val combinedTeamCodeColNumber = grades.size + val sortedGrades = grades.sortedBy { it.id } + val sortedTeams = teams.sortedBy { it.id } + + val groupedTimesheets = timesheets + .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } + .groupBy { timesheetEntry -> + Triple( + timesheetEntry.project?.teamLead?.team?.id, + timesheetEntry.staff?.team?.id, + timesheetEntry.staff?.grade?.id + ) + } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + mutableMapOf().apply { + this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) + } + } else if (timesheet.otConsumed != null) { + mutableMapOf().apply { + this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) + } + } else { + mutableMapOf().apply { + this["manHour"] = 0.0 + this["salary"] = 0.0 } } } + } - if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) { - rowIndex = 3 - sortedTeams.forEach { team: Team -> + if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) { + rowIndex = 3 + sortedTeams.forEach { team: Team -> - // Team - sheet.createRow(rowIndex++).apply { - createCell(0).apply { - setCellValue("Team to be charged:") - cellStyle = boldFontWithBorderStyle.apply { - alignment = HorizontalAlignment.LEFT - } - } + // Team + sheet.createRow(rowIndex++).apply { + createCell(0).apply { + setCellValue("Team to be charged:") + cellStyle = boldFontWithBorderStyle + CellUtil.setAlignment(this, HorizontalAlignment.LEFT) + } - val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 1 + combinedTeamCodeColNumber) - sheet.addMergedRegion(rangeAddress) - RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet) - RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet) - RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet) - RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet) + val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 2 + combinedTeamCodeColNumber) + sheet.addMergedRegion(rangeAddress) + RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet) - createCell(1).apply { - setCellValue(team.code) - cellStyle = normalFontWithBorderStyle.apply { - alignment = HorizontalAlignment.CENTER - } - } + createCell(1).apply { + setCellValue(team.code) + cellStyle = normalFontWithBorderStyle + CellUtil.setAlignment(this, HorizontalAlignment.CENTER) + } + + } + // Grades + sheet.createRow(rowIndex++).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle } - // Grades - sheet.createRow(rowIndex++).apply { - columnIndex = 0 + + sortedGrades.forEach { grade: Grade -> createCell(columnIndex++).apply { - setCellValue("") + setCellValue(grade.name) cellStyle = boldFontWithBorderStyle + CellUtil.setAlignment(this, HorizontalAlignment.CENTER) } + } + createCell(columnIndex++).apply { + setCellValue("Total Manhour by Team") + cellStyle = boldFontWithBorderStyle + } - sortedGrades.forEach { grade: Grade -> + createCell(columnIndex).apply { + setCellValue("Total Cost Adjusted by Salary Point by Team") + cellStyle = boldFontWithBorderStyle + } + } + + // Team + Manhour + val startRow = rowIndex + var endRow = rowIndex + sortedTeams.forEach { chargedTeam: Team -> + if (team.id != chargedTeam.id) { + endRow++ + sheet.createRow(rowIndex++).apply { + columnIndex = 0 createCell(columnIndex++).apply { - setCellValue(grade.name) - cellStyle = boldFontWithBorderStyle.apply { - alignment = HorizontalAlignment.CENTER - } + setCellValue(chargedTeam.code) + cellStyle = normalFontWithBorderStyle + CellUtil.setAlignment(this, HorizontalAlignment.CENTER) } - } - createCell(columnIndex).apply { - setCellValue("Total Manhour by Team") - cellStyle = boldFontWithBorderStyle - } - } - - // Team + Manhour - val startRow = rowIndex - var endRow = rowIndex - sortedTeams.forEach { chargedTeam: Team -> - if (team.id != chargedTeam.id) { - endRow++ - sheet.createRow(rowIndex++).apply { - columnIndex = 0 + var totalSalary = 0.0 + sortedGrades.forEach { grade: Grade -> createCell(columnIndex++).apply { - setCellValue(chargedTeam.code) - cellStyle = normalFontWithBorderStyle + setCellValue( + groupedTimesheets[Triple( + team.id, + chargedTeam.id, + grade.id + )]?.sumOf { it.getValue("manHour") } ?: 0.0) + + totalSalary += groupedTimesheets[Triple( + team.id, + chargedTeam.id, + grade.id + )]?.sumOf { it.getValue("salary") } ?: 0.0 + + cellStyle = normalFontWithBorderStyle.apply { + dataFormat = accountingStyle + } } + } - sortedGrades.forEach { grade: Grade -> - createCell(columnIndex++).apply { - setCellValue( - groupedTimesheets[Triple(team.id, chargedTeam.id, grade.id)]?.sum() ?: 0.0 - ) - cellStyle = normalFontWithBorderStyle.apply { - dataFormat = accountingStyle - } - } + createCell(columnIndex++).apply { + val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1) + cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})" + cellStyle = boldFontWithBorderStyle.apply { + dataFormat = accountingStyle } + } - createCell(columnIndex).apply { - val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1) - cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})" - cellStyle = boldFontWithBorderStyle.apply { - dataFormat = accountingStyle - } + createCell(columnIndex).apply { + setCellValue(totalSalary) + cellStyle = boldFontWithBorderStyle.apply { + dataFormat = accountingStyle } } } } + } - // Total Manhour by grade - sheet.createRow(rowIndex).apply { - columnIndex = 0 - createCell(columnIndex++).apply { - setCellValue("Total Manhour by Grade") - cellStyle = boldFontWithBorderStyle - } + // Total Manhour by grade + sheet.createRow(rowIndex).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("Total Manhour by Grade") + cellStyle = boldFontWithBorderStyle + } - sortedGrades.forEach { grade: Grade -> - createCell(columnIndex++).apply { - val currentCellLetter = CellReference.convertNumToColString(this.columnIndex) - cellFormula = "sum(${currentCellLetter}${startRow}:${currentCellLetter}${endRow})" - cellStyle = normalFontWithBorderStyle.apply { - dataFormat = accountingStyle - } + sortedGrades.forEach { grade: Grade -> + createCell(columnIndex++).apply { + val currentCellLetter = CellReference.convertNumToColString(this.columnIndex) + cellFormula = "sum(${currentCellLetter}${startRow}:${currentCellLetter}${endRow})" + cellStyle = normalFontWithBorderStyle.apply { + dataFormat = accountingStyle } } + } - createCell(columnIndex).apply { - setCellValue("") - cellStyle = boldFontWithBorderStyle - } + createCell(columnIndex++).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle + } + + createCell(columnIndex).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle } - rowIndex += 2 } + rowIndex += 2 } } +// } return workbook } diff --git a/src/main/resources/templates/report/Cross Team Charge Report.xlsx b/src/main/resources/templates/report/Cross Team Charge Report.xlsx index 26e201fe936564e94d206545a128e17f0ac0eab3..e3c005a23f087aa5389b235725ec40af2280a760 100644 GIT binary patch delta 5760 zcmZ8lbx;)E{#{BMmTm-gSyDi{dzVI60qF)oVktp#Y3W)@KsuBT0YPd(=~C%hN?=I= z0YT#DdvE4_=lAX(ckayRGiPp`J7>;mOvRZ`!KvTBha4y!^~%Eo02qk@07?J=@XA%# z58>rvi$J&ty>fG{Hnc#jOVb8hIp0NOR4l(1$8g4KYec5J)pm#sb@Ssc9R`;h>#bKG z&`vtt{;?#){lJzuHx9ENuo~E22|OG-t8gE|dm`m25A0L`JTi#Ys!2UvT4Y2Ls3CyqcPG z_vjuB-dMI1&7LusS8CER^Vg@8CRhcF%_SQ#;*N5yC8Aj4TgfO?FX|um-E8RCzWTJQhXNZN|Le)@u6(u69b3ShuzI z)$pzyJJZD%Vd(#uOdaG~M2WUyYV0;TF}-{r_XLhB#p79oXg<`KQfyh(-e zV8}0L^1KL z$5pz!)tH`TJo9JQW=Njuyq0WDFG2tvKlTK@h9nrB>}PxXj=B?keK_cmswe%#f@OYE z^x*LUhnYW*{bEERpYe;ft-|NX=vwzA)<=F3lQVd;4QEBUbD%mYZCFEvZH*%T*E_iB zVn2BAo*nK6)+xU1>oYZ;#Y# zZR^)#Z{pVg?aPk29e(mMVY;|H-@sIYjgW}K0luc+L zXwTTJ0oGdlS-5xo6gw-7YLco%GQQq#LdG&sjm+ zMJ9Bmh?azB4j5XU?RuOkA~_)9W1%BdD{jU2wf({8m}r?vL{?*yg5I&6N8FnVL#b8M zm!a7su=Xs#7tQ12gW~DvF_xV`HQo&;yH!-L)Ls_x1*WDHEVZz^098(E&B3%P#c4d; z2>kZvHn|tb%yKMvbC3No66>6%+4a1P(Abx&dakd-!m%UGFxILmD4RPAD#|-Zz%V{XFm*<2JD1mE=k=es6AQ z!aJQHm42!B_o?n(r`x(=%s~TN7sXUd#3||`S0&Bu+tpNU~}66q;TU@AglwD)~z2tT~Za6{3Bvx~~B!KaWfnPg(CJZ)bP1g8vj-IRk}MU zn>3Zv{D#X^zi)ewmTOM7k5wlU~>3VEA?vBNnF6eJ;{4eN&If# zY)W3mx16eXe}&@iKa+Wi8A*f)5ZX%Zh{I;*=8PNxTjwgaP^tf{qNCYA*NaJ(K9yjl z80}Q6bumL!x8_7+tjI{fM6-)^q>Bf*#iu00>mD74_rxXOwIwx$xK!c{JMSEz(l2@) zjSl0(#a?+>OTY5^xxPAv=D}mkseUY)AH*Tu?>$2X0J zIYP{H7xd553Czd~@Vl^G3EjviS|sHe@MiKb)&ubZ_wJaum_|IT&)sty4|?ZY{v|;2 zyG>CoX{Gu!kYFbeMj?Wn6;iN_uT<|o4$}0HWG3H|;k!3KmbF%xJs=e!&$fOkI8hgG za!shmv%*6eAdB5eQ}X(=q_9&+f3Bs_L;jOLt{k7rWbZ&A#|j&X?9IcCBT zV6y#Y(ZXe$pc3-CU9ts$(7ipmr^4w0p4bZxNnhDleQ%ncDRUw_PFz-$uh_;{t*?JI zFP*8x2l~|Q1y7qHf!3sSb=dZX*~&{!{!jdHs`ZbWuVog5gP-Qg31te9R-U9tOFktZ ze2q~i@zY1=`|NEv5^6=;?>|N|K_DL_3*cj_~nSg=)ykKzrkR0xm ziH98`3U$Is4tcci3=CxMx5-@7@t?Gq0Np{C>nl{2F5>gf_pXSCOZEs^rnkf)dg%j8WpWoXE@Wlp8lcBV=m_h?RmOzkjs z0Jbiq@dFGIb^o|1{}^QAFqOwY7Rr4JVPYQ^&;BA3??68w=;Efb7lwfvC%5(nXe-u! z#dRL)9@2Ca)=b)bP6OrCHJ!_u#v!#>~%k4PdNy5M&{m!TN`ZaIo-Rn6ZWL+T~l zr~g{(`IgSfGcOXqB|fNiS|VfNb7-w# zT+XcPv!~>S6D1Z$ClfK0(r^?1k_O z(096DJBvH1>KEADvNB^a6^ZCVfQM+F23W7B?AynAC$M z`#3&jT%msYz#*-dLuQ7hA%unwSuWHRt;O3Lom?B0g!N`qsp4nfHtKCnKcWihC2(|5 zGxVCs=82^dkz!~RPVx3F(WeC+>xyJ55Xb5g*~e1nxLn&M>F{)^)7XcL)uuAZ6E*|w zTl0fddY_|a{ok7tp4P}kfGb&~iU!-9yf(HVeR`R>tWq_Dk#&J096$c#Cr0k}E2yJp zK5~=0Eu}?Ex@+|@xAGPz%EeCgKZs*a40JL_ra&XVyHoW}PJRqFED|1@)0t_o77pHk zcN(3wZ`R2E5z!UlZ5F;_H}3uVEMKL(#~NaotE%_S^(g#h@wUa78UH5_C%y_eSN zStXKyVhIv^9Zr@JaOp@QL!Ijb#e580|Wl~m&xPR%^2Y8ms3CW8ah#`Iq-=a=g*(zk3!nr{Q(6+{1e$I9? zs>vIf@WjeYzr+(=bLaoQ0Blpe`H-S6frO->6dCl;k&K_r;f%Haeqbo1L*z0E5z}fl zEfsD5`|LK6xL$O5Z~j>W8{9Tx>L8I35j*i812Ml)zO3r}Jh&SpdVdi$R@X zr;vXY|HIzq_1`I`Y&Ac)SsxgcjOL+rz0{QC{+>>X3jV{WB1xihw$d`IgB+>L_-80F zA6}Sq0D^>)hf8{AG4y)zY;5zc@<6Lgoi~MGPMi9t%o2_N0*2}m-S?YM95v-?{3YJS zrlDGm%?n8&>V91AtzB5@2k(TPnNEK?$Tb>yQO0vBqLWPa#!g+GjwX$WA#^X>B&$pi zmhtF|eD9e}3~|Le@F5Pr>hNEt6*4{6B|v7LWm%ih<0Fcm{ zaYNc0Z(2n`G2O|olW{)PlIi!D%;|=I@ypGo*V#)B^Sg+Hod%M};dd$bK&^UfVEb)C z0a_OrzXAa#cGMXh6$d2lHqYr?7@ovs16P87Z%0w5y4R#Q-uiQ9Bg_rB^+56p{9%=L zf4eIE7Nu%3>AtJ^J&GOt22$G+QZ0-l)-tf`^=v;ht(Ggnxt6pc^h;I2tyoVNS(6?c z3)N}rVjd?%0+w&VAOZjYj}ZVM|BtQp_VIUh@cx&n9W^$KUxd+y2>((ddXQdfQ1xln z%M}q!ga3`&r*-mm57!7N@SV`OQ%CAk+$hx{om(xWvegxWdS_zi4b#xKg*}V;6&*|0 zv9F^tn6{*G=K@DKuLWn?I{0w#ssWzy6nXH zViB>xXA!*<6b;M>h35HgGL@ZXR+itAK`jIFM&V?L@JMy`DDF3v*gOtITB92L9dpji zV-V#)Cn%^c`mI13c%>uH$9^Q8do5i>6i3WL@sFkK)!L4x+{}lOZQ;3-v8M2!Fkn~U zC(RvF==IzD6E`JG#jI&}voQzXMHM7$ga=uZNl=HF0I&d65^%PfXAN`p!iAfot4TUWF;V)Ww*!80giSU^<%3 z+(zGg?j79L-rN#lLQ*CF-W}<}F3YHMXh5WQKWa_pbfFH-udn;lg5F@5+dM{)&E$uD zGBEwJB!;*Fkg!OBHKTmF`1q*YGt!n2I(9dcz{M4crMGT5&FHw4H*W{z zsL>1bSkzw0OAWuQ>w%|Pq9VBpPU>(I2i&JkdwuDenD#5@m))PxFCkrfD$DzgNVf*i zwB|!iP;uDjANN`Jz1}m2uPsh5(ap~GbBtOJ)-s;UhMK7U4EzZ^l^(HcbHtbDC`|;W zuTUXE!bER5(Uog3d80|=mv0RicR2j&A7$r?c|HKM&#}oqm8+w8L|<+QY$o>(tc@9! z0S6pWy6|7i%|VM}2*#JGefuQ%~vz*}Xa99a?(plPc!nOVW zZ~N*Um^ZDJ;?3oc4Xc8?8-o)ET#_yY3zyyU`Df91-;D(Sd^XBSOs$m$0c|J(^p>%n zUFLx+Tj4km6Xg8vNlQ|r$@^Dk3EIkQ#3UV1O<^NpIj_rWnw(WAzVV4g-LavnOnj2L zVz=#gy0?$g3!`3T^uEKrEh9i%r^D|)GbNd;Ill;Yq_r1FTE5TUBC$jHy0(OAVO7n+ zHn687lQO$VYP!aVxz`|Tb9ifYLdR62RuDgENvep=Kc1yoSHkZ@N|F!9lu2h%iPUMcfb$S zAiLkyG`a3)cOdfRFEtw#v!X5t76!G;c*%%Pl#jJFUuu4J={kEhD3-~Gj?-3&GZk@q zyNu!PDDU|8v$~4;Lx#1y#*^q1S*V7KOI*X62JahUbG0@PD{Puc>lBjETtt%61*U;5fvtrn^|Te48H%)&FTMQm3f7(~6j78k9>;E)WbBozT*?QymIY?Z9nZ`E#ygj{%?xNobppD$nqU{~N!eFzWg^ z6(}L-1J-{6oxic{U+wy59Fw8Q$eB?YFd*xHE$+krZR!A`I-%+~2ox?10{LGZ{0B>n z>)&DTzf}>H0;5B1^8iWzDTkp#U@EvPyeKs65zZ>A2gZs*@ROjVH285h_)#(9VAN|3 xVH{ditA+s%6h)>9V)-{sZ~y=+TmXRTAE3W06iQoD5r+{Ks|mVys`jta{{g?W!ae{1 delta 5825 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 Date: Thu, 4 Jul 2024 14:45:19 +0800 Subject: [PATCH 17/22] add sub-contract fee to project --- .../java/com/ffii/tsms/modules/project/entity/Project.kt | 3 +++ .../com/ffii/tsms/modules/project/service/ProjectsService.kt | 5 ++++- .../tsms/modules/project/web/models/EditProjectDetails.kt | 3 ++- .../tsms/modules/project/web/models/NewProjectRequest.kt | 3 ++- .../changes/20240704_01_cyril/01_update_project.sql | 5 +++++ 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20240704_01_cyril/01_update_project.sql diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt b/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt index 533dcd3..e52f80b 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt @@ -77,6 +77,9 @@ open class Project : BaseEntity() { @Column(name = "expectedTotalFee") open var expectedTotalFee: Double? = null + @Column(name = "subContractFee") + open var subContractFee: Double? = null + @ManyToOne @JoinColumn(name = "serviceTypeId") open var serviceType: ServiceType? = null diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index bc3fbd4..ba83321 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -197,6 +197,7 @@ open class ProjectsService( project ) else this.code expectedTotalFee = request.expectedProjectFee + subContractFee = request.subContractFee totalManhour = request.totalManhour actualStart = request.projectActualStart actualEnd = request.projectActualEnd @@ -402,7 +403,8 @@ open class ProjectsService( ) }) }, - expectedProjectFee = it.expectedTotalFee + expectedProjectFee = it.expectedTotalFee, + subContractFee = it.subContractFee ) } } @@ -644,6 +646,7 @@ open class ProjectsService( clientId = clientId, clientSubsidiaryId = subsidiary?.id, expectedProjectFee = row.getCell(9).numericCellValue, + subContractFee = null, totalManhour = row.getCell(11).numericCellValue, locationId = 1, // HK buildingTypeIds = mutableListOf(buildingType!!.id!!), diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt index 704f0fd..0cb0f1e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt @@ -40,5 +40,6 @@ data class EditProjectDetails( val milestones: Map, // Miscellaneous - val expectedProjectFee: Double? + val expectedProjectFee: Double?, + val subContractFee: Double? ) diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt index aab4473..5fdea98 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt @@ -45,7 +45,8 @@ data class NewProjectRequest( val milestones: Map, // Miscellaneous - val expectedProjectFee: Double + val expectedProjectFee: Double, + val subContractFee: Double? ) data class TaskGroupAllocation( diff --git a/src/main/resources/db/changelog/changes/20240704_01_cyril/01_update_project.sql b/src/main/resources/db/changelog/changes/20240704_01_cyril/01_update_project.sql new file mode 100644 index 0000000..275afac --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240704_01_cyril/01_update_project.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:project + +ALTER TABLE `project` + ADD COLUMN `subContractFee` DOUBLE NULL DEFAULT NULL AFTER `expectedTotalFee`; \ No newline at end of file From b706674184917d14d53c855540a1729176a4c8d0 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 4 Jul 2024 15:26:58 +0800 Subject: [PATCH 18/22] update sub-project & report --- .../ffii/tsms/modules/project/service/ProjectsService.kt | 3 ++- .../tsms/modules/project/web/models/MainProjectDetails.kt | 1 + .../com/ffii/tsms/modules/report/service/ReportService.kt | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index ba83321..712fc72 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -463,7 +463,8 @@ open class ProjectsService( clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id, clientSubsidiaryId = project.customerSubsidiary?.id, - expectedProjectFee = project.expectedTotalFee + expectedProjectFee = project.expectedTotalFee, + subContractFee = project.subContractFee, ) } } diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt index 3c2a748..ba2e4bc 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt @@ -27,4 +27,5 @@ data class MainProjectDetails ( val clientSubsidiaryId: Long?, val expectedProjectFee: Double?, + val subContractFee: Double?, ) \ 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 6c492fd..558819f 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 @@ -1842,13 +1842,13 @@ open class ReportService( val sql = StringBuilder( "WITH teamNormalConsumed AS (" + " SELECT" - + " tns.projectId," + + " tns.project_id," + " SUM(tns.totalConsumed) AS totalConsumed, " + " sum(tns.totalBudget) as totalBudget " + " FROM ( " + " SELECT" + " t.staffId," - + " t.projectId AS projectId," + + " t.projectId AS project_id," + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " + " FROM timesheet t" @@ -1856,7 +1856,7 @@ open class ReportService( + " left join salary sal on sal.salaryPoint = s.salaryId " + " GROUP BY t.staffId, t.projectId" + " ) AS tns" - + " GROUP BY projectId" + + " GROUP BY project_id" + " ) " + " SELECT " + " p.code, " From 4f1c05e6082b97765bf0eb076e985c2420e713df Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Thu, 4 Jul 2024 16:57:55 +0800 Subject: [PATCH 19/22] update --- .../modules/data/service/DashboardService.kt | 97 ++++++++++++++++++- .../modules/data/web/DashboardController.kt | 24 +++++ 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index d5decd7..606a729 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -117,6 +117,15 @@ open class DashboardService( } sql.append(" group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone") + + if (args["tableSorting"] == "ProjectName") { + sql.append(" ORDER BY p.name ASC") + } else if (args["tableSorting"] == "PercentageASC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") + } else if (args["tableSorting"] == "PercentageDESC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc") + } + return jdbcDao.queryForList(sql.toString(), args) } @@ -162,7 +171,13 @@ open class DashboardService( // + " and (tg.name != '5. Miscellaneous' or tg.name is null)" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) - + if (args["tableSorting"] == "ProjectName") { + sql.append(" ORDER BY p.name ASC") + } else if (args["tableSorting"] == "PercentageASC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") + } else if (args["tableSorting"] == "PercentageDESC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc") + } return jdbcDao.queryForList(sql.toString(), args) } @@ -315,9 +330,55 @@ open class DashboardService( // + " and (tg.name != '5. Miscellaneous' or tg.name is null)" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) + if (args["tableSorting"] == "ProjectName") { + sql.append(" ORDER BY p.name ASC") + } else if (args["tableSorting"] == "PercentageASC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") + } else if (args["tableSorting"] == "PercentageDESC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc") + } + + return jdbcDao.queryForList(sql.toString(), args) + } + + fun searchTeamConsumption(args: Map): List> + { + val sql = StringBuilder( + "select" + + " ROW_NUMBER() OVER (ORDER BY te.code, s.name, project.budgetedManhour) AS id," + + " te.code as team," + + " s.name as teamLead," + + " project.budgetedManhour as budgetedManhour," + + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + + " COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," + + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage" + + " from team te" + + " left join project p on p.teamLead = te.teamLead" + + " left join project_task pt on p.id = pt.project_id" + + " left join timesheet t on pt.id = t.projectTaskId" + + " left join staff s on te.teamLead = s.id" + + " left join (" + + " select sum(p2.totalManhour) as budgetedManhour" + + " from team t2" + + " left join project p2 on p2.teamLead = t2.teamLead" + + " where p2.teamLead in (:teamIds)" + + " ) as project on 1 = 1" + + " where p.teamLead in (:teamIds)" + + " and p.status not in ('Pending to Start','Completed','Deleted')" + + " group by te.code, s.name, project.budgetedManhour" + ) + + if (args["tableSorting"] == "ProjectName") { + sql.append(" ORDER BY te.code ASC") + } else if (args["tableSorting"] == "PercentageASC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) asc") + } else if (args["tableSorting"] == "PercentageDESC") { + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) desc") + } return jdbcDao.queryForList(sql.toString(), args) } + fun searchFinancialSummaryCard(args: Map): List> { val sql = StringBuilder( "select" @@ -327,13 +388,19 @@ open class DashboardService( + " coalesce(pj.totalFee,0) as totalFee," + " coalesce(pj.totalBudget,0) as totalBudget," + " coalesce(sum(i.issueAmount),0) as totalInvoiced," + + " coalesce(pj.totalFee,0) - coalesce(sum(i.issueAmount),0) as unInvoiced," + " coalesce(sum(i.paidAmount),0) as totalReceived," + " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure," + " case" + " when coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) >= 1 then 'Positive'" + " when coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) < 1 then 'Negative'" + " end as cashFlowStatus," - + " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi" + + " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi," + + " coalesce(round(coalesce(pj.totalFee,0) / (expenditure.cumulativeExpenditure),2),0) as projectedCpi," + + " case" + + " when coalesce(round(coalesce(pj.totalFee,0) / (expenditure.cumulativeExpenditure),2),0) >= 1 then 'Positive'" + + " when coalesce(round(coalesce(pj.totalFee,0) / (expenditure.cumulativeExpenditure),2),0) < 1 then 'Negative'" + + " end as projectedCashFlowStatus" + " from team t" + " left join (" + " select" @@ -404,12 +471,18 @@ open class DashboardService( + " round(sum(p.expectedTotalFee) * 0.8,2) as totalBudget," + " round(expenditure.cumulativeExpenditure,2) as cumulativeExpenditure," + " sum(i.issueAmount) as totalInvoiced," + + " sum(p.expectedTotalFee) - sum(i.issueAmount) as unInvoiced," + " sum(i.paidAmount) as totalReceived," + " case" + " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) >= 1 then 'Positive'" + " when round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) < 1 then 'Negative'" + " end as cashFlowStatus," - + " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi" + + " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi," + + " round(sum(p.expectedTotalFee) / (expenditure.cumulativeExpenditure),2) as projectedCpi," + + " case" + + " when round(sum(p.expectedTotalFee) / (expenditure.cumulativeExpenditure),2) >= 1 then 'Positive'" + + " when round(sum(p.expectedTotalFee) / (expenditure.cumulativeExpenditure),2) < 1 then 'Negative'" + + " end as projectedCashFlowStatus" + " from project p" + " left join (" + " select" @@ -550,6 +623,11 @@ open class DashboardService( + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + " end as cashFlowStatus," + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," + + " coalesce(round(p.totalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as projectedCpi," + + " case" + + " when coalesce(round(p.totalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'" + + " when coalesce(round(p.totalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + + " end as projectedCashFlowStatus," + " case" + " when p.totalFee - sum(i.issueAmount) >= 0 then coalesce(round(p.totalFee - sum(i.issueAmount),2),0)" + " when p.totalFee - sum(i.issueAmount) < 0 then 0" @@ -653,6 +731,11 @@ open class DashboardService( + " when coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + " end as cashFlowStatus," + " coalesce(round(sum(i.issueAmount) / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as cpi," + + " coalesce(round(p.expectedTotalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) as projectedCpi," + + " case" + + " when coalesce(round(p.expectedTotalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) >= 1 then 'Positive'" + + " when coalesce(round(p.expectedTotalFee / (COALESCE(expenditure.cumulativeExpenditure,0)),2),0) < 1 then 'Negative'" + + " end as projectedCashFlowStatus," // + " case" // + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) <= COALESCE(expenditure.cumulativeExpenditure,0) then coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) - coalesce(sum(i.issueAmount),0)" // + " when coalesce(round(sum(p.expectedTotalFee) * 0.8,2),0) > COALESCE(expenditure.cumulativeExpenditure,0) and COALESCE(expenditure.cumulativeExpenditure,0) < coalesce(sum(i.issueAmount),0) then 0" @@ -850,7 +933,9 @@ open class DashboardService( + " coalesce (sum(i.issueAmount) - sum(i.paidAmount),0) as receivable," + " coalesce (round(sum(p.expectedTotalFee)*0.8,2),0) as totalBudget," + " coalesce (expenditure.expenditure,0) as totalExpenditure," - + " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable" + + " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable," + + " sum(p.expectedTotalFee) as totalProjectFee," + + " coalesce (round(sum(i.issueAmount)/sum(p.expectedTotalFee)*100,0),0) as invoicedPercentage" + " from project p" + " left join (" + " select" @@ -1194,7 +1279,9 @@ open class DashboardService( "select" + " concat(p.code,'-',p.name) as projectCodeAndName," + " p.expectedTotalFee as totalFee," - + " expenditure.expenditure as expenditure," + + " p.expectedTotalFee * 0.8 as totalBudget," + + " coalesce (expenditure.expenditure,0) as expenditure," + + " (p.expectedTotalFee * 0.8) - coalesce (expenditure.expenditure,0) as remainingBudget," + " case" + " when p.expectedTotalFee - expenditure.expenditure >= 0 then 'Within Budget'" + " when p.expectedTotalFee - expenditure.expenditure < 0 then 'Overconsumption'" diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index 6cb4910..60475c3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -51,6 +51,7 @@ class DashboardController( fun searchCustomerSubsidiaryProject(request: HttpServletRequest?): List> { val customerId = request?.getParameter("customerId") val subsidiaryId = request?.getParameter("subsidiaryId") + val tableSorting = request?.getParameter("tableSorting") val args = mutableMapOf() var result: List> = emptyList() if (customerId != null) { @@ -59,6 +60,9 @@ class DashboardController( if (subsidiaryId != null) { args["subsidiaryId"] = subsidiaryId } + if (tableSorting != null) { + args["tableSorting"] = tableSorting + } if (customerId != null && subsidiaryId != null) { result = dashboardService.searchCustomerSubsidiaryProject(args) @@ -80,16 +84,36 @@ class DashboardController( @GetMapping("/searchTeamProject") fun searchTeamProject(request: HttpServletRequest?): List> { val teamLeadId = request?.getParameter("teamLeadId") + val tableSorting = request?.getParameter("tableSorting") val args = mutableMapOf() var result: List> = emptyList() if (teamLeadId != null) { args["teamLeadId"] = teamLeadId } + if (tableSorting != null) { + args["tableSorting"] = tableSorting + } result = dashboardService.searchTeamProject(args) return result } + @GetMapping("/searchTeamConsumption") + fun searchTeamConsumption(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val teamIdList = request?.getParameter("teamIdList") + val tableSorting = request?.getParameter("tableSorting") + val teamIds = teamIdList?.split(",")?.map { it.toInt() }?.toList() + var result: List> = emptyList() + if (teamIds != null) { + args["teamIds"] = teamIds + } + if (tableSorting != null) { + args["tableSorting"] = tableSorting + } + result = dashboardService.searchTeamProject(args) + return result + } @GetMapping("/searchFinancialSummaryCard") fun searchFinancialSummaryCard(request: HttpServletRequest?): List> { val authority = dashboardService.viewDashboardAuthority() From 16256746018f6124ee8b4683fb0da6f95850b25b Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Fri, 5 Jul 2024 00:01:42 +0800 Subject: [PATCH 20/22] update api --- .../modules/data/service/DashboardService.kt | 24 +++++++++++++++---- .../modules/data/web/DashboardController.kt | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 606a729..120bad3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -80,7 +80,11 @@ open class DashboardService( + " 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," - + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone" + + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone," + + " case" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" + + " end as alert" + " from project p" + " left join project_task pt on p.id = pt.project_id" + " left join timesheet t on pt.id = t.projectTaskId" @@ -143,7 +147,11 @@ open class DashboardService( + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," - + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone" + + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone," + + " case" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" + + " end as alert" + " from project p" + " left join project_task pt on p.id = pt.project_id" + " left join timesheet t on pt.id = t.projectTaskId" @@ -303,7 +311,11 @@ open class DashboardService( + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," - + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone" + + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone," + + " case" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" + + " end as alert" + " from project p" + " left join project_task pt on p.id = pt.project_id" + " left join timesheet t on pt.id = t.projectTaskId" @@ -351,7 +363,11 @@ open class DashboardService( + " project.budgetedManhour as budgetedManhour," + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + " COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," - + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage" + + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage," + + " case" + + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" + + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" + + " end as alert" + " from team te" + " left join project p on p.teamLead = te.teamLead" + " left join project_task pt on p.id = pt.project_id" diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index 60475c3..679a734 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -111,7 +111,7 @@ class DashboardController( if (tableSorting != null) { args["tableSorting"] = tableSorting } - result = dashboardService.searchTeamProject(args) + result = dashboardService.searchTeamConsumption(args) return result } @GetMapping("/searchFinancialSummaryCard") From 8be66a31efd0a39d0de8260644cfe4c04abf634b Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 5 Jul 2024 12:18:30 +0800 Subject: [PATCH 21/22] update --- .../tsms/modules/data/web/SkillController.kt | 2 +- .../project/service/ProjectsService.kt | 134 +++++++++++++----- 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt b/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt index 0b70d12..77a40ca 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt @@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.* open class SkillController(private val skillService: SkillService) { @PostMapping("/save") - @PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')") +// @PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')") open fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill { return skillService.saveOrUpdate(newSkill) } diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index 712fc72..66f2a71 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -111,7 +111,8 @@ open class ProjectsService( code = project.code!!, name = project.name!!, status = project.status, - tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }.sortedBy { it.taskGroup?.id }, + tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task } + .sortedBy { it.taskGroup?.id }, milestones = milestoneRepository.findAllByProject(project) .filter { milestone -> milestone.taskGroup?.id != null } .associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> @@ -202,9 +203,9 @@ open class ProjectsService( actualStart = request.projectActualStart actualEnd = request.projectActualEnd status = if (this.status == "Deleted" || this.deleted == true) "Deleted" - else if (this.actualStart != null && this.actualEnd != null) "Completed" - else if (this.actualStart != null) "On-going" - else request.projectStatus ?: "Pending To Start" + else if (this.actualStart != null && this.actualEnd != null) "Completed" + else if (this.actualStart != null) "On-going" + else request.projectStatus ?: "Pending To Start" isClpProject = request.isClpProject this.mainProject = mainProject @@ -303,7 +304,8 @@ open class ProjectsService( val savedProject = projectRepository.saveAndFlush(project) val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet()) - val tasksToDelete = projectTaskRepository.findAllByProject(project).subtract(tasksToSave.toSet()) + val mapTasksToSave = tasksToSave.map { it.task!!.id } + val tasksToDelete = projectTaskRepository.findAllByProject(project).filter { !mapTasksToSave.contains(it.task!!.id) } val gradeAllocationsToDelete = gradeAllocationRepository.findByProject(project).subtract(gradeAllocations.toSet()) milestoneRepository.deleteAll(milestonesToDelete) @@ -489,21 +491,22 @@ open class ProjectsService( logger.info("---------client-------") logger.info(customerRepository.findByName(row.getCell(5).stringCellValue.trim())) val tempClient = customerRepository.findByName(row.getCell(5).stringCellValue.trim()) - val currentClient = if (tempClient.isPresent) tempClient.orElseThrow() else customerService.saveCustomer( - SaveCustomerRequest( - code = customerService.createClientCode(), - name = ExcelUtils.getStringValue(row.getCell(5)), - typeId = ExcelUtils.getIntValue(row.getCell(6)).toLong(), - deleteSubsidiaryIds = mutableListOf(), - deleteContactIds = mutableListOf(), - addSubsidiaryIds = mutableListOf(), - addContacts = mutableListOf(), - address = null, - brNo = null, - id = null, - district = null, - ) - ).customer + val currentClient = + if (tempClient.isPresent) tempClient.orElseThrow() else customerService.saveCustomer( + SaveCustomerRequest( + code = customerService.createClientCode(), + name = ExcelUtils.getStringValue(row.getCell(5)), + typeId = ExcelUtils.getIntValue(row.getCell(6)).toLong(), + deleteSubsidiaryIds = mutableListOf(), + deleteContactIds = mutableListOf(), + addSubsidiaryIds = mutableListOf(), + addContacts = mutableListOf(), + address = null, + brNo = null, + id = null, + district = null, + ) + ).customer val clientId = currentClient.id!! logger.info("customer: $clientId") @@ -521,14 +524,20 @@ open class ProjectsService( logger.info("splitProjectCode: " + splitProjectCode[1].split(')')[0]) projectCode = - splitMainProjectCode[0] + '-' + String.format("%04d", splitMainProjectCode[1].toInt()) + '-' + String.format("%03d", splitProjectCode[1].split(')')[0].toInt()) + splitMainProjectCode[0] + '-' + String.format( + "%04d", + splitMainProjectCode[1].toInt() + ) + '-' + String.format("%03d", splitProjectCode[1].split(')')[0].toInt()) val mainProject = projectRepository.findByCode(splitProjectCode[0]) ?: projectRepository.saveAndFlush( Project().apply { name = row.getCell(1).stringCellValue description = row.getCell(1).stringCellValue - code = splitMainProjectCode[0] + '-' + String.format("%04d", splitMainProjectCode[1].toInt()) + code = splitMainProjectCode[0] + '-' + String.format( + "%04d", + splitMainProjectCode[1].toInt() + ) status = "Completed" projectCategory = projectCategoryRepository.findById(1).orElseThrow() customer = currentClient @@ -541,7 +550,8 @@ open class ProjectsService( projectCode = splitProjectCode[0] + '-' + String.format("%04d", splitProjectCode[1].toInt()) } - val projectId = projectRepository.findByCode(projectCode)?.id + val project = projectRepository.findByCode(projectCode) + val projectId = project?.id logger.info("projectCode :$projectCode") // process subsidiary @@ -627,9 +637,45 @@ open class ProjectsService( // get task template - TW-Full QS - stage 5 logger.info("---------task template-------") val taskTemplate = taskTemplateRepository.findById(3).orElseThrow() + val tasks = taskTemplate.tasks.groupBy { it.taskGroup!!.id } + val groups = taskTemplate.groupAllocations.associateBy( + keySelector = { it.taskGroup!!.id!! }, + valueTransform = { it.percentage!! } + ) + val projectTasks = if (project != null) projectTaskRepository.findAllByProject(project) else mutableListOf() + var groupedProjectTasks = mapOf>() + if (projectTasks.isNotEmpty()) { + groupedProjectTasks = projectTasks.groupBy { + it.task!!.taskGroup!!.id!! + }.mapValues { (_, projectTask) -> + projectTask.map { it.task!!.id!! } + } + } + val taskGroups = mutableMapOf() + groups.entries.forEach{ (key, value) -> + var taskIds = tasks[key]!!.map { it.id!! } + if (groupedProjectTasks.isNotEmpty() && !groupedProjectTasks[key].isNullOrEmpty()) { + taskIds = (taskIds + groupedProjectTasks[key]!!).distinct() + } + if (key == 5L) { + taskGroups[key] = TaskGroupAllocation(taskIds.plus(41).distinct(), value) + logger.info("taskGroups[${key}]: ${taskGroups[key]}") + } else { + taskGroups[key] = TaskGroupAllocation(taskIds, value) + logger.info("taskGroups[${key}]: ${taskGroups[key]}") + } + } logger.info("---------Import-------") val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = ExcelUtils.getDateValue( + row.getCell(2), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).format(formatter) + val endDate = ExcelUtils.getDateValue( + row.getCell(3), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).format(formatter) saveProject( NewProjectRequest( projectCode = projectCode, @@ -669,18 +715,40 @@ open class ProjectsService( ), projectActualEnd = null, taskTemplateId = taskTemplate.id, - taskGroups = mapOf(Pair(5, TaskGroupAllocation(mutableListOf(41), 100.0))), + taskGroups = taskGroups, milestones = mapOf( + Pair( + 1, com.ffii.tsms.modules.project.web.models.Milestone( + startDate = startDate, + endDate = endDate, + payments = mutableListOf() + ) + ), + Pair( + 2, com.ffii.tsms.modules.project.web.models.Milestone( + startDate = startDate, + endDate = endDate, + payments = mutableListOf() + ) + ), + Pair( + 3, com.ffii.tsms.modules.project.web.models.Milestone( + startDate = startDate, + endDate = endDate, + payments = mutableListOf() + ) + ), + Pair( + 4, com.ffii.tsms.modules.project.web.models.Milestone( + startDate = startDate, + endDate = endDate, + payments = mutableListOf() + ) + ), Pair( 5, com.ffii.tsms.modules.project.web.models.Milestone( - startDate = ExcelUtils.getDateValue( - row.getCell(2), - DateTimeFormatter.ofPattern("MM/dd/yyyy") - ).format(formatter), - endDate = ExcelUtils.getDateValue( - row.getCell(3), - DateTimeFormatter.ofPattern("MM/dd/yyyy") - ).format(formatter), + startDate = startDate, + endDate = endDate, payments = mutableListOf( PaymentInputs( -1, "Manhour Import", ExcelUtils.getDateValue( @@ -690,7 +758,7 @@ open class ProjectsService( ) ) ) - ) + ), )) ) } From 939909579a13a89f18cfc6bb132f040b659113db Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Sun, 7 Jul 2024 20:37:26 +0800 Subject: [PATCH 22/22] update api --- .../modules/data/service/DashboardService.kt | 107 +++++++++++++----- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 120bad3..e9c8b73 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -356,40 +356,89 @@ open class DashboardService( fun searchTeamConsumption(args: Map): List> { val sql = StringBuilder( - "select" - + " ROW_NUMBER() OVER (ORDER BY te.code, s.name, project.budgetedManhour) AS id," - + " te.code as team," - + " s.name as teamLead," - + " project.budgetedManhour as budgetedManhour," - + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," - + " COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," - + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage," - + " case" - + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" - + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" - + " end as alert" - + " from team te" - + " left join project p on p.teamLead = te.teamLead" - + " left join project_task pt on p.id = pt.project_id" - + " left join timesheet t on pt.id = t.projectTaskId" - + " left join staff s on te.teamLead = s.id" - + " left join (" - + " select sum(p2.totalManhour) as budgetedManhour" - + " from team t2" - + " left join project p2 on p2.teamLead = t2.teamLead" - + " where p2.teamLead in (:teamIds)" - + " ) as project on 1 = 1" - + " where p.teamLead in (:teamIds)" - + " and p.status not in ('Pending to Start','Completed','Deleted')" - + " group by te.code, s.name, project.budgetedManhour" +// "select" +// + " ROW_NUMBER() OVER (ORDER BY te.code, s.name, project.budgetedManhour) AS id," +// + " te.code as team," +// + " s.name as teamLead," +// + " project.budgetedManhour as budgetedManhour," +// + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," +// + " COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," +// + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) as manhourConsumptionPercentage," +// + " case" +// + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" +// + " when COALESCE (project.budgetedManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" +// + " end as alert" +// + " from team te" +// + " left join project p on p.teamLead = te.teamLead" +// + " left join project_task pt on p.id = pt.project_id" +// + " left join timesheet t on pt.id = t.projectTaskId" +// + " left join staff s on te.teamLead = s.id" +// + " left join (" +// + " select sum(p2.totalManhour) as budgetedManhour" +// + " from team t2" +// + " left join project p2 on p2.teamLead = t2.teamLead" +// + " where p2.teamLead in (:teamIds)" +// + " ) as project on 1 = 1" +// + " where p.teamLead in (:teamIds)" +// + " and p.status not in ('Pending to Start','Completed','Deleted')" +// + " group by te.code, s.name, project.budgetedManhour" + "select" + + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + + " p.id as id," + + " p.id as projectId," + + " p.code as projectCode," + + " p.name as projectName," + + " te.code as team," + + " s.name as teamLead," + + " GROUP_CONCAT(DISTINCT tg.name ORDER BY tg.name) as expectedStage," + + " p.totalManhour as budgetedManhour," + + " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour," + + " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour," + + " coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + + " COALESCE (DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d'),'NA') as comingPaymentMilestone," + + " case" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) > 0 then 0" + + " when COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) <= 0 then 1" + + " end as alert" + + " 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 pid, MIN(comingPaymentMilestone) AS comingPaymentMilestone" + + " FROM (" + + " SELECT p.id AS pid, 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.teamLead in (:teamIds)" + + " AND p.status NOT IN ('Pending to Start', 'Completed', 'Deleted')" + + " AND mp.date >= CURDATE()" + + " ) AS subquery" + + " GROUP BY pid" + + " ORDER BY comingPaymentMilestone ASC" + + " ) milestonePayment on milestonePayment.pid = p.id" + + " where p.teamLead in (:teamIds)" + + " and p.status not in ('Pending to Start','Completed','Deleted')" + + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) +// if (args["tableSorting"] == "ProjectName") { +// sql.append(" ORDER BY te.code ASC") +// } else if (args["tableSorting"] == "PercentageASC") { +// sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) asc") +// } else if (args["tableSorting"] == "PercentageDESC") { +// sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) desc") +// } if (args["tableSorting"] == "ProjectName") { - sql.append(" ORDER BY te.code ASC") + sql.append(" ORDER BY p.name ASC") } else if (args["tableSorting"] == "PercentageASC") { - sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) asc") + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") } else if (args["tableSorting"] == "PercentageDESC") { - sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/project.budgetedManhour)*100,2),0) desc") + sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc") } return jdbcDao.queryForList(sql.toString(), args)