Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/UUIT-Recording-closing-report-uniformity-dashboard

This commit is contained in:
ragilap
2026-01-23 11:57:38 +07:00
42 changed files with 1808 additions and 720 deletions
@@ -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,
)
@@ -171,6 +171,7 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
var days []int
if err := tx.Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
Where("deleted_at IS NULL").
Where("day IS NOT NULL").
Pluck("day", &days).Error; err != nil {
return 0, err
@@ -399,7 +400,7 @@ func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordin
}
err = tx.
Table("recording_eggs").
Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(recording_eggs.qty * COALESCE(recording_eggs.weight, 0)), 0) AS total_weight_grams").
Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(COALESCE(recording_eggs.weight, 0) * 1000), 0) AS total_weight_grams").
Where("recording_eggs.recording_id = ?", recordingID).
Scan(&result).Error
if err != nil {
@@ -485,7 +486,7 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
var result float64
err := r.DB().WithContext(ctx).
Table("recording_eggs").
Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000").
Select("COALESCE(SUM(recording_eggs.weight), 0)").
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
@@ -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,
}
}
@@ -159,14 +165,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 +213,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 +231,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 +246,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,
@@ -274,7 +278,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 +298,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 +310,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 +353,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 +442,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 +477,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 +494,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 +534,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 +712,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 +734,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 +793,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 +838,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 +905,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 +984,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 +1042,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) {
@@ -929,6 +1112,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,
@@ -963,27 +1180,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: &note,
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 +1272,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 +1320,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 +1339,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 {
@@ -1157,7 +1397,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
}
current := existingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight
current.Weight += weight
existingTotals[egg.ProductWarehouseId] = current
}
@@ -1169,7 +1409,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
}
current := incomingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight
current.Weight += weight
incomingTotals[egg.ProductWarehouseId] = current
}
@@ -1328,7 +1568,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMass = totalEggWeightGrams / remainingChick
eggMass = (totalEggWeightGrams / remainingChick) / 1000
updates["egg_mass"] = eggMass
recording.EggMass = &eggMass
} else {
@@ -70,7 +70,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id))
result, approval, err := u.TransferLayingService.GetOne(c, uint(id))
if err != nil {
return err
}
@@ -28,8 +28,7 @@ import (
type TransferLayingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
@@ -156,14 +155,15 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
return transferLayings, total, nil
}
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
return nil, nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
if err != nil {
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
return nil, err
return nil, nil, err
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
@@ -174,15 +174,6 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran
transferLaying.LatestApproval = latestApproval
}
return transferLaying, nil
}
func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
transferLaying, err := s.GetOne(c, id)
if err != nil {
return nil, nil, err
}
return transferLaying, transferLaying.LatestApproval, nil
}
@@ -406,7 +397,12 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
}
return s.GetOne(c, createBody.Id)
laying_transfer, _, err := s.GetOne(c, createBody.Id)
if err != nil {
return nil, err
}
return laying_transfer, nil
}
func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
@@ -582,7 +578,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return nil, err
}
return s.GetOne(c, id)
layingTransfer, _, err := s.GetOne(c, id)
return layingTransfer, err
}
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
@@ -773,7 +771,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
updated := make([]entity.LayingTransfer, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
transfer, err := s.GetOne(c, approvableID)
transfer, _, err := s.GetOne(c, approvableID)
if err != nil {
return nil, err
}