Ver a proveniência

update report

tags/Baseline_30082024_BACKEND_UAT
cyril.tsui há 1 ano
ascendente
cometimento
04e9594feb
15 ficheiros alterados com 306 adições e 110 eliminações
  1. +0
    -97
      src/main/java/com/ffii/tsms/modules/common/service/ExcelReportService.kt
  2. +43
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/Invoice.kt
  3. +9
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt
  4. +2
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/Milestone.kt
  5. +2
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/MilestonePayment.kt
  6. +1
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/MilestonePaymentRepository.kt
  7. +1
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/Project.kt
  8. +11
    -3
      src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt
  9. +160
    -0
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  10. +19
    -9
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  11. +1
    -1
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  12. +39
    -0
      src/main/resources/db/changelog/changes/20240503_01_cyril/01_create_invoice.sql
  13. +9
    -0
      src/main/resources/db/changelog/changes/20240503_01_cyril/02_update_invoice.sql
  14. +9
    -0
      src/main/resources/db/changelog/changes/20240503_01_cyril/03_update_invoice.sql
  15. BIN
      src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx

+ 0
- 97
src/main/java/com/ffii/tsms/modules/common/service/ExcelReportService.kt Ver ficheiro

@@ -1,97 +0,0 @@
package com.ffii.tsms.modules.common.service

import com.ffii.tsms.modules.project.entity.Project
import org.apache.poi.ss.usermodel.Cell
import org.apache.poi.ss.usermodel.CellStyle
import org.apache.poi.ss.usermodel.HorizontalAlignment
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.ss.util.CellUtil
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

@Service
open class ExcelReportService {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER)

private val EX02_PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx"

// ==============================|| GENERATE REPORT ||============================== //
@Throws(IOException::class)
fun generateEX02ProjectCashFlowReport(project: Project): ByteArray {
// Generate the Excel report with query results
val workbook: Workbook = createEX02ProjectCashFlowReport(project, EX02_PROJECT_CASH_FLOW_REPORT)

// Write the workbook to a ByteArrayOutputStream
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()

return outputStream.toByteArray()
}

// ==============================|| CREATE REPORT ||============================== //
@Throws(IOException::class)
private fun createEX02ProjectCashFlowReport(
project: Project,
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)

// val alignLeftStyle: CellStyle = workbook.createCellStyle().apply {
// alignment = HorizontalAlignment.LEFT // Set the alignment to left
// }
//
// val alignRightStyle: CellStyle = workbook.createCellStyle().apply {
// alignment = HorizontalAlignment.RIGHT // Set the alignment to right
// }

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

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

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

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

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

rowIndex = 9
sheet.getRow(rowIndex).apply {
getCell(1).setCellValue(project.expectedTotalFee!! * 0.8)
getCell(2).setCellValue(project.expectedTotalFee!!)
}

return workbook
}
}

+ 43
- 0
src/main/java/com/ffii/tsms/modules/project/entity/Invoice.kt Ver ficheiro

@@ -0,0 +1,43 @@
package com.ffii.tsms.modules.project.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.tsms.modules.data.entity.Customer
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull
import java.time.LocalDate

@Entity
@Table(name = "invoice")
open class Invoice : BaseEntity<Long>(){

@NotNull
@Column(name = "invoiceNo", length = 45)
open var invoiceNo: String? = null

@NotNull
@ManyToOne
@JoinColumn(name = "milestonePaymentId")
open var milestonePayment: MilestonePayment? = null

@NotNull
@Column(name = "invoiceDate")
open var invoiceDate: LocalDate? = null

@NotNull
@Column(name = "dueDate")
open var dueDate: LocalDate? = null

@Column(name = "receiptDate")
open var receiptDate: LocalDate? = null

@NotNull
@Column(name = "unpaidAmount")
open var unpaidAmount: Double? = null

@Column(name = "paidAmount")
open var paidAmount: Double? = null
}

+ 9
- 0
src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt Ver ficheiro

@@ -0,0 +1,9 @@
package com.ffii.tsms.modules.project.entity

import com.ffii.core.support.AbstractRepository

interface InvoiceRepository : AbstractRepository<Invoice, Long> {

fun findAllByPaidAmountIsNotNullAndMilestonePaymentIn(milestonePayment: List<MilestonePayment>): List<Invoice>

}

+ 2
- 0
src/main/java/com/ffii/tsms/modules/project/entity/Milestone.kt Ver ficheiro

@@ -1,5 +1,6 @@
package com.ffii.tsms.modules.project.entity

import com.fasterxml.jackson.annotation.JsonManagedReference
import com.ffii.core.entity.IdEntity
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
@@ -15,6 +16,7 @@ open class Milestone : IdEntity<Long>() {
@Column(name = "description")
open var description: String? = null

@JsonManagedReference
@OneToMany(mappedBy = "milestone", cascade = [CascadeType.ALL], orphanRemoval = true)
open var milestonePayments: MutableList<MilestonePayment> = mutableListOf()



+ 2
- 0
src/main/java/com/ffii/tsms/modules/project/entity/MilestonePayment.kt Ver ficheiro

@@ -1,5 +1,6 @@
package com.ffii.tsms.modules.project.entity

import com.fasterxml.jackson.annotation.JsonBackReference
import com.ffii.core.entity.IdEntity
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
@@ -21,6 +22,7 @@ open class MilestonePayment : IdEntity<Long>() {
open var description: String? = null

@ManyToOne
@JsonBackReference
@JoinColumn(name = "milestoneId")
open var milestone: Milestone? = null
}

+ 1
- 0
src/main/java/com/ffii/tsms/modules/project/entity/MilestonePaymentRepository.kt Ver ficheiro

@@ -3,4 +3,5 @@ package com.ffii.tsms.modules.project.entity;
import com.ffii.core.support.AbstractRepository

interface MilestonePaymentRepository : AbstractRepository<MilestonePayment, Long> {
fun findAllByMilestoneIn(milestones: List<Milestone>): List<MilestonePayment>
}

+ 1
- 0
src/main/java/com/ffii/tsms/modules/project/entity/Project.kt Ver ficheiro

@@ -1,5 +1,6 @@
package com.ffii.tsms.modules.project.entity

import com.fasterxml.jackson.annotation.JsonManagedReference
import com.ffii.core.entity.BaseEntity
import com.ffii.tsms.modules.data.entity.*
import jakarta.persistence.*


+ 11
- 3
src/main/java/com/ffii/tsms/modules/project/service/InvoiceService.kt Ver ficheiro

@@ -4,9 +4,8 @@ import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.AbstractIdEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.utils.PdfUtils
import com.ffii.tsms.modules.project.entity.*

import com.ffii.tsms.modules.project.entity.MilestonePayment
import com.ffii.tsms.modules.project.entity.MilestonePaymentRepository
import com.ffii.tsms.modules.project.entity.projections.InvoicePDFReq
import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo
import net.sf.jasperreports.engine.JasperCompileManager
@@ -21,9 +20,18 @@ import java.time.format.DateTimeFormatter

@Service
open class InvoiceService(
private val invoiceRepository: InvoiceRepository,
private val milestoneRepository: MilestoneRepository,
private val milestonePaymentRepository: MilestonePaymentRepository,
private val jdbcDao: JdbcDao,
) : AbstractIdEntityService<MilestonePayment, Long, MilestonePaymentRepository>(jdbcDao, milestonePaymentRepository){
) : AbstractIdEntityService<Invoice, Long, InvoiceRepository>(jdbcDao, invoiceRepository){

open fun findAllByProjectAndPaidAmountIsNotNull(project: Project): List<Invoice> {
val milestones = milestoneRepository.findAllByProject(project)
val milestonePayments = milestonePaymentRepository.findAllByMilestoneIn(milestones)
return invoiceRepository.findAllByPaidAmountIsNotNullAndMilestonePaymentIn(milestonePayments)
}

open fun allMilestonePayments(): List<Map<String, Any>> {
val sql = StringBuilder(" select "
+ " mp.id, "


+ 160
- 0
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Ver ficheiro

@@ -0,0 +1,160 @@
package com.ffii.tsms.modules.report.service

import com.ffii.tsms.modules.project.entity.Invoice
import com.ffii.tsms.modules.project.entity.Project
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFDataFormat
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Service
open class ReportService {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd")
private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER)

private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx"

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

// Write the workbook to a ByteArrayOutputStream
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()

return outputStream.toByteArray()
}

// ==============================|| CREATE REPORT ||============================== //
@Throws(IOException::class)
private fun createProjectCashFlowReport(
project: Project,
invoices: List<Invoice>,
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);_(* \"-\"??_);_(@_)")

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

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

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

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

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

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

getCell(2).apply {
setCellValue(project.expectedTotalFee!! / 0.8)
cellStyle.dataFormat = accountingStyle
}
}

rowIndex = 10
val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! }
sheet.getRow(rowIndex).apply {
getCell(1).apply {
// TODO: Replace by actual expenditure
setCellValue(actualIncome * 0.8)
cellStyle.dataFormat = accountingStyle
}

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

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

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

// TODO: Add expenditure
// formula =IF(B17>0,D16-B17,D16+C17)
rowIndex = 15
val dateFormatter = DateTimeFormatter.ofPattern("MMM YYYY")
invoices.forEach { invoice: Invoice ->
sheet.getRow(rowIndex++).apply {
getCell(0).apply {
setCellValue(invoice.receiptDate!!.format(dateFormatter))
}

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

getCell(2).apply {
setCellValue(invoice.paidAmount!!)
cellStyle.dataFormat = accountingStyle
}

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

getCell(4).apply {
setCellValue(invoice.milestonePayment!!.description!!)
}
}
}

return workbook
}
}

+ 19
- 9
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Ver ficheiro

@@ -1,36 +1,46 @@
package com.ffii.tsms.modules.report.web

import com.ffii.tsms.modules.common.service.ExcelReportService
import com.ffii.tsms.modules.project.entity.ProjectRepository
import com.ffii.tsms.modules.report.web.model.EX02ProjectCashFlowReportRequest
import com.ffii.tsms.modules.project.entity.*
import com.ffii.tsms.modules.report.service.ReportService
import com.ffii.tsms.modules.project.service.InvoiceService
import com.ffii.tsms.modules.report.web.model.ProjectCashFlowReportRequest
import jakarta.validation.Valid
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.io.IOException
import java.time.LocalDate

@RestController
@RequestMapping("/reports")
class ReportController(private val excelReportService: ExcelReportService, private val projectRepository: ProjectRepository) {
class ReportController(private val invoiceRepository: InvoiceRepository, private val milestonePaymentRepository: MilestonePaymentRepository, private val excelReportService: ReportService, private val projectRepository: ProjectRepository, private val invoiceService: InvoiceService) {

@PostMapping("/EX02-ProjectCashFlowReport")
@PostMapping("/ProjectCashFlowReport")
@Throws(ServletRequestBindingException::class, IOException::class)
fun getEx02ProjectCashFlowReport(@RequestBody @Valid request: EX02ProjectCashFlowReportRequest): ResponseEntity<Resource> {
fun getProjectCashFlowReport(@RequestBody @Valid request: ProjectCashFlowReportRequest): ResponseEntity<Resource> {

val project = projectRepository.findById(request.projectId).orElseThrow()
val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project)

val reportResult: ByteArray = excelReportService.generateEX02ProjectCashFlowReport(project)
val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices)
// val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
return ResponseEntity.ok()
// .contentType(mediaType)
.header("filename", "EX02 - Project Cash Flow Report - " + LocalDate.now() + ".xlsx")
.header("filename", "Project Cash Flow Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult))
}

@GetMapping("/test/{id}")
fun test(@PathVariable id: Long): List<Invoice> {
val project = projectRepository.findById(id).orElseThrow()
return invoiceService.findAllByProjectAndPaidAmountIsNotNull(project)
}
}

+ 1
- 1
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt Ver ficheiro

@@ -1,5 +1,5 @@
package com.ffii.tsms.modules.report.web.model

data class EX02ProjectCashFlowReportRequest (
data class ProjectCashFlowReportRequest (
val projectId: Long
)

+ 39
- 0
src/main/resources/db/changelog/changes/20240503_01_cyril/01_create_invoice.sql Ver ficheiro

@@ -0,0 +1,39 @@
-- liquibase formatted sql
-- changeset cyril:create invoice

CREATE TABLE `invoice` (
`id` INT NOT NULL AUTO_INCREMENT,
`version` INT NOT NULL DEFAULT '0',
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdBy` VARCHAR(30) NULL DEFAULT NULL,
`modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modifiedBy` VARCHAR(30) NULL DEFAULT NULL,
`deleted` TINYINT(1) NOT NULL DEFAULT '0',
`invoiceNo` VARCHAR(45) NOT NULL,
`projectId` INT NOT NULL,
`milestonePaymentId` INT NOT NULL,
`clientId` INT NOT NULL,
`invoiceDate` DATE NOT NULL,
`dueDate` DATE NOT NULL,
`receiptDate` DATE NULL,
`unpaidAmount` DECIMAL(16,2) NOT NULL,
`paidAmount` DECIMAL(16,2) NULL,
PRIMARY KEY (`id`),
INDEX `FK_INVOICE_ON_PROJECTID` (`projectId` ASC) VISIBLE,
INDEX `FK_INVOICE_ON_MILESTONEPAYMENTID` (`milestonePaymentId` ASC) VISIBLE,
INDEX `FK_INVOICE_ON_CLIENTID` (`clientId` ASC) VISIBLE,
CONSTRAINT `FK_INVOICE_ON_PROJECTID`
FOREIGN KEY (`projectId`)
REFERENCES `project` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `FK_INVOICE_ON_MILESTONEPAYMENTID`
FOREIGN KEY (`milestonePaymentId`)
REFERENCES `milestone_payment` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `FK_INVOICE_ON_CLIENTID`
FOREIGN KEY (`clientId`)
REFERENCES `customer` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION);

+ 9
- 0
src/main/resources/db/changelog/changes/20240503_01_cyril/02_update_invoice.sql Ver ficheiro

@@ -0,0 +1,9 @@
-- liquibase formatted sql
-- changeset cyril:update invoice

ALTER TABLE `invoice`
DROP FOREIGN KEY `FK_INVOICE_ON_CLIENTID`;
ALTER TABLE `invoice`
DROP COLUMN `clientId`,
DROP INDEX `FK_INVOICE_ON_CLIENTID` ;
;

+ 9
- 0
src/main/resources/db/changelog/changes/20240503_01_cyril/03_update_invoice.sql Ver ficheiro

@@ -0,0 +1,9 @@
-- liquibase formatted sql
-- changeset cyril:update invoice

ALTER TABLE `invoice`
DROP FOREIGN KEY `FK_INVOICE_ON_PROJECTID`;
ALTER TABLE `invoice`
DROP COLUMN `projectId`,
DROP INDEX `FK_INVOICE_ON_PROJECTID` ;
;

BIN
src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx Ver ficheiro


Carregando…
Cancelar
Guardar