From 7f2401311b3cc458e8b72aaad513b248a337197a Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 15 Jan 2026 13:48:00 +0700 Subject: [PATCH 1/2] [FIX/BE-US] add response warehouse and project flock kandang --- ...115062032_add_fifo_recording_eggs.down.sql | 3 + ...60115062032_add_fifo_recording_eggs.up.sql | 7 ++ internal/entities/recording_egg.go | 2 + .../controllers/projectflock.controller.go | 7 ++ .../dto/projectflock_kandang.dto.go | 30 ++++---- .../services/projectflock.service.go | 26 +++++++ .../recordings/dto/recording.dto.go | 4 +- .../modules/production/recordings/module.go | 16 ++++ .../recordings/services/recording.service.go | 74 ++++++++++++++++++- internal/utils/fifo/constants.go | 1 + 10 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 internal/database/migrations/20260115062032_add_fifo_recording_eggs.down.sql create mode 100644 internal/database/migrations/20260115062032_add_fifo_recording_eggs.up.sql diff --git a/internal/database/migrations/20260115062032_add_fifo_recording_eggs.down.sql b/internal/database/migrations/20260115062032_add_fifo_recording_eggs.down.sql new file mode 100644 index 00000000..8fb42a96 --- /dev/null +++ b/internal/database/migrations/20260115062032_add_fifo_recording_eggs.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE recording_eggs + DROP COLUMN IF EXISTS total_used, + DROP COLUMN IF EXISTS total_qty; diff --git a/internal/database/migrations/20260115062032_add_fifo_recording_eggs.up.sql b/internal/database/migrations/20260115062032_add_fifo_recording_eggs.up.sql new file mode 100644 index 00000000..dbadd9e3 --- /dev/null +++ b/internal/database/migrations/20260115062032_add_fifo_recording_eggs.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE recording_eggs + ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL, + ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL; + +UPDATE recording_eggs +SET total_qty = qty +WHERE total_qty = 0; diff --git a/internal/entities/recording_egg.go b/internal/entities/recording_egg.go index 68269728..b48c49ca 100644 --- a/internal/entities/recording_egg.go +++ b/internal/entities/recording_egg.go @@ -7,6 +7,8 @@ type RecordingEgg struct { RecordingId uint `gorm:"column:recording_id;not null;index"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` Qty int `gorm:"column:qty;not null"` + TotalQty float64 `gorm:"column:total_qty"` + TotalUsed float64 `gorm:"column:total_used"` Weight *float64 `gorm:"column:weight"` CreatedBy uint `gorm:"column:created_by"` CreatedAt time.Time `gorm:"autoCreateTime"` diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 4315b948..c13baab6 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" @@ -281,6 +282,12 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { dtoResult := dto.ToProjectFlockKandangDTO(*result) dtoResult.AvailableQuantity = float64(availableStock) + if warehouse, werr := u.ProjectflockService.GetWarehouseByKandangID(c, result.KandangId); werr != nil { + return werr + } else if warehouse != nil { + mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse) + dtoResult.Warehouse = &mapped + } if withPopulation { population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id) if err != nil { diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 8dedaf15..b6bbf852 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -7,6 +7,7 @@ import ( kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -17,24 +18,25 @@ type KandangWithPivotDTO struct { type ProjectFlockWithPivotDTO struct { ProjectFlockRelationDTO - Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` - Category string `json:"category"` - Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` - Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` - Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ProjectFlockId uint `json:"project_flock_id"` - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` - Population *float64 `json:"population,omitempty"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` + Population *float64 `json:"population,omitempty"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 3dbe3f4b..05e21894 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -38,6 +38,7 @@ type ProjectflockService interface { GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) + GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) @@ -518,6 +519,31 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } +func (s projectflockService) GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) { + if kandangID == 0 || s.WarehouseRepo == nil { + return nil, nil + } + + var warehouse entity.Warehouse + err := s.WarehouseRepo.DB().WithContext(ctx.Context()). + Preload("Area"). + Preload("Location"). + Preload("Kandang"). + Where("kandang_id = ?", kandangID). + Where("deleted_at IS NULL"). + Order("id DESC"). + First(&warehouse).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + if err != nil { + s.Log.Errorf("Failed to fetch warehouse for kandang %d: %+v", kandangID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouse") + } + + return &warehouse, nil +} + func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { if len(projectIDs) == 0 { return map[uint]int{}, nil diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 4deb9f7e..ebb093ba 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -82,11 +82,11 @@ type RecordingListDTO struct { CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"` } type RecordingDetailDTO struct { RecordingListDTO - Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"` ProductCategory string `json:"product_category"` Depletions []RecordingDepletionDTO `json:"depletions"` Stocks []RecordingStockDTO `json:"stocks"` @@ -133,7 +133,6 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { return RecordingDetailDTO{ RecordingListDTO: listDTO, - Warehouse: recordingWarehouseDTO(e), ProductCategory: recordingProductCategory(e), Depletions: ToRecordingDepletionDTOs(e.Depletions), Stocks: ToRecordingStockDTOs(e.Stocks), @@ -203,6 +202,7 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO { CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, + Warehouse: recordingWarehouseDTO(e), } } diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go index 91b024ac..11a1e152 100644 --- a/internal/modules/production/recordings/module.go +++ b/internal/modules/production/recordings/module.go @@ -43,6 +43,22 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate ) fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) + if err := fifoService.RegisterStockable(fifo.StockableConfig{ + Key: fifo.StockableKeyRecordingEgg, + Table: "recording_eggs", + Columns: fifo.StockableColumns{ + ID: "id", + ProductWarehouseID: "product_warehouse_id", + TotalQuantity: "total_qty", + TotalUsedQuantity: "total_used", + CreatedAt: "created_at", + }, + OrderBy: []string{"created_at ASC", "id ASC"}, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register recording egg stockable workflow: %v", err)) + } + } if err := fifoService.RegisterUsable(fifo.UsableConfig{ Key: fifo.UsableKeyRecordingStock, Table: "recording_stocks", diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 88ed4cf7..5dabad9f 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -290,8 +290,19 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent s.Log.Errorf("Failed to persist eggs: %+v", err) return err } + if s.FifoSvc != nil { + if err := s.replenishRecordingEggs(ctx, tx, mappedEggs); err != nil { + return err + } + } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)); err != nil { + var warehouseDeltas map[uint]float64 + if s.FifoSvc != nil { + warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil) + } else { + warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) + } + if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil { s.Log.Errorf("Failed to adjust product warehouses: %+v", err) return err } @@ -438,6 +449,16 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if hasEggChanges { + if s.FifoSvc != nil { + if err := ensureRecordingEggsUnused(existingEggs); 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 + } + } + if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil { s.Log.Errorf("Failed to clear eggs: %+v", err) return err @@ -449,9 +470,15 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return err } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, mappedEggs)); err != nil { - s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) - return err + if s.FifoSvc != nil { + if err := s.replenishRecordingEggs(ctx, tx, mappedEggs); err != nil { + return err + } + } else { + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, mappedEggs)); err != nil { + s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) + return err + } } } @@ -626,6 +653,11 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to list eggs before delete: %+v", err) return err } + if s.FifoSvc != nil { + if err := ensureRecordingEggsUnused(oldEggs); err != nil { + return err + } + } oldStocks, err := s.Repository.ListStocks(tx, id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -802,6 +834,32 @@ 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 { + if len(eggs) == 0 || s.FifoSvc == nil { + return nil + } + + 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 + } + } + + return nil +} + type desiredStock struct { Usage float64 Pending float64 @@ -922,6 +980,14 @@ type eggTotals struct { Weight float64 } +func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error { + for _, egg := range eggs { + if egg.TotalUsed > 0 { + return fiber.NewError(fiber.StatusBadRequest, "Recording egg sudah digunakan sehingga tidak dapat diubah") + } + } + return nil +} func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { hasPending := false diff --git a/internal/utils/fifo/constants.go b/internal/utils/fifo/constants.go index 03f61f82..076d960d 100644 --- a/internal/utils/fifo/constants.go +++ b/internal/utils/fifo/constants.go @@ -15,4 +15,5 @@ const ( StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION" + StockableKeyRecordingEgg StockableKey = "RECORDING_EGG" ) From f1032b44d17a7dc96487f35a7f0f0607b463feb1 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 15 Jan 2026 15:42:57 +0700 Subject: [PATCH 2/2] [FIX/BE-US] adjustment recording --- .../controllers/projectflock.controller.go | 12 +++++++----- .../dto/projectflock_kandang.dto.go | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index c13baab6..e82d3af5 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -279,9 +279,14 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { if err != nil { return err } + _ = availableStock dtoResult := dto.ToProjectFlockKandangDTO(*result) - dtoResult.AvailableQuantity = float64(availableStock) + if population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id); err != nil { + return err + } else { + dtoResult.AvailableQuantity = population + } if warehouse, werr := u.ProjectflockService.GetWarehouseByKandangID(c, result.KandangId); werr != nil { return werr } else if warehouse != nil { @@ -289,10 +294,7 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { dtoResult.Warehouse = &mapped } if withPopulation { - population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id) - if err != nil { - return err - } + population := dtoResult.AvailableQuantity dtoResult.Population = &population } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index b6bbf852..c18f3f65 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -18,13 +18,14 @@ type KandangWithPivotDTO struct { type ProjectFlockWithPivotDTO struct { ProjectFlockRelationDTO - Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` - Category string `json:"category"` - Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` - ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` - Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` - Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` + ProductionStandardId uint `json:"production_standard_id"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` } type ProjectFlockKandangDTO struct { @@ -55,7 +56,8 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD Period: e.Period, FlockName: e.ProjectFlock.FlockName, }, - Category: e.ProjectFlock.Category, + Category: e.ProjectFlock.Category, + ProductionStandardId: e.ProjectFlock.ProductionStandardId, } if e.ProjectFlock.Area.Id != 0 {