diff --git a/internal/modules/closings/dto/closingSapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go index 4c5db68d..a169a816 100644 --- a/internal/modules/closings/dto/closingSapronak.dto.go +++ b/internal/modules/closings/dto/closingSapronak.dto.go @@ -153,6 +153,20 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin } return normalized } + normalizeCutOverToken := func(raw string) string { + normalized := strings.ToUpper(strings.TrimSpace(raw)) + normalized = strings.ReplaceAll(normalized, "-", "") + normalized = strings.ReplaceAll(normalized, " ", "") + return normalized + } + containsCutOver := func(values ...string) bool { + for _, value := range values { + if strings.Contains(normalizeCutOverToken(value), "CUTOVER") { + return true + } + } + return false + } filter := normalizeFlag(flag) byFlag := map[string]**SapronakCategoryDTO{} @@ -258,6 +272,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin UnitPrice: item.Harga, Notes: "-", } + isCutOver := containsCutOver(baseRow.ProductCategory, baseRow.Description, item.ProductName) row := getOrCreateRow(productKey, baseRow) @@ -289,11 +304,21 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin } row.QtyUsed += item.QtyKeluar row.TotalAmount += item.QtyKeluar * price - case "adjustment keluar", "mutasi keluar", "penjualan": + case "adjustment keluar": price := row.UnitPrice if price == 0 { price = item.Harga } + if row.UnitPrice == 0 { + row.UnitPrice = price + } + if isCutOver { + row.QtyUsed += item.QtyKeluar + row.TotalAmount += item.QtyKeluar * price + continue + } + row.QtyOut += item.QtyKeluar + case "mutasi keluar", "penjualan": row.QtyOut += item.QtyKeluar if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" { ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi)) diff --git a/internal/modules/closings/dto/closingSapronak.dto_test.go b/internal/modules/closings/dto/closingSapronak.dto_test.go new file mode 100644 index 00000000..0aabe5a3 --- /dev/null +++ b/internal/modules/closings/dto/closingSapronak.dto_test.go @@ -0,0 +1,124 @@ +package dto + +import "testing" + +func TestToSapronakProjectAggregatedFromReportMovesCutOverAdjustmentOutToQtyUsed(t *testing.T) { + tests := []struct { + name string + groupFlag string + filter string + productFlag string + }{ + { + name: "pakan cut-over", + groupFlag: "PAKAN", + filter: "PAKAN", + productFlag: "PAKAN CUT-OVER", + }, + { + name: "ovk cut over", + groupFlag: "OVK", + filter: "OVK", + productFlag: "OVK CUT OVER", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + report := &SapronakReportDTO{ + Groups: []SapronakGroupDTO{ + { + Flag: tc.groupFlag, + Items: []SapronakDetailDTO{ + { + ProductID: 1, + ProductName: "CUTOVER ITEM", + NoReferensi: "ADJ-CUT-01", + JenisTransaksi: "Adjustment Keluar", + QtyKeluar: 5, + Harga: 15000, + }, + }, + }, + }, + } + + result := ToSapronakProjectAggregatedFromReport( + report, + tc.filter, + map[uint][]string{ + 1: {tc.productFlag}, + }, + ) + + var cat *SapronakCategoryDTO + if tc.groupFlag == "PAKAN" { + cat = result.Pakan + } else { + cat = result.Ovk + } + if cat == nil { + t.Fatalf("expected category payload for %s", tc.groupFlag) + } + if len(cat.Rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(cat.Rows)) + } + + row := cat.Rows[0] + if row.QtyOut != 0 { + t.Fatalf("expected qty_out 0 for cut-over adjustment, got %.2f", row.QtyOut) + } + if row.QtyUsed != 5 { + t.Fatalf("expected qty_used 5 for cut-over adjustment, got %.2f", row.QtyUsed) + } + if row.UnitPrice != 15000 { + t.Fatalf("expected unit_price 15000, got %.2f", row.UnitPrice) + } + if row.TotalAmount != 75000 { + t.Fatalf("expected total_amount 75000, got %.2f", row.TotalAmount) + } + }) + } +} + +func TestToSapronakProjectAggregatedFromReportKeepsNonCutOverAdjustmentOutInQtyOut(t *testing.T) { + report := &SapronakReportDTO{ + Groups: []SapronakGroupDTO{ + { + Flag: "PAKAN", + Items: []SapronakDetailDTO{ + { + ProductID: 7, + ProductName: "PAKAN REGULER", + NoReferensi: "ADJ-REG-01", + JenisTransaksi: "Adjustment Keluar", + QtyKeluar: 3, + Harga: 12000, + }, + }, + }, + }, + } + + result := ToSapronakProjectAggregatedFromReport( + report, + "PAKAN", + map[uint][]string{ + 7: {"PAKAN"}, + }, + ) + if result.Pakan == nil || len(result.Pakan.Rows) != 1 { + t.Fatalf("expected 1 pakan row, got %+v", result.Pakan) + } + + row := result.Pakan.Rows[0] + if row.QtyOut != 3 { + t.Fatalf("expected qty_out 3 for non cut-over adjustment, got %.2f", row.QtyOut) + } + if row.QtyUsed != 0 { + t.Fatalf("expected qty_used 0 for non cut-over adjustment, got %.2f", row.QtyUsed) + } + if row.TotalAmount != 0 { + t.Fatalf("expected total_amount 0 for non cut-over adjustment, got %.2f", row.TotalAmount) + } +} diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 3e8071ce..14f2d9c9 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -1588,11 +1588,20 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { poByWarehouse := r.DB(). - Table("purchase_items pi"). - Select("DISTINCT ON (pi.product_warehouse_id) pi.product_warehouse_id, po.po_number, pi.received_date"). - Joins("JOIN purchases po ON po.id = pi.purchase_id"). - Where("pi.received_date IS NOT NULL"). - Order("pi.product_warehouse_id, pi.received_date ASC") + Table("(?) AS ranked_po", + r.DB(). + Table("purchase_items pi"). + Select(` + pi.product_warehouse_id, + po.po_number, + pi.received_date, + ROW_NUMBER() OVER (PARTITION BY pi.product_warehouse_id ORDER BY pi.received_date ASC, pi.id ASC) AS rn + `). + Joins("JOIN purchases po ON po.id = pi.purchase_id"). + Where("pi.received_date IS NOT NULL"), + ). + Select("ranked_po.product_warehouse_id, ranked_po.po_number, ranked_po.received_date"). + Where("ranked_po.rn = 1") incomingQuery := r.withCtx(ctx). Table("adjustment_stocks AS ast"). @@ -1601,10 +1610,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka p.name AS product_name, f.name AS flag, ast.created_at AS date, - CONCAT('ADJ-', ast.id) AS reference, + 'ADJ-' || CAST(ast.id AS TEXT) AS reference, COALESCE(ast.total_qty, 0) AS qty_in, 0 AS qty_out, - COALESCE(p.product_price, 0) AS price + COALESCE(ast.price, p.product_price, 0) AS price `). Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). @@ -1627,10 +1636,18 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka p.name AS product_name, f.name AS flag, COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date, - COALESCE(po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference, + COALESCE( + po.po_number, + st.movement_number, + lt.transfer_number, + pfp_po.po_number, + CASE WHEN ast_in.id IS NOT NULL THEN 'ADJ-' || CAST(ast_in.id AS TEXT) END, + CASE WHEN ast.id IS NOT NULL THEN 'ADJ-' || CAST(ast.id AS TEXT) END, + CASE WHEN pc.id IS NOT NULL THEN 'CHICKIN-' || CAST(pc.id AS TEXT) END + ) AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, - COALESCE(p.product_price, 0) AS price + COALESCE(pi.price, ast_in.price, ast.price, p.product_price, 0) AS price `). Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()). Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). @@ -1651,7 +1668,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)). - Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price") + Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, pi.price, ast_in.price, ast.price, p.product_price") outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoingQuery = applyDateRange(outgoingQuery, dateExpr, start, end) outgoing, err := scanAndGroupDetails(outgoingQuery) diff --git a/internal/modules/closings/repositories/closing.repository_test.go b/internal/modules/closings/repositories/closing.repository_test.go index 75dad419..9866cf22 100644 --- a/internal/modules/closings/repositories/closing.repository_test.go +++ b/internal/modules/closings/repositories/closing.repository_test.go @@ -89,6 +89,63 @@ func TestFetchSapronakIncomingIncludesAttributedFarmPurchasesAndHistoricalWareho } } +func TestFetchSapronakAdjustmentsUsesAdjustmentReferenceAndPrice(t *testing.T) { + db := setupClosingRepositoryTestDB(t) + repo := NewClosingRepository(db) + ctx := context.Background() + + statements := []string{ + `INSERT INTO warehouses (id, kandang_id) VALUES (5, 5)`, + `INSERT INTO product_categories (id, code) VALUES (1, 'OBT')`, + `INSERT INTO products (id, name, product_category_id, product_price) VALUES (17, 'OVK CUT-OVER', 1, 1)`, + `INSERT INTO flags (id, flagable_id, flagable_type, name) VALUES (1, 17, 'products', 'OVK')`, + `INSERT INTO product_warehouses (id, product_id, warehouse_id, project_flock_kandang_id) VALUES (1365, 17, 5, 66)`, + `INSERT INTO adjustment_stocks (id, product_warehouse_id, total_qty, usage_qty, price) VALUES + (1139, 1365, 1, 0, 298594487), + (1140, 1365, 0, 1, 298594487)`, + fmt.Sprintf(`INSERT INTO stock_allocations (id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty, allocation_purpose, status) VALUES + (25990, 1365, '%s', 1139, '%s', 1140, 1, 'CONSUME', 'ACTIVE')`, + fifo.StockableKeyAdjustmentIn.String(), + fifo.UsableKeyAdjustmentOut.String(), + ), + } + for _, stmt := range statements { + if err := db.Exec(stmt).Error; err != nil { + t.Fatalf("failed seeding schema: %v", err) + } + } + + incoming, outgoing, err := repo.FetchSapronakAdjustments(ctx, 5, nil, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + incomingRows := incoming[17] + if len(incomingRows) != 1 { + t.Fatalf("expected 1 incoming row for product 17, got %d", len(incomingRows)) + } + if incomingRows[0].Reference != "ADJ-1139" { + t.Fatalf("expected incoming reference ADJ-1139, got %q", incomingRows[0].Reference) + } + if incomingRows[0].Price != 298594487 { + t.Fatalf("expected incoming price 298594487 from adjustment_stocks.price, got %.3f", incomingRows[0].Price) + } + + outgoingRows := outgoing[17] + if len(outgoingRows) != 1 { + t.Fatalf("expected 1 outgoing row for product 17, got %d", len(outgoingRows)) + } + if outgoingRows[0].Reference != "ADJ-1139" { + t.Fatalf("expected outgoing reference ADJ-1139, got %q", outgoingRows[0].Reference) + } + if outgoingRows[0].Reference == "CHICKIN-" { + t.Fatalf("expected outgoing reference to avoid CHICKIN- placeholder") + } + if outgoingRows[0].Price != 298594487 { + t.Fatalf("expected outgoing price 298594487 from adjustment_stocks.price, got %.3f", outgoingRows[0].Price) + } +} + func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB { t.Helper() @@ -134,6 +191,7 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB { purchase_id INTEGER NOT NULL, product_id INTEGER NOT NULL, warehouse_id INTEGER NOT NULL, + product_warehouse_id INTEGER NULL, project_flock_kandang_id INTEGER NULL, total_qty NUMERIC(15,3) NOT NULL DEFAULT 0, price NUMERIC(15,3) NOT NULL DEFAULT 0, @@ -153,7 +211,13 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB { )`, `CREATE TABLE project_chickins ( id INTEGER PRIMARY KEY, - project_flock_kandang_id INTEGER NOT NULL + project_flock_kandang_id INTEGER NOT NULL, + chick_in_date TIMESTAMP NULL + )`, + `CREATE TABLE project_flock_populations ( + id INTEGER PRIMARY KEY, + project_chickin_id INTEGER NULL, + product_warehouse_id INTEGER NULL )`, `CREATE TABLE stock_allocations ( id INTEGER PRIMARY KEY, @@ -180,6 +244,16 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB { movement_number TEXT NULL, reason TEXT NULL )`, + `CREATE TABLE laying_transfers ( + id INTEGER PRIMARY KEY, + transfer_date TIMESTAMP NULL, + transfer_number TEXT NULL + )`, + `CREATE TABLE laying_transfer_targets ( + id INTEGER PRIMARY KEY, + laying_transfer_id INTEGER NOT NULL, + product_warehouse_id INTEGER NULL + )`, `CREATE TABLE stock_transfer_details ( id INTEGER PRIMARY KEY, stock_transfer_id INTEGER NOT NULL, @@ -194,6 +268,7 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB { product_warehouse_id INTEGER NOT NULL, total_qty NUMERIC(15,3) NOT NULL DEFAULT 0, usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0, + price NUMERIC(15,3) NOT NULL DEFAULT 0, adj_number TEXT NULL, created_at TIMESTAMP NULL )`, diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 360a39f9..91a8c7d4 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -573,17 +573,21 @@ func (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID return nil, nil, err } - var minChickin *time.Time + var firstChickin struct { + ChickInDate time.Time `gorm:"column:chick_in_date"` + } if err := db.Table("project_chickins"). - Select("MIN(chick_in_date)"). - Where("project_flock_kandang_id = ?", pfk.Id). - Scan(&minChickin).Error; err != nil { + Select("chick_in_date"). + Where("project_flock_kandang_id = ? AND chick_in_date IS NOT NULL", pfk.Id). + Order("chick_in_date ASC"). + Limit(1). + Scan(&firstChickin).Error; err != nil { return nil, nil, err } start := pfk.CreatedAt - if minChickin != nil && !minChickin.IsZero() { - start = *minChickin + if !firstChickin.ChickInDate.IsZero() { + start = firstChickin.ChickInDate } startDate := dateOnlyUTC(start) @@ -596,26 +600,34 @@ func (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID return &startDate, endDate, nil } - var minCreated time.Time + var firstPFK entity.ProjectFlockKandang if err := db.Model(&entity.ProjectFlockKandang{}). - Select("MIN(created_at)"). + Select("created_at"). Where("project_flock_id = ?", projectFlockID). - Scan(&minCreated).Error; err != nil { + Order("created_at ASC"). + First(&firstPFK).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, nil + } return nil, nil, err } - var minChickin *time.Time + var firstChickin struct { + ChickInDate time.Time `gorm:"column:chick_in_date"` + } if err := db.Table("project_chickins pc"). - Select("MIN(pc.chick_in_date)"). + Select("pc.chick_in_date"). Joins("JOIN project_flock_kandangs pfk ON pfk.id = pc.project_flock_kandang_id"). - Where("pfk.project_flock_id = ?", projectFlockID). - Scan(&minChickin).Error; err != nil { + Where("pfk.project_flock_id = ? AND pc.chick_in_date IS NOT NULL", projectFlockID). + Order("pc.chick_in_date ASC"). + Limit(1). + Scan(&firstChickin).Error; err != nil { return nil, nil, err } - start := minCreated - if minChickin != nil && !minChickin.IsZero() { - start = *minChickin + start := firstPFK.CreatedAt + if !firstChickin.ChickInDate.IsZero() { + start = firstChickin.ChickInDate } startDate := dateOnlyUTC(start) @@ -627,15 +639,19 @@ func (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID return nil, nil, err } if openCount == 0 { - var maxClosed *time.Time + var latestClosed entity.ProjectFlockKandang if err := db.Model(&entity.ProjectFlockKandang{}). - Select("MAX(closed_at)"). - Where("project_flock_id = ?", projectFlockID). - Scan(&maxClosed).Error; err != nil { + Select("closed_at"). + Where("project_flock_id = ? AND closed_at IS NOT NULL", projectFlockID). + Order("closed_at DESC"). + First(&latestClosed).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &startDate, nil, nil + } return nil, nil, err } - if maxClosed != nil && !maxClosed.IsZero() { - d := dateOnlyUTC(*maxClosed) + if latestClosed.ClosedAt != nil && !latestClosed.ClosedAt.IsZero() { + d := dateOnlyUTC(*latestClosed.ClosedAt) endDate = &d } } diff --git a/internal/modules/closings/services/closing.service_test.go b/internal/modules/closings/services/closing.service_test.go new file mode 100644 index 00000000..bf05b08e --- /dev/null +++ b/internal/modules/closings/services/closing.service_test.go @@ -0,0 +1,94 @@ +package service + +import ( + "context" + "testing" + "time" + + "github.com/glebarez/sqlite" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" + "gorm.io/gorm" +) + +func TestGetSapronakDateRange_ProjectWithoutChickin_DoesNotError(t *testing.T) { + db := setupClosingServiceTestDB(t) + repo := repository.NewClosingRepository(db) + svc := closingService{Repository: repo} + + createdAt := time.Date(2026, 4, 15, 7, 0, 0, 0, time.UTC) + if err := db.Exec(`INSERT INTO project_flock_kandangs (id, project_flock_id, created_at, closed_at) VALUES (66, 47, ?, NULL)`, createdAt).Error; err != nil { + t.Fatalf("failed seeding project_flock_kandangs: %v", err) + } + + start, end, err := svc.getSapronakDateRange(context.Background(), 47, nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if start == nil { + t.Fatalf("expected non-nil start date") + } + expected := dateOnlyUTC(createdAt) + if !start.Equal(expected) { + t.Fatalf("expected start %s, got %s", expected.Format(time.RFC3339), start.Format(time.RFC3339)) + } + if end != nil { + t.Fatalf("expected nil end date for open kandang, got %v", end) + } +} + +func TestGetSapronakDateRange_KandangWithoutChickin_DoesNotError(t *testing.T) { + db := setupClosingServiceTestDB(t) + repo := repository.NewClosingRepository(db) + svc := closingService{Repository: repo} + + createdAt := time.Date(2026, 4, 15, 7, 0, 0, 0, time.UTC) + if err := db.Exec(`INSERT INTO project_flock_kandangs (id, project_flock_id, created_at, closed_at) VALUES (66, 47, ?, NULL)`, createdAt).Error; err != nil { + t.Fatalf("failed seeding project_flock_kandangs: %v", err) + } + + pfkID := uint(66) + start, end, err := svc.getSapronakDateRange(context.Background(), 47, &pfkID) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if start == nil { + t.Fatalf("expected non-nil start date") + } + expected := dateOnlyUTC(createdAt) + if !start.Equal(expected) { + t.Fatalf("expected start %s, got %s", expected.Format(time.RFC3339), start.Format(time.RFC3339)) + } + if end != nil { + t.Fatalf("expected nil end date for open kandang, got %v", end) + } +} + +func setupClosingServiceTestDB(t *testing.T) *gorm.DB { + t.Helper() + + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{}) + if err != nil { + t.Fatalf("failed opening sqlite db: %v", err) + } + + stmts := []string{ + `CREATE TABLE project_flock_kandangs ( + id INTEGER PRIMARY KEY, + project_flock_id INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + closed_at TIMESTAMP NULL + )`, + `CREATE TABLE project_chickins ( + id INTEGER PRIMARY KEY, + project_flock_kandang_id INTEGER NOT NULL, + chick_in_date TIMESTAMP NULL + )`, + } + for _, stmt := range stmts { + if err := db.Exec(stmt).Error; err != nil { + t.Fatalf("failed preparing schema: %v", err) + } + } + + return db +} diff --git a/internal/modules/closings/services/sapronak.service.go b/internal/modules/closings/services/sapronak.service.go index f548820a..6eb8a015 100644 --- a/internal/modules/closings/services/sapronak.service.go +++ b/internal/modules/closings/services/sapronak.service.go @@ -371,7 +371,9 @@ func buildSapronakDetails( addRows(result.Incoming, incomingRows, "Pembelian", true) addRows(result.Usage, usageRows, "Pemakaian", false) addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true) - addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false) + // Outgoing adjustment rows here are sourced from stock allocation + // consume flow (adjustment_stocks.usage_qty), so treat them as usage. + addRows(result.AdjOutgoing, adjOutgoingRows, "Pemakaian", false) addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true) addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false) addRows(result.SalesOut, salesOutRows, "Penjualan", false) diff --git a/internal/modules/closings/services/sapronak.service_test.go b/internal/modules/closings/services/sapronak.service_test.go new file mode 100644 index 00000000..ce348658 --- /dev/null +++ b/internal/modules/closings/services/sapronak.service_test.go @@ -0,0 +1,45 @@ +package service + +import ( + "testing" + + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" +) + +func TestBuildSapronakDetailsMapsAdjustmentOutgoingAsUsage(t *testing.T) { + res := buildSapronakDetails( + map[uint][]repository.SapronakDetailRow{}, + map[uint][]repository.SapronakDetailRow{}, + map[uint][]repository.SapronakDetailRow{}, + map[uint][]repository.SapronakDetailRow{ + 17: { + { + ProductID: 17, + ProductName: "PAKAN GROWING CRUMBLE 8603 MALINDO", + Flag: "PAKAN", + QtyOut: 9000, + Price: 6450, + }, + }, + }, + map[uint][]repository.SapronakDetailRow{}, + map[uint][]repository.SapronakDetailRow{}, + map[uint][]repository.SapronakDetailRow{}, + ) + + rows := res.AdjOutgoing[17] + if len(rows) != 1 { + t.Fatalf("expected 1 adjustment outgoing row, got %d", len(rows)) + } + + row := rows[0] + if row.JenisTransaksi != "Pemakaian" { + t.Fatalf("expected jenis_transaksi Pemakaian, got %q", row.JenisTransaksi) + } + if row.QtyKeluar != 9000 { + t.Fatalf("expected qty_keluar 9000, got %.3f", row.QtyKeluar) + } + if row.Nilai != 58050000 { + t.Fatalf("expected nilai 58050000, got %.3f", row.Nilai) + } +}