Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'

feat(BE-281): adjustment recording table with handhouse and deleting weight...

See merge request mbugroup/lti-api!122
This commit is contained in:
Hafizh A. Y.
2025-12-31 03:01:09 +00:00
9 changed files with 394 additions and 259 deletions
@@ -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;
@@ -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;
+13 -3
View File
@@ -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:"-"`
}
-15
View File
@@ -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"`
}
@@ -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 {
@@ -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) {
@@ -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
@@ -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"`
@@ -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
}