@@ -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` ; | |||
; |