Procházet zdrojové kódy

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

tags/Baseline_30082024_BACKEND_UAT
MSI\2Fi před 1 rokem
rodič
revize
7f79cd1fee
15 změnil soubory, kde provedl 306 přidání a 110 odebrání
  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ární
      src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx

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

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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 Zobrazit soubor

@@ -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ární
src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx Zobrazit soubor


Načítá se…
Zrušit
Uložit