From f74b6476dee1b5c9c921bdabd7197f9cb4c2fe90 Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 6 Feb 2026 14:13:05 +0700 Subject: [PATCH] [FEAT/BE] Add filter delivery order, adjust response purchase and fcr growing recording --- .../common/service/common.fifo.service.go | 62 ++++++---- .../marketing/dto/deliveryorder.dto.go | 54 ++++++++- .../repositories/recording.repository.go | 28 +++++ .../recordings/services/recording.service.go | 35 +++++- .../services/uniformity.service.go | 109 +++++++++++++++++- .../purchases/services/purchase.service.go | 18 +++ 6 files changed, 274 insertions(+), 32 deletions(-) diff --git a/internal/common/service/common.fifo.service.go b/internal/common/service/common.fifo.service.go index fd1812fb..100c8fcc 100644 --- a/internal/common/service/common.fifo.service.go +++ b/internal/common/service/common.fifo.service.go @@ -26,6 +26,7 @@ type FifoService interface { Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error + ResolvePending(ctx context.Context, req PendingResolveRequest) ([]PendingResolution, error) } type fifoService struct { @@ -111,6 +112,11 @@ type PendingResolution struct { Quantity float64 } +type PendingResolveRequest struct { + ProductWarehouseID uint + Tx *gorm.DB +} + type StockReplenishResult struct { AddedQuantity float64 PendingResolved []PendingResolution @@ -227,6 +233,23 @@ func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) return result, nil } +func (s *fifoService) ResolvePending(ctx context.Context, req PendingResolveRequest) ([]PendingResolution, error) { + if req.ProductWarehouseID == 0 { + return nil, errors.New("product warehouse id is required") + } + + var resolved []PendingResolution + err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error { + var err error + resolved, err = s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID) + return err + }) + if err != nil { + return nil, err + } + return resolved, nil +} + func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) { if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" { return nil, errors.New("usable key and id are required") @@ -849,8 +872,8 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p if cfg.Columns.CreatedAt == cfg.Columns.ID { var rows []struct { ID uint - Pending float64 - CreatedAt int64 + Pending float64 `gorm:"column:pending_qty"` + CreatedAt int64 `gorm:"column:created_at"` } query := tx.Table(cfg.Table). @@ -867,27 +890,26 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p query = query.Order(order) } - if err := query.Find(&rows).Error; err != nil { - return nil, err + if err := query.Find(&rows).Error; err != nil { + return nil, err + } + for _, row := range rows { + if row.Pending <= 0 { + continue } - - for _, row := range rows { - if row.Pending <= 0 { - continue - } - candidates = append(candidates, pendingCandidate{ - UsableKey: key, - Config: cfg, - UsableID: row.ID, - Pending: row.Pending, - CreatedAt: time.Unix(0, row.CreatedAt), - }) - } - } else { + candidates = append(candidates, pendingCandidate{ + UsableKey: key, + Config: cfg, + UsableID: row.ID, + Pending: row.Pending, + CreatedAt: time.Unix(0, row.CreatedAt), + }) + } + } else { var rows []struct { ID uint - Pending float64 - CreatedAt time.Time + Pending float64 `gorm:"column:pending_qty"` + CreatedAt time.Time `gorm:"column:created_at"` } query := tx.Table(cfg.Table). diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index bd4b2a0b..20b3e42b 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "sort" + "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -17,6 +18,7 @@ import ( type MarketingRelationDTO struct { Id uint `json:"id"` SoNumber string `json:"so_number"` + DoNumber *string `json:"do_number"` SoDate time.Time `json:"so_date"` Notes string `json:"notes,omitempty"` } @@ -95,9 +97,16 @@ type DeliveryMarketingProductDTO struct { } func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { + var doNumber *string + if doNumbers := collectDoNumbers(marketing); len(doNumbers) > 0 { + value := doNumbers[0] + doNumber = &value + } + return MarketingRelationDTO{ Id: marketing.Id, SoNumber: marketing.SoNumber, + DoNumber: doNumber, SoDate: marketing.SoDate, Notes: marketing.Notes, } @@ -182,7 +191,6 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) } } - return MarketingListDTO{ MarketingRelationDTO: ToMarketingRelationDTO(marketing), Customer: customer, @@ -239,7 +247,6 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) latestApproval = mapped } - return MarketingDetailDTO{ MarketingRelationDTO: ToMarketingRelationDTO(marketing), SoDocs: marketing.SoDocs, @@ -346,11 +353,46 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri } func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string { - dateStr := "" - if deliveryDate != nil { - dateStr = deliveryDate.Format("20060102") + numberPrefix := soNumber + if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(soNumber)), "SO-") { + numberPrefix = "DO-" + soNumber[3:] } - return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) + return numberPrefix +} + +func collectDoNumbers(marketing *entity.Marketing) []string { + if marketing == nil || len(marketing.Products) == 0 { + return nil + } + + seen := make(map[string]struct{}) + for _, product := range marketing.Products { + if product.DeliveryProduct == nil || product.DeliveryProduct.DeliveryDate == nil { + continue + } + warehouseID := product.ProductWarehouse.WarehouseId + if warehouseID == 0 && product.ProductWarehouse.Warehouse.Id != 0 { + warehouseID = product.ProductWarehouse.Warehouse.Id + } + if warehouseID == 0 { + continue + } + + doNumber := GenerateDeliveryOrderNumber(marketing.SoNumber, product.DeliveryProduct.DeliveryDate, warehouseID) + if doNumber != "" { + seen[doNumber] = struct{}{} + } + } + + if len(seen) == 0 { + return nil + } + result := make([]string, 0, len(seen)) + for value := range seen { + result = append(result, value) + } + sort.Strings(result) + return result } func getVehicleNumber(e entity.MarketingProduct) string { diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index b9867d2b..3ed66f87 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -40,6 +40,7 @@ type RecordingRepository interface { SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) + GetUniformityMeanBwByWeek(tx *gorm.DB, projectFlockKandangId uint, week int) (float64, bool, error) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) @@ -331,6 +332,33 @@ func (r *RecordingRepositoryImpl) GetCumulativeDepletionByProjectFlockKandangUnt return total, err } +func (r *RecordingRepositoryImpl) GetUniformityMeanBwByWeek(tx *gorm.DB, projectFlockKandangId uint, week int) (float64, bool, error) { + if projectFlockKandangId == 0 || week <= 0 { + return 0, false, nil + } + + var row struct { + ID uint + MeanUp float64 + } + if err := tx. + Table("project_flock_kandang_uniformity"). + Select("id, mean_up"). + Where("project_flock_kandang_id = ?", projectFlockKandangId). + Where("week = ?", week). + Order("id DESC"). + Limit(1). + Scan(&row).Error; err != nil { + return 0, false, err + } + if row.ID == 0 { + return 0, false, nil + } + + meanBw := row.MeanUp / 1.10 + return meanBw, true, nil +} + func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) { if currentDay <= 1 { return nil, nil diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 28329041..b9b1126e 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1178,13 +1178,40 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm } var fcrValue float64 - if usageInGrams > 0 && totalEggWeightGrams > 0 { - fcrValue = usageInGrams / totalEggWeightGrams + isGrowing := false + if s.ProjectFlockKandangRepo != nil { + if pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, recording.ProjectFlockKandangId); err == nil { + if strings.EqualFold(pfk.ProjectFlock.Category, string(utils.ProjectFlockCategoryGrowing)) { + isGrowing = true + } + } + } + + if isGrowing { + week := 0 + if recording.Day != nil && *recording.Day > 0 { + week = (*recording.Day-1)/7 + 1 + } + if week > 0 && s.Repository != nil { + meanBw, ok, err := s.Repository.GetUniformityMeanBwByWeek(tx, recording.ProjectFlockKandangId, week) + if err != nil { + return fmt.Errorf("getUniformityMeanBwByWeek: %w", err) + } + if ok && meanBw > 0 && feedIntake > 0 { + fcrValue = feedIntake / meanBw + } + } updates["fcr_value"] = fcrValue recording.FcrValue = &fcrValue } else { - updates["fcr_value"] = gorm.Expr("NULL") - recording.FcrValue = nil + if usageInGrams > 0 && totalEggWeightGrams > 0 { + fcrValue = usageInGrams / totalEggWeightGrams + updates["fcr_value"] = fcrValue + recording.FcrValue = &fcrValue + } else { + updates["fcr_value"] = gorm.Expr("NULL") + recording.FcrValue = nil + } } if usageInGrams > 0 && totalChick > 0 { diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 1e4ccbd5..5a747fca 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -465,11 +465,16 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file ); err != nil { return err } + if strings.EqualFold(category, string(utils.ProjectFlockCategoryGrowing)) { + if err := s.updateGrowingFcrForWeek(tx, createBody.ProjectFlockKandangId, createBody.Week, calculation.MeanUp); err != nil { + return err + } + } return nil }); err != nil { s.Log.Errorf("Failed to create uniformity: %+v", err) return nil, err - } + } if s.DocumentSvc != nil { actorIDCopy := actorID @@ -633,6 +638,9 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui return nil, err } + if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil { + return nil, err + } return s.GetOne(c, id) } @@ -694,6 +702,10 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui } } + if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil { + return nil, err + } + return s.GetOne(c, id) } @@ -724,7 +736,48 @@ func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error { return err } - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + type uniformityContext struct { + ID uint + Week int + ProjectFlockKandangId uint + Category string + } + var ctxRow uniformityContext + if err := s.Repository.DB().WithContext(c.Context()). + Table("project_flock_kandang_uniformity u"). + Select("u.id, u.week, u.project_flock_kandang_id, pf.category"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where("u.id = ?", id). + Scan(&ctxRow).Error; err != nil { + return err + } + + if ctxRow.ID == 0 { + return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + + if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + repoTx := s.Repository.WithTx(tx) + if err := repoTx.DeleteOne(c.Context(), id); err != nil { + return err + } + + if strings.EqualFold(ctxRow.Category, string(utils.ProjectFlockCategoryGrowing)) { + startDay := (ctxRow.Week-1)*7 + 1 + endDay := ctxRow.Week * 7 + if ctxRow.Week > 0 { + if err := tx.Model(&entity.Recording{}). + Where("project_flock_kandangs_id = ?", ctxRow.ProjectFlockKandangId). + Where("day BETWEEN ? AND ?", startDay, endDay). + Update("fcr_value", 0).Error; err != nil { + return err + } + } + } + + return nil + }); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") } @@ -734,6 +787,58 @@ func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } +func (s *uniformityService) updateGrowingFcrFromUniformity(ctx context.Context, uniformityID uint) error { + if uniformityID == 0 { + return nil + } + + type uniformityRow struct { + ID uint + Week int + MeanUp float64 + ProjectFlockKandangId uint + Category string + } + var row uniformityRow + if err := s.Repository.DB().WithContext(ctx). + Table("project_flock_kandang_uniformity u"). + Select("u.id, u.week, u.mean_up, u.project_flock_kandang_id, pf.category"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where("u.id = ?", uniformityID). + Scan(&row).Error; err != nil { + return err + } + if row.ID == 0 { + return nil + } + if !strings.EqualFold(row.Category, string(utils.ProjectFlockCategoryGrowing)) { + return nil + } + + return s.updateGrowingFcrForWeek(s.Repository.DB().WithContext(ctx), row.ProjectFlockKandangId, row.Week, row.MeanUp) +} + +func (s *uniformityService) updateGrowingFcrForWeek(tx *gorm.DB, projectFlockKandangID uint, week int, meanUp float64) error { + if tx == nil || projectFlockKandangID == 0 || week <= 0 { + return nil + } + startDay := (week-1)*7 + 1 + endDay := week * 7 + meanBw := meanUp / 1.10 + if meanBw <= 0 { + return tx.Model(&entity.Recording{}). + Where("project_flock_kandangs_id = ?", projectFlockKandangID). + Where("day BETWEEN ? AND ?", startDay, endDay). + Update("fcr_value", 0).Error + } + + return tx.Model(&entity.Recording{}). + Where("project_flock_kandangs_id = ?", projectFlockKandangID). + Where("day BETWEEN ? AND ?", startDay, endDay). + Update("fcr_value", gorm.Expr("CASE WHEN feed_intake IS NULL OR feed_intake = 0 THEN 0 ELSE feed_intake / ? END", meanBw)).Error +} + func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) { if err := s.Validate.Struct(req); err != nil { return nil, err diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index dabfad39..703c04b9 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -912,6 +912,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation pwID uint qty float64 }, 0, len(prepared)) + resolvePendingIDs := make(map[uint]struct{}) logEntries := make([]struct { itemID uint pwID uint @@ -952,6 +953,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation pwID uint qty float64 }{itemID: item.Id, pwID: *newPWID, qty: deltaQty}) + resolvePendingIDs[*newPWID] = struct{}{} } else { deltas[*newPWID] += deltaQty totalQtyDeltas[item.Id] += deltaQty @@ -964,11 +966,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation qty float64 }{itemID: item.Id, pwID: *newPWID, qty: deltaQty}) affected[*newPWID] = struct{}{} + resolvePendingIDs[*newPWID] = struct{}{} } else { deltas[*newPWID] += deltaQty // negative affected[*newPWID] = struct{}{} totalQtyDeltas[item.Id] += deltaQty } + case newPWID != nil: + resolvePendingIDs[*newPWID] = struct{}{} } dateCopy := prep.receivedDate @@ -1066,6 +1071,19 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation return err } } + for pwID := range resolvePendingIDs { + if pwID == 0 { + continue + } + resolved, err := s.FifoSvc.ResolvePending(c.Context(), commonSvc.PendingResolveRequest{ + ProductWarehouseID: pwID, + Tx: tx, + }) + if err != nil { + return err + } + s.Log.Infof("ResolvePending purchase=%d pw=%d resolved=%d", purchase.Id, pwID, len(resolved)) + } } if len(logEntries) > 0 {