diff --git a/internal/database/migrations/20260608135241_deactivate_phases.down.sql b/internal/database/migrations/20260608135241_deactivate_phases.down.sql new file mode 100644 index 00000000..640e6620 --- /dev/null +++ b/internal/database/migrations/20260608135241_deactivate_phases.down.sql @@ -0,0 +1,2 @@ +UPDATE phases SET is_active = true +WHERE id IN (2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26); diff --git a/internal/database/migrations/20260608135241_deactivate_phases.up.sql b/internal/database/migrations/20260608135241_deactivate_phases.up.sql new file mode 100644 index 00000000..7262224d --- /dev/null +++ b/internal/database/migrations/20260608135241_deactivate_phases.up.sql @@ -0,0 +1,2 @@ +UPDATE phases SET is_active = false +WHERE id IN (2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26); diff --git a/internal/entities/recording_stock.go b/internal/entities/recording_stock.go index 29d47163..fcd30a6d 100644 --- a/internal/entities/recording_stock.go +++ b/internal/entities/recording_stock.go @@ -7,7 +7,20 @@ type RecordingStock struct { ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id;index"` UsageQty *float64 `gorm:"column:usage_qty"` PendingQty *float64 `gorm:"column:pending_qty"` + TotalPrice float64 `gorm:"-"` + Allocations []RecordingStockAlloc `gorm:"-"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` } + +type RecordingStockAlloc struct { + SourceType string + SourceId uint + PrNumber string + PoNumber string + AdjNumber string + Qty float64 + UnitPrice float64 + Subtotal float64 +} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 91f43d06..a4e2fde9 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -1215,7 +1215,9 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati } if len(phaseIDs) > 0 { - phases, err := s.PhaseRepo.GetByIDs(c.Context(), phaseIDs, nil) + phases, err := s.PhaseRepo.GetByIDs(c.Context(), phaseIDs, func(db *gorm.DB) *gorm.DB { + return db.Where("is_active = true") + }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusBadRequest, "Phase not found") diff --git a/internal/modules/master/kandang-groups/services/kandang_group.service.go b/internal/modules/master/kandang-groups/services/kandang_group.service.go index 4293bfce..2fe415b4 100644 --- a/internal/modules/master/kandang-groups/services/kandang_group.service.go +++ b/internal/modules/master/kandang-groups/services/kandang_group.service.go @@ -72,9 +72,9 @@ func (s kandangGroupService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } if params.OrderBy == "desc" || params.OrderBy == "" { - db = db.Order(fmt.Sprintf("kandang_groups.%s DESC", params.SortBy)) + db = db.Order(fmt.Sprintf("kandang_groups.%s DESC, kandang_groups.id ASC", params.SortBy)) } else { - db = db.Order(fmt.Sprintf("kandang_groups.%s ASC", params.SortBy)) + db = db.Order(fmt.Sprintf("kandang_groups.%s ASC, kandang_groups.id ASC", params.SortBy)) } return db diff --git a/internal/modules/master/kandang-groups/validations/kandang_group.validation.go b/internal/modules/master/kandang-groups/validations/kandang_group.validation.go index b1edd49a..174727e7 100644 --- a/internal/modules/master/kandang-groups/validations/kandang_group.validation.go +++ b/internal/modules/master/kandang-groups/validations/kandang_group.validation.go @@ -20,6 +20,6 @@ type Query struct { Search string `query:"search" validate:"omitempty,max=50"` LocationId int `query:"location_id" validate:"omitempty,number,gt=0"` PicId int `query:"pic_id" validate:"omitempty,number,gt=0"` - SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"updated_at"` - OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"desc"` + SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"name"` + OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"asc"` } diff --git a/internal/modules/master/phasess/services/phases.service.go b/internal/modules/master/phasess/services/phases.service.go index bd5cf08f..d045e696 100644 --- a/internal/modules/master/phasess/services/phases.service.go +++ b/internal/modules/master/phasess/services/phases.service.go @@ -50,6 +50,7 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity. phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = db.Where("is_active = true") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index f8e6cf22..3f1d4892 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -131,10 +131,23 @@ type RecordingDepletionDTO struct { ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` } +type RecordingStockAllocDTO struct { + SourceType string `json:"source_type"` + SourceId uint `json:"source_id"` + PrNumber string `json:"pr_number"` + PoNumber string `json:"po_number"` + AdjNumber string `json:"adj_number"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + Subtotal float64 `json:"subtotal"` +} + type RecordingStockDTO struct { ProductWarehouseId uint `json:"product_warehouse_id"` UsageAmount float64 `json:"usage_amount"` PendingQty float64 `json:"pending_qty"` + TotalPrice float64 `json:"total_price"` + Allocations []RecordingStockAllocDTO `json:"allocations"` ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` } @@ -197,10 +210,26 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO { pendingQty = *s.PendingQty } + allocs := make([]RecordingStockAllocDTO, len(s.Allocations)) + for j, a := range s.Allocations { + allocs[j] = RecordingStockAllocDTO{ + SourceType: a.SourceType, + SourceId: a.SourceId, + PrNumber: a.PrNumber, + PoNumber: a.PoNumber, + AdjNumber: a.AdjNumber, + Qty: a.Qty, + UnitPrice: a.UnitPrice, + Subtotal: a.Subtotal, + } + } + result[i] = RecordingStockDTO{ ProductWarehouseId: s.ProductWarehouseId, UsageAmount: usageAmount, PendingQty: pendingQty, + TotalPrice: s.TotalPrice, + Allocations: allocs, ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse), } } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 7a829ead..9fde92d3 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -80,6 +80,7 @@ type RecordingRepository interface { ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error) GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error) + GetStockAllocationsByIDs(ctx context.Context, stockIDs []uint) (map[uint][]entity.RecordingStockAlloc, error) } type RecordingRepositoryImpl struct { @@ -1231,3 +1232,71 @@ func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectF return result.TotalWeight, err } + +func (r *RecordingRepositoryImpl) GetStockAllocationsByIDs(ctx context.Context, stockIDs []uint) (map[uint][]entity.RecordingStockAlloc, error) { + if len(stockIDs) == 0 { + return map[uint][]entity.RecordingStockAlloc{}, nil + } + + type row struct { + RecordingStockId uint + SourceType string + SourceId uint + PrNumber string + PoNumber string + AdjNumber string + Qty float64 + UnitPrice float64 + Subtotal float64 + } + + var rows []row + err := r.DB().WithContext(ctx).Raw(` + SELECT + sa.usable_id AS recording_stock_id, + sa.stockable_type AS source_type, + sa.stockable_id AS source_id, + COALESCE(p.pr_number, '') AS pr_number, + COALESCE(p.po_number, '') AS po_number, + COALESCE(ast.adj_number, '') AS adj_number, + sa.qty AS qty, + COALESCE(CASE + WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN pi.price + WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN ast.price + END, 0) AS unit_price, + sa.qty * COALESCE(CASE + WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN pi.price + WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN ast.price + END, 0) AS subtotal + FROM stock_allocations sa + LEFT JOIN purchase_items pi + ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS' + LEFT JOIN purchases p + ON p.id = pi.purchase_id + LEFT JOIN adjustment_stocks ast + ON ast.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN' + WHERE sa.usable_type = 'RECORDING_STOCK' + AND sa.usable_id IN ? + AND sa.status = 'ACTIVE' + AND sa.allocation_purpose = 'CONSUME' + ORDER BY sa.usable_id, sa.id + `, stockIDs).Scan(&rows).Error + if err != nil { + return nil, err + } + + result := make(map[uint][]entity.RecordingStockAlloc) + for _, row := range rows { + result[row.RecordingStockId] = append(result[row.RecordingStockId], entity.RecordingStockAlloc{ + SourceType: row.SourceType, + SourceId: row.SourceId, + PrNumber: row.PrNumber, + PoNumber: row.PoNumber, + AdjNumber: row.AdjNumber, + Qty: row.Qty, + UnitPrice: row.UnitPrice, + Subtotal: row.Subtotal, + }) + } + return result, nil +} diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 46083db7..4012f785 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -278,6 +278,26 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro s.Log.Errorf("Failed get recording by id: %+v", err) return nil, err } + if len(recording.Stocks) > 0 { + stockIDs := make([]uint, len(recording.Stocks)) + for i, s := range recording.Stocks { + stockIDs[i] = s.Id + } + if allocMap, err := s.Repository.GetStockAllocationsByIDs(c.Context(), stockIDs); err != nil { + s.Log.Warnf("Failed to get stock allocations for recording %d: %+v", id, err) + } else { + for i := range recording.Stocks { + allocs := allocMap[recording.Stocks[i].Id] + recording.Stocks[i].Allocations = allocs + var total float64 + for _, a := range allocs { + total += a.Subtotal + } + recording.Stocks[i].TotalPrice = total + } + } + } + if err := recordingutil.AttachLatestApproval(c.Context(), recording, s.ApprovalSvc, s.Log); err != nil { return nil, err } diff --git a/internal/modules/repports/dto/repportMarketing.dto_test.go b/internal/modules/repports/dto/repportMarketing.dto_test.go new file mode 100644 index 00000000..5476af42 --- /dev/null +++ b/internal/modules/repports/dto/repportMarketing.dto_test.go @@ -0,0 +1,71 @@ +package dto + +import ( + "math" + "testing" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +func TestToMarketingReportItemsUsesDeliveryProductTotalWeight(t *testing.T) { + mdps := []entity.MarketingDeliveryProduct{ + { + Id: 1, + UsageQty: 10, + AvgWeight: 2.5, + TotalWeight: 17.75, + UnitPrice: 1000, + }, + } + + got := ToMarketingReportItems(mdps, nil, nil, nil) + + if len(got) != 1 { + t.Fatalf("expected 1 marketing report item, got %d", len(got)) + } + if got[0].TotalWeightKg != 17.75 { + t.Fatalf("expected total_weight_kg to use delivery product total_weight 17.75, got %.2f", got[0].TotalWeightKg) + } + if got[0].Qty != 10 { + t.Fatalf("expected qty to stay from usage_qty, got %.2f", got[0].Qty) + } + if got[0].AverageWeightKg != 2.5 { + t.Fatalf("expected average_weight_kg to stay from avg_weight, got %.2f", got[0].AverageWeightKg) + } + if got[0].SalesAmount != 17750 { + t.Fatalf("expected sales_amount to use delivery product total_weight, got %.2f", got[0].SalesAmount) + } +} + +func TestMarketingSummaryUsesReportItemTotalWeight(t *testing.T) { + items := []RepportMarketingItemDTO{ + { + Qty: 10, + TotalWeightKg: 17.75, + SalesAmount: 17750, + }, + { + Qty: 5, + TotalWeightKg: 8.25, + SalesAmount: 8250, + }, + } + + got := ToSummaryFromDTOItems(items) + + if got == nil { + t.Fatal("expected summary, got nil") + } + if got.TotalWeightKg != 26 { + t.Fatalf("expected summary total_weight_kg to sum item total weights, got %.2f", got.TotalWeightKg) + } + if diff := math.Abs(got.AverageWeightKg - (26.0 / 15.0)); diff > 0.000001 { + t.Fatalf("expected summary average_weight_kg to use total_weight_kg / total_qty, got %.6f", got.AverageWeightKg) + } + if got.TotalQty != 15 { + t.Fatalf("expected total qty 15, got %d", got.TotalQty) + } + if got.TotalSalesAmount != 26000 { + t.Fatalf("expected total sales amount 26000, got %d", got.TotalSalesAmount) + } +}