mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/excel-po-mrk' into 'development'
[FEAT][BE]: add export po and marketing See merge request mbugroup/lti-api!438
This commit is contained in:
@@ -23,6 +23,8 @@ type DeliveryOrdersController struct {
|
|||||||
DeliveryOrdersService service.DeliveryOrdersService
|
DeliveryOrdersService service.DeliveryOrdersService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const marketingExcelExportFetchLimit = 100
|
||||||
|
|
||||||
func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController {
|
func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController {
|
||||||
return &DeliveryOrdersController{
|
return &DeliveryOrdersController{
|
||||||
DeliveryOrdersService: deliveryOrdersService,
|
DeliveryOrdersService: deliveryOrdersService,
|
||||||
@@ -49,26 +51,6 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusOK).Send(content)
|
return c.Status(fiber.StatusOK).Send(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
parseUintListParam := func(param string) ([]uint, error) {
|
|
||||||
if param == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
parts := strings.Split(param, ",")
|
|
||||||
ids := make([]uint, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
trimmed := strings.TrimSpace(part)
|
|
||||||
if trimmed == "" {
|
|
||||||
return nil, strconv.ErrSyntax
|
|
||||||
}
|
|
||||||
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ids = append(ids, uint(parsed))
|
|
||||||
}
|
|
||||||
return ids, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
productIDs, err := parseUintListParam(c.Query("product_ids", ""))
|
productIDs, err := parseUintListParam(c.Query("product_ids", ""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid product_ids")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid product_ids")
|
||||||
@@ -84,6 +66,14 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
|||||||
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isAllExcelExportRequest(c) {
|
||||||
|
allResults, err := u.getAllMarketingRowsForExcel(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return exportMarketingListExcel(c, allResults)
|
||||||
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
}
|
}
|
||||||
@@ -108,6 +98,56 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DeliveryOrdersController) getAllMarketingRowsForExcel(c *fiber.Ctx, baseQuery *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, error) {
|
||||||
|
query := *baseQuery
|
||||||
|
query.Page = 1
|
||||||
|
query.Limit = marketingExcelExportFetchLimit
|
||||||
|
|
||||||
|
results := make([]dto.MarketingListDTO, 0)
|
||||||
|
for {
|
||||||
|
pageResults, total, err := u.DeliveryOrdersService.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 parseUintListParam(param string) ([]uint, error) {
|
||||||
|
if param == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(param, ",")
|
||||||
|
ids := make([]uint, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, strconv.ErrSyntax
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids = append(ids, uint(parsed))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||||
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deliveryOrdersServiceStub struct {
|
||||||
|
getAllCalls []validation.DeliveryOrderQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ service.DeliveryOrdersService = (*deliveryOrdersServiceStub)(nil)
|
||||||
|
|
||||||
|
func (s *deliveryOrdersServiceStub) GetAll(_ *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) {
|
||||||
|
callCopy := *params
|
||||||
|
callCopy.ProductIDs = append([]uint(nil), params.ProductIDs...)
|
||||||
|
s.getAllCalls = append(s.getAllCalls, callCopy)
|
||||||
|
|
||||||
|
switch params.Page {
|
||||||
|
case 1:
|
||||||
|
return []dto.MarketingListDTO{
|
||||||
|
buildMarketingListForControllerTest("SO-00001"),
|
||||||
|
buildMarketingListForControllerTest("SO-00002"),
|
||||||
|
}, 3, nil
|
||||||
|
case 2:
|
||||||
|
return []dto.MarketingListDTO{
|
||||||
|
buildMarketingListForControllerTest("SO-00003"),
|
||||||
|
}, 3, nil
|
||||||
|
default:
|
||||||
|
return []dto.MarketingListDTO{}, 3, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deliveryOrdersServiceStub) GetOne(_ *fiber.Ctx, _ uint) (*dto.MarketingDetailDTO, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deliveryOrdersServiceStub) CreateOne(_ *fiber.Ctx, _ *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deliveryOrdersServiceStub) UpdateOne(_ *fiber.Ctx, _ *validation.DeliveryOrderUpdate, _ uint) (*dto.MarketingDetailDTO, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deliveryOrdersServiceStub) BulkApproveToStatus(_ *fiber.Ctx, _ *validation.BulkApprovalRequest, _ approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deliveryOrdersServiceStub) GetProgressRows(_ *fiber.Ctx, _ *exportprogress.Query) ([]exportprogress.Row, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryOrdersControllerGetAllExportAllIgnoresRequestLimit(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
stub := &deliveryOrdersServiceStub{}
|
||||||
|
ctrl := NewDeliveryOrdersController(stub)
|
||||||
|
app.Get("/marketing", ctrl.GetAll)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/marketing?export=excel&type=all&page=9&limit=1&search=delivery&status=delivery_order&product_ids=1,2&customer_id=7&marketing_id=99",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected app.Test error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if !strings.Contains(contentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
t.Fatalf("unexpected content-type: %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
disposition := resp.Header.Get("Content-Disposition")
|
||||||
|
if !strings.Contains(disposition, "marketings_all_") {
|
||||||
|
t.Fatalf("unexpected content-disposition: %s", disposition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stub.getAllCalls) != 2 {
|
||||||
|
t.Fatalf("expected 2 GetAll calls, got %d", len(stub.getAllCalls))
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCall := stub.getAllCalls[0]
|
||||||
|
secondCall := stub.getAllCalls[1]
|
||||||
|
|
||||||
|
if firstCall.Page != 1 || secondCall.Page != 2 {
|
||||||
|
t.Fatalf("expected internal paging to use pages 1 and 2, got %d and %d", firstCall.Page, secondCall.Page)
|
||||||
|
}
|
||||||
|
if firstCall.Limit != marketingExcelExportFetchLimit || secondCall.Limit != marketingExcelExportFetchLimit {
|
||||||
|
t.Fatalf("expected internal limit %d, got %d and %d", marketingExcelExportFetchLimit, firstCall.Limit, secondCall.Limit)
|
||||||
|
}
|
||||||
|
if firstCall.Status != "delivery order" {
|
||||||
|
t.Fatalf("expected status to normalize underscore to space, got %q", firstCall.Status)
|
||||||
|
}
|
||||||
|
if firstCall.Search != "delivery" {
|
||||||
|
t.Fatalf("expected search to be forwarded, got %q", firstCall.Search)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(firstCall.ProductIDs, []uint{1, 2}) {
|
||||||
|
t.Fatalf("unexpected product_ids: %+v", firstCall.ProductIDs)
|
||||||
|
}
|
||||||
|
if firstCall.CustomerId != 7 || firstCall.MarketingId != 99 {
|
||||||
|
t.Fatalf("expected customer_id=7 and marketing_id=99, got customer_id=%d marketing_id=%d", firstCall.CustomerId, firstCall.MarketingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read excel payload: %v", err)
|
||||||
|
}
|
||||||
|
file, err := excelize.OpenReader(bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse excel payload: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if got, _ := file.GetCellValue(marketingExportSheetName, "A1"); got != "No. Order" {
|
||||||
|
t.Fatalf("expected A1 header to be No. Order, got %q", got)
|
||||||
|
}
|
||||||
|
if got, _ := file.GetCellValue(marketingExportSheetName, "A2"); got != "SO-00001" {
|
||||||
|
t.Fatalf("expected first row order number SO-00001, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryOrdersControllerGetAllKeepsPaginationValidationForNonExportAll(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
stub := &deliveryOrdersServiceStub{}
|
||||||
|
ctrl := NewDeliveryOrdersController(stub)
|
||||||
|
app.Get("/marketing", ctrl.GetAll)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/marketing?page=1&limit=0", nil)
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected app.Test error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != fiber.StatusBadRequest {
|
||||||
|
t.Fatalf("expected status 400, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMarketingListForControllerTest(orderNumber string) dto.MarketingListDTO {
|
||||||
|
return dto.MarketingListDTO{
|
||||||
|
MarketingRelationDTO: dto.MarketingRelationDTO{
|
||||||
|
SoNumber: orderNumber,
|
||||||
|
SoDate: time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC),
|
||||||
|
Notes: "tes",
|
||||||
|
},
|
||||||
|
Customer: customerDTO.CustomerRelationDTO{
|
||||||
|
Name: "AJAT",
|
||||||
|
},
|
||||||
|
SalesOrder: []dto.DeliveryMarketingProductDTO{
|
||||||
|
{TotalPrice: 5206200000},
|
||||||
|
},
|
||||||
|
LatestApproval: approvalDTO.ApprovalRelationDTO{
|
||||||
|
StepName: "Pengajuan",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const marketingExportSheetName = "Marketings"
|
||||||
|
|
||||||
|
func isAllExcelExportRequest(c *fiber.Ctx) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") &&
|
||||||
|
strings.EqualFold(strings.TrimSpace(c.Query("type")), "all")
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportMarketingListExcel(c *fiber.Ctx, items []dto.MarketingListDTO) error {
|
||||||
|
content, err := buildMarketingExportWorkbook(items)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("marketings_all_%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 buildMarketingExportWorkbook(items []dto.MarketingListDTO) ([]byte, error) {
|
||||||
|
file := excelize.NewFile()
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
||||||
|
if defaultSheet != marketingExportSheetName {
|
||||||
|
if err := file.SetSheetName(defaultSheet, marketingExportSheetName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setMarketingExportColumns(file, marketingExportSheetName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := setMarketingExportHeaders(file, marketingExportSheetName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := setMarketingExportRows(file, marketingExportSheetName, items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := file.SetPanes(marketingExportSheetName, &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 setMarketingExportColumns(file *excelize.File, sheet string) error {
|
||||||
|
columnWidths := map[string]float64{
|
||||||
|
"A": 16,
|
||||||
|
"B": 14,
|
||||||
|
"C": 18,
|
||||||
|
"D": 20,
|
||||||
|
"E": 18,
|
||||||
|
"F": 60,
|
||||||
|
"G": 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
for col, width := range columnWidths {
|
||||||
|
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 setMarketingExportHeaders(file *excelize.File, sheet string) error {
|
||||||
|
headers := []string{
|
||||||
|
"No. Order",
|
||||||
|
"Tanggal",
|
||||||
|
"Status",
|
||||||
|
"Customer",
|
||||||
|
"Grand Total",
|
||||||
|
"Products",
|
||||||
|
"Notes",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, header := range headers {
|
||||||
|
colName, err := excelize.ColumnNumberToName(i + 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cell := colName + "1"
|
||||||
|
if err := file.SetCellValue(sheet, cell, 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", "G1", headerStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMarketingExportRows(file *excelize.File, sheet string, items []dto.MarketingListDTO) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range items {
|
||||||
|
rowNumber := i + 2
|
||||||
|
if err := file.SetCellValue(sheet, "A"+strconv.Itoa(rowNumber), safeMarketingExportText(item.SoNumber)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "B"+strconv.Itoa(rowNumber), formatMarketingExportDate(item.SoDate)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "C"+strconv.Itoa(rowNumber), formatMarketingExportStatus(item)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "F"+strconv.Itoa(rowNumber), formatMarketingProducts(item.SalesOrder)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "G"+strconv.Itoa(rowNumber), safeMarketingExportText(item.Notes)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRow := len(items) + 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", "G"+strconv.Itoa(lastRow), dataStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
moneyStyle, 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, "E2", "E"+strconv.Itoa(lastRow), moneyStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMarketingExportDate(value time.Time) string {
|
||||||
|
if value.IsZero() {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err == nil {
|
||||||
|
value = value.In(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Format("02-01-2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMarketingExportStatus(item dto.MarketingListDTO) string {
|
||||||
|
if item.LatestApproval.Action != nil && strings.EqualFold(strings.TrimSpace(*item.LatestApproval.Action), string(entity.ApprovalActionRejected)) {
|
||||||
|
return "Ditolak"
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeMarketingExportText(item.LatestApproval.StepName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMarketingProducts(items []dto.DeliveryMarketingProductDTO) string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
names := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ProductWarehouse == nil || item.ProductWarehouse.Product == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(item.ProductWarehouse.Product.Name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := seen[name]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 {
|
||||||
|
total := 0.0
|
||||||
|
for _, item := range items {
|
||||||
|
total += item.TotalPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildMarketingExportWorkbookHeadersAndRows(t *testing.T) {
|
||||||
|
items := []dto.MarketingListDTO{
|
||||||
|
{
|
||||||
|
MarketingRelationDTO: dto.MarketingRelationDTO{
|
||||||
|
SoNumber: "SO-00762",
|
||||||
|
SoDate: time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC),
|
||||||
|
Notes: "tes",
|
||||||
|
},
|
||||||
|
Customer: customerDTO.CustomerRelationDTO{
|
||||||
|
Name: "AJAT",
|
||||||
|
},
|
||||||
|
SalesOrder: []dto.DeliveryMarketingProductDTO{
|
||||||
|
buildMarketingProductForExportTest("PAKAN GROWING CRUMBLE 8603 MALINDO", 5206200000),
|
||||||
|
buildMarketingProductForExportTest("PAKAN GROWING CRUMBLE 8603 MALINDO", 0),
|
||||||
|
buildMarketingProductForExportTest("295 GOLD PELLET", 0),
|
||||||
|
},
|
||||||
|
LatestApproval: approvalDTO.ApprovalRelationDTO{
|
||||||
|
StepName: "Pengajuan",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MarketingRelationDTO: dto.MarketingRelationDTO{
|
||||||
|
SoNumber: "SO-00761",
|
||||||
|
SoDate: time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC),
|
||||||
|
Notes: "",
|
||||||
|
},
|
||||||
|
Customer: customerDTO.CustomerRelationDTO{
|
||||||
|
Name: "DHENIS",
|
||||||
|
},
|
||||||
|
SalesOrder: []dto.DeliveryMarketingProductDTO{
|
||||||
|
buildMarketingProductForExportTest("HS30 FOAM @20 LITER", 75000),
|
||||||
|
},
|
||||||
|
LatestApproval: approvalDTO.ApprovalRelationDTO{
|
||||||
|
StepName: "Delivery Order",
|
||||||
|
Action: strPtr("REJECTED"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := buildMarketingExportWorkbook(items)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildMarketingExportWorkbook returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := excelize.OpenReader(bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open workbook bytes: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
expectedHeaders := map[string]string{
|
||||||
|
"A1": "No. Order",
|
||||||
|
"B1": "Tanggal",
|
||||||
|
"C1": "Status",
|
||||||
|
"D1": "Customer",
|
||||||
|
"E1": "Grand Total",
|
||||||
|
"F1": "Products",
|
||||||
|
"G1": "Notes",
|
||||||
|
}
|
||||||
|
for cell, expected := range expectedHeaders {
|
||||||
|
got, err := file.GetCellValue(marketingExportSheetName, cell)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCellValue(%s) failed: %v", cell, err)
|
||||||
|
}
|
||||||
|
if got != expected {
|
||||||
|
t.Fatalf("expected %s=%q, got %q", cell, expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertCellEquals(t, file, "A2", "SO-00762")
|
||||||
|
assertCellEquals(t, file, "B2", "22-04-2026")
|
||||||
|
assertCellEquals(t, file, "C2", "Pengajuan")
|
||||||
|
assertCellEquals(t, file, "D2", "AJAT")
|
||||||
|
assertCellEquals(t, file, "E2", "Rp 5.206.200.000")
|
||||||
|
assertCellEquals(t, file, "F2", "PAKAN GROWING CRUMBLE 8603 MALINDO, 295 GOLD PELLET")
|
||||||
|
assertCellEquals(t, file, "G2", "tes")
|
||||||
|
|
||||||
|
assertCellEquals(t, file, "A3", "SO-00761")
|
||||||
|
assertCellEquals(t, file, "C3", "Ditolak")
|
||||||
|
assertCellEquals(t, file, "E3", "Rp 75.000")
|
||||||
|
assertCellEquals(t, file, "F3", "HS30 FOAM @20 LITER")
|
||||||
|
assertCellEquals(t, file, "G3", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertCellEquals(t *testing.T, file *excelize.File, cell, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
got, err := file.GetCellValue(marketingExportSheetName, cell)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCellValue(%s) failed: %v", cell, err)
|
||||||
|
}
|
||||||
|
if got != expected {
|
||||||
|
t.Fatalf("expected %s=%q, got %q", cell, expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMarketingProductForExportTest(name string, totalPrice float64) dto.DeliveryMarketingProductDTO {
|
||||||
|
return dto.DeliveryMarketingProductDTO{
|
||||||
|
TotalPrice: totalPrice,
|
||||||
|
ProductWarehouse: &productwarehouseDTO.ProductWarehousNestedDTO{
|
||||||
|
Product: &productDTO.ProductRelationDTO{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func strPtr(value string) *string {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
@@ -24,6 +25,8 @@ type PurchaseController struct {
|
|||||||
service service.PurchaseService
|
service service.PurchaseService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const purchaseExcelExportFetchLimit = 100
|
||||||
|
|
||||||
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
|
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
|
||||||
return &PurchaseController{service: s}
|
return &PurchaseController{service: s}
|
||||||
}
|
}
|
||||||
@@ -48,20 +51,14 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusOK).Send(content)
|
return c.Status(fiber.StatusOK).Send(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := &validation.Query{
|
query := buildPurchaseQuery(c)
|
||||||
Page: c.QueryInt("page", 1),
|
|
||||||
Limit: c.QueryInt("limit", 10),
|
if isAllPurchaseExcelExportRequest(c) {
|
||||||
Search: strings.TrimSpace(c.Query("search")),
|
results, err := ctrl.getAllPurchasesForExcel(c, query)
|
||||||
ApprovalStatus: strings.TrimSpace(c.Query("approval_status")),
|
if err != nil {
|
||||||
PoDate: strings.TrimSpace(c.Query("po_date")),
|
return err
|
||||||
PoDateFrom: strings.TrimSpace(c.Query("po_date_from")),
|
}
|
||||||
PoDateTo: strings.TrimSpace(c.Query("po_date_to")),
|
return exportPurchaseListExcel(c, results)
|
||||||
CreatedFrom: strings.TrimSpace(c.Query("created_from")),
|
|
||||||
CreatedTo: strings.TrimSpace(c.Query("created_to")),
|
|
||||||
SupplierID: uint(c.QueryInt("supplier_id", 0)),
|
|
||||||
AreaID: uint(c.QueryInt("area_id", 0)),
|
|
||||||
LocationID: uint(c.QueryInt("location_id", 0)),
|
|
||||||
ProductCategoryID: strings.TrimSpace(c.Query("product_category_id")),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
@@ -88,6 +85,51 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildPurchaseQuery(c *fiber.Ctx) *validation.Query {
|
||||||
|
return &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
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")),
|
||||||
|
SupplierID: uint(c.QueryInt("supplier_id", 0)),
|
||||||
|
AreaID: uint(c.QueryInt("area_id", 0)),
|
||||||
|
LocationID: uint(c.QueryInt("location_id", 0)),
|
||||||
|
ProductCategoryID: strings.TrimSpace(c.Query("product_category_id")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) getAllPurchasesForExcel(c *fiber.Ctx, baseQuery *validation.Query) ([]entity.Purchase, error) {
|
||||||
|
query := *baseQuery
|
||||||
|
query.Page = 1
|
||||||
|
query.Limit = purchaseExcelExportFetchLimit
|
||||||
|
|
||||||
|
results := make([]entity.Purchase, 0)
|
||||||
|
for {
|
||||||
|
pageResults, total, err := ctrl.service.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 (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error {
|
func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type purchaseServiceStub struct {
|
||||||
|
getAllCalls []validation.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ service.PurchaseService = (*purchaseServiceStub)(nil)
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) GetAll(_ *fiber.Ctx, params *validation.Query) ([]entity.Purchase, int64, error) {
|
||||||
|
callCopy := *params
|
||||||
|
s.getAllCalls = append(s.getAllCalls, callCopy)
|
||||||
|
|
||||||
|
switch params.Page {
|
||||||
|
case 1:
|
||||||
|
return []entity.Purchase{
|
||||||
|
buildPurchaseForControllerTest(1, "PR-00001"),
|
||||||
|
buildPurchaseForControllerTest(2, "PR-00002"),
|
||||||
|
}, 3, nil
|
||||||
|
case 2:
|
||||||
|
return []entity.Purchase{
|
||||||
|
buildPurchaseForControllerTest(3, "PR-00003"),
|
||||||
|
}, 3, nil
|
||||||
|
default:
|
||||||
|
return []entity.Purchase{}, 3, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) GetOne(_ *fiber.Ctx, _ uint) (*entity.Purchase, error) {
|
||||||
|
return &entity.Purchase{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) CreateOne(_ *fiber.Ctx, _ *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
||||||
|
return &entity.Purchase{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) ApproveStaffPurchase(_ *fiber.Ctx, _ uint, _ *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) {
|
||||||
|
return &entity.Purchase{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) ApproveManagerPurchase(_ *fiber.Ctx, _ uint, _ *validation.ApproveManagerPurchaseRequest) (*entity.Purchase, error) {
|
||||||
|
return &entity.Purchase{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) ReceiveProducts(_ *fiber.Ctx, _ uint, _ *validation.ReceivePurchaseRequest) (*entity.Purchase, error) {
|
||||||
|
return &entity.Purchase{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) DeleteItems(_ *fiber.Ctx, _ uint, _ *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
|
||||||
|
return &entity.Purchase{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) DeletePurchase(_ *fiber.Ctx, _ uint) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseServiceStub) GetProgressRows(_ *fiber.Ctx, _ *exportprogress.Query) ([]exportprogress.Row, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPurchaseControllerGetAllExportAllIgnoresRequestLimit(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
stub := &purchaseServiceStub{}
|
||||||
|
ctrl := NewPurchaseController(stub)
|
||||||
|
app.Get("/purchases", ctrl.GetAll)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/purchases?export=excel&type=all&page=9&limit=1&search=po&supplier_id=7&area_id=4&location_id=2&product_category_id=1,2&approval_status=pending&po_date_from=2026-01-01&po_date_to=2026-01-31&created_from=2026-02-01&created_to=2026-02-20",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected app.Test error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if !strings.Contains(contentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
t.Fatalf("unexpected content-type: %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
disposition := resp.Header.Get("Content-Disposition")
|
||||||
|
if !strings.Contains(disposition, "purchases_all_") {
|
||||||
|
t.Fatalf("unexpected content-disposition: %s", disposition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stub.getAllCalls) != 2 {
|
||||||
|
t.Fatalf("expected 2 GetAll calls, got %d", len(stub.getAllCalls))
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCall := stub.getAllCalls[0]
|
||||||
|
secondCall := stub.getAllCalls[1]
|
||||||
|
if firstCall.Page != 1 || secondCall.Page != 2 {
|
||||||
|
t.Fatalf("expected internal paging page 1 and 2, got %d and %d", firstCall.Page, secondCall.Page)
|
||||||
|
}
|
||||||
|
if firstCall.Limit != purchaseExcelExportFetchLimit || secondCall.Limit != purchaseExcelExportFetchLimit {
|
||||||
|
t.Fatalf("expected internal limit %d, got %d and %d", purchaseExcelExportFetchLimit, firstCall.Limit, secondCall.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstCall.Search != "po" ||
|
||||||
|
firstCall.SupplierID != 7 ||
|
||||||
|
firstCall.AreaID != 4 ||
|
||||||
|
firstCall.LocationID != 2 ||
|
||||||
|
firstCall.ProductCategoryID != "1,2" ||
|
||||||
|
firstCall.ApprovalStatus != "pending" ||
|
||||||
|
firstCall.PoDateFrom != "2026-01-01" ||
|
||||||
|
firstCall.PoDateTo != "2026-01-31" ||
|
||||||
|
firstCall.CreatedFrom != "2026-02-01" ||
|
||||||
|
firstCall.CreatedTo != "2026-02-20" {
|
||||||
|
t.Fatalf("unexpected forwarded filters: %+v", firstCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read excel payload: %v", err)
|
||||||
|
}
|
||||||
|
file, err := excelize.OpenReader(bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse excel payload: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if got, _ := file.GetCellValue(purchaseExportSheetName, "A1"); got != "PR Number" {
|
||||||
|
t.Fatalf("expected A1 header to be PR Number, got %q", got)
|
||||||
|
}
|
||||||
|
if got, _ := file.GetCellValue(purchaseExportSheetName, "A2"); got != "PR-00001" {
|
||||||
|
t.Fatalf("expected first row PR-00001, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPurchaseControllerGetAllKeepsPaginationValidationForNonExportAll(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
stub := &purchaseServiceStub{}
|
||||||
|
ctrl := NewPurchaseController(stub)
|
||||||
|
app.Get("/purchases", ctrl.GetAll)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/purchases?page=1&limit=0", nil)
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected app.Test error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != fiber.StatusBadRequest {
|
||||||
|
t.Fatalf("expected status 400, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPurchaseForControllerTest(id uint, prNumber string) entity.Purchase {
|
||||||
|
poNumber := "PO-" + strings.TrimPrefix(prNumber, "PR-")
|
||||||
|
poDate := time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC)
|
||||||
|
notes := "catatan"
|
||||||
|
approvalAction := entity.ApprovalActionApproved
|
||||||
|
|
||||||
|
return entity.Purchase{
|
||||||
|
Id: id,
|
||||||
|
PrNumber: prNumber,
|
||||||
|
PoNumber: &poNumber,
|
||||||
|
PoDate: &poDate,
|
||||||
|
Notes: ¬es,
|
||||||
|
Supplier: entity.Supplier{
|
||||||
|
Id: 10,
|
||||||
|
Name: "Supplier A",
|
||||||
|
},
|
||||||
|
LatestApproval: &entity.Approval{
|
||||||
|
Id: 1,
|
||||||
|
StepName: "Manager Purchase",
|
||||||
|
Action: &approvalAction,
|
||||||
|
},
|
||||||
|
Items: []entity.PurchaseItem{
|
||||||
|
{
|
||||||
|
Id: id*10 + 1,
|
||||||
|
TotalPrice: 1000000,
|
||||||
|
Product: &entity.Product{
|
||||||
|
Id: id*100 + 1,
|
||||||
|
Name: "Pakan Starter",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const purchaseExportSheetName = "Purchases"
|
||||||
|
|
||||||
|
func isAllPurchaseExcelExportRequest(c *fiber.Ctx) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") &&
|
||||||
|
strings.EqualFold(strings.TrimSpace(c.Query("type")), "all")
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportPurchaseListExcel(c *fiber.Ctx, purchases []entity.Purchase) error {
|
||||||
|
content, err := buildPurchaseExportWorkbook(purchases)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("purchases_all_%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 buildPurchaseExportWorkbook(purchases []entity.Purchase) ([]byte, error) {
|
||||||
|
file := excelize.NewFile()
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
||||||
|
if defaultSheet != purchaseExportSheetName {
|
||||||
|
if err := file.SetSheetName(defaultSheet, purchaseExportSheetName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listItems := dto.ToPurchaseListDTOs(purchases)
|
||||||
|
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, listItems, grandTotals); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := file.SetPanes(purchaseExportSheetName, &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 setPurchaseExportColumns(file *excelize.File, sheet string) error {
|
||||||
|
columnWidths := map[string]float64{
|
||||||
|
"A": 16,
|
||||||
|
"B": 16,
|
||||||
|
"C": 14,
|
||||||
|
"D": 22,
|
||||||
|
"E": 18,
|
||||||
|
"F": 18,
|
||||||
|
"G": 52,
|
||||||
|
"H": 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
for col, width := range columnWidths {
|
||||||
|
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 setPurchaseExportHeaders(file *excelize.File, sheet string) error {
|
||||||
|
headers := []string{
|
||||||
|
"PR Number",
|
||||||
|
"PO Number",
|
||||||
|
"Tanggal PO",
|
||||||
|
"Supplier",
|
||||||
|
"Status",
|
||||||
|
"Grand Total",
|
||||||
|
"Products",
|
||||||
|
"Notes",
|
||||||
|
}
|
||||||
|
|
||||||
|
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", "H1", headerStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.PurchaseListDTO, grandTotals map[uint]float64) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range items {
|
||||||
|
row := strconv.Itoa(i + 2)
|
||||||
|
if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(item.PrNumber)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "B"+row, safePurchaseExportPointerText(item.PoNumber)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(item.PoDate)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "D"+row, safePurchaseSupplierName(item)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "E"+row, formatPurchaseExportStatus(item)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "F"+row, formatPurchaseRupiah(grandTotals[item.Id])); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "G"+row, formatPurchaseProducts(item)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.SetCellValue(sheet, "H"+row, safePurchaseExportPointerText(item.Notes)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRow := len(items) + 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", "H"+strconv.Itoa(lastRow), dataStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
moneyStyle, 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, "F2", "F"+strconv.Itoa(lastRow), moneyStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func safePurchaseSupplierName(item dto.PurchaseListDTO) string {
|
||||||
|
if item.Supplier == nil {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return safePurchaseExportText(item.Supplier.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPurchaseExportStatus(item dto.PurchaseListDTO) string {
|
||||||
|
if item.LatestApproval == nil {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.LatestApproval.Action != nil &&
|
||||||
|
strings.EqualFold(strings.TrimSpace(*item.LatestApproval.Action), string(entity.ApprovalActionRejected)) {
|
||||||
|
return "Ditolak"
|
||||||
|
}
|
||||||
|
|
||||||
|
return safePurchaseExportText(item.LatestApproval.StepName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPurchaseExportDate(value *time.Time) string {
|
||||||
|
if value == nil || value.IsZero() {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
t := *value
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err == nil {
|
||||||
|
t = t.In(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Format("02-01-2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPurchaseProducts(item dto.PurchaseListDTO) string {
|
||||||
|
if len(item.Products) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
names := make([]string, 0, len(item.Products))
|
||||||
|
for i := range item.Products {
|
||||||
|
name := strings.TrimSpace(item.Products[i].Name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[name]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(names)
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func safePurchaseExportPointerText(value *string) string {
|
||||||
|
if value == nil {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return safePurchaseExportText(*value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func safePurchaseExportText(value string) string {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
|
||||||
|
content, err := buildPurchaseExportWorkbook([]entity.Purchase{
|
||||||
|
buildPurchaseForExportTest(
|
||||||
|
1,
|
||||||
|
"PR-00011",
|
||||||
|
"PO-00011",
|
||||||
|
time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC),
|
||||||
|
"Supplier A",
|
||||||
|
"Manager Purchase",
|
||||||
|
nil,
|
||||||
|
"catatan",
|
||||||
|
[]entity.PurchaseItem{
|
||||||
|
buildPurchaseItemForExportTest(11, "Pakan Starter", 1000000),
|
||||||
|
buildPurchaseItemForExportTest(12, "Vitamin A", 350000),
|
||||||
|
buildPurchaseItemForExportTest(11, "Pakan Starter", 0),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
buildPurchaseForExportTest(
|
||||||
|
2,
|
||||||
|
"PR-00012",
|
||||||
|
"",
|
||||||
|
time.Time{},
|
||||||
|
"Supplier B",
|
||||||
|
"Manager Purchase",
|
||||||
|
ptrApprovalAction(entity.ApprovalActionRejected),
|
||||||
|
"",
|
||||||
|
[]entity.PurchaseItem{
|
||||||
|
buildPurchaseItemForExportTest(21, "Obat X", 75000),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildPurchaseExportWorkbook returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := excelize.OpenReader(bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open workbook bytes: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
expectedHeaders := map[string]string{
|
||||||
|
"A1": "PR Number",
|
||||||
|
"B1": "PO Number",
|
||||||
|
"C1": "Tanggal PO",
|
||||||
|
"D1": "Supplier",
|
||||||
|
"E1": "Status",
|
||||||
|
"F1": "Grand Total",
|
||||||
|
"G1": "Products",
|
||||||
|
"H1": "Notes",
|
||||||
|
}
|
||||||
|
for cell, expected := range expectedHeaders {
|
||||||
|
got, err := file.GetCellValue(purchaseExportSheetName, cell)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCellValue(%s) failed: %v", cell, err)
|
||||||
|
}
|
||||||
|
if got != expected {
|
||||||
|
t.Fatalf("expected %s=%q, got %q", cell, expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", "Manager Purchase")
|
||||||
|
assertPurchaseCellEquals(t, file, "F2", "Rp 1.350.000")
|
||||||
|
assertPurchaseCellEquals(t, file, "G2", "Pakan Starter, Vitamin A")
|
||||||
|
assertPurchaseCellEquals(t, file, "H2", "catatan")
|
||||||
|
|
||||||
|
assertPurchaseCellEquals(t, file, "A3", "PR-00012")
|
||||||
|
assertPurchaseCellEquals(t, file, "B3", "-")
|
||||||
|
assertPurchaseCellEquals(t, file, "C3", "-")
|
||||||
|
assertPurchaseCellEquals(t, file, "E3", "Ditolak")
|
||||||
|
assertPurchaseCellEquals(t, file, "F3", "Rp 75.000")
|
||||||
|
assertPurchaseCellEquals(t, file, "G3", "Obat X")
|
||||||
|
assertPurchaseCellEquals(t, file, "H3", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertPurchaseCellEquals(t *testing.T, file *excelize.File, cell, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
got, err := file.GetCellValue(purchaseExportSheetName, cell)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCellValue(%s) failed: %v", cell, err)
|
||||||
|
}
|
||||||
|
if got != expected {
|
||||||
|
t.Fatalf("expected %s=%q, got %q", cell, expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPurchaseForExportTest(
|
||||||
|
id uint,
|
||||||
|
prNumber, poNumber string,
|
||||||
|
poDate time.Time,
|
||||||
|
supplierName, stepName string,
|
||||||
|
action *entity.ApprovalAction,
|
||||||
|
notes string,
|
||||||
|
items []entity.PurchaseItem,
|
||||||
|
) entity.Purchase {
|
||||||
|
var poNumberRef *string
|
||||||
|
if poNumber != "" {
|
||||||
|
poNumberRef = &poNumber
|
||||||
|
}
|
||||||
|
var poDateRef *time.Time
|
||||||
|
if !poDate.IsZero() {
|
||||||
|
poDateRef = &poDate
|
||||||
|
}
|
||||||
|
var notesRef *string
|
||||||
|
if notes != "" {
|
||||||
|
notesRef = ¬es
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity.Purchase{
|
||||||
|
Id: id,
|
||||||
|
PrNumber: prNumber,
|
||||||
|
PoNumber: poNumberRef,
|
||||||
|
PoDate: poDateRef,
|
||||||
|
Notes: notesRef,
|
||||||
|
Supplier: entity.Supplier{
|
||||||
|
Id: id + 100,
|
||||||
|
Name: supplierName,
|
||||||
|
},
|
||||||
|
LatestApproval: &entity.Approval{
|
||||||
|
Id: id + 1000,
|
||||||
|
StepName: stepName,
|
||||||
|
Action: action,
|
||||||
|
},
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPurchaseItemForExportTest(productID uint, productName string, totalPrice float64) entity.PurchaseItem {
|
||||||
|
return entity.PurchaseItem{
|
||||||
|
ProductId: productID,
|
||||||
|
TotalPrice: totalPrice,
|
||||||
|
Product: &entity.Product{
|
||||||
|
Id: productID,
|
||||||
|
Name: productName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrApprovalAction(value entity.ApprovalAction) *entity.ApprovalAction {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user