| @@ -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 | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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> | |||
| } | |||
| @@ -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() | |||
| @@ -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 | |||
| } | |||
| @@ -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,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.* | |||
| @@ -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, " | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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,5 +1,5 @@ | |||
| package com.ffii.tsms.modules.report.web.model | |||
| data class EX02ProjectCashFlowReportRequest ( | |||
| data class ProjectCashFlowReportRequest ( | |||
| val projectId: Long | |||
| ) | |||
| @@ -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); | |||
| @@ -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` ; | |||
| ; | |||
| @@ -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` ; | |||
| ; | |||