diff --git a/internal/modules/finance/transactions/controllers/transaction.controller.go b/internal/modules/finance/transactions/controllers/transaction.controller.go index 200f7c54..d8c48872 100644 --- a/internal/modules/finance/transactions/controllers/transaction.controller.go +++ b/internal/modules/finance/transactions/controllers/transaction.controller.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/validations" @@ -13,6 +14,8 @@ import ( "github.com/gofiber/fiber/v2" ) +const transactionExcelExportFetchLimit = 99999999 + type TransactionController struct { TransactionService service.TransactionService } @@ -107,6 +110,14 @@ func (u *TransactionController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } + if isTransactionExcelExportRequest(c) { + results, err := u.getAllTransactionsForExcel(c, query) + if err != nil { + return err + } + return exportTransactionListExcel(c, results) + } + result, totalResults, err := u.TransactionService.GetAll(c, query) if err != nil { return err @@ -149,6 +160,32 @@ func (u *TransactionController) GetOne(c *fiber.Ctx) error { }) } +func isTransactionExcelExportRequest(c *fiber.Ctx) bool { + return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") +} + +func (u *TransactionController) getAllTransactionsForExcel(c *fiber.Ctx, baseQuery *validation.Query) ([]entity.Payment, error) { + query := *baseQuery + query.Page = 1 + query.Limit = transactionExcelExportFetchLimit + results := make([]entity.Payment, 0) + for { + pageResults, total, err := u.TransactionService.GetAll(c, &query) + if err != nil { + return nil, err + } + if len(pageResults) == 0 || total == 0 { + break + } + results = append(results, pageResults...) + if int64(len(results)) >= total { + break + } + query.Page++ + } + return results, nil +} + func (u *TransactionController) DeleteOne(c *fiber.Ctx) error { param := c.Params("id") diff --git a/internal/modules/finance/transactions/controllers/transaction.export.go b/internal/modules/finance/transactions/controllers/transaction.export.go new file mode 100644 index 00000000..4bbfbd94 --- /dev/null +++ b/internal/modules/finance/transactions/controllers/transaction.export.go @@ -0,0 +1,307 @@ +package controller + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/xuri/excelize/v2" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +const transactionExportSheetName = "Transaksi" + +func exportTransactionListExcel(c *fiber.Ctx, payments []entity.Payment) error { + content, err := buildTransactionExportWorkbook(payments) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") + } + + filename := fmt.Sprintf("transaksi_%s.xlsx", time.Now().Format("20060102_150405")) + 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 buildTransactionExportWorkbook(payments []entity.Payment) ([]byte, error) { + file := excelize.NewFile() + defer file.Close() + + defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) + if defaultSheet != transactionExportSheetName { + if err := file.SetSheetName(defaultSheet, transactionExportSheetName); err != nil { + return nil, err + } + } + + if err := setTransactionExportColumns(file); err != nil { + return nil, err + } + if err := setTransactionExportHeaders(file); err != nil { + return nil, err + } + if err := setTransactionExportRows(file, payments); err != nil { + return nil, err + } + if err := file.SetPanes(transactionExportSheetName, &excelize.Panes{ + Freeze: true, + YSplit: 1, + TopLeftCell: "A2", + ActivePane: "bottomLeft", + }); err != nil { + return nil, err + } + + buffer, err := file.WriteToBuffer() + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func setTransactionExportColumns(file *excelize.File) error { + columnWidths := map[string]float64{ + "A": 20, + "B": 22, + "C": 18, + "D": 25, + "E": 14, + "F": 16, + "G": 16, + "H": 22, + "I": 22, + "J": 18, + "K": 18, + "L": 18, + "M": 30, + "N": 22, + "O": 20, + } + + sheet := transactionExportSheetName + for col, width := range columnWidths { + if err := file.SetColWidth(sheet, col, col, width); err != nil { + return err + } + } + + return file.SetRowHeight(sheet, 1, 24) +} + +func setTransactionExportHeaders(file *excelize.File) error { + sheet := transactionExportSheetName + headers := []string{ + "Kode Pembayaran", + "No. Referensi", + "Tipe Transaksi", + "Pihak", + "Tipe Pihak", + "Tanggal Bayar", + "Metode Bayar", + "Bank", + "No. Rekening Bank", + "Pemasukan", + "Pengeluaran", + "Nominal", + "Catatan", + "Dibuat Oleh", + "Status", + } + + for i, header := range headers { + colName, err := excelize.ColumnNumberToName(i + 1) + if err != nil { + return err + } + if err := file.SetCellValue(sheet, colName+"1", header); err != nil { + return err + } + } + + headerStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "1F2937"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DCEBFA"}}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, + Border: []excelize.Border{ + {Type: "left", Color: "D1D5DB", Style: 1}, + {Type: "top", Color: "D1D5DB", Style: 1}, + {Type: "bottom", Color: "D1D5DB", Style: 1}, + {Type: "right", Color: "D1D5DB", Style: 1}, + }, + }) + if err != nil { + return err + } + + return file.SetCellStyle(sheet, "A1", "O1", headerStyle) +} + +func setTransactionExportRows(file *excelize.File, payments []entity.Payment) error { + if len(payments) == 0 { + return nil + } + + sheet := transactionExportSheetName + for i, p := range payments { + row := strconv.Itoa(i + 2) + if err := writeTransactionExportRow(file, sheet, row, p); err != nil { + return err + } + } + + lastRow := strconv.Itoa(len(payments) + 1) + + dataStyle, err := file.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true}, + Border: []excelize.Border{ + {Type: "left", Color: "D1D5DB", Style: 1}, + {Type: "top", Color: "D1D5DB", Style: 1}, + {Type: "bottom", Color: "D1D5DB", Style: 1}, + {Type: "right", Color: "D1D5DB", Style: 1}, + }, + }) + if err != nil { + return err + } + if err := file.SetCellStyle(sheet, "A2", "O"+lastRow, dataStyle); err != nil { + return err + } + + numericStyle, err := file.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{Horizontal: "right", Vertical: "center"}, + Border: []excelize.Border{ + {Type: "left", Color: "D1D5DB", Style: 1}, + {Type: "top", Color: "D1D5DB", Style: 1}, + {Type: "bottom", Color: "D1D5DB", Style: 1}, + {Type: "right", Color: "D1D5DB", Style: 1}, + }, + }) + if err != nil { + return err + } + + return file.SetCellStyle(sheet, "J2", "L"+lastRow, numericStyle) +} + +func writeTransactionExportRow(file *excelize.File, sheet, row string, p entity.Payment) error { + incomeAmount, expenseAmount := txAmounts(p.Direction, p.Nominal) + + values := []interface{}{ + safeTxText(p.PaymentCode), + safeTxRefNumber(p.ReferenceNumber), + safeTxText(txTransactionType(p)), + safeTxText(txPartyName(p)), + safeTxText(p.PartyType), + formatTxDate(p.PaymentDate), + safeTxText(p.PaymentMethod), + safeTxBank(p), + safeTxBankAccount(p), + incomeAmount, + expenseAmount, + p.Nominal, + safeTxText(p.Notes), + safeTxText(txCreatedBy(p)), + formatTxStatus(p), + } + + for colIdx, val := range values { + colName, err := excelize.ColumnNumberToName(colIdx + 1) + if err != nil { + return err + } + if err := file.SetCellValue(sheet, colName+row, val); err != nil { + return err + } + } + + return nil +} + +func safeTxText(s string) string { + trimmed := strings.TrimSpace(s) + if trimmed == "" { + return "-" + } + return trimmed +} + +func safeTxRefNumber(s *string) string { + if s == nil { + return "-" + } + return safeTxText(*s) +} + +func safeTxBank(p entity.Payment) string { + if p.BankWarehouse.Id == 0 { + return "-" + } + return safeTxText(p.BankWarehouse.Name) +} + +func safeTxBankAccount(p entity.Payment) string { + if p.BankWarehouse.Id == 0 { + return "-" + } + return safeTxText(p.BankWarehouse.AccountNumber) +} + +func formatTxDate(t time.Time) string { + if t.IsZero() { + return "-" + } + loc, err := time.LoadLocation("Asia/Jakarta") + if err == nil { + t = t.In(loc) + } + return t.Format("02-01-2006") +} + +func formatTxStatus(p entity.Payment) string { + if p.LatestApproval == nil { + return "-" + } + return safeTxText(p.LatestApproval.StepName) +} + +func txTransactionType(p entity.Payment) string { + if p.TransactionType != "" { + return p.TransactionType + } + return p.Direction +} + +func txPartyName(p entity.Payment) string { + switch p.PartyType { + case "CUSTOMER": + if p.Customer != nil && p.Customer.Id != 0 { + return p.Customer.Name + } + case "SUPPLIER": + if p.Supplier != nil && p.Supplier.Id != 0 { + return p.Supplier.Name + } + } + return "" +} + +func txCreatedBy(p entity.Payment) string { + if p.CreatedUser.Id == 0 { + return "" + } + return p.CreatedUser.Name +} + +func txAmounts(direction string, nominal float64) (income, expense float64) { + switch strings.ToUpper(direction) { + case "IN": + return nominal, 0 + case "OUT": + return 0, nominal + default: + return 0, 0 + } +} diff --git a/internal/modules/finance/transactions/validations/transaction.validation.go b/internal/modules/finance/transactions/validations/transaction.validation.go index 4b6d7c5a..93750e3d 100644 --- a/internal/modules/finance/transactions/validations/transaction.validation.go +++ b/internal/modules/finance/transactions/validations/transaction.validation.go @@ -10,7 +10,7 @@ type Update struct { type Query struct { Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` TransactionTypes []string `query:"transaction_types" validate:"omitempty,dive,max=50"` BankIDs []uint `query:"bank_ids" validate:"omitempty,dive,gt=0"` diff --git a/internal/modules/marketing/controllers/deliveryorder.export.go b/internal/modules/marketing/controllers/deliveryorder.export.go index 48751929..7d26191a 100644 --- a/internal/modules/marketing/controllers/deliveryorder.export.go +++ b/internal/modules/marketing/controllers/deliveryorder.export.go @@ -2,7 +2,6 @@ package controller import ( "fmt" - "math" "strconv" "strings" "time" @@ -153,7 +152,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke if err := file.SetCellValue(sheet, "D"+strconv.Itoa(rowNumber), safeMarketingExportText(item.Customer.Name)); err != nil { return err } - if err := file.SetCellValue(sheet, "E"+strconv.Itoa(rowNumber), formatMarketingRupiah(sumMarketingGrandTotal(item.SalesOrder))); err != nil { + if err := file.SetCellValue(sheet, "E"+strconv.Itoa(rowNumber), sumMarketingGrandTotal(item.SalesOrder)); err != nil { return err } if err := file.SetCellValue(sheet, "F"+strconv.Itoa(rowNumber), formatMarketingProducts(item.SalesOrder)); err != nil { @@ -266,40 +265,6 @@ func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 { return total } -func formatMarketingRupiah(value float64) string { - if math.IsNaN(value) || math.IsInf(value, 0) { - return "Rp 0" - } - - rounded := int64(math.Round(value)) - sign := "" - if rounded < 0 { - sign = "-" - rounded = -rounded - } - - raw := strconv.FormatInt(rounded, 10) - if raw == "" { - raw = "0" - } - - var grouped strings.Builder - rem := len(raw) % 3 - if rem > 0 { - grouped.WriteString(raw[:rem]) - if len(raw) > rem { - grouped.WriteString(".") - } - } - for i := rem; i < len(raw); i += 3 { - grouped.WriteString(raw[i : i+3]) - if i+3 < len(raw) { - grouped.WriteString(".") - } - } - - return "Rp " + sign + grouped.String() -} func safeMarketingExportText(value string) string { trimmed := strings.TrimSpace(value) diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go index 9906523e..5cc1dd10 100644 --- a/internal/modules/purchases/controllers/purchase.controller.go +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -91,11 +91,9 @@ func buildPurchaseQuery(c *fiber.Ctx) *validation.Query { Limit: c.QueryInt("limit", 10), Search: strings.TrimSpace(c.Query("search")), ApprovalStatus: strings.TrimSpace(c.Query("approval_status")), - PoDate: strings.TrimSpace(c.Query("po_date")), - PoDateFrom: strings.TrimSpace(c.Query("po_date_from")), - PoDateTo: strings.TrimSpace(c.Query("po_date_to")), - CreatedFrom: strings.TrimSpace(c.Query("created_from")), - CreatedTo: strings.TrimSpace(c.Query("created_to")), + StartDate: strings.TrimSpace(c.Query("start_date")), + EndDate: strings.TrimSpace(c.Query("end_date")), + FilterBy: strings.TrimSpace(c.Query("filter_by")), SupplierID: uint(c.QueryInt("supplier_id", 0)), AreaID: uint(c.QueryInt("area_id", 0)), LocationID: uint(c.QueryInt("location_id", 0)), diff --git a/internal/modules/purchases/controllers/purchase.export.go b/internal/modules/purchases/controllers/purchase.export.go index 046291df..7a9a0696 100644 --- a/internal/modules/purchases/controllers/purchase.export.go +++ b/internal/modules/purchases/controllers/purchase.export.go @@ -2,7 +2,6 @@ package controller import ( "fmt" - "math" "strconv" "strings" "time" @@ -43,15 +42,13 @@ func buildPurchaseExportWorkbook(purchases []entity.Purchase) ([]byte, error) { } } - grandTotals := buildPurchaseGrandTotalMap(purchases) - if err := setPurchaseExportColumns(file, purchaseExportSheetName); err != nil { return nil, err } if err := setPurchaseExportHeaders(file, purchaseExportSheetName); err != nil { return nil, err } - if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases, grandTotals); err != nil { + if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases); err != nil { return nil, err } if err := file.SetPanes(purchaseExportSheetName, &excelize.Panes{ @@ -80,9 +77,17 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error { "F": 22, "G": 22, "H": 32, - "I": 18, - "J": 18, - "K": 24, + "I": 10, + "J": 12, + "K": 16, + "L": 16, + "M": 22, + "N": 12, + "O": 16, + "P": 16, + "Q": 18, + "R": 18, + "S": 24, } for col, width := range columnWidths { @@ -99,17 +104,25 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error { func setPurchaseExportHeaders(file *excelize.File, sheet string) error { headers := []string{ - "PR Number", - "PO Number", - "Tanggal PO", - "Tanggal Terima", - "Supplier", - "Lokasi", - "Gudang", - "Product", - "Status", - "Grand Total", - "Notes", + "PR Number", // A + "PO Number", // B + "Tanggal PO", // C + "Tanggal Terima", // D + "Supplier", // E + "Lokasi", // F + "Gudang", // G + "Product", // H + "Qty", // I + "Satuan", // J + "Price", // K + "Total Produk", // L + "Vendor Ekspedisi",// M + "Qty Ekspedisi", // N + "Price Ekspedisi", // O + "Total Ekspedisi", // P + "Grand Total All", // Q + "Status", // R + "Notes", // S } for i, header := range headers { @@ -137,34 +150,36 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error { return err } - return file.SetCellStyle(sheet, "A1", "K1", headerStyle) + return file.SetCellStyle(sheet, "A1", "S1", headerStyle) } -func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase, grandTotals map[uint]float64) error { +func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase) error { if len(purchases) == 0 { return nil } + var sumL, sumP, sumQ float64 + rowIdx := 2 for p := range purchases { purchase := &purchases[p] - total := grandTotals[purchase.Id] if len(purchase.Items) == 0 { - if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, total); err != nil { + if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, &sumL, &sumP, &sumQ); err != nil { return err } rowIdx++ continue } for it := range purchase.Items { - if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], total); err != nil { + if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], &sumL, &sumP, &sumQ); err != nil { return err } rowIdx++ } } - lastRow := rowIdx - 1 + lastDataRow := rowIdx - 1 + dataStyle, err := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{ Horizontal: "left", @@ -181,7 +196,7 @@ func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity if err != nil { return err } - if err := file.SetCellStyle(sheet, "A2", "K"+strconv.Itoa(lastRow), dataStyle); err != nil { + if err := file.SetCellStyle(sheet, "A2", "S"+strconv.Itoa(lastDataRow), dataStyle); err != nil { return err } @@ -200,14 +215,17 @@ func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity if err != nil { return err } + if err := file.SetCellStyle(sheet, "K2", "Q"+strconv.Itoa(lastDataRow), moneyStyle); err != nil { + return err + } - return file.SetCellStyle(sheet, "J2", "J"+strconv.Itoa(lastRow), moneyStyle) + return addPurchaseExportSumRow(file, sheet, rowIdx, sumL, sumP, sumQ) } -func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, grandTotal float64) error { +func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, sumL, sumP, sumQ *float64) error { row := strconv.Itoa(rowIdx) - // Purchase-level columns (repeat across rows of the same purchase) + // Purchase-level columns (repeat for every item row of the same purchase) if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(purchase.PrNumber)); err != nil { return err } @@ -220,26 +238,40 @@ func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purch if err := file.SetCellValue(sheet, "E"+row, safePurchaseExportEntitySupplierName(purchase)); err != nil { return err } - if err := file.SetCellValue(sheet, "I"+row, formatPurchaseExportEntityStatus(purchase)); err != nil { + if err := file.SetCellValue(sheet, "R"+row, formatPurchaseExportEntityStatus(purchase)); err != nil { return err } - if err := file.SetCellValue(sheet, "J"+row, formatPurchaseRupiah(grandTotal)); err != nil { - return err - } - if err := file.SetCellValue(sheet, "K"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil { + if err := file.SetCellValue(sheet, "S"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil { return err } - // Item-level columns if item == nil { - for _, col := range []string{"D", "F", "G", "H"} { + for _, col := range []string{"D", "F", "G", "H", "J", "M"} { if err := file.SetCellValue(sheet, col+row, "-"); err != nil { return err } } + for _, col := range []string{"I", "K", "L", "N", "O", "P", "Q"} { + if err := file.SetCellValue(sheet, col+row, 0); err != nil { + return err + } + } return nil } + // Item-level columns + var expeditionQty, expeditionPrice, expeditionTotal float64 + if item.ExpenseNonstock != nil { + expeditionQty = item.ExpenseNonstock.Qty + expeditionPrice = item.ExpenseNonstock.Price + expeditionTotal = expeditionQty * expeditionPrice + } + itemGrandTotal := item.TotalPrice + expeditionTotal + + *sumL += item.TotalPrice + *sumP += expeditionTotal + *sumQ += itemGrandTotal + if err := file.SetCellValue(sheet, "D"+row, formatPurchaseExportDate(item.ReceivedDate)); err != nil { return err } @@ -252,20 +284,96 @@ func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purch if err := file.SetCellValue(sheet, "H"+row, safePurchaseItemProductName(item)); err != nil { return err } + if err := file.SetCellValue(sheet, "I"+row, item.TotalQty); err != nil { + return err + } + if err := file.SetCellValue(sheet, "J"+row, safePurchaseItemUomName(item)); err != nil { + return err + } + if err := file.SetCellValue(sheet, "K"+row, item.Price); err != nil { + return err + } + if err := file.SetCellValue(sheet, "L"+row, item.TotalPrice); err != nil { + return err + } + if err := file.SetCellValue(sheet, "M"+row, safePurchaseItemExpeditionVendorName(item)); err != nil { + return err + } + if err := file.SetCellValue(sheet, "N"+row, expeditionQty); err != nil { + return err + } + if err := file.SetCellValue(sheet, "O"+row, expeditionPrice); err != nil { + return err + } + if err := file.SetCellValue(sheet, "P"+row, expeditionTotal); err != nil { + return err + } + if err := file.SetCellValue(sheet, "Q"+row, itemGrandTotal); err != nil { + return err + } return nil } -func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 { - result := make(map[uint]float64, len(items)) - for i := range items { - total := 0.0 - for j := range items[i].Items { - total += items[i].Items[j].TotalPrice - } - result[items[i].Id] = total +func addPurchaseExportSumRow(file *excelize.File, sheet string, rowIdx int, sumL, sumP, sumQ float64) error { + row := strconv.Itoa(rowIdx) + + sumStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "1F2937"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FEF3C7"}}, + Alignment: &excelize.Alignment{ + Horizontal: "left", + Vertical: "center", + }, + Border: []excelize.Border{ + {Type: "left", Color: "D1D5DB", Style: 1}, + {Type: "top", Color: "D1D5DB", Style: 2}, + {Type: "bottom", Color: "D1D5DB", Style: 1}, + {Type: "right", Color: "D1D5DB", Style: 1}, + }, + }) + if err != nil { + return err } - return result + + sumMoneyStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "1F2937"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FEF3C7"}}, + Alignment: &excelize.Alignment{ + Horizontal: "right", + Vertical: "center", + }, + Border: []excelize.Border{ + {Type: "left", Color: "D1D5DB", Style: 1}, + {Type: "top", Color: "D1D5DB", Style: 2}, + {Type: "bottom", Color: "D1D5DB", Style: 1}, + {Type: "right", Color: "D1D5DB", Style: 1}, + }, + }) + if err != nil { + return err + } + + if err := file.SetCellStyle(sheet, "A"+row, "S"+row, sumStyle); err != nil { + return err + } + if err := file.SetCellStyle(sheet, "L"+row, "L"+row, sumMoneyStyle); err != nil { + return err + } + if err := file.SetCellStyle(sheet, "P"+row, "Q"+row, sumMoneyStyle); err != nil { + return err + } + + if err := file.SetCellValue(sheet, "A"+row, "TOTAL"); err != nil { + return err + } + if err := file.SetCellValue(sheet, "L"+row, sumL); err != nil { + return err + } + if err := file.SetCellValue(sheet, "P"+row, sumP); err != nil { + return err + } + return file.SetCellValue(sheet, "Q"+row, sumQ) } func safePurchaseExportEntitySupplierName(purchase *entity.Purchase) string { @@ -296,6 +404,24 @@ func safePurchaseItemProductName(item *entity.PurchaseItem) string { return safePurchaseExportText(item.Product.Name) } +func safePurchaseItemUomName(item *entity.PurchaseItem) string { + if item.Product == nil || item.Product.Uom.Id == 0 { + return "-" + } + return safePurchaseExportText(item.Product.Uom.Name) +} + +func safePurchaseItemExpeditionVendorName(item *entity.PurchaseItem) string { + if item.ExpenseNonstock == nil || item.ExpenseNonstock.Expense == nil { + return "-" + } + exp := item.ExpenseNonstock.Expense + if exp.Supplier == nil || exp.Supplier.Id == 0 { + return "-" + } + return safePurchaseExportText(exp.Supplier.Name) +} + func formatPurchaseExportEntityStatus(purchase *entity.Purchase) string { if purchase.LatestApproval == nil { return "-" @@ -338,37 +464,3 @@ func safePurchaseExportText(value string) string { return trimmed } -func formatPurchaseRupiah(value float64) string { - if math.IsNaN(value) || math.IsInf(value, 0) { - return "Rp 0" - } - - rounded := int64(math.Round(value)) - sign := "" - if rounded < 0 { - sign = "-" - rounded = -rounded - } - - raw := strconv.FormatInt(rounded, 10) - if raw == "" { - raw = "0" - } - - var grouped strings.Builder - rem := len(raw) % 3 - if rem > 0 { - grouped.WriteString(raw[:rem]) - if len(raw) > rem { - grouped.WriteString(".") - } - } - for i := rem; i < len(raw); i += 3 { - grouped.WriteString(raw[i : i+3]) - if i+3 < len(raw) { - grouped.WriteString(".") - } - } - - return "Rp " + sign + grouped.String() -} diff --git a/internal/modules/purchases/controllers/purchase.export_test.go b/internal/modules/purchases/controllers/purchase.export_test.go index d93bf108..fad9a84f 100644 --- a/internal/modules/purchases/controllers/purchase.export_test.go +++ b/internal/modules/purchases/controllers/purchase.export_test.go @@ -22,9 +22,8 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) { nil, "catatan", []entity.PurchaseItem{ - buildPurchaseItemForExportTest(11, "Pakan Starter", 1000000, "Location A"), - buildPurchaseItemForExportTest(12, "Vitamin A", 350000, "Location B"), - buildPurchaseItemForExportTest(11, "Pakan Starter", 0, ""), + buildPurchaseItemForExportTest(11, "Pakan Starter", 500, 2, 1000000, "Location A", "kg"), + buildPurchaseItemForExportTest(12, "Vitamin A", 350, 1, 350000, "Location B", "botol"), }, ), buildPurchaseForExportTest( @@ -37,7 +36,7 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) { ptrApprovalAction(entity.ApprovalActionRejected), "", []entity.PurchaseItem{ - buildPurchaseItemForExportTest(21, "Obat X", 75000, ""), + buildPurchaseItemForExportTest(21, "Obat X", 75000, 1, 75000, "", ""), }, ), }) @@ -51,16 +50,27 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) { } defer file.Close() + // Verify all 19 headers expectedHeaders := map[string]string{ "A1": "PR Number", "B1": "PO Number", "C1": "Tanggal PO", - "D1": "Supplier", - "E1": "Lokasi", - "F1": "Status", - "G1": "Grand Total", - "H1": "Products", - "I1": "Notes", + "D1": "Tanggal Terima", + "E1": "Supplier", + "F1": "Lokasi", + "G1": "Gudang", + "H1": "Product", + "I1": "Qty", + "J1": "Satuan", + "K1": "Price", + "L1": "Total Produk", + "M1": "Vendor Ekspedisi", + "N1": "Qty Ekspedisi", + "O1": "Price Ekspedisi", + "P1": "Total Ekspedisi", + "Q1": "Grand Total All", + "R1": "Status", + "S1": "Notes", } for cell, expected := range expectedHeaders { got, err := file.GetCellValue(purchaseExportSheetName, cell) @@ -72,24 +82,46 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) { } } + // Row 2: Purchase 1, Item 1 (Pakan Starter) assertPurchaseCellEquals(t, file, "A2", "PR-00011") assertPurchaseCellEquals(t, file, "B2", "PO-00011") assertPurchaseCellEquals(t, file, "C2", "22-04-2026") - assertPurchaseCellEquals(t, file, "D2", "Supplier A") - assertPurchaseCellEquals(t, file, "E2", "Location A") - assertPurchaseCellEquals(t, file, "F2", "Manager Purchase") - assertPurchaseCellEquals(t, file, "G2", "Rp 1.350.000") - assertPurchaseCellEquals(t, file, "H2", "Pakan Starter, Vitamin A") - assertPurchaseCellEquals(t, file, "I2", "catatan") + assertPurchaseCellEquals(t, file, "E2", "Supplier A") + assertPurchaseCellEquals(t, file, "F2", "Location A") + assertPurchaseCellEquals(t, file, "H2", "Pakan Starter") + assertPurchaseCellEquals(t, file, "J2", "kg") + assertPurchaseCellEquals(t, file, "K2", "500") + assertPurchaseCellEquals(t, file, "L2", "1000000") + assertPurchaseCellEquals(t, file, "M2", "-") + assertPurchaseCellEquals(t, file, "P2", "0") + assertPurchaseCellEquals(t, file, "Q2", "1000000") + assertPurchaseCellEquals(t, file, "R2", "Manager Purchase") + assertPurchaseCellEquals(t, file, "S2", "catatan") - assertPurchaseCellEquals(t, file, "A3", "PR-00012") - assertPurchaseCellEquals(t, file, "B3", "-") - assertPurchaseCellEquals(t, file, "C3", "-") - assertPurchaseCellEquals(t, file, "E3", "-") - assertPurchaseCellEquals(t, file, "F3", "Ditolak") - assertPurchaseCellEquals(t, file, "G3", "Rp 75.000") - assertPurchaseCellEquals(t, file, "H3", "Obat X") - assertPurchaseCellEquals(t, file, "I3", "-") + // Row 3: Purchase 1, Item 2 (Vitamin A) + assertPurchaseCellEquals(t, file, "A3", "PR-00011") + assertPurchaseCellEquals(t, file, "H3", "Vitamin A") + assertPurchaseCellEquals(t, file, "J3", "botol") + assertPurchaseCellEquals(t, file, "L3", "350000") + assertPurchaseCellEquals(t, file, "Q3", "350000") + + // Row 4: Purchase 2, Item 1 (Obat X) — no location, rejected + assertPurchaseCellEquals(t, file, "A4", "PR-00012") + assertPurchaseCellEquals(t, file, "B4", "-") + assertPurchaseCellEquals(t, file, "C4", "-") + assertPurchaseCellEquals(t, file, "F4", "-") + assertPurchaseCellEquals(t, file, "H4", "Obat X") + assertPurchaseCellEquals(t, file, "J4", "-") + assertPurchaseCellEquals(t, file, "L4", "75000") + assertPurchaseCellEquals(t, file, "Q4", "75000") + assertPurchaseCellEquals(t, file, "R4", "Ditolak") + assertPurchaseCellEquals(t, file, "S4", "-") + + // Row 5: SUM row — total produk=1425000, ekspedisi=0, grand total all=1425000 + assertPurchaseCellEquals(t, file, "A5", "TOTAL") + assertPurchaseCellEquals(t, file, "L5", "1425000") + assertPurchaseCellEquals(t, file, "P5", "0") + assertPurchaseCellEquals(t, file, "Q5", "1425000") } func assertPurchaseCellEquals(t *testing.T, file *excelize.File, cell, expected string) { @@ -144,13 +176,20 @@ func buildPurchaseForExportTest( } } -func buildPurchaseItemForExportTest(productID uint, productName string, totalPrice float64, locationName string) entity.PurchaseItem { +func buildPurchaseItemForExportTest(productID uint, productName string, price, totalQty, totalPrice float64, locationName, uomName string) entity.PurchaseItem { + uomID := uint(0) + if uomName != "" { + uomID = productID + 2000 + } item := entity.PurchaseItem{ ProductId: productID, + Price: price, + TotalQty: totalQty, TotalPrice: totalPrice, Product: &entity.Product{ Id: productID, Name: productName, + Uom: entity.Uom{Id: uomID, Name: uomName}, }, } diff --git a/internal/modules/purchases/dto/purchase.dto.go b/internal/modules/purchases/dto/purchase.dto.go index fd1c859a..25336562 100644 --- a/internal/modules/purchases/dto/purchase.dto.go +++ b/internal/modules/purchases/dto/purchase.dto.go @@ -32,12 +32,15 @@ type PurchaseListDTO struct { RequesterName string `json:"requester_name"` PoExpedition []PoExpeditionDTO `json:"po_expedition"` Items []PurchaseItemDTO `json:"items"` - Products []productDTO.ProductRelationDTO `json:"products"` - Location *locationDTO.LocationRelationDTO `json:"location"` - Area *areaDTO.AreaRelationDTO `json:"area"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` + Products []productDTO.ProductRelationDTO `json:"products"` + Location *locationDTO.LocationRelationDTO `json:"location"` + Area *areaDTO.AreaRelationDTO `json:"area"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` + ProductsTotal float64 `json:"products_total"` + ExpeditionTotal float64 `json:"expedition_total"` + GrandTotalAll float64 `json:"grand_total_all"` } type PurchaseDetailDTO struct { @@ -69,6 +72,8 @@ type PurchaseItemDTO struct { VehicleNumber *string `json:"vehicle_number"` TransportPerItem *float64 `json:"transport_per_item,omitempty"` ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"` + ExpeditionQty float64 `json:"expedition_qty"` + ExpeditionTotal float64 `json:"expedition_total"` HasChickin bool `json:"has_chickin"` } @@ -127,6 +132,8 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { if item.ExpenseNonstock != nil { priceCopy := item.ExpenseNonstock.Price dto.TransportPerItem = &priceCopy + dto.ExpeditionQty = item.ExpenseNonstock.Qty + dto.ExpeditionTotal = item.ExpenseNonstock.Qty * item.ExpenseNonstock.Price if item.ExpenseNonstock.Expense != nil { exp := item.ExpenseNonstock.Expense @@ -173,15 +180,21 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO { } var ( - poExpedition = make([]PoExpeditionDTO, 0) - location *locationDTO.LocationRelationDTO - area *areaDTO.AreaRelationDTO - receivedDate *time.Time + poExpedition = make([]PoExpeditionDTO, 0) + location *locationDTO.LocationRelationDTO + area *areaDTO.AreaRelationDTO + receivedDate *time.Time + productsTotal float64 + expeditionTotal float64 ) productMap := make(map[uint]productDTO.ProductRelationDTO) expeditionRefSet := make(map[uint64]struct{}) for i := range p.Items { item := p.Items[i] + productsTotal += item.TotalPrice + if item.ExpenseNonstock != nil { + expeditionTotal += item.ExpenseNonstock.Qty * item.ExpenseNonstock.Price + } if item.Product != nil && item.Product.Id != 0 { if _, exists := productMap[item.Product.Id]; !exists { productMap[item.Product.Id] = productDTO.ToProductRelationDTO(*item.Product) @@ -235,6 +248,9 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO { CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, LatestApproval: latestApproval, + ProductsTotal: productsTotal, + ExpeditionTotal: expeditionTotal, + GrandTotalAll: productsTotal + expeditionTotal, } } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index a0a5284b..1b788be3 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -145,33 +145,16 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti offset := (params.Page - 1) * params.Limit - createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo) - if err != nil { - return nil, 0, utils.BadRequest(err.Error()) - } - productCategoryIDs, err := parseUintCSVFilter(params.ProductCategoryID, "product_category_id") if err != nil { return nil, 0, utils.BadRequest(err.Error()) } - var poDateStart *time.Time - var poDateEnd *time.Time - - if strings.TrimSpace(params.PoDate) != "" { - poDate, parseErr := utils.ParseDateString(strings.TrimSpace(params.PoDate)) - if parseErr != nil { - return nil, 0, utils.BadRequest("po_date must use format YYYY-MM-DD") - } - poDateStart = &poDate - poDateEndValue := poDate.AddDate(0, 0, 1) - poDateEnd = &poDateEndValue - } else { - poDateStart, poDateEnd, err = parsePoDateRangeForQuery(params.PoDateFrom, params.PoDateTo) - if err != nil { - return nil, 0, utils.BadRequest(err.Error()) - } + dateStart, dateEnd, err := parsePurchaseDateRangeForQuery(params.StartDate, params.EndDate, "date") + if err != nil { + return nil, 0, utils.BadRequest(err.Error()) } + filterBy := strings.TrimSpace(params.FilterBy) search := strings.ToLower(strings.TrimSpace(params.Search)) approvalStatuses := parseStringCSVFilter(params.ApprovalStatus) @@ -187,23 +170,41 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti db = db.Where("supplier_id = ?", params.SupplierID) } - if createdFrom != nil { - db = db.Where("created_at >= ?", *createdFrom) - } - - if createdTo != nil { - db = db.Where("created_at < ?", *createdTo) - } - if poDateStart != nil { - db = db.Where("purchases.po_date >= ?", *poDateStart) - } - - if poDateStart != nil { - db = db.Where("purchases.po_date >= ?", *poDateStart) - } - - if poDateEnd != nil { - db = db.Where("purchases.po_date < ?", *poDateEnd) + switch filterBy { + case "po_date": + if dateStart != nil { + db = db.Where("purchases.po_date >= ?", *dateStart) + } + if dateEnd != nil { + db = db.Where("purchases.po_date < ?", *dateEnd) + } + case "due_date": + if dateStart != nil { + db = db.Where("purchases.due_date >= ?", *dateStart) + } + if dateEnd != nil { + db = db.Where("purchases.due_date < ?", *dateEnd) + } + case "received_date": + if dateStart != nil { + db = db.Where( + `EXISTS (SELECT 1 FROM purchase_items pi WHERE pi.purchase_id = purchases.id AND pi.received_date >= ?)`, + *dateStart, + ) + } + if dateEnd != nil { + db = db.Where( + `EXISTS (SELECT 1 FROM purchase_items pi WHERE pi.purchase_id = purchases.id AND pi.received_date < ?)`, + *dateEnd, + ) + } + default: + if dateStart != nil { + db = db.Where("purchases.created_at >= ?", *dateStart) + } + if dateEnd != nil { + db = db.Where("purchases.created_at < ?", *dateEnd) + } } if scope.Restrict { @@ -2238,30 +2239,29 @@ func (s *purchaseService) attachLatestApprovals(ctx context.Context, items []ent return nil } -func parsePoDateRangeForQuery(fromStr, toStr string) (*time.Time, *time.Time, error) { +func parsePurchaseDateRangeForQuery(fromStr, toStr, fieldName string) (*time.Time, *time.Time, error) { var fromPtr *time.Time var toPtr *time.Time if strings.TrimSpace(fromStr) != "" { parsed, err := utils.ParseDateString(strings.TrimSpace(fromStr)) if err != nil { - return nil, nil, errors.New("po_date_from must use format YYYY-MM-DD") + return nil, nil, errors.New(fieldName + "_from must use format YYYY-MM-DD") } - fromValue := parsed - fromPtr = &fromValue + fromPtr = &parsed } if strings.TrimSpace(toStr) != "" { parsed, err := utils.ParseDateString(strings.TrimSpace(toStr)) if err != nil { - return nil, nil, errors.New("po_date_to must use format YYYY-MM-DD") + return nil, nil, errors.New(fieldName + "_to must use format YYYY-MM-DD") } nextDay := parsed.AddDate(0, 0, 1) toPtr = &nextDay } if fromPtr != nil && toPtr != nil && fromPtr.After(*toPtr) { - return nil, nil, errors.New("po_date_from must be earlier than po_date_to") + return nil, nil, errors.New(fieldName + "_from must be earlier than " + fieldName + "_to") } return fromPtr, toPtr, nil diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index 1f49eaca..f891cc81 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -75,12 +75,10 @@ type Query struct { ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` ProductCategoryID string `query:"product_category_id" validate:"omitempty,max=500"` ApprovalStatus string `query:"approval_status" validate:"omitempty,max=500"` - PoDate string `query:"po_date" validate:"omitempty,datetime=2006-01-02"` - PoDateFrom string `query:"po_date_from" validate:"omitempty,datetime=2006-01-02"` - PoDateTo string `query:"po_date_to" validate:"omitempty,datetime=2006-01-02"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=po_date due_date received_date created_at"` Search string `query:"search" validate:"omitempty,max=100"` - CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"` - CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"` SortBy string `query:"sort_by" validate:"omitempty,oneof=po_expedition supplier requester_name products location po_date received_date due_date status created_at po_number"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc ASC DESC"` } diff --git a/internal/modules/repports/controllers/repport.customer_payment.export.go b/internal/modules/repports/controllers/repport.customer_payment.export.go index a0e9e4b0..55dbd667 100644 --- a/internal/modules/repports/controllers/repport.customer_payment.export.go +++ b/internal/modules/repports/controllers/repport.customer_payment.export.go @@ -214,8 +214,7 @@ func writeCustomerPaymentSheet(file *excelize.File, sheet string, item dto.Custo } // Row 2: saldo awal - initialFormatted := formatCPRupiah(item.InitialBalance) - if err := file.SetCellValue(sheet, "N2", initialFormatted); err != nil { + if err := file.SetCellValue(sheet, "N2", item.InitialBalance); err != nil { return err } if item.InitialBalance < 0 { @@ -248,14 +247,14 @@ func writeCustomerPaymentSheet(file *excelize.File, sheet string, item dto.Custo totalRowNum := len(item.Rows) + 3 totalRowStr := fmt.Sprintf("%d", totalRowNum) - totalCells := map[string]string{ + totalCells := map[string]interface{}{ "A": "Total", "G": formatCPIDInteger(item.Summary.TotalQty), "H": formatCPIDInteger(item.Summary.TotalWeight), - "K": formatCPRupiah(item.Summary.TotalFinalAmount), - "L": formatCPRupiah(item.Summary.TotalGrandAmount), - "M": formatCPRupiah(item.Summary.TotalPayment), - "N": formatCPRupiah(item.Summary.TotalAccountsReceivable), + "K": item.Summary.TotalFinalAmount, + "L": item.Summary.TotalGrandAmount, + "M": item.Summary.TotalPayment, + "N": item.Summary.TotalAccountsReceivable, } for col, val := range totalCells { if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil { @@ -369,8 +368,7 @@ func writeCustomerPaymentAllRows(file *excelize.File, sheet string, items []dto. if err := file.SetCellValue(sheet, "A"+saldoStr, name); err != nil { return err } - initialFormatted := formatCPRupiah(item.InitialBalance) - if err := file.SetCellValue(sheet, "O"+saldoStr, initialFormatted); err != nil { + if err := file.SetCellValue(sheet, "O"+saldoStr, item.InitialBalance); err != nil { return err } if err := file.SetCellStyle(sheet, "A"+saldoStr, lastHeaderCol+saldoStr, dataStyle); err != nil { @@ -409,15 +407,15 @@ func writeCustomerPaymentAllRows(file *excelize.File, sheet string, items []dto. // Total row totalStr := fmt.Sprintf("%d", currentRow) - totalCells := map[string]string{ + totalCells := map[string]interface{}{ "A": name, "B": "Total", "H": formatCPIDInteger(item.Summary.TotalQty), "I": formatCPIDInteger(item.Summary.TotalWeight), - "L": formatCPRupiah(item.Summary.TotalFinalAmount), - "M": formatCPRupiah(item.Summary.TotalGrandAmount), - "N": formatCPRupiah(item.Summary.TotalPayment), - "O": formatCPRupiah(item.Summary.TotalAccountsReceivable), + "L": item.Summary.TotalFinalAmount, + "M": item.Summary.TotalGrandAmount, + "N": item.Summary.TotalPayment, + "O": item.Summary.TotalAccountsReceivable, } for col, val := range totalCells { if err := file.SetCellValue(sheet, col+totalStr, val); err != nil { @@ -453,11 +451,11 @@ func customerPaymentRowCells(row dto.CustomerPaymentReportRow, seq int) []interf formatCPIDInteger(row.Qty), formatCPIDInteger(row.Weight), formatCPAvg(row.AverageWeight), - formatCPRupiah(row.UnitPrice), - formatCPRupiah(row.FinalPrice), - formatCPRupiah(row.TotalPrice), - formatCPRupiah(row.PaymentAmount), - formatCPRupiah(row.AccountsReceivable), + row.UnitPrice, + row.FinalPrice, + row.TotalPrice, + row.PaymentAmount, + row.AccountsReceivable, safeCPText(row.Status), joinCPStrings(row.PickupInfo), safeCPText(row.SalesPerson), @@ -546,13 +544,6 @@ func formatCPIDInteger(v float64) string { return b.String() } -func formatCPRupiah(v float64) string { - const nbsp = " " - if v < 0 { - return "-Rp" + nbsp + formatCPIDInteger(-v) - } - return "Rp" + nbsp + formatCPIDInteger(v) -} func formatCPAvg(v float64) string { if v == 0 { diff --git a/internal/modules/repports/controllers/repport.marketing.export.go b/internal/modules/repports/controllers/repport.marketing.export.go index 69a58643..5f52f3dc 100644 --- a/internal/modules/repports/controllers/repport.marketing.export.go +++ b/internal/modules/repports/controllers/repport.marketing.export.go @@ -2,7 +2,6 @@ package controller import ( "fmt" - "math" "strconv" "strings" "time" @@ -197,9 +196,9 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte item.Qty, item.AverageWeightKg, item.TotalWeightKg, - formatMarketingRupiah(item.SalesPricePerKg), - formatMarketingRupiah(item.HppPricePerKg), - formatMarketingRupiah(item.SalesAmount), + item.SalesPricePerKg, + item.HppPricePerKg, + item.SalesAmount, } for colIdx, val := range values { @@ -229,13 +228,13 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte if err := file.SetCellValue(sheet, "N"+totalRow, summary.TotalWeightKg); err != nil { return err } - if err := file.SetCellValue(sheet, "O"+totalRow, formatMarketingRupiah(summary.AverageSalesPrice)); err != nil { + if err := file.SetCellValue(sheet, "O"+totalRow, summary.AverageSalesPrice); err != nil { return err } - if err := file.SetCellValue(sheet, "P"+totalRow, formatMarketingRupiah(summary.TotalHppPricePerKg)); err != nil { + if err := file.SetCellValue(sheet, "P"+totalRow, summary.TotalHppPricePerKg); err != nil { return err } - if err := file.SetCellValue(sheet, "Q"+totalRow, formatMarketingRupiah(float64(summary.TotalSalesAmount))); err != nil { + if err := file.SetCellValue(sheet, "Q"+totalRow, float64(summary.TotalSalesAmount)); err != nil { return err } } @@ -333,30 +332,3 @@ func safeMarketingExportText(value string) string { return trimmed } -// formatMarketingRupiah formats a float64 as Indonesian Rupiah string. -// e.g. 1000000 → "Rp 1.000.000" -func formatMarketingRupiah(value float64) string { - rounded := int64(math.Round(value)) - - negative := rounded < 0 - abs := rounded - if negative { - abs = -rounded - } - - numStr := strconv.FormatInt(abs, 10) - n := len(numStr) - - var b strings.Builder - for i, c := range numStr { - if i > 0 && (n-i)%3 == 0 { - b.WriteByte('.') - } - b.WriteRune(c) - } - - if negative { - return "Rp -" + b.String() - } - return "Rp " + b.String() -}