diff --git a/internal/database/migrations/20251228173112_adjustment_recording_egg_and_deleting_recording_bws.down.sql b/internal/database/migrations/20251228173112_adjustment_recording_egg_and_deleting_recording_bws.down.sql new file mode 100644 index 00000000..c42fd7d6 --- /dev/null +++ b/internal/database/migrations/20251228173112_adjustment_recording_egg_and_deleting_recording_bws.down.sql @@ -0,0 +1,54 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS recording_bws ( + id BIGSERIAL PRIMARY KEY, + recording_id BIGINT NOT NULL, + avg_weight NUMERIC(8,2) NOT NULL, + qty NUMERIC(15,3) NOT NULL DEFAULT 1, + total_weight NUMERIC(10,3) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT fk_recording_bws_recording + FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE, + + CONSTRAINT chk_recording_bws_nonneg + CHECK (avg_weight >= 0 AND qty >= 0 AND total_weight >= 0) +); + +CREATE INDEX IF NOT EXISTS idx_recording_bws_recording + ON recording_bws (recording_id); + +ALTER TABLE recordings + DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3; + +ALTER TABLE recordings + DROP COLUMN IF EXISTS hand_day, + DROP COLUMN IF EXISTS hand_house, + DROP COLUMN IF EXISTS feed_intake, + DROP COLUMN IF EXISTS egg_mesh, + DROP COLUMN IF EXISTS egg_weight; + +ALTER TABLE recordings + ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK ( + (total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND + (cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND + (daily_gain IS NULL OR daily_gain >= 0) AND + (avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND + (cum_intake IS NULL OR cum_intake >= 0) AND + (fcr_value IS NULL OR fcr_value >= 0) AND + (total_chick_qty IS NULL OR total_chick_qty >= 0) + ); + +ALTER TABLE recording_eggs + DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty; + +ALTER TABLE recording_eggs + ALTER COLUMN weight TYPE NUMERIC(10,3) USING weight::NUMERIC(10,3); + +ALTER TABLE recording_eggs + ADD CONSTRAINT chk_recording_eggs_qty CHECK ( + qty >= 0 AND (weight IS NULL OR weight >= 0) + ); + +COMMIT; diff --git a/internal/database/migrations/20251228173112_adjustment_recording_egg_and_deleting_recording_bws.up.sql b/internal/database/migrations/20251228173112_adjustment_recording_egg_and_deleting_recording_bws.up.sql new file mode 100644 index 00000000..032d77b5 --- /dev/null +++ b/internal/database/migrations/20251228173112_adjustment_recording_egg_and_deleting_recording_bws.up.sql @@ -0,0 +1,44 @@ +BEGIN; + +ALTER TABLE recordings + DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2; + +ALTER TABLE recordings + ADD COLUMN IF NOT EXISTS hand_day NUMERIC(15,3), + ADD COLUMN IF NOT EXISTS hand_house NUMERIC(15,3), + ADD COLUMN IF NOT EXISTS feed_intake NUMERIC(15,3), + ADD COLUMN IF NOT EXISTS egg_mesh NUMERIC(15,3), + ADD COLUMN IF NOT EXISTS egg_weight NUMERIC(15,3); + +ALTER TABLE recordings + ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK ( + (total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND + (cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND + (daily_gain IS NULL OR daily_gain >= 0) AND + (avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND + (cum_intake IS NULL OR cum_intake >= 0) AND + (fcr_value IS NULL OR fcr_value >= 0) AND + (total_chick_qty IS NULL OR total_chick_qty >= 0) AND + (hand_day IS NULL OR hand_day >= 0) AND + (hand_house IS NULL OR hand_house >= 0) AND + (feed_intake IS NULL OR feed_intake >= 0) AND + (egg_mesh IS NULL OR egg_mesh >= 0) AND + (egg_weight IS NULL OR egg_weight >= 0) + ); + +ALTER TABLE recording_eggs + ALTER COLUMN weight TYPE NUMERIC(15,3) USING weight::NUMERIC(15,3); + +ALTER TABLE recording_eggs + DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty; + +ALTER TABLE recording_eggs + ADD CONSTRAINT chk_recording_eggs_qty CHECK ( + qty >= 0 AND + (weight IS NULL OR weight >= 0) + ); + +DROP INDEX IF EXISTS idx_recording_bws_recording; +DROP TABLE IF EXISTS recording_bws; + +COMMIT; diff --git a/internal/entities/recording.go b/internal/entities/recording.go index 42535365..7f952a62 100644 --- a/internal/entities/recording.go +++ b/internal/entities/recording.go @@ -13,11 +13,14 @@ type Recording struct { Day *int `gorm:"column:day"` TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` - DailyGain *float64 `gorm:"column:daily_gain"` - AvgDailyGain *float64 `gorm:"column:avg_daily_gain"` CumIntake *int `gorm:"column:cum_intake"` FcrValue *float64 `gorm:"column:fcr_value"` TotalChickQty *float64 `gorm:"column:total_chick_qty"` + HandDay *float64 `gorm:"column:hand_day"` + HandHouse *float64 `gorm:"column:hand_house"` + FeedIntake *float64 `gorm:"column:feed_intake"` + EggMesh *float64 `gorm:"column:egg_mesh"` + EggWeight *float64 `gorm:"column:egg_weight"` CreatedBy uint `gorm:"column:created_by"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` @@ -25,10 +28,17 @@ type Recording struct { ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` - BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"` Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"` Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"` Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"` LatestApproval *Approval `gorm:"-" json:"-"` + + StandardHandDay *float64 `gorm:"-"` + StandardHandHouse *float64 `gorm:"-"` + StandardFeedIntake *float64 `gorm:"-"` + StandardMaxDepletion *float64 `gorm:"-"` + StandardEggMesh *float64 `gorm:"-"` + StandardEggWeight *float64 `gorm:"-"` + StandardFcr *float64 `gorm:"-"` } diff --git a/internal/entities/recording_bw.go b/internal/entities/recording_bw.go deleted file mode 100644 index 041df0f6..00000000 --- a/internal/entities/recording_bw.go +++ /dev/null @@ -1,15 +0,0 @@ -package entities - -import "time" - -type RecordingBW struct { - Id uint `gorm:"primaryKey"` - RecordingId uint `gorm:"column:recording_id;not null;index"` - AvgWeight float64 `gorm:"column:avg_weight;not null"` - Qty float64 `gorm:"column:qty;not null"` - TotalWeight float64 `gorm:"column:total_weight;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - - Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` -} diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 51fba8a4..c34651ba 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -22,11 +22,21 @@ type RecordingRelationDTO struct { ProjectFlockCategory string `json:"project_flock_category"` TotalDepletionQty float64 `json:"total_depletion_qty"` CumDepletionRate float64 `json:"cum_depletion_rate"` - DailyGain float64 `json:"daily_gain"` - AvgDailyGain float64 `json:"avg_daily_gain"` CumIntake int `json:"cum_intake"` FcrValue float64 `json:"fcr_value"` TotalChickQty float64 `json:"total_chick_qty"` + HandDay float64 `json:"hand_day"` + HandHouse float64 `json:"hand_house"` + FeedIntake float64 `json:"feed_intake"` + EggMesh float64 `json:"egg_mesh"` + EggWeight float64 `json:"egg_weight"` + StandardHandDay *float64 `json:"hand_day_std,omitempty"` + StandardHandHouse *float64 `json:"hand_house_std,omitempty"` + StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"` + StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"` + StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"` + StandardEggWeight *float64 `json:"egg_weight_std,omitempty"` + StandardFcr *float64 `json:"fcr_std,omitempty"` Approval approvalDTO.ApprovalRelationDTO `json:"approval"` } @@ -39,16 +49,9 @@ type RecordingListDTO struct { type RecordingDetailDTO struct { RecordingListDTO - BodyWeights []RecordingBodyWeightDTO `json:"body_weights"` - Depletions []RecordingDepletionDTO `json:"depletions"` - Stocks []RecordingStockDTO `json:"stocks"` - Eggs []RecordingEggDTO `json:"eggs"` -} - -type RecordingBodyWeightDTO struct { - AvgWeight float64 `json:"avg_weight"` - Qty float64 `json:"qty"` - TotalWeight float64 `json:"total_weight"` + Depletions []RecordingDepletionDTO `json:"depletions"` + Stocks []RecordingStockDTO `json:"stocks"` + Eggs []RecordingEggDTO `json:"eggs"` } type RecordingDepletionDTO struct { @@ -88,11 +91,14 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { day int totalDepletionQty float64 cumDepletionRate float64 - dailyGain float64 - avgDailyGain float64 cumIntake int fcrValue float64 totalChickQty float64 + handDay float64 + handHouse float64 + feedIntake float64 + eggMesh float64 + eggWeight float64 ) if e.Day != nil { @@ -104,12 +110,6 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { if e.CumDepletionRate != nil { cumDepletionRate = *e.CumDepletionRate } - if e.DailyGain != nil { - dailyGain = *e.DailyGain - } - if e.AvgDailyGain != nil { - avgDailyGain = *e.AvgDailyGain - } if e.CumIntake != nil { cumIntake = *e.CumIntake } @@ -119,6 +119,21 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { if e.TotalChickQty != nil { totalChickQty = *e.TotalChickQty } + if e.HandDay != nil { + handDay = *e.HandDay + } + if e.HandHouse != nil { + handHouse = *e.HandHouse + } + if e.FeedIntake != nil { + feedIntake = *e.FeedIntake + } + if e.EggMesh != nil { + eggMesh = *e.EggMesh + } + if e.EggWeight != nil { + eggWeight = *e.EggWeight + } if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 { category := e.ProjectFlockKandang.ProjectFlock.Category @@ -139,11 +154,21 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { ProjectFlockCategory: projectFlockCategory, TotalDepletionQty: totalDepletionQty, CumDepletionRate: cumDepletionRate, - DailyGain: dailyGain, - AvgDailyGain: avgDailyGain, CumIntake: cumIntake, FcrValue: fcrValue, TotalChickQty: totalChickQty, + HandDay: handDay, + HandHouse: handHouse, + FeedIntake: feedIntake, + EggMesh: eggMesh, + EggWeight: eggWeight, + StandardHandDay: e.StandardHandDay, + StandardHandHouse: e.StandardHandHouse, + StandardFeedIntake: e.StandardFeedIntake, + StandardMaxDepletion: e.StandardMaxDepletion, + StandardEggMesh: e.StandardEggMesh, + StandardEggWeight: e.StandardEggWeight, + StandardFcr: e.StandardFcr, Approval: latestApproval, } } @@ -183,25 +208,12 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { return RecordingDetailDTO{ RecordingListDTO: listDTO, - BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights), Depletions: ToRecordingDepletionDTOs(e.Depletions), Stocks: ToRecordingStockDTOs(e.Stocks), Eggs: eggs, } } -func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO { - result := make([]RecordingBodyWeightDTO, len(bodyWeights)) - for i, bw := range bodyWeights { - result[i] = RecordingBodyWeightDTO{ - AvgWeight: bw.AvgWeight, - Qty: bw.Qty, - TotalWeight: bw.TotalWeight, - } - } - return result -} - func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO { result := make([]RecordingDepletionDTO, len(depletions)) for i, d := range depletions { diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index a615692f..d75060ad 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -20,9 +20,6 @@ type RecordingRepository interface { GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) - CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error - DeleteBodyWeights(tx *gorm.DB, recordingID uint) error - CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error DeleteStocks(tx *gorm.DB, recordingID uint) error ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) @@ -42,10 +39,11 @@ type RecordingRepository interface { SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) - GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) + GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) - GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) - GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) + GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) + GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) + GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) @@ -67,7 +65,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { Preload("CreatedUser"). Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang.ProjectFlock"). - Preload("BodyWeights"). Preload("Depletions"). Preload("Depletions.ProductWarehouse"). Preload("Depletions.ProductWarehouse.Product"). @@ -114,17 +111,6 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda return nextRecordingDay(days), nil } -func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error { - if len(bodyWeights) == 0 { - return nil - } - return tx.Create(&bodyWeights).Error -} - -func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error { - return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error -} - func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error { if len(stocks) == 0 { return nil @@ -293,21 +279,18 @@ func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandang return int64(math.Round(total)), nil } -func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) { - var result struct { - TotalWeight float64 - TotalQty float64 - } - if err := tx.Model(&entity.RecordingBW{}). - Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty"). - Where("recording_id = ?", recordingID). - Scan(&result).Error; err != nil { - return 0, err - } - if result.TotalQty == 0 { +func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) { + if projectFlockKandangId == 0 { return 0, nil } - return result.TotalWeight / result.TotalQty, nil + + var result float64 + err := tx. + Table("project_chickins"). + Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId). + Scan(&result).Error + return result, err } func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { @@ -344,22 +327,48 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u return total, nil } -func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) { +func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) { + if recordingID == 0 { + return 0, 0, nil + } + var result struct { - FcrID uint + TotalQty float64 + TotalWeightGrams float64 } - if err := tx.Table("project_flock_kandangs"). - Select("project_flocks.fcr_id AS fcr_id"). - Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). - Where("project_flock_kandangs.id = ?", projectFlockKandangId). - Scan(&result).Error; err != nil { - return 0, err + 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"). + Where("recording_eggs.recording_id = ?", recordingID). + Scan(&result).Error + if err != nil { + return 0, 0, err } - return result.FcrID, nil + return result.TotalQty, result.TotalWeightGrams, nil } -func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) { - if fcrId == 0 { +func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang( + tx *gorm.DB, + projectFlockKandangId uint, + recordTime time.Time, +) (float64, error) { + if projectFlockKandangId == 0 { + return 0, nil + } + + var result float64 + err := tx. + Table("recording_eggs"). + Select("COALESCE(SUM(recording_eggs.qty), 0)"). + Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id"). + Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId). + Where("recordings.record_datetime <= ?", recordTime). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) { + if fcrId == 0 || currentWeightKg <= 0 { return 0, false, nil } @@ -382,49 +391,12 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint return 0, false, err } - weight := standard.Weight - if weight > 10 { - return weight / 1000, true, nil - } - return weight, true, nil + return standard.FcrNumber, true, nil } func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { - if projectFlockID == 0 { - return 0, 0, nil - } - - totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID) - if err != nil { - return 0, 0, err - } - - totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) - if err != nil { - return 0, 0, err - } - - actualQty := totalChickinQty - totalDepletion - - avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) - if err != nil { - return 0, 0, err - } - - totalWeight = actualQty * avgWeight - - return totalWeight, actualQty, nil -} - -func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { - var result float64 - err := r.DB().WithContext(ctx). - Table("project_chickins"). - Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id"). - Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). - Scan(&result).Error - return result, err + // Body-weight tracking is removed; keep stub for report compatibility. + return 0, 0, nil } func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { @@ -440,16 +412,8 @@ func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context. } func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { - var result float64 - err := r.DB().WithContext(ctx). - Table("recording_bws"). - Select("COALESCE(AVG(recording_bws.avg_weight), 0)"). - Joins("JOIN recordings ON recordings.id = recording_bws.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). - Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID). - Scan(&result).Error - return result, err + // Body-weight tracking is removed; keep stub for report compatibility. + return 0, nil } func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index a83c1128..5b09d003 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -9,6 +9,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" 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" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" @@ -121,6 +122,9 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti if err := s.attachLatestApprovals(c.Context(), recordings); err != nil { return nil, 0, err } + if err := s.attachProductionStandards(c.Context(), recordings); err != nil { + return nil, 0, err + } return recordings, total, nil } @@ -138,6 +142,9 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro if err := s.attachLatestApproval(c.Context(), recording); err != nil { return nil, err } + if err := s.attachProductionStandard(c.Context(), recording); err != nil { + return nil, err + } return recording, nil } @@ -233,12 +240,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } - mappedBodyWeights := recordingutil.MapBodyWeights(createdRecording.Id, req.BodyWeights) - if err := s.Repository.CreateBodyWeights(tx, mappedBodyWeights); err != nil { - s.Log.Errorf("Failed to persist body weights: %+v", err) - return err - } - mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks) if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { s.Log.Errorf("Failed to persist stocks: %+v", err) @@ -261,7 +262,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, nil, nil, mappedEggs)); err != nil { + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)); err != nil { s.Log.Errorf("Failed to adjust product warehouses: %+v", err) return err } @@ -291,7 +292,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return nil, err } - if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { + if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { return s.GetOne(c, id) } @@ -311,12 +312,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } recordingEntity = recording - hasBodyChanges := req.BodyWeights != nil hasStockChanges := req.Stocks != nil hasDepletionChanges := req.Depletions != nil hasEggChanges := req.Eggs != nil - if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges { + if !hasStockChanges && !hasDepletionChanges && !hasEggChanges { return nil } @@ -346,17 +346,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } - if hasBodyChanges { - if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil { - s.Log.Errorf("Failed to clear body weights: %+v", err) - return err - } - if err := s.Repository.CreateBodyWeights(tx, recordingutil.MapBodyWeights(recordingEntity.Id, req.BodyWeights)); err != nil { - s.Log.Errorf("Failed to update body weights: %+v", err) - return err - } - } - if hasStockChanges { existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) if err != nil { @@ -402,7 +391,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return err } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil, nil, nil)); err != nil { + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil { s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) return err } @@ -426,13 +415,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return err } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, nil, nil, existingEggs, mappedEggs)); err != nil { + 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 hasBodyChanges || hasStockChanges || hasDepletionChanges { + if hasStockChanges || hasDepletionChanges { if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { s.Log.Errorf("Failed to recompute recording metrics: %+v", err) return err @@ -596,7 +585,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { return err } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, nil, nil, oldEggs, nil)); err != nil { + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldEggs, nil)); err != nil { return err } @@ -724,7 +713,6 @@ func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm. func buildWarehouseDeltas( oldDepletions, newDepletions []entity.RecordingDepletion, - oldStocks, newStocks []entity.RecordingStock, oldEggs, newEggs []entity.RecordingEgg, ) map[uint]float64 { deltas := make(map[uint]float64) @@ -775,7 +763,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var prevCumDepletionQty float64 var prevCumIntake float64 - var prevAvgWeight float64 if prevRecording != nil { if prevRecording.TotalDepletionQty != nil { prevCumDepletionQty = *prevRecording.TotalDepletionQty @@ -783,10 +770,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm if prevRecording.CumIntake != nil { prevCumIntake = float64(*prevRecording.CumIntake) } - prevAvgWeight, err = s.Repository.GetAverageBodyWeight(tx, prevRecording.Id) - if err != nil { - return fmt.Errorf("getAverageBodyWeight(prev): %w", err) - } } totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId) @@ -794,20 +777,25 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm return fmt.Errorf("getTotalChick: %w", err) } - currentAvgWeight, err := s.Repository.GetAverageBodyWeight(tx, recording.Id) - if err != nil { - return fmt.Errorf("getAverageBodyWeight(current): %w", err) - } - usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id) if err != nil { return fmt.Errorf("getFeedUsageInGrams: %w", err) } - currentAvgGrams := recordingutil.ToGrams(currentAvgWeight) - currentAvgKg := recordingutil.GramsToKg(currentAvgGrams) - prevAvgGrams := recordingutil.ToGrams(prevAvgWeight) - prevAvgKg := recordingutil.GramsToKg(prevAvgGrams) + totalEggQty, totalEggWeightGrams, err := s.Repository.GetEggSummaryByRecording(tx, recording.Id) + if err != nil { + return fmt.Errorf("getEggSummaryByRecording: %w", err) + } + + cumulativeEggQty, err := s.Repository.GetCumulativeEggQtyByProjectFlockKandang(tx, recording.ProjectFlockKandangId, recording.RecordDatetime) + if err != nil { + return fmt.Errorf("getCumulativeEggQtyByProjectFlockKandang: %w", err) + } + + initialChickin, err := s.Repository.GetTotalChickinByProjectFlockKandang(tx, recording.ProjectFlockKandangId) + if err != nil { + return fmt.Errorf("getTotalChickinByProjectFlockKandang: %w", err) + } currentDepletion := float64(totalDepletionQty) cumDepletionQty := prevCumDepletionQty + currentDepletion @@ -840,24 +828,64 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm recording.CumDepletionRate = nil } - if currentAvgGrams > 0 && prevAvgGrams > 0 { - dailyGainKg := (currentAvgGrams - prevAvgGrams) / 1000 - updates["daily_gain"] = dailyGainKg - recording.DailyGain = &dailyGainKg + var feedIntake float64 + if remainingChick > 0 && usageInGrams > 0 { + feedIntake = (usageInGrams / remainingChick) * 1000 + updates["feed_intake"] = feedIntake + recording.FeedIntake = &feedIntake } else { - dailyGainKg := 0.0 - updates["daily_gain"] = dailyGainKg - recording.DailyGain = &dailyGainKg + updates["feed_intake"] = gorm.Expr("NULL") + recording.FeedIntake = nil } - if currentAvgKg > 0 && remainingChick > 0 { - avgDailyGain := (currentAvgKg - prevAvgKg) / remainingChick - updates["avg_daily_gain"] = avgDailyGain - recording.AvgDailyGain = &avgDailyGain + var handDay float64 + if remainingChick > 0 && totalEggQty >= 0 { + handDay = (totalEggQty / remainingChick) * 100 + updates["hand_day"] = handDay + recording.HandDay = &handDay } else { - avgDailyGain := 0.0 - updates["avg_daily_gain"] = avgDailyGain - recording.AvgDailyGain = &avgDailyGain + updates["hand_day"] = gorm.Expr("NULL") + recording.HandDay = nil + } + + var handHouse float64 + if initialChickin > 0 && cumulativeEggQty >= 0 { + handHouse = cumulativeEggQty / initialChickin + updates["hand_house"] = handHouse + recording.HandHouse = &handHouse + } else { + updates["hand_house"] = gorm.Expr("NULL") + recording.HandHouse = nil + } + + var eggMesh float64 + if remainingChick > 0 && totalEggWeightGrams > 0 { + eggMesh = (totalEggWeightGrams / remainingChick) * 1000 + updates["egg_mesh"] = eggMesh + recording.EggMesh = &eggMesh + } else { + updates["egg_mesh"] = gorm.Expr("NULL") + recording.EggMesh = nil + } + + var eggWeight float64 + if totalEggQty > 0 && totalEggWeightGrams > 0 { + eggWeight = (totalEggWeightGrams / totalEggQty) * 1000 + updates["egg_weight"] = eggWeight + recording.EggWeight = &eggWeight + } else { + updates["egg_weight"] = gorm.Expr("NULL") + recording.EggWeight = nil + } + + var fcrValue float64 + if usageInGrams > 0 && totalEggWeightGrams > 0 { + fcrValue = totalEggWeightGrams / usageInGrams + updates["fcr_value"] = fcrValue + recording.FcrValue = &fcrValue + } else { + updates["fcr_value"] = gorm.Expr("NULL") + recording.FcrValue = nil } if usageInGrams > 0 && totalChick > 0 { @@ -882,16 +910,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm recording.CumIntake = nil } - if usageInGrams > 0 && currentAvgKg > 0 { - feedUsageKg := usageInGrams / 1000 - fcrValue := feedUsageKg / currentAvgKg - updates["fcr_value"] = fcrValue - recording.FcrValue = &fcrValue - } else { - updates["fcr_value"] = gorm.Expr("NULL") - recording.FcrValue = nil - } - if err := s.Repository.WithTx(tx).PatchOne(ctx, recording.Id, updates, nil); err != nil { return err } @@ -997,6 +1015,104 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit return nil } +type productionStandardValues struct { + HandDay *float64 + HandHouse *float64 + FeedIntake *float64 + MaxDepletion *float64 + EggMesh *float64 + EggWeight *float64 +} + +func (s *recordingService) attachProductionStandards(ctx context.Context, items []entity.Recording) error { + if len(items) == 0 { + return nil + } + + for i := range items { + if err := s.attachProductionStandard(ctx, &items[i]); err != nil { + s.Log.Warnf("Unable to load production standard for recording %d: %+v", items[i].Id, err) + } + } + return nil +} + +func (s *recordingService) attachProductionStandard(ctx context.Context, item *entity.Recording) error { + if item == nil || item.Id == 0 { + return nil + } + if item.Day == nil || *item.Day <= 0 { + return nil + } + if item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 { + return nil + } + + standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId + if standardID == 0 { + return nil + } + + week := ((int(*item.Day) - 1) / 7) + 1 + if week <= 0 { + return nil + } + + category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category) + db := s.Repository.DB() + standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) + growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) + + var standard productionStandardValues + var standardFcr *float64 + if category == string(utils.ProjectFlockCategoryLaying) { + detail, err := standardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if detail != nil { + standard.HandDay = detail.TargetHenDayProduction + standard.HandHouse = detail.TargetHenHouseProduction + standard.EggWeight = detail.TargetEggWeight + standard.EggMesh = detail.TargetEggMass + } + } + + growthDetail, err := growthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if growthDetail != nil { + standard.FeedIntake = growthDetail.FeedIntake + standard.MaxDepletion = growthDetail.MaxDepletion + if category == string(utils.ProjectFlockCategoryLaying) && growthDetail.TargetMeanBw != nil && item.ProjectFlockKandang.ProjectFlock.FcrId > 0 { + targetWeight := *growthDetail.TargetMeanBw + if targetWeight > 10 { + targetWeight = targetWeight / 1000 + } + if targetWeight > 0 { + fcrStd, ok, err := s.Repository.GetFcrStandardNumber(db, item.ProjectFlockKandang.ProjectFlock.FcrId, targetWeight) + if err != nil { + return err + } + if ok { + standardFcr = &fcrStd + } + } + } + } + + item.StandardHandDay = standard.HandDay + item.StandardHandHouse = standard.HandHouse + item.StandardFeedIntake = standard.FeedIntake + item.StandardMaxDepletion = standard.MaxDepletion + item.StandardEggMesh = standard.EggMesh + item.StandardEggWeight = standard.EggWeight + item.StandardFcr = standardFcr + + return nil +} + func uniqueUintSlice(values []uint) []uint { if len(values) == 0 { return nil diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index 28c38ff5..a1d6aaf7 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -1,12 +1,6 @@ package validation type ( - BodyWeight struct { - AvgWeight float64 `json:"avg_weight" validate:"required"` - Qty float64 `json:"qty" validate:"required,gt=0"` - TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"` - } - Stock struct { ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` Qty float64 `json:"qty" validate:"required,gte=0"` @@ -27,14 +21,12 @@ type ( type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` - BodyWeights []BodyWeight `json:"body_weights" validate:"dive"` Stocks []Stock `json:"stocks" validate:"dive"` Depletions []Depletion `json:"depletions" validate:"dive"` Eggs []Egg `json:"eggs" validate:"omitempty,dive"` } type Update struct { - BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"` diff --git a/internal/utils/recording/util.recording.go b/internal/utils/recording/util.recording.go index f10926dc..91c9cc4b 100644 --- a/internal/utils/recording/util.recording.go +++ b/internal/utils/recording/util.recording.go @@ -5,31 +5,6 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" ) -func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.RecordingBW { - if len(items) == 0 { - return nil - } - - result := make([]entity.RecordingBW, 0, len(items)) - for _, item := range items { - var totalWeight float64 - if item.TotalWeight != nil { - totalWeight = *item.TotalWeight - } - if totalWeight <= 0 { - totalWeight = item.AvgWeight * item.Qty - } - - result = append(result, entity.RecordingBW{ - RecordingId: recordingID, - AvgWeight: item.AvgWeight, - Qty: item.Qty, - TotalWeight: totalWeight, - }) - } - return result -} - func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock { if len(items) == 0 { return nil @@ -86,20 +61,3 @@ func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity. } return result } - -func ToGrams(weight float64) float64 { - if weight <= 0 { - return 0 - } - if weight < 10 { - return weight * 1000 - } - return weight -} - -func GramsToKg(grams float64) float64 { - if grams <= 0 { - return 0 - } - return grams / 1000 -}