mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-25 15:55:44 +00:00
Merge branch 'feat/export-report-purchases-per-supplier' into 'development'
[FEAT][BE] Export Report Purchases Per Supplier See merge request mbugroup/lti-api!560
This commit is contained in:
@@ -324,6 +324,13 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPurchaseSupplierExcelExportRequest(ctx) {
|
||||||
|
return exportPurchaseSupplierExcel(ctx, result)
|
||||||
|
}
|
||||||
|
if isPurchaseSupplierExcelAllExportRequest(ctx) {
|
||||||
|
return exportPurchaseSupplierExcelAll(ctx, result)
|
||||||
|
}
|
||||||
|
|
||||||
filters := map[string]interface{}{
|
filters := map[string]interface{}{
|
||||||
"area_id": query.AreaIDs,
|
"area_id": query.AreaIDs,
|
||||||
"supplier_id": query.SupplierIDs,
|
"supplier_id": query.SupplierIDs,
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isPurchaseSupplierExcelExportRequest(c *fiber.Ctx) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPurchaseSupplierExcelAllExportRequest(c *fiber.Ctx) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel-all")
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportPurchaseSupplierExcel(c *fiber.Ctx, items []dto.PurchaseSupplierDTO) error {
|
||||||
|
content, err := buildPurchaseSupplierWorkbook(items)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("laporan-pembelian-supplier-%s.xlsx", time.Now().Format("2006-01-02-1504"))
|
||||||
|
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
return c.Status(fiber.StatusOK).Send(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportPurchaseSupplierExcelAll(c *fiber.Ctx, items []dto.PurchaseSupplierDTO) error {
|
||||||
|
content, err := buildPurchaseSupplierAllWorkbook(items)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("laporan-pembelian-supplier-all-%s.xlsx", time.Now().Format("2006-01-02-1504"))
|
||||||
|
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
return c.Status(fiber.StatusOK).Send(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPurchaseSupplierWorkbook creates a workbook with one sheet per supplier.
|
||||||
|
func buildPurchaseSupplierWorkbook(items []dto.PurchaseSupplierDTO) ([]byte, error) {
|
||||||
|
file := excelize.NewFile()
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
if err := writePurchaseSupplierSheet(file, defaultSheet, dto.PurchaseSupplierDTO{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buf, err := file.WriteToBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, item := range items {
|
||||||
|
sheetName := sanitizePurchaseSupplierSheetName(purchaseSupplierName(item))
|
||||||
|
if sheetName == "" {
|
||||||
|
sheetName = fmt.Sprintf("Supplier %d", idx+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
if defaultSheet != sheetName {
|
||||||
|
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := file.NewSheet(sheetName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writePurchaseSupplierSheet(file, sheetName, item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := file.WriteToBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPurchaseSupplierAllWorkbook creates a single-sheet workbook with all suppliers.
|
||||||
|
func buildPurchaseSupplierAllWorkbook(items []dto.PurchaseSupplierDTO) ([]byte, error) {
|
||||||
|
file := excelize.NewFile()
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
const sheet = "Rekap Pembelian Supplier"
|
||||||
|
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
||||||
|
if defaultSheet != sheet {
|
||||||
|
if err := file.SetSheetName(defaultSheet, sheet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setPurchaseSupplierAllColumns(file, sheet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := setPurchaseSupplierAllHeaders(file, sheet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := writePurchaseSupplierAllRows(file, sheet, items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := file.SetPanes(sheet, &excelize.Panes{
|
||||||
|
Freeze: true,
|
||||||
|
YSplit: 1,
|
||||||
|
TopLeftCell: "A2",
|
||||||
|
ActivePane: "bottomLeft",
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := file.WriteToBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var purchaseSupplierSheetHeaders = []string{
|
||||||
|
"No",
|
||||||
|
"Tanggal Terima",
|
||||||
|
"Tanggal PO",
|
||||||
|
"No. Referensi",
|
||||||
|
"Nama Produk",
|
||||||
|
"Tujuan",
|
||||||
|
"QTY",
|
||||||
|
"Harga Beli (Rp)",
|
||||||
|
"Value Harga Beli (Rp)",
|
||||||
|
"Transport (Rp)",
|
||||||
|
"Value Transport (Rp)",
|
||||||
|
"Jumlah (Rp)",
|
||||||
|
"Ekspedisi",
|
||||||
|
"Surat Jalan",
|
||||||
|
}
|
||||||
|
|
||||||
|
var purchaseSupplierAllSheetHeaders = append([]string{"Supplier"}, purchaseSupplierSheetHeaders...)
|
||||||
|
|
||||||
|
var purchaseSupplierSheetColumnWidths = map[string]float64{
|
||||||
|
"A": 5,
|
||||||
|
"B": 14,
|
||||||
|
"C": 12,
|
||||||
|
"D": 16,
|
||||||
|
"E": 20,
|
||||||
|
"F": 20,
|
||||||
|
"G": 10,
|
||||||
|
"H": 20,
|
||||||
|
"I": 20,
|
||||||
|
"J": 22,
|
||||||
|
"K": 22,
|
||||||
|
"L": 16,
|
||||||
|
"M": 20,
|
||||||
|
"N": 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
var purchaseSupplierAllSheetColumnWidths = map[string]float64{
|
||||||
|
"A": 24,
|
||||||
|
"B": 6,
|
||||||
|
"C": 14,
|
||||||
|
"D": 12,
|
||||||
|
"E": 16,
|
||||||
|
"F": 20,
|
||||||
|
"G": 20,
|
||||||
|
"H": 10,
|
||||||
|
"I": 20,
|
||||||
|
"J": 20,
|
||||||
|
"K": 22,
|
||||||
|
"L": 22,
|
||||||
|
"M": 16,
|
||||||
|
"N": 20,
|
||||||
|
"O": 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePurchaseSupplierSheet(file *excelize.File, sheet string, item dto.PurchaseSupplierDTO) error {
|
||||||
|
for col, width := range purchaseSupplierSheetColumnWidths {
|
||||||
|
if err := file.SetColWidth(sheet, col, col, width); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, h := range purchaseSupplierSheetHeaders {
|
||||||
|
col, _ := excelize.ColumnNumberToName(i + 1)
|
||||||
|
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, row := range item.Rows {
|
||||||
|
rowNum := i + 2
|
||||||
|
rowStr := fmt.Sprintf("%d", rowNum)
|
||||||
|
|
||||||
|
values := purchaseSupplierRowCells(row, i+1)
|
||||||
|
for colIdx, val := range values {
|
||||||
|
col, _ := excelize.ColumnNumberToName(colIdx + 1)
|
||||||
|
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary row
|
||||||
|
totalRowNum := len(item.Rows) + 2
|
||||||
|
totalRowStr := fmt.Sprintf("%d", totalRowNum)
|
||||||
|
totalCells := map[string]interface{}{
|
||||||
|
"A": "Total",
|
||||||
|
"G": item.Summary.TotalQty,
|
||||||
|
"H": item.Summary.TotalUnitPrice,
|
||||||
|
"I": item.Summary.TotalPurchaseValue,
|
||||||
|
"J": item.Summary.TotalTransportUnitPrice,
|
||||||
|
"K": item.Summary.TotalTransportValue,
|
||||||
|
"L": item.Summary.TotalAmount,
|
||||||
|
}
|
||||||
|
for col, val := range totalCells {
|
||||||
|
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPurchaseSupplierAllColumns(file *excelize.File, sheet string) error {
|
||||||
|
for col, width := range purchaseSupplierAllSheetColumnWidths {
|
||||||
|
if err := file.SetColWidth(sheet, col, col, width); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := file.SetRowHeight(sheet, 1, 24); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPurchaseSupplierAllHeaders(file *excelize.File, sheet string) error {
|
||||||
|
headerStyle, err := file.NewStyle(&excelize.Style{
|
||||||
|
Font: &excelize.Font{Bold: true, Color: "FFFFFF", Family: "Arial", Size: 10},
|
||||||
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
||||||
|
Alignment: &excelize.Alignment{
|
||||||
|
Horizontal: "center",
|
||||||
|
Vertical: "center",
|
||||||
|
WrapText: true,
|
||||||
|
},
|
||||||
|
Border: []excelize.Border{
|
||||||
|
{Type: "left", Color: "000000", Style: 1},
|
||||||
|
{Type: "top", Color: "000000", Style: 1},
|
||||||
|
{Type: "bottom", Color: "000000", Style: 1},
|
||||||
|
{Type: "right", Color: "000000", Style: 1},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, h := range purchaseSupplierAllSheetHeaders {
|
||||||
|
col, _ := excelize.ColumnNumberToName(i + 1)
|
||||||
|
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCol, _ := excelize.ColumnNumberToName(len(purchaseSupplierAllSheetHeaders))
|
||||||
|
return file.SetCellStyle(sheet, "A1", lastCol+"1", headerStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePurchaseSupplierAllRows(file *excelize.File, sheet string, items []dto.PurchaseSupplierDTO) error {
|
||||||
|
borderStyle := []excelize.Border{
|
||||||
|
{Type: "left", Color: "000000", Style: 1},
|
||||||
|
{Type: "top", Color: "000000", Style: 1},
|
||||||
|
{Type: "bottom", Color: "000000", Style: 1},
|
||||||
|
{Type: "right", Color: "000000", Style: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
dataStyle, err := file.NewStyle(&excelize.Style{
|
||||||
|
Font: &excelize.Font{Color: "000000", Family: "Arial", Size: 10},
|
||||||
|
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
|
||||||
|
Border: borderStyle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalStyle, err := file.NewStyle(&excelize.Style{
|
||||||
|
Font: &excelize.Font{Bold: true, Color: "000000", Family: "Arial", Size: 10},
|
||||||
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}},
|
||||||
|
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
|
||||||
|
Border: borderStyle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHeaderCol, _ := excelize.ColumnNumberToName(len(purchaseSupplierAllSheetHeaders))
|
||||||
|
|
||||||
|
currentRow := 2
|
||||||
|
for _, item := range items {
|
||||||
|
supplierName := purchaseSupplierName(item)
|
||||||
|
|
||||||
|
// Data rows
|
||||||
|
for seq, row := range item.Rows {
|
||||||
|
rowStr := fmt.Sprintf("%d", currentRow)
|
||||||
|
if err := file.SetCellValue(sheet, "A"+rowStr, supplierName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values := purchaseSupplierRowCells(row, seq+1)
|
||||||
|
for colIdx, val := range values {
|
||||||
|
col, _ := excelize.ColumnNumberToName(colIdx + 2)
|
||||||
|
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := file.SetCellStyle(sheet, "A"+rowStr, lastHeaderCol+rowStr, dataStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentRow++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary row
|
||||||
|
totalRowStr := fmt.Sprintf("%d", currentRow)
|
||||||
|
totalCells := map[string]interface{}{
|
||||||
|
"A": supplierName,
|
||||||
|
"B": "Total",
|
||||||
|
"H": item.Summary.TotalQty,
|
||||||
|
"I": item.Summary.TotalUnitPrice,
|
||||||
|
"J": item.Summary.TotalPurchaseValue,
|
||||||
|
"K": item.Summary.TotalTransportUnitPrice,
|
||||||
|
"L": item.Summary.TotalTransportValue,
|
||||||
|
"M": item.Summary.TotalAmount,
|
||||||
|
}
|
||||||
|
for col, val := range totalCells {
|
||||||
|
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := file.SetCellStyle(sheet, "A"+totalRowStr, lastHeaderCol+totalRowStr, totalStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentRow++
|
||||||
|
|
||||||
|
// Empty separator row
|
||||||
|
currentRow++
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// purchaseSupplierRowCells returns cell values for one data row.
|
||||||
|
func purchaseSupplierRowCells(row dto.PurchaseSupplierRowDTO, seq int) []interface{} {
|
||||||
|
productName := "-"
|
||||||
|
if row.Product != nil && strings.TrimSpace(row.Product.Name) != "" {
|
||||||
|
productName = row.Product.Name
|
||||||
|
}
|
||||||
|
warehouseName := "-"
|
||||||
|
if row.Warehouse != nil && strings.TrimSpace(row.Warehouse.Name) != "" {
|
||||||
|
warehouseName = row.Warehouse.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return []interface{}{
|
||||||
|
seq,
|
||||||
|
safePurchaseSupplierText(row.ReceiveDate),
|
||||||
|
safePurchaseSupplierText(row.PoDate),
|
||||||
|
safePurchaseSupplierText(row.PoNumber),
|
||||||
|
productName,
|
||||||
|
warehouseName,
|
||||||
|
row.Qty,
|
||||||
|
row.UnitPrice,
|
||||||
|
row.PurchaseValue,
|
||||||
|
row.TransportUnitPrice,
|
||||||
|
row.TransportValue,
|
||||||
|
row.TotalAmount,
|
||||||
|
safePurchaseSupplierText(row.Expedition),
|
||||||
|
safePurchaseSupplierText(row.DeliveryNumber),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func purchaseSupplierName(item dto.PurchaseSupplierDTO) string {
|
||||||
|
if item.Supplier != nil && strings.TrimSpace(item.Supplier.Name) != "" {
|
||||||
|
return item.Supplier.Name
|
||||||
|
}
|
||||||
|
return "Supplier"
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizePurchaseSupplierSheetName(name string) string {
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
":", " ", "\\", " ", "/", " ",
|
||||||
|
"?", " ", "*", " ", "[", " ", "]", " ",
|
||||||
|
)
|
||||||
|
sanitized := strings.TrimSpace(replacer.Replace(name))
|
||||||
|
if sanitized == "" {
|
||||||
|
return "Sheet"
|
||||||
|
}
|
||||||
|
runes := []rune(sanitized)
|
||||||
|
if len(runes) > 31 {
|
||||||
|
return string(runes[:31])
|
||||||
|
}
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
func safePurchaseSupplierText(s string) string {
|
||||||
|
t := strings.TrimSpace(s)
|
||||||
|
if t == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user