From 04e9594feb4b66dc8e24b7149159f5d239bee2f5 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 6 May 2024 11:12:19 +0800 Subject: [PATCH] update report --- .../common/service/ExcelReportService.kt | 97 ----------- .../tsms/modules/project/entity/Invoice.kt | 43 +++++ .../project/entity/InvoiceRepository.kt | 9 + .../tsms/modules/project/entity/Milestone.kt | 2 + .../project/entity/MilestonePayment.kt | 2 + .../entity/MilestonePaymentRepository.kt | 1 + .../tsms/modules/project/entity/Project.kt | 1 + .../modules/project/service/InvoiceService.kt | 14 +- .../modules/report/service/ReportService.kt | 160 ++++++++++++++++++ .../modules/report/web/ReportController.kt | 28 ++- .../modules/report/web/model/ReportRequest.kt | 2 +- .../20240503_01_cyril/01_create_invoice.sql | 39 +++++ .../20240503_01_cyril/02_update_invoice.sql | 9 + .../20240503_01_cyril/03_update_invoice.sql | 9 + .../report/EX02_Project Cash Flow Report.xlsx | Bin 14160 -> 13342 bytes 15 files changed, 306 insertions(+), 110 deletions(-) delete mode 100644 src/main/java/com/ffii/tsms/modules/common/service/ExcelReportService.kt create mode 100644 src/main/java/com/ffii/tsms/modules/project/entity/Invoice.kt create mode 100644 src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt create mode 100644 src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt create mode 100644 src/main/resources/db/changelog/changes/20240503_01_cyril/01_create_invoice.sql create mode 100644 src/main/resources/db/changelog/changes/20240503_01_cyril/02_update_invoice.sql create mode 100644 src/main/resources/db/changelog/changes/20240503_01_cyril/03_update_invoice.sql diff --git a/src/main/java/com/ffii/tsms/modules/common/service/ExcelReportService.kt b/src/main/java/com/ffii/tsms/modules/common/service/ExcelReportService.kt deleted file mode 100644 index 6441c0f..0000000 --- a/src/main/java/com/ffii/tsms/modules/common/service/ExcelReportService.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/Invoice.kt b/src/main/java/com/ffii/tsms/modules/project/entity/Invoice.kt new file mode 100644 index 0000000..cd9115d --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/project/entity/Invoice.kt @@ -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(){ + + @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 +} \ No newline at end of file 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 new file mode 100644 index 0000000..f404d7d --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/project/entity/InvoiceRepository.kt @@ -0,0 +1,9 @@ +package com.ffii.tsms.modules.project.entity + +import com.ffii.core.support.AbstractRepository + +interface InvoiceRepository : AbstractRepository { + + fun findAllByPaidAmountIsNotNullAndMilestonePaymentIn(milestonePayment: List): List + +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/Milestone.kt b/src/main/java/com/ffii/tsms/modules/project/entity/Milestone.kt index 8a760a0..945e616 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/Milestone.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/Milestone.kt @@ -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() { @Column(name = "description") open var description: String? = null + @JsonManagedReference @OneToMany(mappedBy = "milestone", cascade = [CascadeType.ALL], orphanRemoval = true) open var milestonePayments: MutableList = mutableListOf() diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePayment.kt b/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePayment.kt index 4dec858..14ddd1f 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePayment.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePayment.kt @@ -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() { open var description: String? = null @ManyToOne + @JsonBackReference @JoinColumn(name = "milestoneId") open var milestone: Milestone? = null } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePaymentRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePaymentRepository.kt index 0bfbc92..6d7f057 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePaymentRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/MilestonePaymentRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.project.entity; import com.ffii.core.support.AbstractRepository interface MilestonePaymentRepository : AbstractRepository { + fun findAllByMilestoneIn(milestones: List): List } \ No newline at end of file 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 5155527..e51f7d2 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 @@ -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.* 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 1d3700a..e575d00 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 @@ -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(jdbcDao, milestonePaymentRepository){ +) : AbstractIdEntityService(jdbcDao, invoiceRepository){ + + open fun findAllByProjectAndPaidAmountIsNotNull(project: Project): List { + val milestones = milestoneRepository.findAllByProject(project) + val milestonePayments = milestonePaymentRepository.findAllByMilestoneIn(milestones) + return invoiceRepository.findAllByPaidAmountIsNotNullAndMilestonePaymentIn(milestonePayments) + } + open fun allMilestonePayments(): List> { val sql = StringBuilder(" select " + " mp.id, " 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 new file mode 100644 index 0000000..21bcbeb --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -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): 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, + 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 + } +} \ 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 b7ad3c6..d2a0247 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 @@ -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 { + fun getProjectCashFlowReport(@RequestBody @Valid request: ProjectCashFlowReportRequest): ResponseEntity { 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 { + val project = projectRepository.findById(id).orElseThrow() + return invoiceService.findAllByProjectAndPaidAmountIsNotNull(project) + } } \ 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 083368a..99b6e61 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 @@ -1,5 +1,5 @@ package com.ffii.tsms.modules.report.web.model -data class EX02ProjectCashFlowReportRequest ( +data class ProjectCashFlowReportRequest ( val projectId: Long ) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240503_01_cyril/01_create_invoice.sql b/src/main/resources/db/changelog/changes/20240503_01_cyril/01_create_invoice.sql new file mode 100644 index 0000000..e344a6b --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240503_01_cyril/01_create_invoice.sql @@ -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); diff --git a/src/main/resources/db/changelog/changes/20240503_01_cyril/02_update_invoice.sql b/src/main/resources/db/changelog/changes/20240503_01_cyril/02_update_invoice.sql new file mode 100644 index 0000000..e7ce87b --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240503_01_cyril/02_update_invoice.sql @@ -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` ; +; diff --git a/src/main/resources/db/changelog/changes/20240503_01_cyril/03_update_invoice.sql b/src/main/resources/db/changelog/changes/20240503_01_cyril/03_update_invoice.sql new file mode 100644 index 0000000..aaf6f33 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240503_01_cyril/03_update_invoice.sql @@ -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` ; +; diff --git a/src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx b/src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx index ac160ab3209a4df9a79f2f7cab6682fe0614ee7d..7b3763d19695317e6cd09e97b4b4800c699093e7 100644 GIT binary patch delta 5942 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