From 06e92d1c77530641b0af3fcf58a92a6759e8155b Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 22 Jan 2026 11:17:02 +0700 Subject: [PATCH 1/9] [FEAT][BE}: add umur week and day on closing penjualan --- .../closings/dto/closingMarketing.dto.go | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index eb6ff23f..d725b430 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -15,6 +15,7 @@ type SalesDTO struct { Id uint `json:"id"` RealizationDate time.Time `json:"realization_date"` Age int `json:"age"` + Week int `json:"week"` DoNumber string `json:"do_number"` Product *productDTO.ProductRelationDTO `json:"product,omitempty"` Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` @@ -43,7 +44,7 @@ type PenjualanRealisasiResponseDTO struct { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { - age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate) + ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate) var product *productDTO.ProductRelationDTO if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { @@ -73,7 +74,8 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { return SalesDTO{ Id: e.Id, RealizationDate: realizationDate, - Age: age, + Age: ageInDay, + Week: ageInWeeks, DoNumber: doNumber, Product: product, Customer: customer, @@ -124,9 +126,9 @@ func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) Penjua } } -func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int { +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) (int, int) { if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { - return 0 + return 0, 0 } earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate @@ -136,7 +138,16 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de } } - ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) - ageInWeeks := ageInDays / 7 - return ageInWeeks + diff := deliveryDate.Sub(earliestChickinDate) + ageInDays := int(diff.Hours() / 24) + + var ageInWeeks int + if ageInDays <= 0 { + ageInWeeks = 0 + } else { + + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } + + return ageInDays, ageInWeeks } From 04ec8560a765d339a98f19893c12529d32f7e779 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 22 Jan 2026 13:35:46 +0700 Subject: [PATCH 2/9] [FIX/BE-US] adjustment recording and purchase stock log --- .../repositories/constant.repository.go | 2 +- .../modules/production/recordings/module.go | 3 + .../recordings/services/recording.service.go | 256 ++++++++++++++++-- .../purchases/services/purchase.service.go | 47 ++++ internal/utils/constant.go | 2 + 5 files changed, 285 insertions(+), 25 deletions(-) diff --git a/internal/modules/constants/repositories/constant.repository.go b/internal/modules/constants/repositories/constant.repository.go index 493f4cb9..b9c9cc48 100644 --- a/internal/modules/constants/repositories/constant.repository.go +++ b/internal/modules/constants/repositories/constant.repository.go @@ -83,7 +83,7 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { "KANDANG", }, "stock_log": map[string][]string{ - "log_types": []string{"TRANSFER", "ADJUSTMENT"}, + "log_types": []string{"TRANSFER", "ADJUSTMENT", "MARKETING", "CHICKIN", "PURCHASE", "RECORDING"}, "transaction_types": []string{"INCREASE", "DECREASE"}, }, "supplier_categories": []string{ diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go index 71981a9e..23c97788 100644 --- a/internal/modules/production/recordings/module.go +++ b/internal/modules/production/recordings/module.go @@ -16,6 +16,7 @@ import ( rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" @@ -31,6 +32,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) + stockLogRepo := rStockLogs.NewStockLogRepository(db) productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db) productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) @@ -113,6 +115,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate approvalRepo, approvalService, fifoService, + stockLogRepo, productionStandardService, validate, ) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index a5486ab7..4465a039 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -13,6 +13,7 @@ import ( sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -39,8 +40,8 @@ type RecordingService interface { } type RecordingFIFOIntegrationService interface { - ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error - ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error + ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error + ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error } var recordingStockUsableKey = fifo.UsableKeyRecordingStock @@ -57,6 +58,7 @@ type recordingService struct { ApprovalSvc commonSvc.ApprovalService ProductionStandardSvc sProductionStandard.ProductionStandardService FifoSvc commonSvc.FifoService + StockLogRepo rStockLogs.StockLogRepository } func NewRecordingService( @@ -67,6 +69,7 @@ func NewRecordingService( approvalRepo commonRepo.ApprovalRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, + stockLogRepo rStockLogs.StockLogRepository, productionStandardSvc sProductionStandard.ProductionStandardService, validate *validator.Validate, ) RecordingService { @@ -81,6 +84,7 @@ func NewRecordingService( ApprovalSvc: approvalSvc, ProductionStandardSvc: productionStandardSvc, FifoSvc: fifoSvc, + StockLogRepo: stockLogRepo, } } @@ -88,12 +92,14 @@ func NewRecordingFIFOIntegrationService( repo repository.RecordingRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, fifoSvc commonSvc.FifoService, + stockLogRepo rStockLogs.StockLogRepository, ) RecordingFIFOIntegrationService { return &recordingService{ Log: utils.Log, Repository: repo, ProductWarehouseRepo: productWarehouseRepo, FifoSvc: fifoSvc, + StockLogRepo: stockLogRepo, } } @@ -274,7 +280,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil) - if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil { + note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id) + if err := s.consumeRecordingStocks(ctx, tx, mappedStocks, note, actorID); err != nil { return err } @@ -293,7 +300,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } if s.FifoSvc != nil { - if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil { + note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id) + if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil { return err } } @@ -304,7 +312,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } if s.FifoSvc != nil { - if err := s.replenishRecordingEggs(ctx, tx, mappedEggs); err != nil { + note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id) + if err := s.replenishRecordingEggs(ctx, tx, mappedEggs, note, actorID); err != nil { return err } } @@ -346,6 +355,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } ctx := c.Context() + actorID, err := m.ActorIDFromContext(c) + if err != nil { + return nil, err + } var recordingEntity *entity.Recording var updatedRecording *entity.Recording @@ -431,14 +444,16 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if hasStockChanges { - if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil { + note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) + if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, note, actorID); err != nil { return err } } if hasDepletionChanges { if s.FifoSvc != nil { - if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions); err != nil { + note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) + if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions, note, actorID); err != nil { return err } } @@ -464,7 +479,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if s.FifoSvc != nil { - if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil { + note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) + if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil { return err } } @@ -480,6 +496,28 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := ensureRecordingEggsUnused(existingEggs); err != nil { return err } + if s.StockLogRepo != nil { + note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) + logs := make([]*entity.StockLog, 0, len(existingEggs)) + for _, egg := range existingEggs { + if egg.ProductWarehouseId == 0 || egg.Qty <= 0 { + continue + } + logs = append(logs, &entity.StockLog{ + ProductWarehouseId: egg.ProductWarehouseId, + CreatedBy: actorID, + Decrease: float64(egg.Qty), + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: recordingEntity.Id, + Notes: note, + }) + } + if len(logs) > 0 { + if err := s.StockLogRepo.WithTx(tx).CreateMany(ctx, logs, nil); err != nil { + return err + } + } + } if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, nil)); err != nil { s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) return err @@ -498,7 +536,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if s.FifoSvc != nil { - if err := s.replenishRecordingEggs(ctx, tx, mappedEggs); err != nil { + note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) + if err := s.replenishRecordingEggs(ctx, tx, mappedEggs, note, actorID); err != nil { return err } } else { @@ -675,7 +714,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { return err } if s.FifoSvc != nil { - if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions); err != nil { + if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions, "", 0); err != nil { return err } } @@ -697,7 +736,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { return err } - if err := s.releaseRecordingStocks(ctx, tx, oldStocks); err != nil { + if err := s.releaseRecordingStocks(ctx, tx, oldStocks, "", 0); err != nil { return err } @@ -756,10 +795,19 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v return nil } -func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { +func (s *recordingService) consumeRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { if len(stocks) == 0 || s.FifoSvc == nil { return nil } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } for _, stock := range stocks { if stock.Id == 0 { @@ -792,15 +840,42 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm. if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil { return err } + + logDecrease := result.UsageQuantity + if result.PendingQuantity > 0 { + logDecrease += result.PendingQuantity + } + if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: stock.ProductWarehouseId, + CreatedBy: actorID, + Decrease: logDecrease, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: stock.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } } return nil } -func (s *recordingService) consumeRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error { +func (s *recordingService) consumeRecordingDepletions( + ctx context.Context, + tx *gorm.DB, + depletions []entity.RecordingDepletion, + note string, + actorID uint, +) error { if len(depletions) == 0 || s.FifoSvc == nil { return nil } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } for _, depletion := range depletions { if depletion.Id == 0 { @@ -832,19 +907,67 @@ func (s *recordingService) consumeRecordingDepletions(ctx context.Context, tx *g if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil { return err } + + logDecrease := result.UsageQuantity + if result.PendingQuantity > 0 { + logDecrease += result.PendingQuantity + } + if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: sourceWarehouseID, + CreatedBy: actorID, + Decrease: logDecrease, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + + destDelta := depletion.Qty + depletion.PendingQty + if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: depletion.ProductWarehouseId, + CreatedBy: actorID, + Increase: destDelta, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } } return nil } -func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { - return s.consumeRecordingStocks(ctx, tx, stocks) +func (s *recordingService) ConsumeRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { + return s.consumeRecordingStocks(ctx, tx, stocks, note, actorID) } -func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { +func (s *recordingService) releaseRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { if len(stocks) == 0 || s.FifoSvc == nil { return nil } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } for _, stock := range stocks { if stock.Id == 0 { @@ -863,15 +986,38 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm. if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil { return err } + + if stock.UsageQty != nil && *stock.UsageQty > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: stock.ProductWarehouseId, + CreatedBy: actorID, + Increase: *stock.UsageQty, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: stock.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } } return nil } -func (s *recordingService) releaseRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error { +func (s *recordingService) releaseRecordingDepletions( + ctx context.Context, + tx *gorm.DB, + depletions []entity.RecordingDepletion, + note string, + actorID uint, +) error { if len(depletions) == 0 || s.FifoSvc == nil { return nil } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } for _, depletion := range depletions { if depletion.Id == 0 { @@ -898,13 +1044,52 @@ func (s *recordingService) releaseRecordingDepletions(ctx context.Context, tx *g if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil { return err } + + logIncrease := depletion.Qty + if depletion.PendingQty > 0 { + logIncrease += depletion.PendingQty + } + if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: sourceWarehouseID, + CreatedBy: actorID, + Increase: logIncrease, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + + destDelta := depletion.Qty + depletion.PendingQty + if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: depletion.ProductWarehouseId, + CreatedBy: actorID, + Decrease: destDelta, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } } return nil } -func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { - return s.releaseRecordingStocks(ctx, tx, stocks) +func (s *recordingService) ReleaseRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { + return s.releaseRecordingStocks(ctx, tx, stocks, note, actorID) } func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) { @@ -963,27 +1148,48 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context, return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx }) } -func (s *recordingService) replenishRecordingEggs(ctx context.Context, tx *gorm.DB, eggs []entity.RecordingEgg) error { +func (s *recordingService) replenishRecordingEggs( + ctx context.Context, + tx *gorm.DB, + eggs []entity.RecordingEgg, + note string, + actorID uint, +) error { if len(eggs) == 0 || s.FifoSvc == nil { return nil } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } for _, egg := range eggs { if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 { continue } - note := fmt.Sprintf("Recording egg #%d", egg.Id) if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ StockableKey: fifo.StockableKeyRecordingEgg, StockableID: egg.Id, ProductWarehouseID: egg.ProductWarehouseId, Quantity: float64(egg.Qty), - Note: ¬e, Tx: tx, }); err != nil { s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err) return err } + + if strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: egg.ProductWarehouseId, + CreatedBy: actorID, + Increase: float64(egg.Qty), + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: egg.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } } return nil @@ -1034,6 +1240,8 @@ func (s *recordingService) syncRecordingStocks( recordingID uint, existing []entity.RecordingStock, incoming []validation.Stock, + note string, + actorID uint, ) error { if s.FifoSvc == nil { if err := s.Repository.DeleteStocks(tx, recordingID); err != nil { @@ -1080,7 +1288,7 @@ func (s *recordingService) syncRecordingStocks( leftovers = append(leftovers, list...) } if len(leftovers) > 0 { - if err := s.releaseRecordingStocks(ctx, tx, leftovers); err != nil { + if err := s.releaseRecordingStocks(ctx, tx, leftovers, note, actorID); err != nil { return err } ids := make([]uint, 0, len(leftovers)) @@ -1099,7 +1307,7 @@ func (s *recordingService) syncRecordingStocks( if len(stocksToConsume) == 0 { return nil } - return s.consumeRecordingStocks(ctx, tx, stocksToConsume) + return s.consumeRecordingStocks(ctx, tx, stocksToConsume, note, actorID) } type eggTotals struct { diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index b7efbc05..6b423d33 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -18,6 +18,7 @@ import ( rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -830,9 +831,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation receivingAction = entity.ApprovalActionUpdated } } + noteSuffix := "receive" + if receivingAction == entity.ApprovalActionUpdated { + noteSuffix = "edit-receive" + } + receiveNote := fmt.Sprintf("%s#%s", strings.TrimSpace(*purchase.PoNumber), noteSuffix) + transactionErr := s.PurchaseRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { repoTx := rPurchase.NewPurchaseRepository(tx) pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx) + stockLogRepoTx := rStockLogs.NewStockLogRepository(tx) deltas := make(map[uint]float64) affected := make(map[uint]struct{}) @@ -849,6 +857,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation pwID uint qty float64 }, 0, len(prepared)) + logEntries := make([]struct { + itemID uint + pwID uint + delta float64 + }, 0, len(prepared)) for _, prep := range prepared { item := prep.item @@ -869,6 +882,13 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation newPWID = &pwID deltaQty := prep.receivedQty - item.TotalQty + if newPWID != nil && deltaQty != 0 { + logEntries = append(logEntries, struct { + itemID uint + pwID uint + delta float64 + }{itemID: item.Id, pwID: *newPWID, delta: deltaQty}) + } switch { case deltaQty > 0 && newPWID != nil: if s.FifoSvc != nil { @@ -993,6 +1013,33 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation } } + if len(logEntries) > 0 { + logs := make([]*entity.StockLog, 0, len(logEntries)) + for _, entry := range logEntries { + if entry.pwID == 0 || entry.delta == 0 { + continue + } + log := &entity.StockLog{ + ProductWarehouseId: entry.pwID, + CreatedBy: actorID, + LoggableType: string(utils.StockLogTypePurchase), + LoggableId: purchase.Id, + Notes: receiveNote, + } + if entry.delta > 0 { + log.Increase = entry.delta + } else { + log.Decrease = -entry.delta + } + logs = append(logs, log) + } + if len(logs) > 0 { + if err := stockLogRepoTx.CreateMany(c.Context(), logs, nil); err != nil { + return err + } + } + } + if len(affected) > 0 { if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil { return err diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 7d12f5c6..d27b07ef 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -113,6 +113,8 @@ const ( StockLogTypeTransfer StockLogType = "TRANSFER" StockLogTypeMarketing StockLogType = "MARKETING" StockLogTypeChikin StockLogType = "CHICKIN" + StockLogTypePurchase StockLogType = "PURCHASE" + StockLogTypeRecording StockLogType = "RECORDING" ) // ------------------------------------------------------------------- From 645a97b460db1e30ecee7ceb0ab1447dcedc3cf0 Mon Sep 17 00:00:00 2001 From: giovanni Date: Thu, 22 Jan 2026 13:42:36 +0700 Subject: [PATCH 3/9] remove max limit production result --- internal/modules/repports/validations/repport.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 8047f718..48024dbc 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -70,7 +70,7 @@ type HppPerKandangQuery struct { type ProductionResultQuery struct { Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"` } From bac0361df5b4bca4d58dee3cc2a4a6574fb93144 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 22 Jan 2026 13:53:43 +0700 Subject: [PATCH 4/9] [FIX/BE-US] debt-supplier only show receive purchase --- .../repositories/debt_supplier.repository.go | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index 977db610..74039ebf 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -37,6 +37,21 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { return &debtSupplierRepositoryImpl{db: db} } +func (r *debtSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB { + return r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.step_number, a.action"). + Joins(` + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = ? + GROUP BY approvable_id + ) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`, + string(utils.ApprovalWorkflowPurchase), + ) +} + func resolveDebtSupplierDateColumn(filterBy string) string { switch strings.ToLower(strings.TrimSpace(filterBy)) { case "po_date": @@ -54,7 +69,11 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt db := r.db.WithContext(ctx). Model(&entity.Supplier{}). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). - Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id") + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL") if len(filters.SupplierIDs) > 0 { db = db.Where("suppliers.id IN ?", filters.SupplierIDs) @@ -207,7 +226,11 @@ func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplie Table("purchases"). Select("DISTINCT purchases.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). - Where("purchases.supplier_id IN ?", supplierIDs) + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). + Where("purchases.supplier_id IN ?", supplierIDs). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL") if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { @@ -355,7 +378,11 @@ func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Con Table("purchases"). Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("purchases.supplier_id IN ?", supplierIDs). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL"). Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom). Group("purchases.supplier_id"). Scan(&rows).Error; err != nil { From 87973a6c9f7365551793ec9a6d9f298dad3529f8 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 22 Jan 2026 14:15:03 +0700 Subject: [PATCH 5/9] HOTFIX[BE]: update total price calculation based on product flags for delivery and sales orders --- .../salesorder_product.repository.go | 5 +- .../services/deliveryorder.service.go | 52 ++++++++++++++-- .../marketing/services/salesorder.service.go | 59 +++++++++++++++++-- 3 files changed, 106 insertions(+), 10 deletions(-) diff --git a/internal/modules/marketing/repositories/salesorder_product.repository.go b/internal/modules/marketing/repositories/salesorder_product.repository.go index 4d5eb43f..95003939 100644 --- a/internal/modules/marketing/repositories/salesorder_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_product.repository.go @@ -26,7 +26,10 @@ func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository { func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) { var products []entity.MarketingProduct - if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil { + if err := r.DB().WithContext(ctx). + Preload("ProductWarehouse.Product.Flags"). + Where("marketing_id = ?", marketingID). + Find(&products).Error; err != nil { return nil, err } if len(products) == 0 { diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index a521e5bc..b4e3eea0 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -247,9 +247,27 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery itemDeliveryDate = &parsedDate } - // Hitung total_weight dan total_price otomatis + // Cek apakah product punya flag PAKAN atau OVK + isPakanOrOVK := false + if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { + for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { + if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { + isPakanOrOVK = true + break + } + } + } + + // Hitung total_weight dan total_price berdasarkan flag totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - totalPrice := requestedProduct.UnitPrice * totalWeight + var totalPrice float64 + if isPakanOrOVK { + // PAKAN atau OVK: qty × unit_price + totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice + } else { + // Produk lain: total_weight × unit_price + totalPrice = totalWeight * requestedProduct.UnitPrice + } deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -361,9 +379,27 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty - // Hitung total_weight dan total_price otomatis + // Cek apakah product punya flag PAKAN atau OVK + isPakanOrOVK := false + if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { + for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { + if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { + isPakanOrOVK = true + break + } + } + } + + // Hitung total_weight dan total_price berdasarkan flag totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - totalPrice := requestedProduct.UnitPrice * totalWeight + var totalPrice float64 + if isPakanOrOVK { + // PAKAN atau OVK: qty × unit_price + totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice + } else { + // Produk lain: total_weight × unit_price + totalPrice = totalWeight * requestedProduct.UnitPrice + } deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -435,7 +471,13 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor } if pw == nil || pw.Quantity < requestedQty { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { + if pw != nil { + return pw.Quantity + } else { + return 0 + } + }(), requestedQty)) } if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil { diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index e73184dd..e2cfcabb 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -292,9 +292,35 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u for _, rp := range req.MarketingProducts { if old, ok := oldByPW[rp.ProductWarehouseId]; ok { - // Hitung total_weight dan total_price otomatis + // Get product untuk cek flag PAKAN atau OVK + productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { + return db.Preload("Product.Flags") + }) + if err != nil { + return err + } + + // Cek apakah product punya flag PAKAN atau OVK + isPakanOrOVK := false + if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 { + for _, flag := range productWarehouse.Product.Flags { + if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { + isPakanOrOVK = true + break + } + } + } + + // Hitung total_weight dan total_price berdasarkan flag totalWeight := rp.Qty * rp.AvgWeight - totalPrice := rp.UnitPrice * totalWeight + var totalPrice float64 + if isPakanOrOVK { + // PAKAN atau OVK: qty × unit_price + totalPrice = rp.Qty * rp.UnitPrice + } else { + // Produk lain: total_weight × unit_price + totalPrice = totalWeight * rp.UnitPrice + } updateBody := map[string]any{ "product_warehouse_id": rp.ProductWarehouseId, @@ -592,9 +618,34 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { - // Hitung total_weight dan total_price otomatis + // Get product untuk cek flag PAKAN atau OVK + productWarehouse, err := s.ProductWarehouseRepo.GetByID(ctx, rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { + return db.Preload("Product.Flags") + }) + if err != nil { + return err + } + + // Cek apakah product punya flag PAKAN atau OVK + isPakanOrOVK := false + if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 { + for _, flag := range productWarehouse.Product.Flags { + if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { + isPakanOrOVK = true + break + } + } + } + totalWeight := rp.Qty * rp.AvgWeight - totalPrice := rp.UnitPrice * totalWeight + var totalPrice float64 + if isPakanOrOVK { + // PAKAN atau OVK: qty × unit_price + totalPrice = rp.Qty * rp.UnitPrice + } else { + // Produk lain: total_weight × unit_price + totalPrice = totalWeight * rp.UnitPrice + } marketingProduct := &entity.MarketingProduct{ MarketingId: marketingId, From 2e0827dec529f45a260c88bc743da2595791e506 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 22 Jan 2026 14:15:26 +0700 Subject: [PATCH 6/9] [FIX/BE-US] debt-supplier only show receive and changes lunas --- .../repports/services/repport.service.go | 93 +++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 03b1b370..92434b00 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1156,12 +1156,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu return nil, 0, err } - references := collectDebtSupplierReferences(purchases) - paymentSummaries, err := s.DebtSupplierRepo.GetPaymentSummariesByReferences(c.Context(), supplierIDs, references) - if err != nil { - return nil, 0, err - } - location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") @@ -1176,6 +1170,16 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu DeltaBalance float64 CountTotals bool } + type debtSupplierAllocation struct { + RowIndex int + SortTime time.Time + Amount float64 + Purchase entity.Purchase + } + type paymentAllocation struct { + Date time.Time + Amount float64 + } for _, supplierID := range supplierIDs { supplier, exists := supplierMap[supplierID] @@ -1189,19 +1193,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu total := dto.DebtSupplierTotalDTO{} combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) + purchaseAllocations := make([]debtSupplierAllocation, 0, len(items)) for _, purchase := range items { row := buildDebtSupplierRow(purchase, now, location) - if reference := resolveDebtSupplierReference(purchase); reference != "" { - if summary, ok := paymentSummaries[reference]; ok { - if isDebtSupplierPaid(row.TotalPrice, summary.Total) { - row.Status = "Lunas" - if !summary.LatestPaymentDate.IsZero() { - row.Aging = calculateDebtSupplierAging(purchase, summary.LatestPaymentDate, location) - } - } - } - } sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) + rowIndex := len(combinedRows) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, SortTime: sortTime, @@ -1209,6 +1205,24 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu DeltaBalance: -row.TotalPrice, CountTotals: true, }) + purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{ + RowIndex: rowIndex, + SortTime: sortTime, + Amount: row.TotalPrice, + Purchase: purchase, + }) + } + + paymentAllocations := make([]paymentAllocation, 0, len(paymentItems)+1) + initialAllocation := initialBalanceTotals[supplierID] + initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] + paymentCarry := 0.0 + if initialAllocation > 0 && len(purchaseAllocations) > 0 { + paymentAllocations = append(paymentAllocations, paymentAllocation{ + Date: purchaseAllocations[0].SortTime, + Amount: initialAllocation, + }) + } else if initialAllocation < 0 { + paymentCarry = -initialAllocation } for _, payment := range paymentItems { @@ -1221,6 +1235,53 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu DeltaBalance: payment.Nominal, CountTotals: false, }) + paymentAllocations = append(paymentAllocations, paymentAllocation{ + Date: sortTime, + Amount: payment.Nominal, + }) + } + + if len(purchaseAllocations) > 0 && len(paymentAllocations) > 0 { + sort.SliceStable(purchaseAllocations, func(i, j int) bool { + return purchaseAllocations[i].SortTime.Before(purchaseAllocations[j].SortTime) + }) + sort.SliceStable(paymentAllocations, func(i, j int) bool { + return paymentAllocations[i].Date.Before(paymentAllocations[j].Date) + }) + remaining := make([]float64, len(purchaseAllocations)) + for i := range purchaseAllocations { + remaining[i] = purchaseAllocations[i].Amount + } + purchaseIndex := 0 + for _, pay := range paymentAllocations { + amount := pay.Amount + if amount <= 0 { + continue + } + if paymentCarry > 0 { + used := math.Min(amount, paymentCarry) + paymentCarry -= used + amount -= used + } + for amount > 0 && purchaseIndex < len(remaining) { + if remaining[purchaseIndex] <= 0 { + purchaseIndex++ + continue + } + used := math.Min(amount, remaining[purchaseIndex]) + remaining[purchaseIndex] -= used + amount -= used + if remaining[purchaseIndex] <= 0.000001 { + allocation := purchaseAllocations[purchaseIndex] + combinedRows[allocation.RowIndex].Row.Status = "Lunas" + combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location) + purchaseIndex++ + } + } + if purchaseIndex >= len(remaining) { + break + } + } } sort.SliceStable(combinedRows, func(i, j int) bool { From 6bc5e7d2931f4aca15df93adbe098faa1a261759 Mon Sep 17 00:00:00 2001 From: giovanni Date: Thu, 22 Jan 2026 14:19:16 +0700 Subject: [PATCH 7/9] fix get average bw --- internal/modules/repports/services/repport.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 03b1b370..effec76a 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -860,7 +860,7 @@ func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKa var rows []entity.ProjectFlockKandangUniformity if err := s.DB.WithContext(ctx). Model(&entity.ProjectFlockKandangUniformity{}). - Select("week, uniformity, uniform_date, id"). + Select("week, uniformity, uniform_date, id, chart_data"). Where("project_flock_kandang_id = ?", projectFlockKandangID). Where("week IN ?", weeks). Order("uniform_date DESC"). From 202a8ffc6673a780a21e40ebcf7f73c03a00bbab Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 22 Jan 2026 14:29:56 +0700 Subject: [PATCH 8/9] HOTFIX[BE]: filter closing overhead by expense category "BOP" --- .../expenses/repositories/expense_realization.repository.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 60ec97a7..0ccab661 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -70,7 +70,8 @@ func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Contex Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). - Where("expenses.realization_date IS NOT NULL") + Where("expenses.realization_date IS NOT NULL"). + Where("expenses.category = ?", "BOP") if projectFlockKandangID != nil { db = db.Where(`( From 12ed9cd753fef32429fb54b4d01ac9c48e95c7c4 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 22 Jan 2026 14:44:53 +0700 Subject: [PATCH 9/9] [FIX/BE-US] fix day in recording --- .../recordings/services/recording.service.go | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index a5486ab7..c0d0f78b 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -159,14 +159,13 @@ func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) ( return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") } - db := s.Repository.DB().WithContext(c.Context()) - next, err := s.Repository.GenerateNextDay(db, projectFlockKandangId) + day, err := s.computeRecordingDay(c.Context(), projectFlockKandangId, time.Now().UTC()) if err != nil { - s.Log.Errorf("Failed to compute next recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err) + s.Log.Errorf("Failed to compute recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err) return 0, err } - return next, nil + return day, nil } func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) { @@ -208,6 +207,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } } + day, err := s.computeRecordingDay(ctx, pfk.Id, recordTime) + if err != nil { + return nil, err + } + if !isLaying && len(req.Eggs) > 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") } @@ -221,13 +225,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } var createdRecording entity.Recording transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { - nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId) - if err != nil { - s.Log.Errorf("Failed to determine recording day: %+v", err) - return err - } if s.ProductionStandardSvc != nil { - if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, nextDay); err != nil { + if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, day); err != nil { return err } } @@ -241,7 +240,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return fiber.NewError(fiber.StatusBadRequest, "Recording for this project flock today already exists") } - day := nextDay createdRecording = entity.Recording{ ProjectFlockKandangId: req.ProjectFlockKandangId, RecordDatetime: recordTime, @@ -929,6 +927,40 @@ func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, pro return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan") } +func (s *recordingService) computeRecordingDay(ctx context.Context, projectFlockKandangID uint, recordTime time.Time) (int, error) { + if projectFlockKandangID == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid") + } + + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi") + } + + var chickinDate time.Time + for _, pop := range populations { + if pop.ProjectChickin == nil || pop.ProjectChickin.ChickInDate.IsZero() { + continue + } + if chickinDate.IsZero() || pop.ProjectChickin.ChickInDate.Before(chickinDate) { + chickinDate = pop.ProjectChickin.ChickInDate + } + } + if chickinDate.IsZero() { + return 0, fiber.NewError(fiber.StatusBadRequest, "Tanggal chick in tidak ditemukan") + } + + chickinDay := time.Date(chickinDate.Year(), chickinDate.Month(), chickinDate.Day(), 0, 0, 0, 0, time.UTC) + recordDay := time.Date(recordTime.Year(), recordTime.Month(), recordTime.Day(), 0, 0, 0, 0, time.UTC) + diff := int(recordDay.Sub(chickinDay).Hours() / 24) + if diff < 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "Record date tidak boleh sebelum tanggal chick in") + } + + return diff + 1, nil +} + func buildWarehouseDeltas( oldDepletions, newDepletions []entity.RecordingDepletion, oldEggs, newEggs []entity.RecordingEgg,