Compare commits

..

1 Commits

Author SHA1 Message Date
aguhh18 88bdf70994 FIX[BE]: fix wrong get data on transfer stock 2026-01-23 15:40:59 +07:00
119 changed files with 2529 additions and 5635 deletions
+31 -70
View File
@@ -1,6 +1,6 @@
stages: stages:
- build - build
- migrate # - migrate
- deploy - deploy
- seed - seed
@@ -49,80 +49,41 @@ build_production:
# ========================= # =========================
# MIGRATE (PRODUCTION) # MIGRATE (PRODUCTION - MANUAL)
# ========================= # =========================
migrate_production: #migrate_production:
stage: migrate # stage: migrate
rules: # rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' # - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
needs: # when: manual
- job: build_production # allow_failure: false
artifacts: false # needs:
script: | # - job: build_production
set -e # artifacts: false
echo "✅ Running migrations (production) ..." # script: |
# set -e
# cd /opt/deploy/lti
# test -f .env || (echo "❌ .env not found" && exit 1)
cd "$DEPLOY_DIR" # set -a
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1) # . ./.env
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1) # set +a
# ✅ load env dari server # Validasi env wajib
set -a # : "${DB_HOST:?DB_HOST not set}"
. ./.env # : "${DB_PORT:?DB_PORT not set}"
set +a # : "${DB_USER:?DB_USER not set}"
# : "${DB_PASSWORD:?DB_PASSWORD not set}"
# : "${DB_NAME:?DB_NAME not set}"
# ✅ validasi # DB_SSLMODE="${DB_SSLMODE:-require}"
test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1) # export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1)
test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1)
test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1)
test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1)
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}" # echo "✅ Running migrations (production)..."
echo "✅ DATABASE_URL=$DATABASE_URL" # docker run --rm \
# -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
# ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!) # migrate/migrate:v4.15.2 \
echo "✅ Ensuring postgres & redis running ..." # -path=/migrations -database "$DATABASE_URL" up
docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true
# ✅ Ambil network key dari compose
COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')"
echo "✅ Compose network key: $COMPOSE_NETWORK_KEY"
# ✅ Cari network name yang dipakai docker
NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)"
test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1)
echo "✅ Docker network detected: $NETWORK_NAME"
# ✅ Migrations dari repo (CI workspace)
echo "✅ Checking migrations from repo..."
ls -lah "$CI_PROJECT_DIR/internal/database/migrations"
echo "✅ Running migrations via migrate/migrate container"
set +e
out=$(docker run --rm \
--network "$NETWORK_NAME" \
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
migrate/migrate:v4.15.2 \
-path=/migrations -database "$DATABASE_URL" up 2>&1)
code=$?
set -e
echo "$out"
# ✅ Handle no change dengan benar (tidak false-success)
if echo "$out" | grep -qi "no change"; then
echo "✅ No change (already up to date)"
exit 0
fi
if [ $code -ne 0 ]; then
echo "❌ Migration failed with exit code $code"
exit $code
fi
echo "✅ Migration applied successfully"
# ========================= # =========================
+1 -1
View File
@@ -14,8 +14,8 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/database"
"gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier"
"gitlab.com/mbugroup/lti-api.git/internal/route" "gitlab.com/mbugroup/lti-api.git/internal/route"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -20,7 +20,7 @@ type HppCostRepository interface {
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error)
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error)
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
} }
@@ -219,55 +219,25 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang
return totals.TotalPieces, totals.TotalWeightKg, nil return totals.TotalPieces, totals.TotalWeightKg, nil
} }
func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds( func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) {
ctx context.Context, if date == nil {
projectFlockKandangIDs []uint,
startDate *time.Time,
endDate *time.Time,
) (float64, float64, error) {
if endDate == nil {
now := time.Now() now := time.Now()
endDate = &now date = &now
} }
type subResult struct {
UsableID uint
MdpUsageQty float64
MdpWeight float64
}
subQuery := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
DISTINCT sa.usable_id,
mdp.usage_qty AS mdp_usage_qty,
mdp.total_weight AS mdp_weight
`).
Joins("JOIN recording_eggs re ON re.recording_id = r.id").
Joins(
"JOIN stock_allocations sa ON sa.stockable_type = ? AND sa.stockable_id = re.id AND sa.usable_type = ?",
fifo.StockableKeyRecordingEgg.String(),
fifo.UsableKeyMarketingDelivery.String(),
).
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *endDate).
Where("mdp.delivery_date <= ?", *startDate)
var totals struct { var totals struct {
TotalPieces float64 TotalPieces float64
TotalWeight float64 TotalWeight float64
} }
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("(?) AS x", subQuery). Table("recordings AS r").
Select(` Select("COALESCE(SUM(mdp.usage_qty), 0) AS total_pieces, COALESCE(SUM(mdp.total_weight), 0) AS total_weight").
COALESCE(SUM(x.mdp_usage_qty), 0) AS total_pieces, Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
COALESCE(SUM(x.mdp_weight), 0) AS total_weight Joins("JOIN stock_allocations AS sa ON sa.stockable_type = ? AND sa.stockable_id = re.id AND sa.usable_type = ?", fifo.StockableKeyRecordingEgg.String(), fifo.UsableKeyMarketingDelivery.String()).
`). Joins("JOIN marketing_delivery_products AS mdp ON mdp.id = sa.usable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Scan(&totals).Error Scan(&totals).Error
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
@@ -15,7 +15,7 @@ type ApprovalService interface {
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error) CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string, orderByDate string) ([]entity.Approval, int64, error) List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error) ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error) LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error) LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
@@ -70,14 +70,9 @@ func (s *approvalService) List(
approvableID *uint, approvableID *uint,
page, limit int, page, limit int,
search string, search string,
orderByDate string,
) ([]entity.Approval, int64, error) { ) ([]entity.Approval, int64, error) {
module = strings.TrimSpace(strings.ToUpper(module)) module = strings.TrimSpace(strings.ToUpper(module))
search = strings.TrimSpace(search) search = strings.TrimSpace(search)
orderByDate = strings.TrimSpace(strings.ToUpper(orderByDate))
if orderByDate != "ASC" && orderByDate != "DESC" {
orderByDate = "DESC"
}
if limit <= 0 { if limit <= 0 {
limit = 10 limit = 10
@@ -95,7 +90,7 @@ func (s *approvalService) List(
func(db *gorm.DB) *gorm.DB { func(db *gorm.DB) *gorm.DB {
query := db. query := db.
Where("approvable_type = ?", module). Where("approvable_type = ?", module).
Order("action_at " + orderByDate). Order("action_at DESC").
Preload("ActionUser") Preload("ActionUser")
if approvableID != nil { if approvableID != nil {
@@ -20,7 +20,7 @@ import (
) )
const ( const (
defaultDocumentPathLimit = 255 defaultDocumentPathLimit = 50
defaultDocumentKeyPrefix = "docs" defaultDocumentKeyPrefix = "docs"
maxDocumentNameLength = 50 maxDocumentNameLength = 50
) )
@@ -363,19 +363,13 @@ func (s *documentService) generateObjectKey(ext string) (string, error) {
} }
u := uuid.New().String() u := uuid.New().String()
keyPrefix := strings.Trim(s.keyPrefix, "/") key := fmt.Sprintf("%s/%s%s", strings.Trim(s.keyPrefix, "/"), u, normalizedExt)
key := fmt.Sprintf("%s%s", u, normalizedExt) if s.keyPrefix == "" {
if keyPrefix != "" { key = fmt.Sprintf("%s%s", u, normalizedExt)
key = fmt.Sprintf("%s/%s%s", keyPrefix, u, normalizedExt)
} }
if len(key) > s.maxPathLength { if len(key) > s.maxPathLength {
compact := strings.ReplaceAll(u, "-", "") key = fmt.Sprintf("%s%s", u, normalizedExt)
if keyPrefix != "" {
key = fmt.Sprintf("%s/%s%s", keyPrefix, compact, normalizedExt)
} else {
key = fmt.Sprintf("%s%s", compact, normalizedExt)
}
} }
if len(key) > s.maxPathLength { if len(key) > s.maxPathLength {
+34 -38
View File
@@ -11,10 +11,10 @@ import (
type HppService interface { type HppService interface {
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error)
GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) GetTotalProductionCost(projectFlockKandangId uint, date *time.Time, totalDepresiasiGrowing float64) (float64, error)
GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetBudgetKandangLaying(projectFlockKandangId uint, date *time.Time) (float64, error)
GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error) GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error)
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
} }
type HppCostResponse struct { type HppCostResponse struct {
@@ -44,25 +44,17 @@ func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Tim
date = &now date = &now
} }
location, err := time.LoadLocation("Asia/Jakarta") depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, date)
if err != nil { if err != nil {
return nil, err return nil, err
} }
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, date, depresiasiTransfer)
endOfDay := startOfDay.Add(24 * time.Hour)
depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
return nil, err return nil, err
} }
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer) return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, date)
if err != nil {
return nil, err
}
return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
} }
@@ -109,23 +101,23 @@ func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, d
return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil
} }
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) { func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, date *time.Time, depresiasiTransfer float64) (float64, error) {
// if date == nil { if date == nil {
// now := time.Now() now := time.Now()
// date = &now date = &now
// } }
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId) costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
return 0, err return 0, err
} }
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, date)
if err != nil { if err != nil {
return 0, err return 0, err
} }
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, date)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -135,7 +127,7 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate
return 0, err return 0, err
} }
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate) costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, date)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -143,11 +135,11 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate
return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil
} }
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) { func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, date *time.Time) (float64, error) {
// if date == nil { if date == nil {
// now := time.Now() now := time.Now()
// date = &now date = &now
// } }
if s.hppRepo == nil { if s.hppRepo == nil {
return 0, nil return 0, nil
@@ -163,12 +155,12 @@ func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate
return 0, err return 0, err
} }
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate) eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, date)
if err != nil { if err != nil {
return 0, err return 0, err
} }
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -185,11 +177,11 @@ func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate
return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil
} }
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) { func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error) {
// if endDate == nil { if date == nil {
// now := time.Now() now := time.Now()
// endDate = &now date = &now
// } }
if s.hppRepo == nil { if s.hppRepo == nil {
return 0, nil return 0, nil
@@ -213,7 +205,7 @@ func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *
return 0, nil return 0, nil
} }
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate) totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, date)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -221,18 +213,22 @@ func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *
return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil
} }
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
if date == nil {
now := time.Now()
date = &now
}
if s.hppRepo == nil { if s.hppRepo == nil {
return &HppCostResponse{}, nil return &HppCostResponse{}, nil
} }
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date)
if err != nil { if err != nil {
return nil, err return nil, err
} }
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+1 -23
View File
@@ -61,7 +61,6 @@ var (
SSOCookieDomain string SSOCookieDomain string
SSOCookieSecure bool SSOCookieSecure bool
SSOCookieSameSite string SSOCookieSameSite string
SSOAccessTokenMaxBytes int
SSOTokenBlacklistPrefix string SSOTokenBlacklistPrefix string
SSOPKCETTL time.Duration SSOPKCETTL time.Duration
SSOUserSyncDrift time.Duration SSOUserSyncDrift time.Duration
@@ -74,7 +73,6 @@ var (
S3SecretKey string S3SecretKey string
S3ForcePathStyle bool S3ForcePathStyle bool
S3PublicBaseURL string S3PublicBaseURL string
S3EnvPrefix string
S3DocumentKeyPrefix string S3DocumentKeyPrefix string
) )
@@ -125,12 +123,7 @@ func init() {
S3SecretKey = strings.TrimSpace(viper.GetString("S3_SECRET_KEY")) S3SecretKey = strings.TrimSpace(viper.GetString("S3_SECRET_KEY"))
S3ForcePathStyle = viper.GetBool("S3_FORCE_PATH_STYLE") S3ForcePathStyle = viper.GetBool("S3_FORCE_PATH_STYLE")
S3PublicBaseURL = strings.TrimSuffix(strings.TrimSpace(viper.GetString("S3_PUBLIC_BASE_URL")), "/") S3PublicBaseURL = strings.TrimSuffix(strings.TrimSpace(viper.GetString("S3_PUBLIC_BASE_URL")), "/")
S3EnvPrefix = defaultString(strings.Trim(strings.TrimSpace(viper.GetString("S3_ENV_PREFIX")), "/"), "local") S3DocumentKeyPrefix = defaultString(strings.Trim(strings.TrimSpace(viper.GetString("S3_DOCUMENT_PREFIX")), "/"), "docs")
docPrefix := strings.Trim(strings.TrimSpace(viper.GetString("S3_DOCUMENT_PREFIX")), "/")
if docPrefix == "" {
docPrefix = "docs"
}
S3DocumentKeyPrefix = joinPath(S3EnvPrefix, docPrefix)
// SSO integration // SSO integration
SSOIssuer = viper.GetString("SSO_ISSUER") SSOIssuer = viper.GetString("SSO_ISSUER")
@@ -145,10 +138,6 @@ func init() {
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN") SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE") SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax") SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
SSOAccessTokenMaxBytes = viper.GetInt("SSO_ACCESS_TOKEN_MAX_BYTES")
if SSOAccessTokenMaxBytes <= 0 {
SSOAccessTokenMaxBytes = 4096
}
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist") SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 { if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
SSOPKCETTL = time.Duration(ttl) * time.Second SSOPKCETTL = time.Duration(ttl) * time.Second
@@ -253,17 +242,6 @@ func defaultString(v, def string) string {
return v return v
} }
func joinPath(parts ...string) string {
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.Trim(part, "/")
if part != "" {
out = append(out, part)
}
}
return strings.Join(out, "/")
}
func ensureProdConfig() { func ensureProdConfig() {
if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") { if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") {
panic("SSO_AUTHORIZE_URL must be https in production") panic("SSO_AUTHORIZE_URL must be https in production")
@@ -1,6 +0,0 @@
BEGIN;
ALTER TABLE payments
ALTER COLUMN bank_id SET NOT NULL;
COMMIT;
@@ -1,6 +0,0 @@
BEGIN;
ALTER TABLE payments
ALTER COLUMN bank_id DROP NOT NULL;
COMMIT;
@@ -1,3 +0,0 @@
ALTER TABLE adjustment_stocks ADD COLUMN stock_log_id INTEGER;
CREATE INDEX idx_adjustment_stocks_stock_log_id ON adjustment_stocks (stock_log_id);
@@ -1 +0,0 @@
ALTER TABLE adjustment_stocks DROP COLUMN IF EXISTS stock_log_id;
@@ -1,56 +0,0 @@
BEGIN;
DO $$
DECLARE
t text;
seq_name text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'daily_checklist_activity_task_assignments',
'daily_checklist_activity_tasks',
'daily_checklist_phases',
'daily_checklist_tasks',
'daily_checklists',
'employee_kandangs',
'employees',
'phase_activities',
'phases'
]
LOOP
-- Sequence name convention
seq_name := format('public.%I_id_seq', t);
-- 1) Drop default nextval (bigserial behavior)
EXECUTE format(
'ALTER TABLE public.%I ALTER COLUMN id DROP DEFAULT',
t
);
-- 2) Add IDENTITY back (BY DEFAULT is safer for rollback)
EXECUTE format(
'ALTER TABLE public.%I ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY',
t
);
-- 3) Detach & optionally drop sequence (safe)
IF EXISTS (
SELECT 1 FROM pg_class
WHERE relkind = 'S'
AND relname = t || '_id_seq'
) THEN
EXECUTE format(
'ALTER SEQUENCE %s OWNED BY NONE',
seq_name
);
-- Optional: drop sequence (comment if you want to keep it)
EXECUTE format(
'DROP SEQUENCE IF EXISTS %s',
seq_name
);
END IF;
END LOOP;
END $$;
COMMIT;
@@ -1,59 +0,0 @@
BEGIN;
DO $$
DECLARE
t text;
seq_name text;
max_id bigint;
BEGIN
FOREACH t IN ARRAY ARRAY[
'daily_checklist_activity_task_assignments',
'daily_checklist_activity_tasks',
'daily_checklist_phases',
'daily_checklist_tasks',
'daily_checklists',
'employee_kandangs',
'employees',
'phase_activities',
'phases'
]
LOOP
-- Sequence name convention: public.<table>_id_seq
seq_name := format('public.%I_id_seq', t);
-- Drop IDENTITY only if the column is identity (safe to re-run)
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = t
AND column_name = 'id'
AND is_identity = 'YES'
) THEN
EXECUTE format('ALTER TABLE public.%I ALTER COLUMN id DROP IDENTITY', t);
END IF;
-- Ensure sequence exists
EXECUTE format('CREATE SEQUENCE IF NOT EXISTS %s', seq_name);
-- Set default like bigserial
EXECUTE format(
'ALTER TABLE public.%I ALTER COLUMN id SET DEFAULT nextval(''%s'')',
t, seq_name
);
-- Own the sequence by the column
EXECUTE format(
'ALTER SEQUENCE %s OWNED BY public.%I.id',
seq_name, t
);
-- Sync sequence to MAX(id) + 1 to avoid duplicate key
EXECUTE format('SELECT COALESCE(MAX(id), 0) FROM public.%I', t) INTO max_id;
EXECUTE format('SELECT setval(''%s'', $1, false)', seq_name)
USING (max_id + 1);
END LOOP;
END $$;
COMMIT;
@@ -1,2 +0,0 @@
ALTER TABLE stock_logs
DROP COLUMN stock;
@@ -1,18 +0,0 @@
ALTER TABLE stock_logs
ADD COLUMN stock NUMERIC(15, 3) NOT NULL DEFAULT 0;
WITH calc AS (
SELECT
id,
SUM(COALESCE(increase, 0) - COALESCE(decrease, 0))
OVER (
PARTITION BY product_warehouse_id
ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_stock
FROM stock_logs
)
UPDATE stock_logs t
SET stock = c.running_stock
FROM calc c
WHERE t.id = c.id;
@@ -1,7 +0,0 @@
BEGIN;
-- Migration: revert documents.path length
ALTER TABLE documents
ALTER COLUMN path TYPE VARCHAR(50);
COMMIT;
@@ -1,7 +0,0 @@
BEGIN;
-- Migration: extend documents.path length for environment prefixes
ALTER TABLE documents
ALTER COLUMN path TYPE VARCHAR(255);
COMMIT;
@@ -1,2 +0,0 @@
-- Drop transfer laying sequence
DROP SEQUENCE IF EXISTS transfer_laying_seq;
@@ -1,33 +0,0 @@
-- Create sequence for transfer laying movement number
CREATE SEQUENCE transfer_laying_seq START
WITH
1 INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 NO CYCLE;
-- Set sequence starting value based on existing data (if any)
-- This prevents duplicate movement numbers if there's already data
DO $$ DECLARE max_existing INTEGER;
BEGIN
-- Check if table exists and has data
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE
table_schema = 'public'
AND table_name = 'transfer_to_layings'
) THEN
-- Get max ID from existing records
SELECT COALESCE(MAX(id), 0) INTO max_existing
FROM transfer_to_layings;
-- Set sequence to start after the highest existing ID
IF max_existing > 0 THEN PERFORM setval (
'transfer_laying_seq',
max_existing
);
END IF;
END IF;
END $$;
+2 -1
View File
@@ -4,6 +4,7 @@ import "time"
type AdjustmentStock struct { type AdjustmentStock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
TotalQty float64 `gorm:"column:total_qty;default:0"` TotalQty float64 `gorm:"column:total_qty;default:0"`
TotalUsed float64 `gorm:"column:total_used;default:0"` TotalUsed float64 `gorm:"column:total_used;default:0"`
@@ -12,6 +13,6 @@ type AdjustmentStock struct {
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"`
} }
+1 -1
View File
@@ -7,7 +7,7 @@ type Document struct {
DocumentableType string `gorm:"size:50;not null;index:documents_documentable_polymorphic,priority:1"` DocumentableType string `gorm:"size:50;not null;index:documents_documentable_polymorphic,priority:1"`
DocumentableId uint64 `gorm:"not null;index:documents_documentable_polymorphic,priority:2"` DocumentableId uint64 `gorm:"not null;index:documents_documentable_polymorphic,priority:2"`
Type string `gorm:"size:50;not null"` Type string `gorm:"size:50;not null"`
Path string `gorm:"size:255;not null"` Path string `gorm:"size:50;not null"`
Name string `gorm:"size:50;not null"` Name string `gorm:"size:50;not null"`
Ext string `gorm:"size:50;not null"` Ext string `gorm:"size:50;not null"`
Size float64 `gorm:"type:numeric(15,3);not null"` Size float64 `gorm:"type:numeric(15,3);not null"`
-1
View File
@@ -9,7 +9,6 @@ type StockLog struct {
Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"` Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"` Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
Stock float64 `gorm:"column:stock;type:numeric(15,3);not null;default:0"`
LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"` LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
LoggableId uint `gorm:"column:loggable_id;not null"` LoggableId uint `gorm:"column:loggable_id;not null"`
+1 -1
View File
@@ -6,7 +6,7 @@ import "time"
type StockTransferDelivery struct { type StockTransferDelivery struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64 StockTransferId uint64
SupplierId *uint64 SupplierId uint64
VehiclePlate string VehiclePlate string
DriverName string DriverName string
DocumentNumber string DocumentNumber string
+8 -26
View File
@@ -7,8 +7,8 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
@@ -24,10 +24,6 @@ type AuthContext struct {
User *entity.User User *entity.User
Roles []sso.Role Roles []sso.Role
Permissions map[string]struct{} Permissions map[string]struct{}
UserAreaIDs []uint
UserLocationIDs []uint
UserAllArea bool
UserAllLocation bool
} }
// Auth validates the incoming request against the central SSO access token and // Auth validates the incoming request against the central SSO access token and
@@ -71,19 +67,15 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
var roles []sso.Role var roles []sso.Role
permissions := make(map[string]struct{}) permissions := make(map[string]struct{})
var profile *sso.UserProfile
if verification.UserID != 0 { if verification.UserID != 0 {
if p, err := sso.FetchProfile(c.Context(), token, verification); err != nil { if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
} else { } else if profile != nil {
profile = p roles = profile.Roles
} for _, perm := range profile.PermissionNames() {
} if perm != "" {
if profile != nil { permissions[perm] = struct{}{}
roles = profile.Roles }
for _, perm := range profile.PermissionNames() {
if perm != "" {
permissions[perm] = struct{}{}
} }
} }
} }
@@ -94,16 +86,6 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
User: user, User: user,
Roles: roles, Roles: roles,
Permissions: permissions, Permissions: permissions,
UserAreaIDs: nil,
UserLocationIDs: nil,
UserAllArea: false,
UserAllLocation: false,
}
if profile != nil {
ctx.UserAreaIDs = profile.AreaIDs
ctx.UserLocationIDs = profile.LocationIDs
ctx.UserAllArea = profile.AllArea
ctx.UserAllLocation = profile.AllLocation
} }
c.Locals(authContextLocalsKey, ctx) c.Locals(authContextLocalsKey, ctx)
-636
View File
@@ -1,636 +0,0 @@
package middleware
import (
"errors"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
type ScopeFilter struct {
IDs []uint
Restrict bool
}
type roleScope struct {
allArea bool
allLocation bool
areaIDs []uint
locationIDs []uint
hasAnyScopes bool
}
func ResolveAreaScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) {
scope, err := collectRoleScope(c)
if err != nil || !scope.hasAnyScopes {
return ScopeFilter{}, err
}
if scope.allArea || scope.allLocation {
return ScopeFilter{}, nil
}
allowed := uniqueUint(scope.areaIDs)
if len(scope.locationIDs) > 0 {
derived, err := areaIDsByLocationIDs(db, scope.locationIDs)
if err != nil {
return ScopeFilter{}, err
}
allowed = uniqueUint(append(allowed, derived...))
}
if len(allowed) == 0 {
return ScopeFilter{Restrict: true}, nil
}
return ScopeFilter{IDs: allowed, Restrict: true}, nil
}
func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) {
scope, err := collectRoleScope(c)
if err != nil || !scope.hasAnyScopes {
return ScopeFilter{}, err
}
if scope.allLocation || scope.allArea {
return ScopeFilter{}, nil
}
areaIDs := uniqueUint(scope.areaIDs)
locationIDs := uniqueUint(scope.locationIDs)
switch {
case len(locationIDs) > 0 && len(areaIDs) > 0:
filtered, err := filterLocationIDsByAreaIDs(db, locationIDs, areaIDs)
if err != nil {
return ScopeFilter{}, err
}
locationIDs = filtered
case len(locationIDs) == 0 && len(areaIDs) > 0:
derived, err := locationIDsByAreaIDs(db, areaIDs)
if err != nil {
return ScopeFilter{}, err
}
locationIDs = derived
}
locationIDs = uniqueUint(locationIDs)
if len(locationIDs) == 0 {
return ScopeFilter{Restrict: true}, nil
}
return ScopeFilter{IDs: locationIDs, Restrict: true}, nil
}
func ResolveLocationAreaScopes(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, ScopeFilter, error) {
locationScope, err := ResolveLocationScope(c, db)
if err != nil {
return ScopeFilter{}, ScopeFilter{}, err
}
areaScope, err := ResolveAreaScope(c, db)
if err != nil {
return ScopeFilter{}, ScopeFilter{}, err
}
return locationScope, areaScope, nil
}
func collectRoleScope(c *fiber.Ctx) (roleScope, error) {
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return roleScope{}, nil
}
userAreaIDs := uniqueUint(ctx.UserAreaIDs)
userLocationIDs := uniqueUint(ctx.UserLocationIDs)
userScope := roleScope{
allArea: ctx.UserAllArea,
allLocation: ctx.UserAllLocation,
areaIDs: userAreaIDs,
locationIDs: userLocationIDs,
hasAnyScopes: ctx.UserAllArea || ctx.UserAllLocation || len(userAreaIDs) > 0 || len(userLocationIDs) > 0,
}
if userScope.hasAnyScopes {
return userScope, nil
}
return roleScope{}, nil
}
func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) {
if db == nil {
return nil, errors.New("database not configured")
}
if len(locationIDs) == 0 {
return nil, nil
}
var areaIDs []uint
if err := db.Model(&entity.Location{}).
Where("deleted_at IS NULL").
Where("id IN ?", locationIDs).
Distinct("area_id").
Pluck("area_id", &areaIDs).Error; err != nil {
return nil, err
}
return areaIDs, nil
}
func locationIDsByAreaIDs(db *gorm.DB, areaIDs []uint) ([]uint, error) {
if db == nil {
return nil, errors.New("database not configured")
}
if len(areaIDs) == 0 {
return nil, nil
}
var locationIDs []uint
if err := db.Model(&entity.Location{}).
Where("deleted_at IS NULL").
Where("area_id IN ?", areaIDs).
Distinct("id").
Pluck("id", &locationIDs).Error; err != nil {
return nil, err
}
return locationIDs, nil
}
func filterLocationIDsByAreaIDs(db *gorm.DB, locationIDs, areaIDs []uint) ([]uint, error) {
if db == nil {
return nil, errors.New("database not configured")
}
if len(locationIDs) == 0 || len(areaIDs) == 0 {
return nil, nil
}
var filtered []uint
if err := db.Model(&entity.Location{}).
Where("deleted_at IS NULL").
Where("id IN ?", locationIDs).
Where("area_id IN ?", areaIDs).
Distinct("id").
Pluck("id", &filtered).Error; err != nil {
return nil, err
}
return filtered, nil
}
func uniqueUint(ids []uint) []uint {
if len(ids) == 0 {
return nil
}
seen := make(map[uint]struct{}, len(ids))
result := make([]uint, 0, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB {
if db == nil || !scope.Restrict {
return db
}
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
return db.Where(column+" IN ?", scope.IDs)
}
func ApplyLocationScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) {
scopeDB := db
if db != nil {
scopeDB = db.Session(&gorm.Session{NewDB: true})
}
scope, err := ResolveLocationScope(c, scopeDB)
if err != nil {
return db, err
}
return ApplyScopeFilter(db, scope, column), nil
}
func ApplyAreaScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) {
scopeDB := db
if db != nil {
scopeDB = db.Session(&gorm.Session{NewDB: true})
}
scope, err := ResolveAreaScope(c, scopeDB)
if err != nil {
return db, err
}
return ApplyScopeFilter(db, scope, column), nil
}
func ApplyLocationAreaScope(c *fiber.Ctx, db *gorm.DB, locationColumn, areaColumn string) (*gorm.DB, error) {
scopeDB := db
if db != nil {
scopeDB = db.Session(&gorm.Session{NewDB: true})
}
if locationColumn != "" {
locationScope, err := ResolveLocationScope(c, scopeDB)
if err != nil {
return db, err
}
db = ApplyScopeFilter(db, locationScope, locationColumn)
}
if areaColumn != "" {
areaScope, err := ResolveAreaScope(c, scopeDB)
if err != nil {
return db, err
}
db = ApplyScopeFilter(db, areaScope, areaColumn)
}
return db, nil
}
func EnsureWarehouseAccess(c *fiber.Ctx, db *gorm.DB, warehouseID uint) error {
if warehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
var count int64
if err := ApplyScopeFilter(
db.WithContext(c.Context()).
Model(&entity.Warehouse{}).
Where("id = ?", warehouseID),
scope,
"warehouses.location_id",
).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
return nil
}
func EnsureAreaAccess(c *fiber.Ctx, db *gorm.DB, areaID uint) error {
if areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid area id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveAreaScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Area not found")
}
var count int64
if err := ApplyScopeFilter(
db.WithContext(c.Context()).
Model(&entity.Area{}).
Where("id = ?", areaID),
scope,
"areas.id",
).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Area not found")
}
return nil
}
func EnsureLocationAccess(c *fiber.Ctx, db *gorm.DB, locationID uint) error {
if locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Location not found")
}
var count int64
if err := ApplyScopeFilter(
db.WithContext(c.Context()).
Model(&entity.Location{}).
Where("id = ?", locationID),
scope,
"locations.id",
).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Location not found")
}
return nil
}
func EnsureKandangAccess(c *fiber.Ctx, db *gorm.DB, kandangID uint) error {
if kandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
var count int64
if err := ApplyScopeFilter(
db.WithContext(c.Context()).
Model(&entity.Kandang{}).
Where("id = ?", kandangID),
scope,
"kandangs.location_id",
).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
return nil
}
func EnsureProductWarehouseAccess(c *fiber.Ctx, db *gorm.DB, productWarehouseID uint) error {
if productWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid product warehouse id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("product_warehouses pw").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("pw.id = ?", productWarehouseID)
q = ApplyScopeFilter(q, scope, "w.location_id")
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
}
return nil
}
func EnsureStockLogAccess(c *fiber.Ctx, db *gorm.DB, stockLogID uint) error {
if stockLogID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid stock log id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Stock log not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("stock_logs sl").
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("sl.id = ?", stockLogID)
q = ApplyScopeFilter(q, scope, "w.location_id")
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Stock log not found")
}
return nil
}
func EnsureMarketingAccess(c *fiber.Ctx, db *gorm.DB, marketingID uint) error {
if marketingID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid marketing id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Marketing not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("marketings m").
Joins("JOIN marketing_products mp ON mp.marketing_id = m.id").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("m.id = ?", marketingID)
q = ApplyScopeFilter(q, scope, "w.location_id")
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Marketing not found")
}
return nil
}
func EnsureRecordingAccess(c *fiber.Ctx, db *gorm.DB, recordingID uint) error {
if recordingID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid recording id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Recording not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("recordings r").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Where("r.id = ?", recordingID)
q = ApplyScopeFilter(q, scope, "pf.location_id")
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Recording not found")
}
return nil
}
func EnsureUniformityAccess(c *fiber.Ctx, db *gorm.DB, uniformityID uint) error {
if uniformityID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid uniformity id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("project_flock_kandang_uniformity u").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Where("u.id = ?", uniformityID)
q = ApplyScopeFilter(q, scope, "pf.location_id")
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
return nil
}
func EnsureLayingTransferAccess(c *fiber.Ctx, db *gorm.DB, transferID uint) error {
if transferID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid transfer id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("laying_transfers lt").
Joins("JOIN project_flocks pf_from ON pf_from.id = lt.from_project_flock_id").
Joins("JOIN project_flocks pf_to ON pf_to.id = lt.to_project_flock_id").
Where("lt.id = ?", transferID).
Where("(pf_from.location_id IN ? OR pf_to.location_id IN ?)", scope.IDs, scope.IDs)
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
return nil
}
func EnsureProjectFlockAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID uint) error {
if projectFlockID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
var count int64
if err := ApplyScopeFilter(
db.WithContext(c.Context()).
Model(&entity.ProjectFlock{}).
Where("id = ?", projectFlockID),
scope,
"project_flocks.location_id",
).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
return nil
}
func EnsureProjectFlockKandangAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID, projectFlockKandangID uint) error {
if projectFlockKandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock kandang id")
}
if db == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
}
scope, err := ResolveLocationScope(c, db)
if err != nil || !scope.Restrict {
return err
}
if len(scope.IDs) == 0 {
return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
var count int64
q := db.WithContext(c.Context()).
Table("project_flock_kandangs").
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
Where("project_flock_kandangs.id = ?", projectFlockKandangID)
if projectFlockID > 0 {
q = q.Where("project_flock_kandangs.project_flock_id = ?", projectFlockID)
}
q = ApplyScopeFilter(q, scope, "project_flocks.location_id")
if err := q.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
return nil
}
@@ -44,15 +44,6 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
page := c.QueryInt("page", 1) page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 10) limit := c.QueryInt("limit", 10)
search := strings.TrimSpace(c.Query("search", "")) search := strings.TrimSpace(c.Query("search", ""))
orderByDate := strings.TrimSpace(c.Query("order_by_date", ""))
if orderByDate == "" {
orderByDate = "DESC"
} else {
orderByDate = strings.ToUpper(orderByDate)
if orderByDate != "ASC" && orderByDate != "DESC" {
return fiber.NewError(fiber.StatusBadRequest, "order_by_date must be either ASC or DESC")
}
}
query := &validation.Query{ query := &validation.Query{
ModuleName: moduleName, ModuleName: moduleName,
@@ -61,7 +52,6 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
Page: page, Page: page,
Limit: limit, Limit: limit,
Search: search, Search: search,
OrderByDate: orderByDate,
} }
records, totalResults, err := u.ApprovalService.List( records, totalResults, err := u.ApprovalService.List(
@@ -71,7 +61,6 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
query.Page, query.Page,
query.Limit, query.Limit,
query.Search, query.Search,
query.OrderByDate,
) )
if err != nil { if err != nil {
return err return err
@@ -7,5 +7,4 @@ type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
OrderByDate string `query:"order_by_date" validate:"omitempty,oneof=ASC DESC"`
} }
@@ -1,12 +1,8 @@
package dto package dto
import ( // === CLOSING KEUANGAN CODES ===
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// Closing HPP Codes
type ClosingHPPCode string type ClosingHPPCode string
const ( const (
@@ -18,30 +14,36 @@ const (
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI" HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
) )
// Closing Profit Loss Codes
type ClosingProfitLossCode string type ClosingProfitLossCode string
const ( const (
PLCodeSales ClosingProfitLossCode = "SALES" PLCodeSales ClosingProfitLossCode = "SALES"
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK" PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD" PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI" PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
) )
// === NEW CLOSING KEUANGAN DTO ===
// FinancialMetrics represents financial metrics with per unit and total amounts
type FinancialMetrics struct { type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"` RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"` RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
} }
// HPPItem represents an item in HPP section
type HPPItem struct { type HPPItem struct {
ID uint `json:"id"` ID uint `json:"id"`
Category string `json:"category"` Category string `json:"category"` // "purchase" or "overhead"
Code string `json:"code"` Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI"
Label string `json:"label"` Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"` Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"` Realization FinancialMetrics `json:"realization"`
} }
// HPPSummary represents summary for HPP section
type HPPSummary struct { type HPPSummary struct {
Label string `json:"label"` Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"` Budgeting FinancialMetrics `json:"budgeting"`
@@ -50,41 +52,52 @@ type HPPSummary struct {
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
} }
// HPPSection represents HPP data section
type HPPSection struct { type HPPSection struct {
Items []HPPItem `json:"items"` Items []HPPItem `json:"items"`
Summary HPPSummary `json:"summary"` Summary HPPSummary `json:"summary"`
} }
// ProfitLossItem represents an item in Profit & Loss section
type ProfitLossItem struct { type ProfitLossItem struct {
Code string `json:"code"` Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI"
Label string `json:"label"` Label string `json:"label"`
Type string `json:"type"` Type string `json:"type"` // "income", "purchase", "overhead"
RpPerBird float64 `json:"rp_per_bird"` RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"` RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
} }
// ProfitLossSummary represents summary for Profit & Loss section
type ProfitLossSummary struct { type ProfitLossSummary struct {
GrossProfit FinancialMetrics `json:"gross_profit"` GrossProfit FinancialMetrics `json:"gross_profit"`
SubTotal FinancialMetrics `json:"sub_total"` SubTotal FinancialMetrics `json:"sub_total"`
NetProfit FinancialMetrics `json:"net_profit"` NetProfit FinancialMetrics `json:"net_profit"`
} }
// ProfitLossSection represents Profit & Loss data section
type ProfitLossSection struct { type ProfitLossSection struct {
Items []ProfitLossItem `json:"items"` Items []ProfitLossItem `json:"items"`
Summary ProfitLossSummary `json:"summary"` Summary ProfitLossSummary `json:"summary"`
} }
// ClosingKeuanganData represents the main data structure
type ClosingKeuanganData struct { type ClosingKeuanganData struct {
HPP HPPSection `json:"hpp"` HPP HPPSection `json:"hpp"`
ProfitLoss ProfitLossSection `json:"profit_loss"` ProfitLoss ProfitLossSection `json:"profit_loss"`
} }
type MetricsCalculator struct {
TotalPopulation float64 // ClosingKeuanganResponse represents the full API response
ActualPopulation float64 type ClosingKeuanganResponse struct {
TotalWeightProduced float64 Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Data ClosingKeuanganData `json:"data"`
} }
// === MAPPER FUNCTIONS ===
// ToFinancialMetrics creates FinancialMetrics from values
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{ return FinancialMetrics{
RpPerBird: rpPerBird, RpPerBird: rpPerBird,
@@ -93,6 +106,7 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
} }
} }
// ToHPPItem creates HPP item
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem { func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
return HPPItem{ return HPPItem{
ID: id, ID: id,
@@ -104,6 +118,7 @@ func ToHPPItem(id uint, category, code, label string, budgeting, realization Fin
} }
} }
// ToHPPSummary creates HPP summary
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary { func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
return HPPSummary{ return HPPSummary{
Label: label, Label: label,
@@ -114,6 +129,7 @@ func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudg
} }
} }
// ToHPPSection creates HPP section
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection { func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
return HPPSection{ return HPPSection{
Items: items, Items: items,
@@ -121,6 +137,7 @@ func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
} }
} }
// ToProfitLossItem creates Profit & Loss item
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem { func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
return ProfitLossItem{ return ProfitLossItem{
Code: code, Code: code,
@@ -132,6 +149,7 @@ func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount f
} }
} }
// ToProfitLossSummary creates Profit & Loss summary
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary { func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
return ProfitLossSummary{ return ProfitLossSummary{
GrossProfit: grossProfit, GrossProfit: grossProfit,
@@ -140,6 +158,7 @@ func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) Prof
} }
} }
// ToProfitLossSection creates Profit & Loss section
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection { func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
return ProfitLossSection{ return ProfitLossSection{
Items: items, Items: items,
@@ -147,6 +166,7 @@ func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) Prof
} }
} }
// ToClosingKeuanganData creates complete closing keuangan data
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData { func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
return ClosingKeuanganData{ return ClosingKeuanganData{
HPP: hpp, HPP: hpp,
@@ -154,72 +174,12 @@ func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) Closing
} }
} }
func (mc *MetricsCalculator) CalculateMetrics(amount float64) (rpPerBird, rpPerKg float64) { // ToSuccessClosingKeuanganResponse creates success response
if mc.ActualPopulation > 0 { func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse {
rpPerBird = amount / mc.ActualPopulation return ClosingKeuanganResponse{
Code: 200,
Status: "success",
Message: "Get closing keuangan successfully",
Data: data,
} }
if mc.TotalWeightProduced > 0 {
rpPerKg = amount / mc.TotalWeightProduced
}
return
}
func (mc *MetricsCalculator) CalculateProfitLossMetrics(amount float64) (rpPerBird, rpPerKg float64) {
if mc.TotalPopulation > 0 {
rpPerBird = amount / mc.TotalPopulation
}
if mc.TotalWeightProduced > 0 {
rpPerKg = amount / mc.TotalWeightProduced
}
return
}
type ProductFilter struct {
ProjectFlockCategory string
}
func (pf *ProductFilter) IsEggProduct(product entity.Product) bool {
for _, flag := range product.Flags {
flagName := strings.ToUpper(flag.Name)
if flagName == string(utils.FlagTelur) ||
flagName == string(utils.FlagTelurUtuh) ||
flagName == string(utils.FlagTelurPecah) ||
flagName == string(utils.FlagTelurPutih) ||
flagName == string(utils.FlagTelurRetak) {
return true
}
}
return false
}
func (pf *ProductFilter) IsChickenProduct(product entity.Product) bool {
for _, flag := range product.Flags {
flagName := strings.ToUpper(flag.Name)
if flagName == string(utils.FlagAyamAfkir) ||
flagName == string(utils.FlagAyamCulling) ||
flagName == string(utils.FlagAyamMati) {
return true
}
}
return false
}
func (pf *ProductFilter) ShouldIncludeProduct(product entity.Product) bool {
if pf.ProjectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
return pf.IsEggProduct(product)
}
return pf.IsChickenProduct(product) || (!pf.IsEggProduct(product) && !pf.IsChickenProduct(product))
}
func (pf *ProductFilter) FilterDeliveryProducts(deliveries []entity.MarketingDeliveryProduct) []entity.MarketingDeliveryProduct {
filtered := make([]entity.MarketingDeliveryProduct, 0)
for _, delivery := range deliveries {
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue
}
if pf.ShouldIncludeProduct(delivery.MarketingProduct.ProductWarehouse.Product) {
filtered = append(filtered, delivery)
}
}
return filtered
} }
@@ -8,7 +8,6 @@ import (
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === Response DTO === // === Response DTO ===
@@ -45,17 +44,7 @@ type PenjualanRealisasiResponseDTO struct {
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
productFlags := make([]string, len(e.MarketingProduct.ProductWarehouse.Product.Flags)) ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
for i, f := range e.MarketingProduct.ProductWarehouse.Product.Flags {
productFlags[i] = f.Name
}
var category string
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
}
ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
var product *productDTO.ProductRelationDTO var product *productDTO.ProductRelationDTO
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
@@ -101,44 +90,17 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
} }
} }
func ToSalesAgeDTO(e entity.MarketingDeliveryProduct) SalesDTO {
productFlags := make([]string, len(e.MarketingProduct.ProductWarehouse.Product.Flags))
for i, f := range e.MarketingProduct.ProductWarehouse.Product.Flags {
productFlags[i] = f.Name
}
var category string
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
}
ageInDay, _ := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
return SalesDTO{
Age: ageInDay,
Qty: e.UsageQty,
}
}
func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO { func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO {
var totalSalesPrice, totalActualPrice, sumSales, sumActual float64 var totalSalesPrice, totalActualPrice, sumSales, sumActual float64
count := len(e) count := len(e)
if count == 0 {
return SummaryDTO{
TotalSalesPrice: 0,
TotalActualPrice: 0,
AvgSalesPrice: 0,
AvgActualPrice: 0,
}
}
for _, item := range e { for _, item := range e {
totalSalesPrice += item.MarketingProduct.TotalPrice totalSalesPrice += item.MarketingProduct.TotalPrice
totalActualPrice += item.TotalPrice totalActualPrice += item.TotalPrice
sumSales += item.MarketingProduct.UnitPrice sumSales += item.MarketingProduct.UnitPrice
sumActual += item.UnitPrice sumActual += item.UnitPrice
} }
return SummaryDTO{ return SummaryDTO{
@@ -164,30 +126,11 @@ func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) Penjua
} }
} }
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) { func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) (int, int) {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0, 0 return 0, 0
} }
for _, flag := range productFlags {
if flag == string(utils.FlagOVK) ||
flag == string(utils.FlagPakan) ||
flag == string(utils.FlagPreStarter) ||
flag == string(utils.FlagStarter) ||
flag == string(utils.FlagFinisher) ||
flag == string(utils.FlagObat) ||
flag == string(utils.FlagVitamin) ||
flag == string(utils.FlagKimia) ||
flag == string(utils.FlagEkspedisi) ||
flag == string(utils.FlagTelur) ||
flag == string(utils.FlagTelurUtuh) ||
flag == string(utils.FlagTelurPecah) ||
flag == string(utils.FlagTelurPutih) ||
flag == string(utils.FlagTelurRetak) {
return 0, 0
}
}
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
for _, chickin := range projectFlockKandang.Chickins { for _, chickin := range projectFlockKandang.Chickins {
if chickin.ChickInDate.Before(earliestChickinDate) { if chickin.ChickInDate.Before(earliestChickinDate) {
@@ -202,12 +145,8 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de
if ageInDays <= 0 { if ageInDays <= 0 {
ageInWeeks = 0 ageInWeeks = 0
} else { } else {
if category == string(utils.ProjectFlockCategoryLaying) {
ageInDays = ageInDays + 119 ageInWeeks = ((ageInDays - 1) / 7) + 1
ageInWeeks = ((ageInDays - 1) / 7) + 1
} else {
ageInWeeks = ((ageInDays - 1) / 7) + 1
}
} }
return ageInDays, ageInWeeks return ageInDays, ageInWeeks
@@ -196,11 +196,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
for idx, item := range group.Items { for idx, item := range group.Items {
refKey := strings.TrimSpace(item.NoReferensi) productKey := strings.ToUpper(flagKey + "|" + item.ProductName)
productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey)
if refKey == "" {
productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal))
}
baseRow := SapronakCategoryRowDTO{ baseRow := SapronakCategoryRowDTO{
ID: idx + 1, ID: idx + 1,
Date: formatDate(item.Tanggal), Date: formatDate(item.Tanggal),
@@ -216,9 +212,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
switch strings.ToLower(item.JenisTransaksi) { switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk": case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
if item.Tanggal != nil {
row.Date = formatDate(item.Tanggal)
}
if row.UnitPrice == 0 { if row.UnitPrice == 0 {
if item.QtyMasuk > 0 && item.Nilai > 0 { if item.QtyMasuk > 0 && item.Nilai > 0 {
row.UnitPrice = item.Nilai / item.QtyMasuk row.UnitPrice = item.Nilai / item.QtyMasuk
+2 -3
View File
@@ -25,6 +25,7 @@ type ClosingModule struct{}
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
closingRepo := rClosing.NewClosingRepository(db) closingRepo := rClosing.NewClosingRepository(db)
closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
@@ -39,11 +40,9 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
purchaseRepo := rPurchase.NewPurchaseRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
hppCostRepo := commonRepo.NewHppCostRepository(db)
hppService := commonSvc.NewHppService(hppCostRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
closingKeuanganService := sClosing.NewClosingKeuanganService(projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo, hppService, hppCostRepo) closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -32,10 +32,9 @@ type ClosingRepository interface {
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
} }
@@ -355,10 +354,9 @@ func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id"). Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames). Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(mdp.total_weight), 0) AS total_weight, COALESCE(SUM(mdp.usage_qty), 0) AS total_qty, COALESCE(SUM(mdp.total_price), 0) AS total_price"). Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error Scan(&agg).Error
if err != nil { if err != nil {
return 0, 0, 0, err return 0, 0, 0, err
@@ -710,23 +708,6 @@ var (
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet) sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
) )
func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB {
subquery := r.DB().
Table("flags").
Select("DISTINCT ON (flagable_id) flagable_id, name").
Where("flagable_type = ?", entity.FlagableTypeProduct).
Where("name IN ?", sapronakFlagsAll).
Order(fmt.Sprintf(
"flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END",
utils.FlagDOC,
utils.FlagPullet,
utils.FlagPakan,
utils.FlagOVK,
))
return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery)
}
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow { func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
m := make(map[uint][]SapronakDetailRow) m := make(map[uint][]SapronakDetailRow)
for _, row := range rows { for _, row := range rows {
@@ -763,12 +744,11 @@ func (r *ClosingRepositoryImpl) usageQuery(
COALESCE(p.product_price, 0) AS default_price COALESCE(p.product_price, 0) AS default_price
`) `)
db = applyJoins(db, joins...) db = applyJoins(db, joins...)
db = db. return db.
Joins("JOIN product_warehouses pw ON "+pwJoinCond). Joins("JOIN product_warehouses pw ON "+pwJoinCond).
Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where(where, args...) Where(where, args...)
db = r.joinSapronakProductFlag(db, "p")
return db
} }
func (r *ClosingRepositoryImpl) fetchSapronakUsage( func (r *ClosingRepositoryImpl) fetchSapronakUsage(
@@ -798,11 +778,11 @@ func (r *ClosingRepositoryImpl) detailQuery(
) *gorm.DB { ) *gorm.DB {
db := r.withCtx(ctx). db := r.withCtx(ctx).
Table(table). Table(table).
Joins("JOIN product_warehouses pw ON " + pwJoinCond). Joins("JOIN product_warehouses pw ON "+pwJoinCond).
Joins("JOIN products p ON p.id = pw.product_id") Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
db = applyJoins(db, joins...) db = applyJoins(db, joins...)
db = r.joinSapronakProductFlag(db, "p")
return db.Select(selectSQL).Where(where, args...) return db.Select(selectSQL).Where(where, args...)
} }
@@ -892,83 +872,16 @@ func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Con
) )
} }
func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) {
if projectFlockKandangID == 0 {
return map[uint][]SapronakDetailRow{}, nil
}
query := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(
pi.received_date,
st.transfer_date,
lt.transfer_date,
ast.created_at,
pc.chick_in_date,
r.record_datetime
) AS date,
COALESCE(
po.po_number,
st.movement_number,
lt.transfer_number,
CONCAT('ADJ-', ast.id),
CONCAT('CHICKIN-', pc.id),
CAST(r.id AS TEXT),
''
) AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(pi.price, p.product_price, 0) AS price
`).
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()).
Joins("LEFT JOIN recordings r ON r.id = rs.recording_id").
Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("f.name IN ?", sapronakFlagsAll).
Where(`
(sa.usable_type = ? AND r.project_flock_kandangs_id = ?)
OR
(sa.usable_type = ? AND pc_used.project_flock_kandang_id = ?)
`,
fifo.UsableKeyRecordingStock.String(), projectFlockKandangID,
fifo.UsableKeyProjectChickin.String(), projectFlockKandangID,
)
query = r.joinSapronakProductFlag(query, "p").
Group(`
pw.product_id, p.name, f.name,
pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime,
po.po_number, st.movement_number, lt.transfer_number, ast.id, pc.id, r.id,
pi.price, p.product_price
`)
return scanAndGroupDetails(query)
}
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB { func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
db := r.withCtx(ctx). return r.withCtx(ctx).
Table("purchase_items AS pi"). Table("purchase_items AS pi").
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL"). Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
Joins("JOIN products p ON p.id = pi.product_id"). Joins("JOIN products p ON p.id = pi.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN warehouses w ON w.id = pi.warehouse_id"). Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL") Where("pi.received_date IS NOT NULL")
return r.joinSapronakProductFlag(db, "p")
} }
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) { func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
@@ -1035,14 +948,14 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui
COALESCE(sl.increase,0) AS increase, COALESCE(sl.increase,0) AS increase,
COALESCE(sl.decrease,0) AS decrease, COALESCE(sl.decrease,0) AS decrease,
COALESCE(p.product_price,0) AS price, COALESCE(p.product_price,0) AS price,
` + movementSelect + ` `+movementSelect+`
`). `).
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN warehouses w ON w.id = pw.warehouse_id") Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
db = applyJoins(db, joins...) db = applyJoins(db, joins...)
db = r.joinSapronakProductFlag(db, "p")
if err := db. if err := db.
Where("sl.loggable_type = ?", logType). Where("sl.loggable_type = ?", logType).
@@ -1111,10 +1024,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id"). Joins("JOIN products p ON p.id = std.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)"). Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll) Where("f.name IN ?", sapronakFlagsAll)
incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
incoming, err := scanAndGroupDetails(incomingQuery) incoming, err := scanAndGroupDetails(incomingQuery)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -1139,10 +1052,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)"). Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll) Where("f.name IN ?", sapronakFlagsAll)
incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p")
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery) incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -1170,12 +1083,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id"). Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id"). Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id"). Joins("JOIN products p ON p.id = std.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
outgoing, err := scanAndGroupDetails(outgoingQuery) outgoing, err := scanAndGroupDetails(outgoingQuery)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -1201,12 +1114,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price") Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p")
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery) outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -1218,7 +1131,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
return incoming, outgoing, nil return incoming, outgoing, nil
} }
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) { func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
query := r.withCtx(ctx). query := r.withCtx(ctx).
Table("stock_allocations AS sa"). Table("stock_allocations AS sa").
Select(` Select(`
@@ -1235,55 +1148,15 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN marketings m ON m.id = mp.marketing_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
query = r.joinSapronakProductFlag(query, "p") return scanAndGroupDetails(query)
sales, err := scanAndGroupDetails(query)
if err != nil {
return nil, err
}
nonFifoQuery := r.withCtx(ctx).
Table("marketing_delivery_products AS mdp").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(mdp.delivery_date, mdp.created_at) AS date,
COALESCE(m.so_number, '') AS reference,
0 AS qty_in,
COALESCE(mdp.usage_qty, 0) AS qty_out,
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
`).
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN marketings m ON m.id = mp.marketing_id").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN stock_allocations sa ON sa.usable_id = mdp.id AND sa.usable_type = ? AND sa.status = ?",
fifo.UsableKeyMarketingDelivery.String(),
entity.StockAllocationStatusActive,
).
Where("mdp.usage_qty > 0").
Where("sa.id IS NULL").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Where("f.name IN ?", sapronakFlagsAll).
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p")
nonFifoSales, err := scanAndGroupDetails(nonFifoQuery)
if err != nil {
return nil, err
}
for pid, rows := range nonFifoSales {
sales[pid] = append(sales[pid], rows...)
}
return sales, nil
} }
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
@@ -0,0 +1,365 @@
package repository
import (
"context"
"fmt"
"sort"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
// ClosingKeuanganRepository handles database operations for closing keuangan
type ClosingKeuanganRepository interface {
repository.BaseRepository[interface{}]
// All Product Usage
GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error)
// Depletion per kandang
GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
// Weight produced from uniformity per kandang
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
// DB returns the underlying GORM DB instance
DB() *gorm.DB
}
type ClosingKeuanganRepositoryImpl struct {
*repository.BaseRepositoryImpl[interface{}]
}
func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository {
return &ClosingKeuanganRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db),
}
}
// Result Rows
type ProductUsageRow struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
FlagNames string `gorm:"column:flag_names"`
TotalQty float64 `gorm:"column:total_qty"`
Price float64 `gorm:"column:price"`
TotalPengeluaran float64 `gorm:"column:total_pengeluaran"`
}
// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang
// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments
// flagFilters: optional filter to get only specific flags (e.g., ["PAKAN", "OVK"]), empty means get all
func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) {
if projectFlockKandangID == 0 {
return []ProductUsageRow{}, nil
}
type SubQueryResult struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
TotalQty float64 `gorm:"column:total_qty"`
Price float64 `gorm:"column:price"`
}
type AggregatedResult struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
TotalQty float64 `gorm:"column:total_qty"`
Price float64 `gorm:"column:price"`
PriceCount int `gorm:"-"` // For calculating average price
}
type FlagResult struct {
ProductID uint `gorm:"column:product_id"`
FlagNames string `gorm:"column:flag_names"`
}
var allResults []SubQueryResult
// Subquery 1: Recordings
var recordingsResults []SubQueryResult
err := r.DB().WithContext(ctx).
Table("recordings r").
Select("pw.product_id, p.name as product_name, "+
"COALESCE(SUM(CASE "+
"WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(sa.qty, 0) "+
"WHEN sa.stockable_type = 'STOCK_TRANSFER_IN' THEN COALESCE(std.usage_qty, 0) "+
"WHEN sa.stockable_type = 'TRANSFERTOLAYING_IN' THEN COALESCE(ltt.total_used, 0) "+
"WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN COALESCE(adjs.total_used, 0) "+
"WHEN sa.stockable_type = 'PROJECT_FLOCK_POPULATION' THEN COALESCE(pfp.total_used_qty, 0) "+
"ELSE 0 END), 0) as total_qty, "+
"COALESCE(AVG(CASE WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(pi.price, 0) END), 0) as price").
Joins("JOIN recording_stocks rs ON rs.recording_id = r.id").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'").
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'").
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'").
Joins("LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'").
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = 'PROJECT_FLOCK_POPULATION'").
Where("r.project_flock_kandangs_id = ?", projectFlockKandangID).
Where("r.deleted_at IS NULL").
Group("pw.product_id, p.name").
Scan(&recordingsResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get recordings product usage: %w", err)
}
fmt.Printf("[REPO] Recordings query: %d results for projectFlockKandangID=%d\n", len(recordingsResults), projectFlockKandangID)
allResults = append(allResults, recordingsResults...)
// Subquery 2: Chickins
var chickinsResults []SubQueryResult
err = r.DB().WithContext(ctx).
Table("project_chickins pc").
Select("pw.product_id, p.name as product_name, "+
"COALESCE(SUM(pc.usage_qty), 0) as total_qty, "+
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
Joins("JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
Where("pc.usage_qty > 0").
Group("pw.product_id, p.name").
Scan(&chickinsResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get chickins product usage: %w", err)
}
fmt.Printf("[REPO] Chickins query: %d results for projectFlockKandangID=%d\n", len(chickinsResults), projectFlockKandangID)
allResults = append(allResults, chickinsResults...)
// Subquery 3: Marketing Delivery
var marketingResults []SubQueryResult
err = r.DB().WithContext(ctx).
Table("marketing_delivery_products mdp").
Select("pw.product_id, p.name as product_name, "+
"COALESCE(SUM(mdp.usage_qty), 0) as total_qty, "+
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Group("pw.product_id, p.name").
Scan(&marketingResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get marketing product usage: %w", err)
}
fmt.Printf("[REPO] Marketing query: %d results for projectFlockKandangID=%d\n", len(marketingResults), projectFlockKandangID)
allResults = append(allResults, marketingResults...)
// Subquery 4: Laying Transfer Sources
var layingTransferResults []SubQueryResult
err = r.DB().WithContext(ctx).
Table("laying_transfer_sources lts").
Select("pw.product_id, p.name as product_name, "+
"COALESCE(SUM(lts.usage_qty), 0) as total_qty, "+
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
Joins("JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Group("pw.product_id, p.name").
Scan(&layingTransferResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get laying transfer product usage: %w", err)
}
fmt.Printf("[REPO] Laying Transfer query: %d results for projectFlockKandangID=%d\n", len(layingTransferResults), projectFlockKandangID)
allResults = append(allResults, layingTransferResults...)
// Subquery 5: Stock Transfer Details
var stockTransferResults []SubQueryResult
err = r.DB().WithContext(ctx).
Table("stock_transfer_details std").
Select("pw.product_id, p.name as product_name, "+
"COALESCE(SUM(std.usage_qty), 0) as total_qty, "+
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
Joins("JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id").
Joins("JOIN products p ON p.id = std.product_id").
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Group("pw.product_id, p.name").
Scan(&stockTransferResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get stock transfer product usage: %w", err)
}
fmt.Printf("[REPO] Stock Transfer query: %d results for projectFlockKandangID=%d\n", len(stockTransferResults), projectFlockKandangID)
allResults = append(allResults, stockTransferResults...)
// Subquery 6: Adjustment Stocks
var adjustmentResults []SubQueryResult
err = r.DB().WithContext(ctx).
Table("adjustment_stocks ads").
Select("pw.product_id, p.name as product_name, "+
"COALESCE(SUM(ads.usage_qty), 0) as total_qty, "+
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
Joins("JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Where("ads.usage_qty > 0").
Group("pw.product_id, p.name").
Scan(&adjustmentResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get adjustment product usage: %w", err)
}
fmt.Printf("[REPO] Adjustment query: %d results for projectFlockKandangID=%d\n", len(adjustmentResults), projectFlockKandangID)
allResults = append(allResults, adjustmentResults...)
fmt.Printf("[REPO] Total raw results before aggregation: %d items\n", len(allResults))
// Aggregate results by product_id
aggregatedMap := make(map[uint]*AggregatedResult)
for _, result := range allResults {
key := result.ProductID
if existing, exists := aggregatedMap[key]; exists {
existing.TotalQty += result.TotalQty
existing.Price += result.Price
existing.PriceCount++
} else {
aggregatedMap[key] = &AggregatedResult{
ProductID: result.ProductID,
ProductName: result.ProductName,
TotalQty: result.TotalQty,
Price: result.Price,
PriceCount: 1,
}
}
}
fmt.Printf("[REPO] Aggregated to %d unique products\n", len(aggregatedMap))
// Get flags for all products
productIDs := make([]uint, 0, len(aggregatedMap))
for id := range aggregatedMap {
productIDs = append(productIDs, id)
}
var flagResults []FlagResult
if len(productIDs) > 0 {
err = r.DB().WithContext(ctx).
Table("products p").
Select("p.id as product_id, STRING_AGG(DISTINCT f.name, ', ') as flag_names").
Joins("LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id").
Where("p.id IN ?", productIDs).
Group("p.id").
Scan(&flagResults).Error
if err != nil {
return nil, fmt.Errorf("failed to get product flags: %w", err)
}
}
fmt.Printf("[REPO] Fetched flags for %d products\n", len(flagResults))
// Build flag map
flagMap := make(map[uint]string)
for _, flag := range flagResults {
flagMap[flag.ProductID] = flag.FlagNames
}
// Combine results and calculate average price
results := make([]ProductUsageRow, 0, len(aggregatedMap))
for _, agg := range aggregatedMap {
avgPrice := float64(0)
if agg.PriceCount > 0 {
avgPrice = agg.Price / float64(agg.PriceCount)
}
flagNames := flagMap[agg.ProductID]
// Apply flag filters if provided
if len(flagFilters) > 0 {
// Check if any of the flagFilters exist in flagNames
matched := false
for _, filter := range flagFilters {
if containsIgnoreCase(flagNames, filter) {
matched = true
break
}
}
if !matched {
continue // Skip this product if no flag matches
}
}
results = append(results, ProductUsageRow{
ProductID: agg.ProductID,
ProductName: agg.ProductName,
FlagNames: flagNames,
TotalQty: agg.TotalQty,
Price: avgPrice,
TotalPengeluaran: agg.TotalQty * avgPrice,
})
}
fmt.Printf("[REPO] After filtering with flagFilters=%v: %d results\n", flagFilters, len(results))
for i, r := range results {
fmt.Printf("[REPO] Result[%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f\n",
i, r.ProductID, r.ProductName, r.FlagNames, r.TotalQty, r.Price, r.TotalPengeluaran)
}
// Sort by product name
sort.Slice(results, func(i, j int) bool {
return results[i].ProductName < results[j].ProductName
})
fmt.Printf("[REPO] Final sorted results: %d items\n", len(results))
return results, nil
}
// GetTotalDepletionByProjectFlockKandangID gets total depletion for a specific kandang
func (r *ClosingKeuanganRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var result float64
err := r.DB().WithContext(ctx).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.id = ?", projectFlockKandangID).
Scan(&result).Error
return result, err
}
// GetTotalWeightProducedFromUniformityByProjectFlockKandangID calculates total weight produced from uniformity data for a specific kandang
// Formula: (mean_up / 1.10) * chick_qty_of_weight / 1000
func (r *ClosingKeuanganRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
if projectFlockKandangID == 0 {
return 0, nil
}
var uniformity struct {
MeanUp float64
ChickQtyOfWeight float64
}
err := r.DB().WithContext(ctx).
Table("project_flock_kandang_uniformity").
Select("mean_up, chick_qty_of_weight").
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Order("id DESC").
Limit(1).
Scan(&uniformity).Error
if err != nil {
return 0, err
}
// Calculate weight: (mean_up / 1.10) * chick_qty_of_weight / 1000
totalWeight := (uniformity.MeanUp / 1.10) * uniformity.ChickQtyOfWeight / 1000
return totalWeight, nil
}
// containsIgnoreCase checks if a string contains a substring (case-insensitive)
func containsIgnoreCase(str, substr string) bool {
return strings.Contains(strings.ToUpper(str), strings.ToUpper(substr))
}
@@ -12,7 +12,6 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
@@ -99,11 +98,6 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
return nil, 0, err return nil, 0, err
} }
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
statusFilter := "" statusFilter := ""
if params.ProjectStatus != nil { if params.ProjectStatus != nil {
@@ -117,12 +111,6 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withClosingRelations(db) db = s.withClosingRelations(db)
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id")
}
if params.LocationID != nil { if params.LocationID != nil {
db = db.Where("location_id = ?", *params.LocationID) db = db.Where("location_id = ?", *params.LocationID)
} }
@@ -162,10 +150,6 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
} }
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations) projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
@@ -177,13 +161,6 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
} }
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
if projectFlockKandangID != nil {
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil {
return nil, err
}
} else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil {
return nil, err
}
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID) realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil { if err != nil {
@@ -197,8 +174,8 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF
} }
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) { func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) {
if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { if projectFlockID == 0 {
return nil, err return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
if kandangID != nil { if kandangID != nil {
@@ -282,7 +259,7 @@ func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectF
statusProject := "Belum Selesai" statusProject := "Belum Selesai"
var approvalDate string var approvalDate string
if s.ApprovalSvc != nil { if s.ApprovalSvc != nil {
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "", "") records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err) s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
@@ -344,8 +321,8 @@ func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectF
} }
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { if projectFlockID == 0 {
return nil, 0, err return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
if params == nil { if params == nil {
@@ -367,7 +344,15 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
} }
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID, params.KandangID) if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock") return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
@@ -451,7 +436,7 @@ func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID u
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID, params.KandangID) warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
@@ -494,16 +479,13 @@ func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID u
return items, nil return items, nil
} }
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) { func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
var kandangIDs []uint var kandangIDs []uint
db := s.Repository.DB().WithContext(ctx) db := s.Repository.DB().WithContext(ctx)
query := db.Model(&entity.ProjectFlockKandang{}). if err := db.Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID) Where("project_flock_id = ?", projectFlockID).
if kandangID != nil && *kandangID > 0 { Pluck("kandang_id", &kandangIDs).Error; err != nil {
query = query.Where("id = ?", *kandangID)
}
if err := query.Pluck("kandang_id", &kandangIDs).Error; err != nil {
return nil, err return nil, err
} }
@@ -555,7 +537,7 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
return "", "Belum Selesai", nil return "", "Belum Selesai", nil
} }
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "", "") records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@@ -598,14 +580,6 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
} }
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) { func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
if projectFlockKandangID != nil {
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil {
return nil, err
}
} else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil {
return nil, err
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -694,12 +668,8 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl
} }
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
if projectFlockKandangID != nil { if projectFlockID == 0 {
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
return nil, err
}
} else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil {
return nil, err
} }
rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
@@ -773,10 +743,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week")
} }
if !isGrowing && currentWeek != 0 {
currentWeek = currentWeek + 17
}
targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing) targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing)
if err != nil { if err != nil {
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
@@ -844,7 +810,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data")
} }
} }
age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID) age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
@@ -864,7 +830,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
// FeedUsedPerHead: feedUsedPerHead, // FeedUsedPerHead: feedUsedPerHead,
} }
chickenFlagNames := []string{string(utils.FlagPullet), string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagLayer)} chickenFlagNames := []string{string(utils.FlagPullet)}
chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames) chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err)
@@ -1031,24 +997,38 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return &result, nil return &result, nil
} }
func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) (float64, error) { func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint) (float64, error) {
penjualan, err := s.MarketingDeliveryProductRepo.GetClosingPenjualanForAgeChickDataProduction(ctx, projectFlockID, projectFlockKandangID) deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(ctx, projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins")
})
if err != nil { if err != nil {
return 0, err return 0, err
} }
acumulateAgeQty := 0.0
totalQty := 0.0 var (
for _, v := range penjualan { totalQty float64
sale := dto.ToSalesAgeDTO(v) totalAgeWeeks float64
acumulateAgeQty += float64(sale.Age) * sale.Qty )
totalQty += sale.Qty
} for _, product := range deliveryProducts {
if totalQty > 0 { if product.UsageQty == 0 {
averageAge := acumulateAgeQty / totalQty continue
return averageAge, nil }
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
totalAgeWeeks += float64(ageWeeks) * product.UsageQty
totalQty += product.UsageQty
} }
return 0, err if totalQty == 0 {
return 0, nil
}
return totalAgeWeeks / totalQty, nil
} }
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) { func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
@@ -2,19 +2,20 @@ package service
import ( import (
"errors" "errors"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -24,29 +25,9 @@ type ClosingKeuanganService interface {
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
} }
// CostData holds all cost-related information
type CostData struct {
FeedCost float64
OvkCost float64
ChickenCost float64
ExpeditionCost float64
BudgetOperational float64
RealizationOperational float64
}
// ProductionData holds all production and sales related information
type ProductionData struct {
TotalPopulationIn float64
TotalDepletion float64
TotalWeightProduced float64
TotalEggWeightKg float64
TotalWeightSold float64
TotalBirdSold float64
TotalSalesAmount float64
}
type closingKeuanganService struct { type closingKeuanganService struct {
Log *logrus.Logger Log *logrus.Logger
ClosingKeuanganRepo repository.ClosingKeuanganRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
@@ -54,11 +35,10 @@ type closingKeuanganService struct {
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository ChickinRepo chickinRepository.ProjectChickinRepository
RecordingRepo recordingRepository.RecordingRepository RecordingRepo recordingRepository.RecordingRepository
HppSvc commonSvc.HppService
HppRepo commonRepo.HppCostRepository
} }
func NewClosingKeuanganService( func NewClosingKeuanganService(
closingKeuanganRepo repository.ClosingKeuanganRepository,
projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockRepo projectflockRepository.ProjectflockRepository,
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
@@ -66,11 +46,10 @@ func NewClosingKeuanganService(
projectBudgetRepo projectflockRepository.ProjectBudgetRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
chickinRepo chickinRepository.ProjectChickinRepository, chickinRepo chickinRepository.ProjectChickinRepository,
recordingRepo recordingRepository.RecordingRepository, recordingRepo recordingRepository.RecordingRepository,
hppSvc commonSvc.HppService,
hppRepo commonRepo.HppCostRepository,
) ClosingKeuanganService { ) ClosingKeuanganService {
return &closingKeuanganService{ return &closingKeuanganService{
Log: utils.Log, Log: utils.Log,
ClosingKeuanganRepo: closingKeuanganRepo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
@@ -78,8 +57,6 @@ func NewClosingKeuanganService(
ProjectBudgetRepo: projectBudgetRepo, ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo, ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo, RecordingRepo: recordingRepo,
HppSvc: hppSvc,
HppRepo: hppRepo,
} }
} }
@@ -96,12 +73,30 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
// Preload Nonstock.Flags manually
var budgetIDs []uint
for _, b := range budgets {
budgetIDs = append(budgetIDs, b.Id)
}
if len(budgetIDs) > 0 {
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
Preload("Nonstock.Flags").
Where("id IN ?", budgetIDs).
Find(&budgets).Error
}
// Get all kandang for this project flock
kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
} }
return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
} }
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) { func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
@@ -112,11 +107,12 @@ func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projec
return nil, err return nil, err
} }
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) // Validate and fetch project flock kandang
kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
} }
if projectFlockKandang.ProjectFlockId != projectFlockID { if kandang.ProjectFlockId != projectFlockID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock") return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
} }
@@ -125,250 +121,417 @@ func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projec
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
projectFlockKandangs := []entity.ProjectFlockKandang{*projectFlockKandang} budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang) (*dto.ClosingKeuanganData, error) {
var projectFlockKandangIDs []uint
for _, projectFlockKandang := range projectFlockKandangs {
projectFlockKandangIDs = append(projectFlockKandangIDs, projectFlockKandang.Id)
} }
isPerKandang := len(projectFlockKandangs) == 1 // Preload Nonstock.Flags manually
var budgetIDs []uint
for _, b := range budgets {
budgetIDs = append(budgetIDs, b.Id)
}
if len(budgetIDs) > 0 {
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
Preload("Nonstock.Flags").
Where("id IN ?", budgetIDs).
Find(&budgets).Error
}
kandangs := []entity.ProjectFlockKandang{*kandang}
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
}
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) {
// Define flag filters using constants
pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)}
ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}
ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}
allFilters := append(pakanFilters, ovkFilters...)
allFilters = append(allFilters, ayamFilters...)
var allProductUsageRows []repository.ProductUsageRow
// Get ALL product usage
for _, kandang := range kandangs {
rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters)
if err == nil {
allProductUsageRows = append(allProductUsageRows, rows...)
}
}
// Classify into categories based on flag priority
var pakanProductUsageRows []repository.ProductUsageRow
var ovkProductUsageRows []repository.ProductUsageRow
var ayamProductUsageRows []repository.ProductUsageRow
for _, row := range allProductUsageRows {
// Parse flag names from comma-separated string
flagNames := strings.Split(row.FlagNames, ",")
hasPakanFlag := false
hasOvkFlag := false
hasAyamFlag := false
for _, flag := range flagNames {
flag = strings.TrimSpace(flag)
if containsItem(pakanFilters, flag) {
hasPakanFlag = true
}
if containsItem(ovkFilters, flag) {
hasOvkFlag = true
}
if containsItem(ayamFilters, flag) {
hasAyamFlag = true
}
}
// Priority: PAKAN > OVK > AYAM
if hasPakanFlag {
pakanProductUsageRows = append(pakanProductUsageRows, row)
} else if hasOvkFlag {
ovkProductUsageRows = append(ovkProductUsageRows, row)
} else if hasAyamFlag {
ayamProductUsageRows = append(ayamProductUsageRows, row)
} else {
continue
}
}
// Calculate total price for each category
var totalPakanPrice, totalOvkPrice, totalAyamPrice float64
for _, row := range pakanProductUsageRows {
totalPakanPrice += row.TotalPengeluaran
}
for _, row := range ovkProductUsageRows {
totalOvkPrice += row.TotalPengeluaran
}
for _, row := range ayamProductUsageRows {
totalAyamPrice += row.TotalPengeluaran
}
// Determine if this is per-kandang or per-project-flock scope
isPerKandang := len(kandangs) == 1
var projectFlockKandangID *uint var projectFlockKandangID *uint
if isPerKandang { if isPerKandang {
kandangID := projectFlockKandangs[0].Id kandangID := kandangs[0].Id
projectFlockKandangID = &kandangID projectFlockKandangID = &kandangID
} }
costs, err := s.calculateCosts(c, projectFlock, projectFlockKandangs, projectFlockKandangIDs, projectFlockKandangID)
if err != nil {
return nil, err
}
productionData, err := s.calculateProductionData(c, projectFlock, projectFlockKandangIDs, projectFlockKandangID)
if err != nil {
return nil, err
}
hppSection := s.buildHPPSection(c, projectFlock, projectFlockKandangs, costs, productionData)
profitLossSection := s.buildProfitLossSection(projectFlock, costs, productionData)
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
return &data, nil
}
func (s closingKeuanganService) calculateCosts(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*CostData, error) {
costs := &CostData{}
var err error var err error
costs.FeedCost, err = s.HppRepo.GetFeedUsageCost(c.Context(), projectFlockKandangIDs, nil) // Fetch realizations
if err != nil { var realizations []entity.ExpenseRealization
costs.FeedCost = 0 if isPerKandang && projectFlockKandangID != nil {
} realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID)
costs.OvkCost, err = s.HppRepo.GetOvkUsageCost(c.Context(), projectFlockKandangIDs, nil)
if err != nil {
costs.OvkCost = 0
}
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
for _, projectFlockKandang := range projectFlockKandangs {
depresiasiCost, err := s.HppSvc.GetDepresiasiTransfer(projectFlockKandang.Id, nil)
if err == nil {
costs.ChickenCost += depresiasiCost
}
pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id)
if err == nil {
costs.ChickenCost += pulletCost
}
}
} else { } else {
for _, projectFlockKandang := range projectFlockKandangs { realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil)
pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id)
if err == nil {
costs.ChickenCost += pulletCost
}
}
}
costs.ExpeditionCost, err = s.HppRepo.GetExpedisionCost(c.Context(), projectFlockKandangIDs)
if err != nil {
costs.ExpeditionCost = 0
}
if budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlock.Id); err == nil {
totalBudget := 0.0
for _, budget := range budgets {
totalBudget += budget.Price * budget.Qty
}
if projectFlockKandangID != nil {
allKandangs, errKandang := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
if errKandang == nil && len(allKandangs) > 0 {
costs.BudgetOperational = totalBudget / float64(len(allKandangs))
}
} else {
costs.BudgetOperational = totalBudget
}
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch budgets for project_flock_id=%d: %+v", projectFlock.Id, err)
}
if realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID); err == nil {
for _, realization := range realizations {
amount := realization.Price * realization.Qty
isEkspedisi := realization.ExpenseNonstock != nil &&
realization.ExpenseNonstock.Nonstock != nil &&
containsFlag(realization.ExpenseNonstock.Nonstock.Flags, "EKSPEDISI")
if !isEkspedisi {
costs.RealizationOperational += amount
}
}
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch realizations for project_flock_id=%d: %+v", projectFlock.Id, err)
}
return costs, nil
}
func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*ProductionData, error) {
data := &ProductionData{}
var err error
data.TotalPopulationIn, err = s.HppRepo.GetTotalPopulation(c.Context(), projectFlockKandangIDs)
if err != nil {
data.TotalPopulationIn = 0
}
if projectFlockKandangID != nil {
data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
} }
if err != nil { if err != nil {
data.TotalDepletion = 0 return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
} }
if projectFlockKandangID != nil { deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB {
data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) db = db.Preload("MarketingProduct").
} else { Preload("MarketingProduct.ProductWarehouse").
data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) Preload("MarketingProduct.ProductWarehouse.Product")
} return db
if err != nil { })
data.TotalWeightProduced = 0
}
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
_, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil)
if err != nil {
data.TotalEggWeightKg = 0
}
}
var deliveryProducts []entity.MarketingDeliveryProduct
if projectFlockKandangID != nil {
deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualanByCategory(c.Context(), projectFlock.Id, projectFlockKandangID, projectFlock.Category)
} else {
deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualanByCategory(c.Context(), projectFlock.Id, nil, projectFlock.Category)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data penjualan") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
} }
// Filter by kandang if scope is per-kandang (manual filtering after fetch)
if isPerKandang && projectFlockKandangID != nil {
filteredProducts := make([]entity.MarketingDeliveryProduct, 0)
for _, dp := range deliveryProducts {
pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId
if pfKandangID != nil && *pfKandangID == *projectFlockKandangID {
filteredProducts = append(filteredProducts, dp)
}
}
deliveryProducts = filteredProducts
}
// Fetch chickins
var chickins []entity.ProjectChickin
if isPerKandang && projectFlockKandangID != nil {
chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
// Get total depletion
var totalDepletion float64
if isPerKandang && projectFlockKandangID != nil {
totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
totalDepletion = 0
}
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id)
if err != nil {
}
// Try to get actual weight from uniformity data
var totalWeightFromUniformity float64
if isPerKandang && projectFlockKandangID != nil {
totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
} else if totalWeightFromUniformity > 0 {
totalWeightProduced = totalWeightFromUniformity
}
// Fetch egg data only for Laying category
var totalEggWeightKg float64
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
// TODO: Replace with actual method to get egg weight from RecordingRepo
// totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id)
// For now, set to 0 as placeholder
totalEggWeightKg = 0
} else {
totalEggWeightKg = 0
}
// Build new DTO structure
// Calculate totals
var totalPopulation float64
for _, chickin := range chickins {
totalPopulation += chickin.UsageQty
}
// Calculate actual population (total population - depletion)
actualPopulation := totalPopulation - totalDepletion
// Calculate budget totals by category
calculateBudgetByFlag := func(flags []string) float64 {
var total float64
for _, budget := range budgets {
if budget.Nonstock != nil {
for _, nonstockFlag := range budget.Nonstock.Flags {
flagName := strings.ToUpper(nonstockFlag.Name)
for _, targetFlag := range flags {
if flagName == strings.ToUpper(targetFlag) {
total += budget.Price * budget.Qty
break
}
}
}
}
}
return total
}
// Budget per category
budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"})
budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"})
budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"})
budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"})
// Operational budget = total budget - pakan - ovk - ayam - ekspedisi
totalBudgetAmount := 0.0
for _, budget := range budgets {
totalBudgetAmount += budget.Price * budget.Qty
}
budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi
// Calculate realization totals
var totalRealizationAmount float64
var totalEkspedisiRealization float64
for _, realization := range realizations {
amount := realization.Price * realization.Qty
totalRealizationAmount += amount
// Check if this is ekspedisi (need to check nonstock flags)
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil {
for _, flag := range realization.ExpenseNonstock.Nonstock.Flags {
if flag.Name == "EKSPEDISI" {
totalEkspedisiRealization += amount
break
}
}
}
}
totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization
// Filter delivery products based on category
var filteredDeliveryProducts []entity.MarketingDeliveryProduct
for _, delivery := range deliveryProducts { for _, delivery := range deliveryProducts {
// Get product from delivery
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue continue
} }
data.TotalWeightSold += delivery.TotalWeight
data.TotalBirdSold += delivery.UsageQty product := delivery.MarketingProduct.ProductWarehouse.Product
data.TotalSalesAmount += delivery.TotalPrice isEggProduct := false
isChickenProduct := false
// Check product flags
for _, flag := range product.Flags {
flagName := strings.ToUpper(flag.Name)
// Egg product flags
if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" ||
flagName == "TELURPUTIH" || flagName == "TELURRETAK" {
isEggProduct = true
}
// Chicken product flags
if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" {
isChickenProduct = true
}
}
// Filter based on project flock category
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
// Laying: only egg products
if isEggProduct {
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
}
} else {
// Growing/Contract Growing: only chicken products
if isChickenProduct || (!isEggProduct && !isChickenProduct) {
// Include if chicken product or if no specific flags (default to chicken)
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
}
}
} }
return data, nil
}
func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection { // Calculate total weight sold and sales amount from filtered products
var totalWeightSold float64
actualPopulation := production.TotalPopulationIn - production.TotalDepletion var totalSalesAmount float64
totalWeightProduced := production.TotalWeightProduced for _, delivery := range filteredDeliveryProducts {
totalEggWeightKg := production.TotalEggWeightKg totalWeightSold += delivery.TotalWeight
totalSalesAmount += delivery.TotalPrice
weightForCalculation := totalWeightProduced
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
weightForCalculation = totalEggWeightKg
} }
// Calculate metrics - always use kg ayam for rp_per_kg
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 { if actualPopulation > 0 {
rpPerBird = amount / actualPopulation rpPerBird = amount / actualPopulation // Use actual population
} }
if weightForCalculation > 0 { if totalWeightProduced > 0 {
rpPerKg = amount / weightForCalculation rpPerKg = amount / totalWeightProduced
} }
return return
} }
createHPPItem := func(id uint, category, code, label string, budgetAmount, realizationAmount float64) dto.HPPItem { // Calculate metrics for profit loss (use total population and total weight produced)
budgetRpPerBird, budgetRpPerKg := calculateMetrics(budgetAmount) calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
realizationRpPerBird, realizationRpPerKg := calculateMetrics(realizationAmount) if totalPopulation > 0 {
return dto.ToHPPItem( rpPerBird = amount / totalPopulation
id, }
category, if totalWeightProduced > 0 {
code, rpPerKg = amount / totalWeightProduced
label, }
dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), return
dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
)
} }
// Build HPP Items using constants
hppItems := []dto.HPPItem{} hppItems := []dto.HPPItem{}
hppItems = append(hppItems, createHPPItem(1, "purchase", string(dto.HPPCodePakan), "Pembelian Pakan", costs.FeedCost, costs.FeedCost)) // PAKAN item
hppItems = append(hppItems, createHPPItem(2, "purchase", string(dto.HPPCodeOVK), "Pembelian OVK", costs.OvkCost, costs.OvkCost)) pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan)
pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice)
hppItems = append(hppItems, dto.ToHPPItem(
1,
"purchase",
string(dto.HPPCodePakan),
"Pembelian Pakan",
dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan),
dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice),
))
// OVK item
ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk)
ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice)
hppItems = append(hppItems, dto.ToHPPItem(
2,
"purchase",
string(dto.HPPCodeOVK),
"Pembelian OVK",
dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk),
dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice),
))
// DOC/DEPRESIASI item
docCode := string(dto.HPPCodeDOC) docCode := string(dto.HPPCodeDOC)
docLabel := "Pembelian DOC" docLabel := "Pembelian DOC"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
docCode = string(dto.HPPCodeDepresiasi) docCode = string(dto.HPPCodeDepresiasi)
docLabel = "Depresiasi" docLabel = "Depresiasi"
} }
hppItems = append(hppItems, createHPPItem(3, "purchase", docCode, docLabel, costs.ChickenCost, costs.ChickenCost)) docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam)
hppItems = append(hppItems, createHPPItem(4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", costs.BudgetOperational, costs.RealizationOperational)) docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice)
hppItems = append(hppItems, createHPPItem(5, "overhead", string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", costs.ExpeditionCost, costs.ExpeditionCost)) hppItems = append(hppItems, dto.ToHPPItem(
3,
"purchase",
docCode,
docLabel,
dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam),
dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice),
))
totalBudgetHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.BudgetOperational + costs.ExpeditionCost // OVERHEAD item
totalRealizationHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.RealizationOperational + costs.ExpeditionCost overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational)
overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization)
hppItems = append(hppItems, dto.ToHPPItem(
4,
"overhead",
string(dto.HPPCodeOverhead),
"Pengeluaran Overhead",
dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational),
dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization),
))
// EKSPEDISI item
ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi)
ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization)
hppItems = append(hppItems, dto.ToHPPItem(
5,
"overhead",
string(dto.HPPCodeEkspedisi),
"Beban Ekspedisi",
dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi),
dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization),
))
// HPP Summary
totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi
totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp) hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp) hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
var eggBudgeting, eggRealization *dto.FinancialMetrics var eggBudgeting, eggRealization *dto.FinancialMetrics
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 {
accumulateEggMetrics := func(metrics **dto.FinancialMetrics, amount, rpPerKg float64) { eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg
if *metrics == nil { eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg
*metrics = &dto.FinancialMetrics{ eggBudgeting = &dto.FinancialMetrics{
RpPerBird: 0, RpPerBird: 0,
RpPerKg: rpPerKg, RpPerKg: eggBudgetRpPerKg,
Amount: amount, Amount: totalBudgetHpp,
}
} else {
(*metrics).Amount += amount
if totalEggWeightKg > 0 {
(*metrics).RpPerKg = (*metrics).Amount / totalEggWeightKg
}
}
} }
eggRealization = &dto.FinancialMetrics{
for _, projectFlockKandang := range projectFlockKandangs { RpPerBird: 0,
hppResponse, err := s.HppSvc.CalculateHppCost(projectFlockKandang.Id, nil) RpPerKg: eggRealizationRpPerKg,
if err == nil { Amount: totalRealizationHpp,
accumulateEggMetrics(&eggBudgeting, hppResponse.Estimation.Total, hppResponse.Estimation.HargaKg)
accumulateEggMetrics(&eggRealization, hppResponse.Real.Total, hppResponse.Real.HargaKg)
}
} }
} }
@@ -380,82 +543,15 @@ func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *enti
eggRealization, eggRealization,
) )
return dto.ToHPPSection(hppItems, hppSummary) hppSection := dto.ToHPPSection(hppItems, hppSummary)
}
func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection {
totalWeightProduced := production.TotalWeightProduced
totalEggWeightKg := production.TotalEggWeightKg
totalSalesAmount := production.TotalSalesAmount
totalWeightSold := production.TotalWeightSold
totalBirdSold := production.TotalBirdSold
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying)
// Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual
calculateSalesMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if isLaying {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
} else {
if totalBirdSold > 0 {
rpPerBird = amount / totalBirdSold
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
}
return
}
// Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi
calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if isLaying {
if totalEggWeightKg > 0 {
rpPerKg = amount / totalEggWeightKg
}
} else {
if totalWeightProduced > 0 {
rpPerKg = amount / totalWeightProduced
}
}
return
}
// Fungsi untuk overhead/ekspedisi: LAYING = populasi aktual, GROWING = ekor terjual
calculateOverheadMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if isLaying {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
} else {
if totalBirdSold > 0 {
rpPerBird = amount / totalBirdSold
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
}
return
}
// Build Profit Loss Items using constants
plItems := []dto.ProfitLossItem{} plItems := []dto.ProfitLossItem{}
salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount) // SALES item
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
salesLabel := "Penjualan Ayam" salesLabel := "Penjualan Ayam"
if isLaying { if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
salesLabel = "Penjualan Telur" salesLabel = "Penjualan Telur"
} }
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
@@ -467,71 +563,76 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
totalSalesAmount, totalSalesAmount,
)) ))
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost // SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK
sapronakRpPerBird := 0.0 totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice
sapronakRpPerKg := 0.0 sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg
rpPerBird, rpPerKg := calculateCostMetrics(amount) sapronakLabel := "Pengeluaran Sapronak"
sapronakRpPerBird += rpPerBird
sapronakRpPerKg += rpPerKg
}
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSapronak), string(dto.PLCodeSapronak),
"Pengeluaran Sapronak", sapronakLabel,
"purchase", "purchase",
sapronakRpPerBird, sapronakRpPerBird,
sapronakRpPerKg, sapronakRpPerKg,
totalSapronakAmount, totalSapronakAmount,
)) ))
overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational) // OVERHEAD item
overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization)
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeOverhead), string(dto.PLCodeOverhead),
"Overhead", "Overhead",
"overhead", "overhead",
overheadRpPerBird, overheadRpPerBird,
overheadRpPerKg, overheadRpPerKg,
costs.RealizationOperational, totalOperationalRealization,
)) ))
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost) // EKSPEDISI item
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeEkspedisi), string(dto.PLCodeEkspedisi),
"Ekspedisi", "Ekspedisi",
"overhead", "overhead",
ekspedisiRpPerBird, ekspedisiRealizationRpPerBird,
ekspedisiRpPerKg, ekspedisiRealizationRpPerKg,
costs.ExpeditionCost, totalEkspedisiRealization,
)) ))
costOfGoodsSold := costs.ChickenCost + costs.FeedCost + costs.OvkCost // Profit Loss Summary
// Gross Profit = Sales - (DOC + PAKAN + OVK) only
// Gross Profit should NOT include overhead and ekspedisi
costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice
costOfGoodsSoldRpPerBird := sapronakRpPerBird costOfGoodsSoldRpPerBird := sapronakRpPerBird
costOfGoodsSoldRpPerKg := sapronakRpPerKg
grossProfit := totalSalesAmount - costOfGoodsSold grossProfit := totalSalesAmount - costOfGoodsSold
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
grossProfitRpPerKg := salesRpPerKg - costOfGoodsSoldRpPerKg
totalOperatingExpenses := costs.RealizationOperational + costs.ExpeditionCost // Operating Expenses (Overhead + Ekspedisi)
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRpPerBird totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization
totalOperatingExpensesRpPerKg := overheadRpPerKg + ekspedisiRpPerKg totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird
// Net Profit = Gross Profit - Operating Expenses
netProfit := grossProfit - totalOperatingExpenses netProfit := grossProfit - totalOperatingExpenses
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
netProfitRpPerKg := grossProfitRpPerKg - totalOperatingExpensesRpPerKg
plSummary := dto.ToProfitLossSummary( plSummary := dto.ToProfitLossSummary(
dto.ToFinancialMetrics(grossProfitRpPerBird, grossProfitRpPerKg, grossProfit), dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit),
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, totalOperatingExpensesRpPerKg, totalOperatingExpenses), dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses),
dto.ToFinancialMetrics(netProfitRpPerBird, netProfitRpPerKg, netProfit), dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit),
) )
return dto.ToProfitLossSection(plItems, plSummary) profitLossSection := dto.ToProfitLossSection(plItems, plSummary)
// Build complete response
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
return &data, nil
} }
func containsFlag(flags []entity.Flag, name string) bool { // containsItem checks if a string exists in a slice
for _, flag := range flags { func containsItem(slice []string, item string) bool {
if flag.Name == name { for _, s := range slice {
if strings.EqualFold(s, item) {
return true return true
} }
} }
@@ -347,14 +347,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
usageAllocatedDetails, err := s.Repository.FetchSapronakUsageAllocatedDetails(ctx, pfk.Id)
if err != nil {
return nil, nil, 0, 0, err
}
if len(usageAllocatedDetails) > 0 {
usageDetailsRows = usageAllocatedDetails
chickinUsageDetailsRows = map[uint][]repository.SapronakDetailRow{}
}
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId) adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
@@ -363,7 +355,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.Id) salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
@@ -2,7 +2,6 @@ package controller
import ( import (
"math" "math"
"mime/multipart"
"strconv" "strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
@@ -363,9 +362,6 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
} }
req.Documents = form.File["documents"] req.Documents = form.File["documents"]
if err := validateDailyChecklistDocumentSizes(req.Documents); err != nil {
return err
}
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
@@ -385,16 +381,6 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
}) })
} }
func validateDailyChecklistDocumentSizes(files []*multipart.FileHeader) error {
const maxDailyChecklistDocumentBytes = 5 * 1024 * 1024 // 5MB
for _, file := range files {
if file != nil && file.Size > maxDailyChecklistDocumentBytes {
return fiber.NewError(fiber.StatusRequestEntityTooLarge, "Document size must be <= 5MB")
}
}
return nil
}
func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error { func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist") param := c.Params("idDailyChecklist")
@@ -3,14 +3,13 @@ package service
import ( import (
"errors" "errors"
"math" "math"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware" middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -135,87 +134,6 @@ func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Kandang") return db.Preload("Kandang")
} }
func (s dailyChecklistService) ensureChecklistAccess(c *fiber.Ctx, checklistID uint) error {
if checklistID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid checklist id")
}
db := s.Repository.DB().WithContext(c.Context()).
Table("daily_checklists dc").
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("dc.id = ?", checklistID)
scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if err != nil {
return err
}
var count int64
if err := scopedDB.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
}
return nil
}
func (s dailyChecklistService) ensureKandangAccess(c *fiber.Ctx, kandangID uint) error {
if kandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id")
}
db := s.Repository.DB().WithContext(c.Context()).
Table("kandangs k").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("k.id = ?", kandangID)
scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if err != nil {
return err
}
var count int64
if err := scopedDB.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
return nil
}
func (s dailyChecklistService) ensureTaskAccess(c *fiber.Ctx, taskID uint) error {
if taskID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid task id")
}
db := s.Repository.DB().WithContext(c.Context()).
Table("daily_checklist_activity_tasks t").
Joins("JOIN daily_checklists dc ON dc.id = t.checklist_id").
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("t.id = ?", taskID)
scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if err != nil {
return err
}
var count int64
if err := scopedDB.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Task not found")
}
return nil
}
func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) { func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
@@ -225,15 +143,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
db := s.Repository.DB().WithContext(c.Context()). db := s.Repository.DB().WithContext(c.Context()).
Table("daily_checklists dc"). Table("daily_checklists dc").
Joins("JOIN kandangs k ON k.id = dc.kandang_id"). Joins("JOIN kandangs k ON k.id = dc.kandang_id")
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id")
var scopeErr error
db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if scopeErr != nil {
return nil, 0, scopeErr
}
if params.DateFrom != "" { if params.DateFrom != "" {
dateFrom, err := time.Parse("2006-01-02", params.DateFrom) dateFrom, err := time.Parse("2006-01-02", params.DateFrom)
@@ -260,9 +170,8 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
} }
if params.Search != "" { if params.Search != "" {
re := regexp.MustCompile("[^a-zA-Z0-9]") like := "%" + params.Search + "%"
like := re.ReplaceAll([]byte("%"+params.Search+"%"), []byte("")) db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like)
db = db.Where("(regexp_replace(k.name, '[^a-zA-Z0-9]', '', 'g') ILIKE ? OR regexp_replace(dc.category::text, '[^a-zA-Z0-9]', '', 'g') ILIKE ?)", string(like), string(like))
} }
countDB := db.Session(&gorm.Session{}) countDB := db.Session(&gorm.Session{})
@@ -385,9 +294,6 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
} }
func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) { func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) {
if err := s.ensureChecklistAccess(c, id); err != nil {
return nil, err
}
dailyChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations) dailyChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
@@ -493,9 +399,6 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := s.ensureKandangAccess(c, req.KandangId); err != nil {
return nil, err
}
date, err := time.Parse("2006-01-02", req.Date) date, err := time.Parse("2006-01-02", req.Date)
if err != nil { if err != nil {
@@ -528,9 +431,6 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := s.ensureChecklistAccess(c, id); err != nil {
return nil, err
}
deletedIDs := make([]uint, 0) deletedIDs := make([]uint, 0)
if req.DeletedDocumentIDs != nil { if req.DeletedDocumentIDs != nil {
@@ -556,7 +456,7 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
updateBody["reject_reason"] = *req.RejectReason updateBody["reject_reason"] = *req.RejectReason
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := middleware.ActorIDFromContext(c)
if err != nil { if err != nil {
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
} }
@@ -602,9 +502,6 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
} }
func (s dailyChecklistService) DeleteOne(c *fiber.Ctx, id uint) error { func (s dailyChecklistService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.ensureChecklistAccess(c, id); err != nil {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
@@ -619,9 +516,6 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return err return err
} }
if err := s.ensureChecklistAccess(c, id); err != nil {
return err
}
if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -703,9 +597,6 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati
} }
func (s dailyChecklistService) RemoveAssignment(c *fiber.Ctx, id uint, employeeID uint) error { func (s dailyChecklistService) RemoveAssignment(c *fiber.Ctx, id uint, employeeID uint) error {
if err := s.ensureChecklistAccess(c, id); err != nil {
return err
}
if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
@@ -743,9 +634,6 @@ func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entit
if checklistID == 0 { if checklistID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required")
} }
if err := s.ensureChecklistAccess(c, checklistID); err != nil {
return nil, err
}
if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -770,9 +658,6 @@ func (s dailyChecklistService) GetChecklistPhaseIDs(c *fiber.Ctx, checklistID ui
if checklistID == 0 { if checklistID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required")
} }
if err := s.ensureChecklistAccess(c, checklistID); err != nil {
return nil, err
}
if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -802,9 +687,6 @@ func (s dailyChecklistService) UpdateAssignment(c *fiber.Ctx, req *validation.Up
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return err return err
} }
if err := s.ensureTaskAccess(c, req.TaskID); err != nil {
return err
}
task := new(entity.DailyChecklistActivityTask) task := new(entity.DailyChecklistActivityTask)
if err := s.Repository.DB().WithContext(c.Context()).First(task, req.TaskID).Error; err != nil { if err := s.Repository.DB().WithContext(c.Context()).First(task, req.TaskID).Error; err != nil {
@@ -926,9 +808,6 @@ func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validatio
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return err return err
} }
if err := s.ensureChecklistAccess(c, id); err != nil {
return err
}
employeeIDs, err := parseIDs(req.EmployeeIDs) employeeIDs, err := parseIDs(req.EmployeeIDs)
if err != nil { if err != nil {
@@ -1021,16 +900,8 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa
Joins("JOIN daily_checklists d ON d.id = t.checklist_id"). Joins("JOIN daily_checklists d ON d.id = t.checklist_id").
Joins("JOIN kandangs k ON k.id = d.kandang_id"). Joins("JOIN kandangs k ON k.id = d.kandang_id").
Joins("JOIN employees e ON e.id = a.employee_id"). Joins("JOIN employees e ON e.id = a.employee_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas ar ON ar.id = loc.area_id").
Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED") Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED")
var scopeErr error
db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "ar.id")
if scopeErr != nil {
return nil, scopeErr
}
if params.Category != "" { if params.Category != "" {
db = db.Where("d.category = ?", params.Category) db = db.Where("d.category = ?", params.Category)
} }
@@ -1075,15 +946,6 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
return nil, 0, err return nil, 0, err
} }
locationScope, err := m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
areaScope, err := m.ResolveAreaScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
buildBase := func() *gorm.DB { buildBase := func() *gorm.DB {
@@ -1100,9 +962,6 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year).
Where("dc.status = ?", "APPROVED") Where("dc.status = ?", "APPROVED")
db = m.ApplyScopeFilter(db, locationScope, "loc.id")
db = m.ApplyScopeFilter(db, areaScope, "a.id")
if params.AreaID != nil { if params.AreaID != nil {
db = db.Where("a.id = ?", *params.AreaID) db = db.Where("a.id = ?", *params.AreaID)
} }
@@ -29,7 +29,7 @@ type Query struct {
} }
type AssignPhases struct { type AssignPhases struct {
PhaseIDs string `json:"phase_ids" validate:"omitempty"` PhaseIDs string `json:"phase_ids" validate:"required"`
} }
type AssignTask struct { type AssignTask struct {
@@ -6,7 +6,6 @@ import (
"strings" "strings"
"time" "time"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
@@ -82,20 +81,6 @@ func (u *DashboardController) GetAll(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid include") return fiber.NewError(fiber.StatusBadRequest, "Invalid include")
} }
scope, err := m.ResolveLocationScope(c, u.DashboardService.DB())
if err != nil {
return err
}
if scope.Restrict {
if len(scope.IDs) == 0 {
lokasiIds = []uint{}
} else if len(lokasiIds) > 0 {
lokasiIds = intersectUint(lokasiIds, scope.IDs)
} else {
lokasiIds = scope.IDs
}
}
analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview))) analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview)))
metric := strings.ToLower(strings.TrimSpace(c.Query("metric", ""))) metric := strings.ToLower(strings.TrimSpace(c.Query("metric", "")))
@@ -191,23 +176,6 @@ func defaultUintSlice(values []uint) []uint {
return values return values
} }
func intersectUint(a, b []uint) []uint {
if len(a) == 0 || len(b) == 0 {
return nil
}
set := make(map[uint]struct{}, len(b))
for _, id := range b {
set[id] = struct{}{}
}
out := make([]uint, 0, len(a))
for _, id := range a {
if _, ok := set[id]; ok {
out = append(out, id)
}
}
return out
}
func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) { func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) {
now := time.Now().In(location) now := time.Now().In(location)
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location) startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
+2 -5
View File
@@ -5,8 +5,6 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories" rDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
sDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services" sDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
@@ -18,12 +16,11 @@ type DashboardModule struct{}
func (DashboardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (DashboardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
dashboardRepo := rDashboard.NewDashboardRepository(db) dashboardRepo := rDashboard.NewDashboardRepository(db)
hppCostRepo := commonRepo.NewHppCostRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
hppSvc := commonService.NewHppService(hppCostRepo) dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate)
dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate, hppSvc)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
DashboardRoutes(router, userService, dashboardService) DashboardRoutes(router, userService, dashboardService)
} }
@@ -21,7 +21,6 @@ type DashboardRepository interface {
SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error) SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error)
SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
ListProjectFlockKandangIDsByEggProduction(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]uint, error)
GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error) GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error)
GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error) GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error)
GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error) GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error)
@@ -309,27 +309,6 @@ func (r *DashboardRepositoryImpl) SumEggProductionWeightKg(ctx context.Context,
return grams / 1000, nil return grams / 1000, nil
} }
func (r *DashboardRepositoryImpl) ListProjectFlockKandangIDsByEggProduction(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]uint, error) {
var ids []uint
db := r.DB().WithContext(ctx).
Table("recording_eggs AS re").
Select("DISTINCT r.project_flock_kandangs_id").
Joins("JOIN recordings AS r ON r.id = re.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
db = applyDashboardFilters(db, filters)
if err := db.Scan(&ids).Error; err != nil {
return nil, err
}
return ids, nil
}
func (r *DashboardRepositoryImpl) GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error) { func (r *DashboardRepositoryImpl) GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error) {
var rows []FeedUsageByUom var rows []FeedUsageByUom
@@ -572,23 +551,18 @@ func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context
} }
var rows []ComparisonWeeklyMetric var rows []ComparisonWeeklyMetric
weekExpr := `CASE
WHEN r.day IS NULL OR r.day <= 0 THEN 1
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
ELSE ((r.day - 1) / 7 + 1)
END`
db := r.DB().WithContext(ctx). db := r.DB().WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select(fmt.Sprintf(`%s AS week, Select(fmt.Sprintf(`((r.day - 1) / 7 + 1) AS week,
%s AS series_id, %s AS series_id,
COALESCE(AVG(%s), 0) AS value`, weekExpr, seriesExpr, metricExpr)). COALESCE(AVG(%s), 0) AS value`, seriesExpr, metricExpr)).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN locations AS loc ON loc.id = k.location_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL") Where("r.deleted_at IS NULL").
Where("r.day IS NOT NULL AND r.day > 0")
db = applyDashboardFilters(db, filters) db = applyDashboardFilters(db, filters)
@@ -10,7 +10,6 @@ import (
"strings" "strings"
"time" "time"
commonService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
@@ -18,34 +17,26 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm"
) )
type DashboardService interface { type DashboardService interface {
GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error)
DB() *gorm.DB
} }
type dashboardService struct { type dashboardService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.DashboardRepository Repository repository.DashboardRepository
HppSvc commonService.HppService
} }
func NewDashboardService(repo repository.DashboardRepository, validate *validator.Validate, hppSvc commonService.HppService) DashboardService { func NewDashboardService(repo repository.DashboardRepository, validate *validator.Validate) DashboardService {
return &dashboardService{ return &dashboardService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
HppSvc: hppSvc,
} }
} }
func (s dashboardService) DB() *gorm.DB {
return s.Repository.DB()
}
func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) { func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return dto.DashboardPerformanceOverviewDTO{}, 0, err return dto.DashboardPerformanceOverviewDTO{}, 0, err
@@ -601,13 +592,13 @@ func buildAggregateComparisonPercent(weeks []int, seriesRows []repository.Compar
count++ count++
} }
if count == 0 {
continue
}
if result[week] == nil { if result[week] == nil {
result[week] = map[uint]float64{} result[week] = map[uint]float64{}
} }
if count == 0 {
result[week][series.Id] = 0
continue
}
result[week][series.Id] = sum / count result[week][series.Id] = sum / count
} }
} }
@@ -855,21 +846,6 @@ func percentDelta(current, last float64) float64 {
} }
func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) { func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
if s.HppSvc != nil {
currentHpp, err := s.hppGlobalForPeriod(ctx, startDate, endExclusive)
if err != nil {
return 0, 0, err
}
lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
lastHpp, err := s.hppGlobalForPeriod(ctx, lastMonthStart, lastMonthEndExclusive)
if err != nil {
return 0, 0, err
}
return currentHpp, lastHpp, nil
}
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil) totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
@@ -902,37 +878,6 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, end
return hppCurrent, hppLast, nil return hppCurrent, hppLast, nil
} }
func (s dashboardService) hppGlobalForPeriod(ctx context.Context, startDate, endExclusive time.Time) (float64, error) {
kandangIDs, err := s.Repository.ListProjectFlockKandangIDsByEggProduction(ctx, startDate, endExclusive, nil)
if err != nil {
return 0, err
}
if len(kandangIDs) == 0 {
return 0, nil
}
endOfPeriod := endExclusive.Add(-time.Nanosecond)
totalCost := 0.0
totalWeightKg := 0.0
for _, kandangID := range kandangIDs {
hppCost, err := s.HppSvc.CalculateHppCost(kandangID, &endOfPeriod)
if err != nil {
return 0, err
}
if hppCost == nil {
continue
}
totalCost += hppCost.Estimation.Total
totalWeightKg += hppCost.Estimation.Kg
}
if totalWeightKg <= 0 {
return 0, nil
}
return totalCost / totalWeightKg, nil
}
func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) { func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) {
startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location) startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
currentEndExclusive := endDate.AddDate(0, 0, 1) currentEndExclusive := endDate.AddDate(0, 0, 1)
@@ -328,7 +328,6 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
} }
kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r) kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r)
} else { } else {
directRealisasi = append(directRealisasi, r)
} }
} }
@@ -139,28 +139,9 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
locationID := filters.LocationId locationID := filters.LocationId
areaID := filters.AreaId areaID := filters.AreaId
if filters.AllowedLocationIDs != nil || filters.AllowedAreaIDs != nil || locationID > 0 || areaID > 0 {
db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id")
}
if filters.AllowedLocationIDs != nil {
if len(filters.AllowedLocationIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Where("kandangs.location_id IN ?", filters.AllowedLocationIDs)
}
}
if filters.AllowedAreaIDs != nil {
if len(filters.AllowedAreaIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Joins("JOIN locations ON locations.id = kandangs.location_id").
Where("locations.area_id IN ?", filters.AllowedAreaIDs)
}
}
if locationID > 0 || areaID > 0 { if locationID > 0 || areaID > 0 {
db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id")
if locationID > 0 { if locationID > 0 {
db = db.Where("kandangs.location_id = ?", uint(locationID)) db = db.Where("kandangs.location_id = ?", uint(locationID))
} }
@@ -87,22 +87,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens
return nil, 0, err return nil, 0, err
} }
var scopeErr error
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id")
if params.Search != "" { if params.Search != "" {
return db.Where("category ILIKE ?", "%"+params.Search+"%") return db.Where("category ILIKE ?", "%"+params.Search+"%")
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -123,16 +117,7 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens
} }
func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) {
var scopeErr error expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id")
return db
})
if scopeErr != nil {
return nil, scopeErr
}
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -20,7 +20,7 @@ type InitialRelationDTO struct {
InitialBalanceType string `json:"initial_balance_type"` InitialBalanceType string `json:"initial_balance_type"`
InitialBalanceTypeLabel string `json:"initial_balance_type_label"` InitialBalanceTypeLabel string `json:"initial_balance_type_label"`
Party Party `json:"party"` Party Party `json:"party"`
Bank *bankDTO.BankRelationDTO `json:"bank"` Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
Direction string `json:"direction"` Direction string `json:"direction"`
Nominal float64 `json:"nominal"` Nominal float64 `json:"nominal"`
Notes string `json:"notes"` Notes string `json:"notes"`
@@ -128,12 +128,11 @@ func partyFromInitial(e entity.Payment) Party {
return party return party
} }
func bankFromInitial(e entity.Payment) *bankDTO.BankRelationDTO { func bankFromInitial(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 { if e.BankWarehouse.Id == 0 {
return nil return bankDTO.BankRelationDTO{}
} }
bank := bankDTO.ToBankRelationDTO(e.BankWarehouse) return bankDTO.ToBankRelationDTO(e.BankWarehouse)
return &bank
} }
func userFromInitial(e entity.Payment) userDTO.UserRelationDTO { func userFromInitial(e entity.Payment) userDTO.UserRelationDTO {
@@ -162,7 +161,7 @@ func initialBalanceLabel(balanceType string) string {
} }
func initialBalanceTypeFromPayment(e entity.Payment) string { func initialBalanceTypeFromPayment(e entity.Payment) string {
if e.Nominal < 0 { if strings.EqualFold(e.Direction, "OUT") || e.Nominal < 0 {
return "NEGATIVE" return "NEGATIVE"
} }
return "POSITIVE" return "POSITIVE"
@@ -82,7 +82,6 @@ func (s initialService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
} }
func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) { func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
normalizeOptionalBankId(&req.BankId)
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
@@ -125,7 +124,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
PaymentDate: time.Now(), PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo), PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId, BankId: req.BankId,
Direction: directionForInitialType(party, balanceType), Direction: directionForInitialType(balanceType),
Nominal: signedNominal(balanceType, req.Nominal), Nominal: signedNominal(balanceType, req.Nominal),
Notes: req.Note, Notes: req.Note,
CreatedBy: actorID, CreatedBy: actorID,
@@ -165,7 +164,6 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) { func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
normalizeOptionalBankId(&req.BankId)
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
@@ -188,8 +186,6 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
requiresExisting := req.PartyType != nil || req.PartyId != nil || req.InitialBalanceType != nil || req.Nominal != nil requiresExisting := req.PartyType != nil || req.PartyId != nil || req.InitialBalanceType != nil || req.Nominal != nil
requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil
var existing *entity.Payment var existing *entity.Payment
var resolvedPartyType string
var resolvedPartyId uint
if requiresVerification { if requiresVerification {
current, err := s.Repository.GetByID(c.Context(), id, nil) current, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -203,25 +199,26 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found") return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
} }
existing = current existing = current
resolvedPartyType = existing.PartyType
resolvedPartyId = existing.PartyId
} }
if req.PartyType != nil || req.PartyId != nil { if req.PartyType != nil || req.PartyId != nil {
partyType := existing.PartyType
partyId := existing.PartyId
if req.PartyType != nil { if req.PartyType != nil {
normalized, err := normalizePartyType(*req.PartyType) normalized, err := normalizePartyType(*req.PartyType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resolvedPartyType = normalized partyType = normalized
updateBody["party_type"] = resolvedPartyType updateBody["party_type"] = partyType
} }
if req.PartyId != nil { if req.PartyId != nil {
resolvedPartyId = *req.PartyId partyId = *req.PartyId
updateBody["party_id"] = resolvedPartyId updateBody["party_id"] = partyId
} }
if err := s.ensurePartyExists(c.Context(), resolvedPartyType, resolvedPartyId); err != nil { if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
return nil, err return nil, err
} }
} }
@@ -241,11 +238,8 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
nominal = *req.Nominal nominal = *req.Nominal
} }
updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType) updateBody["direction"] = directionForInitialType(balanceType)
updateBody["nominal"] = signedNominal(balanceType, nominal) updateBody["nominal"] = signedNominal(balanceType, nominal)
} else if req.PartyType != nil {
balanceType := balanceTypeFromPayment(existing)
updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
} }
if len(updateBody) == 0 { if len(updateBody) == 0 {
@@ -268,7 +262,7 @@ func isInitialTransaction(transactionType string) bool {
} }
func balanceTypeFromPayment(payment *entity.Payment) string { func balanceTypeFromPayment(payment *entity.Payment) string {
if payment.Nominal < 0 { if strings.EqualFold(payment.Direction, "OUT") || payment.Nominal < 0 {
return "NEGATIVE" return "NEGATIVE"
} }
return "POSITIVE" return "POSITIVE"
@@ -292,24 +286,11 @@ func normalizeInitialBalanceType(balanceType string) (string, error) {
} }
} }
func directionForInitialType(partyType string, balanceType string) string { func directionForInitialType(balanceType string) string {
switch utils.PaymentParty(strings.ToUpper(strings.TrimSpace(partyType))) { if strings.EqualFold(balanceType, "NEGATIVE") {
case utils.PaymentPartySupplier: return "OUT"
if strings.EqualFold(balanceType, "POSITIVE") {
return "OUT"
}
return "IN"
case utils.PaymentPartyCustomer:
if strings.EqualFold(balanceType, "NEGATIVE") {
return "OUT"
}
return "IN"
default:
if strings.EqualFold(balanceType, "NEGATIVE") {
return "OUT"
}
return "IN"
} }
return "IN"
} }
func signedNominal(balanceType string, nominal float64) float64 { func signedNominal(balanceType string, nominal float64) float64 {
@@ -354,12 +335,3 @@ func (s initialService) ensureBankExists(ctx context.Context, bankId *uint) erro
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists}, commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
) )
} }
func normalizeOptionalBankId(bankId **uint) {
if bankId == nil || *bankId == nil {
return
}
if **bankId == 0 {
*bankId = nil
}
}
@@ -3,7 +3,7 @@ package validation
type Create struct { type Create struct {
PartyType string `json:"party_type" validate:"required_strict,max=50"` PartyType string `json:"party_type" validate:"required_strict,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"` PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
ReferenceNumber string `json:"reference_number" validate:"required_strict,max=100"` ReferenceNumber string `json:"reference_number" validate:"required_strict,max=100"`
InitialBalanceType string `json:"initial_balance_type" validate:"required_strict,oneof=NEGATIVE POSITIVE"` InitialBalanceType string `json:"initial_balance_type" validate:"required_strict,oneof=NEGATIVE POSITIVE"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"` Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
@@ -110,7 +110,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
PaymentDate: adjustmentDate, PaymentDate: adjustmentDate,
PaymentMethod: string(utils.PaymentMethodSaldo), PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId, BankId: req.BankId,
Direction: directionForInjectionNominal(req.Nominal), Direction: "IN",
Nominal: req.Nominal, Nominal: req.Nominal,
Notes: req.Notes, Notes: req.Notes,
CreatedBy: actorID, CreatedBy: actorID,
@@ -186,7 +186,6 @@ func (s injectionService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
if req.Nominal != nil { if req.Nominal != nil {
updateBody["nominal"] = *req.Nominal updateBody["nominal"] = *req.Nominal
updateBody["direction"] = directionForInjectionNominal(*req.Nominal)
} }
if req.Notes != nil { if req.Notes != nil {
updateBody["notes"] = *req.Notes updateBody["notes"] = *req.Notes
@@ -211,13 +210,6 @@ func isInjectionTransaction(transactionType string) bool {
return strings.EqualFold(transactionType, string(utils.TransactionTypeInjection)) return strings.EqualFold(transactionType, string(utils.TransactionTypeInjection))
} }
func directionForInjectionNominal(nominal float64) string {
if nominal < 0 {
return "OUT"
}
return "IN"
}
func (s injectionService) generateInjectionCode(ctx context.Context) (string, error) { func (s injectionService) generateInjectionCode(ctx context.Context) (string, error) {
sequence, err := s.Repository.NextPaymentSequence(ctx) sequence, err := s.Repository.NextPaymentSequence(ctx)
if err != nil { if err != nil {
@@ -3,14 +3,14 @@ package validation
type Create struct { type Create struct {
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"` BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"` AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
Nominal float64 `json:"nominal" validate:"required_strict"` Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"` Notes string `json:"notes" validate:"required_strict,max=500"`
} }
type Update struct { type Update struct {
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"` AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
Nominal *float64 `json:"nominal,omitempty"` Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
@@ -3,7 +3,6 @@ package controller
import ( import (
"math" "math"
"strconv" "strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
@@ -24,46 +23,10 @@ func NewTransactionController(transactionService service.TransactionService) *Tr
} }
func (u *TransactionController) GetAll(c *fiber.Ctx) error { func (u *TransactionController) GetAll(c *fiber.Ctx) error {
parseOptionalUint := func(key string) (*uint, error) {
raw := strings.TrimSpace(c.Query(key, ""))
if raw == "" {
return nil, nil
}
parsed, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid "+key)
}
if parsed == 0 {
return nil, nil
}
value := uint(parsed)
return &value, nil
}
bankId, err := parseOptionalUint("bank_id")
if err != nil {
return err
}
customerId, err := parseOptionalUint("customer_id")
if err != nil {
return err
}
supplierId, err := parseOptionalUint("supplier_id")
if err != nil {
return err
}
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
TransactionType: c.Query("transaction_type", ""),
BankId: bankId,
CustomerId: customerId,
SupplierId: supplierId,
SortDate: c.Query("sort_date", ""),
StartDate: c.Query("start_date", ""),
EndDate: c.Query("end_date", ""),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -21,7 +21,7 @@ type TransactionRelationDTO struct {
Party Party `json:"party"` Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"` PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"` PaymentMethod string `json:"payment_method"`
Bank *bankDTO.BankRelationDTO `json:"bank"` Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
ExpenseAmount float64 `json:"expense_amount"` ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"` IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"` Nominal float64 `json:"nominal"`
@@ -37,7 +37,7 @@ type TransactionListDTO struct {
Party Party `json:"party"` Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"` PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"` PaymentMethod string `json:"payment_method"`
Bank *bankDTO.BankRelationDTO `json:"bank"` Bank bankDTO.BankRelationDTO `json:"bank"`
ExpenseAmount float64 `json:"expense_amount"` ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"` IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"` Nominal float64 `json:"nominal"`
@@ -151,12 +151,11 @@ func partyFromPayment(e entity.Payment) Party {
return party return party
} }
func bankFromPayment(e entity.Payment) *bankDTO.BankRelationDTO { func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 { if e.BankWarehouse.Id == 0 {
return nil return bankDTO.BankRelationDTO{}
} }
bank := bankDTO.ToBankRelationDTO(e.BankWarehouse) return bankDTO.ToBankRelationDTO(e.BankWarehouse)
return &bank
} }
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO { func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"strings" "strings"
"time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -62,19 +61,13 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
return nil, 0, err return nil, 0, err
} }
startDate, endDate, err := parseTransactionDateRange(params.StartDate, params.EndDate)
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
transactions, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transactions, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%" like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
db = db.Where( return db.Where(
`LOWER(payment_code) LIKE ? OR `LOWER(payment_code) LIKE ? OR
LOWER(COALESCE(reference_number, '')) LIKE ? OR LOWER(COALESCE(reference_number, '')) LIKE ? OR
LOWER(COALESCE(transaction_type, '')) LIKE ? OR LOWER(COALESCE(transaction_type, '')) LIKE ? OR
@@ -82,35 +75,7 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
like, like, like, like, like, like, like, like,
) )
} }
return db.Order("payment_date DESC").Order("created_at DESC")
if strings.TrimSpace(params.TransactionType) != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(strings.TrimSpace(params.TransactionType)))
}
if params.BankId != nil {
db = db.Where("bank_id = ?", *params.BankId)
}
if params.CustomerId != nil && params.SupplierId != nil {
db = db.Where(
"(party_type = ? AND party_id = ?) OR (party_type = ? AND party_id = ?)",
string(utils.PaymentPartyCustomer), *params.CustomerId,
string(utils.PaymentPartySupplier), *params.SupplierId,
)
} else if params.CustomerId != nil {
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartyCustomer), *params.CustomerId)
} else if params.SupplierId != nil {
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartySupplier), *params.SupplierId)
}
if startDate != nil {
db = db.Where("payment_date >= ?", *startDate)
}
if endDate != nil {
db = db.Where("payment_date < ?", *endDate)
}
return applyTransactionSort(db, params.SortDate)
}) })
if err != nil { if err != nil {
@@ -208,47 +173,3 @@ func (s transactionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return db.Preload("ActionUser") return db.Preload("ActionUser")
} }
} }
func parseTransactionDateRange(startDate, endDate string) (*time.Time, *time.Time, error) {
start := strings.TrimSpace(startDate)
end := strings.TrimSpace(endDate)
var startPtr *time.Time
var endPtr *time.Time
var endValue *time.Time
if start != "" {
parsed, err := utils.ParseDateString(start)
if err != nil {
return nil, nil, utils.BadRequest("start_date must use format YYYY-MM-DD")
}
startPtr = &parsed
}
if end != "" {
parsed, err := utils.ParseDateString(end)
if err != nil {
return nil, nil, utils.BadRequest("end_date must use format YYYY-MM-DD")
}
endValue = &parsed
nextDay := parsed.AddDate(0, 0, 1)
endPtr = &nextDay
}
if startPtr != nil && endValue != nil && startPtr.After(*endValue) {
return nil, nil, utils.BadRequest("start_date must be earlier than end_date")
}
return startPtr, endPtr, nil
}
func applyTransactionSort(db *gorm.DB, sortDate string) *gorm.DB {
switch strings.ToLower(strings.TrimSpace(sortDate)) {
case "created_at":
return db.Order("created_at DESC").Order("payment_date DESC")
case "payment_date":
return db.Order("payment_date DESC").Order("created_at DESC")
default:
return db.Order("payment_date DESC").Order("created_at DESC")
}
}
@@ -9,14 +9,7 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
TransactionType string `query:"transaction_type" validate:"omitempty,max=50"`
BankId *uint `query:"bank_id" validate:"omitempty,number,gt=0"`
CustomerId *uint `query:"customer_id" validate:"omitempty,number,gt=0"`
SupplierId *uint `query:"supplier_id" validate:"omitempty,number,gt=0"`
SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
} }
@@ -103,7 +103,7 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO { func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
return AdjustmentRelationDTO{ return AdjustmentRelationDTO{
Id: e.Id, Id: e.Id,
Note: "", Note: e.StockLog.Notes,
Increase: e.TotalQty, Increase: e.TotalQty,
Decrease: e.UsageQty, Decrease: e.UsageQty,
ProductWarehouseId: e.ProductWarehouseId, ProductWarehouseId: e.ProductWarehouseId,
@@ -113,17 +113,24 @@ func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO { func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO {
var createdUser *userDTO.UserRelationDTO var createdUser *userDTO.UserRelationDTO
// Get created user from StockLog
if e.StockLog != nil && e.StockLog.CreatedUser != nil { if e.StockLog != nil && e.StockLog.CreatedUser != nil {
mapped := userDTO.ToUserRelationDTO(*e.StockLog.CreatedUser) createdUser = &userDTO.UserRelationDTO{
createdUser = &mapped Id: e.StockLog.CreatedUser.Id,
IdUser: e.StockLog.CreatedUser.IdUser,
Email: e.StockLog.CreatedUser.Email,
Name: e.StockLog.CreatedUser.Name,
}
}
createdAt := time.Time{}
if e.StockLog != nil {
createdAt = e.StockLog.CreatedAt
} }
return AdjustmentListDTO{ return AdjustmentListDTO{
AdjustmentRelationDTO: ToAdjustmentRelationDTO(e), AdjustmentRelationDTO: ToAdjustmentRelationDTO(e),
CreatedUser: createdUser, CreatedUser: createdUser,
CreatedAt: e.CreatedAt, CreatedAt: createdAt,
} }
} }
@@ -9,7 +9,7 @@ import (
type AdjustmentStockRepository interface { type AdjustmentStockRepository interface {
CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error)
WithTx(tx *gorm.DB) AdjustmentStockRepository WithTx(tx *gorm.DB) AdjustmentStockRepository
DB() *gorm.DB DB() *gorm.DB
} }
@@ -30,13 +30,19 @@ func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *ent
return q.Create(data).Error return q.Create(data).Error
} }
func (r *adjustmentStockRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) { func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) {
var record entity.AdjustmentStock var record entity.AdjustmentStock
q := r.db.WithContext(ctx) err := r.db.WithContext(ctx).
if modifier != nil { Preload("StockLog").
q = modifier(q) Preload("StockLog.ProductWarehouse").
} Preload("StockLog.ProductWarehouse.Product").
err := q.First(&record, id).Error Preload("StockLog.ProductWarehouse.Warehouse").
Preload("StockLog.CreatedUser").
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Where("stock_log_id = ?", stockLogID).
First(&record).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -70,15 +70,11 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ProductWarehouse"). Preload("ProductWarehouse").
Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse"). Preload("ProductWarehouse.Warehouse").
Preload("StockLog.CreatedUser") Preload("CreatedUser")
} }
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) { func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
if err := m.EnsureStockLogAccess(c, s.StockLogsRepository.DB(), id); err != nil { adjustmentStock, err := s.AdjustmentStockRepository.GetByStockLogID(c.Context(), id)
return nil, err
}
adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found") return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
@@ -99,9 +95,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := m.EnsureWarehouseAccess(c, s.WarehouseRepo.DB(), uint(req.WarehouseID)); err != nil {
return nil, err
}
if err := common.EnsureRelations(c.Context(), if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists}, common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists}, common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
@@ -160,6 +153,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
afterQuantity := productWarehouse.Quantity
newLog := &entity.StockLog{ newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment), LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: 0, LoggableId: 0,
@@ -168,28 +162,15 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
CreatedBy: actorID, CreatedBy: actorID,
} }
stockLogs, err := s.StockLogsRepository.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
newLog.Stock = latestStockLog.Stock
} else {
newLog.Stock = 0
}
if transactionType == string(utils.StockLogTransactionTypeIncrease) { if transactionType == string(utils.StockLogTransactionTypeIncrease) {
newLog.Increase = req.Quantity afterQuantity += req.Quantity
newLog.Stock += newLog.Increase newLog.Increase = afterQuantity
} else { } else {
if productWarehouse.Quantity < req.Quantity { if productWarehouse.Quantity < req.Quantity {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Current: %.2f, Requested: %.2f", productWarehouse.Quantity, req.Quantity))
} }
newLog.Decrease = req.Quantity afterQuantity -= req.Quantity
newLog.Stock -= newLog.Decrease newLog.Decrease = afterQuantity
} }
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil { if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
@@ -198,6 +179,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
adjustmentStock := &entity.AdjustmentStock{ adjustmentStock := &entity.AdjustmentStock{
StockLogId: newLog.Id,
ProductWarehouseId: productWarehouse.Id, ProductWarehouseId: productWarehouse.Id,
} }
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
@@ -205,12 +187,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
} }
newLog.LoggableType = string(utils.StockLogTypeAdjustment)
newLog.LoggableId = adjustmentStock.Id
if err := s.StockLogsRepository.WithTx(tx).UpdateOne(ctx, newLog.Id, newLog, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to link stock log")
}
if transactionType == string(utils.StockLogTransactionTypeIncrease) { if transactionType == string(utils.StockLogTransactionTypeIncrease) {
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
@@ -240,6 +216,13 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
} }
// LEGACY: Update ProductWarehouse quantity (for backward compatibility/reporting)
productWarehouse.Quantity = afterQuantity
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return err
}
createdAdjustmentStockId = adjustmentStock.Id createdAdjustmentStockId = adjustmentStock.Id
return nil return nil
}) })
@@ -312,36 +295,29 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
var total int64 var total int64
q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}). q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}).
Preload("StockLog").
Preload("StockLog.ProductWarehouse").
Preload("StockLog.ProductWarehouse.Product").
Preload("StockLog.ProductWarehouse.Warehouse").
Preload("StockLog.CreatedUser").
Preload("ProductWarehouse"). Preload("ProductWarehouse").
Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse"). Preload("ProductWarehouse.Warehouse")
Preload("StockLog.CreatedUser")
scope, scopeErr := m.ResolveLocationScope(c, s.AdjustmentStockRepository.DB())
if scopeErr != nil {
return nil, 0, scopeErr
}
if scope.Restrict {
if len(scope.IDs) == 0 {
return []*entity.AdjustmentStock{}, 0, nil
}
q = q.Joins("JOIN product_warehouses pw_scope ON pw_scope.id = adjustment_stocks.product_warehouse_id").
Joins("JOIN warehouses w_scope ON w_scope.id = pw_scope.warehouse_id")
q = m.ApplyScopeFilter(q, scope, "w_scope.location_id")
}
if query.ProductID > 0 { if query.ProductID > 0 {
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
Where("product_warehouses.product_id = ?", query.ProductID) Where("product_warehouses.product_id = ?", query.ProductID)
} }
if query.WarehouseID > 0 { if query.WarehouseID > 0 {
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
Where("product_warehouses.warehouse_id = ?", query.WarehouseID) Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
} }
if query.TransactionType != "" { if query.TransactionType != "" {
q = q.Joins("JOIN stock_logs ON stock_logs.loggable_type = ? AND stock_logs.loggable_id = adjustment_stocks.id", "ADJUSTMENT"). q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType)) Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType))
} }
@@ -4,7 +4,6 @@ import (
"errors" "errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -37,49 +36,19 @@ func NewProductStockService(
} }
} }
func (s productStockService) withRelations(db *gorm.DB, locationScope, areaScope m.ScopeFilter) *gorm.DB { func (s productStockService) withRelations(db *gorm.DB) *gorm.DB {
warehouseScope := func(db *gorm.DB) *gorm.DB {
if locationScope.Restrict {
db = db.Where("warehouses.location_id IN ?", locationScope.IDs)
}
if areaScope.Restrict {
db = db.Where("warehouses.area_id IN ?", areaScope.IDs)
}
return db
}
productWarehouseScope := func(db *gorm.DB) *gorm.DB {
db = db.Joins("JOIN warehouses w ON w.id = product_warehouses.warehouse_id")
if locationScope.Restrict {
db = db.Where("w.location_id IN ?", locationScope.IDs)
}
if areaScope.Restrict {
db = db.Where("w.area_id IN ?", areaScope.IDs)
}
return db
}
stockLogScope := func(db *gorm.DB) *gorm.DB {
db = db.
Joins("JOIN product_warehouses pw ON pw.id = stock_logs.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
if locationScope.Restrict {
db = db.Where("w.location_id IN ?", locationScope.IDs)
}
if areaScope.Restrict {
db = db.Where("w.area_id IN ?", areaScope.IDs)
}
return db.Order("stock_logs.created_at ASC")
}
return db. return db.
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Uom"). Preload("Uom").
Preload("ProductCategory"). Preload("ProductCategory").
Preload("Flags"). Preload("Flags").
Preload("ProductWarehouses", productWarehouseScope). Preload("ProductWarehouses").
Preload("ProductWarehouses.Warehouse", warehouseScope). Preload("ProductWarehouses.Warehouse").
Preload("ProductWarehouses.Warehouse.Location"). Preload("ProductWarehouses.Warehouse.Location").
Preload("ProductWarehouses.Warehouse.Location.Area"). Preload("ProductWarehouses.Warehouse.Location.Area").
Preload("ProductWarehouses.StockLogs", stockLogScope). Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at ASC")
}).
Preload("ProductWarehouses.StockLogs.CreatedUser"). Preload("ProductWarehouses.StockLogs.CreatedUser").
Preload("ProductSuppliers"). Preload("ProductSuppliers").
Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB { Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB {
@@ -92,40 +61,17 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
return nil, 0, err return nil, 0, err
} }
locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
if locationScope.Restrict || areaScope.Restrict { db = db.Where(`EXISTS (
if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { SELECT 1
return db.Where("1 = 0") FROM product_warehouses pw
} WHERE pw.product_id = products.id
db = db.Where(`EXISTS ( AND pw.qty > 0
SELECT 1 )`)
FROM product_warehouses pw
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.product_id = products.id
AND pw.qty > 0
AND (? OR w.location_id IN ?)
AND (? OR w.area_id IN ?)
)`,
!locationScope.Restrict, locationScope.IDs,
!areaScope.Restrict, areaScope.IDs,
)
} else {
db = db.Where(`EXISTS (
SELECT 1
FROM product_warehouses pw
WHERE pw.product_id = products.id
AND pw.qty > 0
)`)
}
db = s.withRelations(db, locationScope, areaScope) db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
db = db.Where("products.name ILIKE ?", "%"+params.Search+"%") db = db.Where("products.name ILIKE ?", "%"+params.Search+"%")
} }
@@ -140,34 +86,7 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
} }
func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB()) product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations)
if err != nil {
return nil, err
}
if locationScope.Restrict || areaScope.Restrict {
if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
var count int64
if err := s.ProductRepository.DB().WithContext(c.Context()).
Table("product_warehouses pw").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("pw.product_id = ?", id).
Where("pw.qty > 0").
Where("(? OR w.location_id IN ?)", !locationScope.Restrict, locationScope.IDs).
Where("(? OR w.area_id IN ?)", !areaScope.Restrict, areaScope.IDs).
Count(&count).Error; err != nil {
return nil, err
}
if count == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
}
product, err := s.ProductRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db, locationScope, areaScope)
})
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
} }
@@ -8,7 +8,6 @@ import (
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -25,13 +24,12 @@ func NewProductWarehouseController(productWarehouseService service.ProductWareho
func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
ProductId: uint(c.QueryInt("product_id", 0)), ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""), Flags: c.Query("flags", ""),
KandangId: uint(c.QueryInt("kandang_id", 0)), KandangId: uint(c.QueryInt("kandang_id", 0)),
TransferContext: c.Query(utils.TransferContextKey, ""),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -86,24 +86,14 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWareho
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
// Cari product warehouse dengan project_flock_kandang yang masih aktif (belum closed)
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT NULL", productId, warehouseId). Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("product_id = ? AND warehouse_id = ? AND (project_flock_kandang_id IS NULL OR project_flock_kandangs.closed_at IS NULL)", productId, warehouseId).
Order("id DESC"). Order("id DESC").
Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang").
First(&productWarehouse).Error First(&productWarehouse).Error
if err == nil {
if productWarehouse.ProjectFlockKandang.ClosedAt == nil {
return &productWarehouse, nil
}
}
err = r.DB().WithContext(ctx).
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NULL", productId, warehouseId).
First(&productWarehouse).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -7,7 +7,6 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -54,19 +53,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
return nil, 0, err return nil, 0, err
} }
applyScope := true
if params.TransferContext == utils.TransferContextInventoryTransfer {
applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
}
var scope m.ScopeFilter
var err error
if applyScope {
scope, err = m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
}
if params.ProductId > 0 { if params.ProductId > 0 {
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId) isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
if err != nil { if err != nil {
@@ -104,17 +90,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
Where("w_scope.deleted_at IS NULL")
if applyScope {
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.Where("w_scope.location_id IN ?", scope.IDs)
}
}
if params.ProductId != 0 { if params.ProductId != 0 {
db = db.Where("product_id = ?", params.ProductId) db = db.Where("product_id = ?", params.ProductId)
} }
@@ -141,33 +116,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
} }
func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) { func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) {
applyScope := true productWarehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if c.Query(utils.TransferContextKey, "") == utils.TransferContextInventoryTransfer {
applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
}
var scope m.ScopeFilter
var err error
if applyScope {
scope, err = m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, err
}
}
productWarehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
Where("w_scope.deleted_at IS NULL")
if applyScope {
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.Where("w_scope.location_id IN ?", scope.IDs)
}
}
return db
})
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
} }
@@ -13,11 +13,10 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
Flags string `query:"flags" validate:"omitempty"` Flags string `query:"flags" validate:"omitempty"`
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
} }
@@ -9,12 +9,12 @@ import (
) )
type TransferRelationDTO struct { type TransferRelationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
MovementNumber string `json:"movement_number"` MovementNumber string `json:"movement_number"`
TransferReason string `json:"transfer_reason"` TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"` TransferDate string `json:"transfer_date"`
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"` SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"` DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
} }
type ProductSimpleDTO struct { type ProductSimpleDTO struct {
@@ -51,16 +51,16 @@ type TransferDetailDTO struct {
} }
type TransferDetailItemDTO struct { type TransferDetailItemDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Product ProductSimpleDTO `json:"product"` Product ProductSimpleDTO `json:"product"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
TransportPerItem *float64 `json:"transport_per_item,omitempty"` // Biaya ekspedisi per item TransportPerItem *float64 `json:"transport_per_item,omitempty"` // Biaya ekspedisi per item
ExpeditionVendor *SupplierSimpleDTO `json:"expedition_vendor,omitempty"` // Vendor ekspedisi ExpeditionVendor *SupplierSimpleDTO `json:"expedition_vendor,omitempty"` // Vendor ekspedisi
} }
type TransferDeliveryDTO struct { type TransferDeliveryDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Supplier *SupplierSimpleDTO `json:"supplier,omitempty"` Supplier SupplierSimpleDTO `json:"supplier"`
VehiclePlate string `json:"vehicle_plate"` VehiclePlate string `json:"vehicle_plate"`
DriverName string `json:"driver_name"` DriverName string `json:"driver_name"`
DocumentNumber string `json:"document_number"` DocumentNumber string `json:"document_number"`
@@ -115,6 +115,7 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
} }
if d.ExpenseNonstock != nil { if d.ExpenseNonstock != nil {
priceCopy := d.ExpenseNonstock.Price priceCopy := d.ExpenseNonstock.Price
detailDTO.TransportPerItem = &priceCopy detailDTO.TransportPerItem = &priceCopy
@@ -154,17 +155,12 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
} }
} }
var supplier *SupplierSimpleDTO deliveries = append(deliveries, TransferDeliveryDTO{
if del.Supplier != nil { Id: del.Id,
supplier = &SupplierSimpleDTO{ Supplier: SupplierSimpleDTO{
Id: del.Supplier.Id, Id: del.Supplier.Id,
Name: del.Supplier.Name, Name: del.Supplier.Name,
} },
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: supplier,
VehiclePlate: del.VehiclePlate, VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName, DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber, DocumentNumber: del.DocumentNumber,
@@ -205,6 +201,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
} }
if d.ExpenseNonstock != nil { if d.ExpenseNonstock != nil {
priceCopy := d.ExpenseNonstock.Price priceCopy := d.ExpenseNonstock.Price
detailDTO.TransportPerItem = &priceCopy detailDTO.TransportPerItem = &priceCopy
@@ -244,17 +241,12 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
} }
} }
var supplier *SupplierSimpleDTO deliveries = append(deliveries, TransferDeliveryDTO{
if del.Supplier != nil { Id: del.Id,
supplier = &SupplierSimpleDTO{ Supplier: SupplierSimpleDTO{
Id: del.Supplier.Id, Id: del.Supplier.Id,
Name: del.Supplier.Name, Name: del.Supplier.Name,
} },
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: supplier,
VehiclePlate: del.VehiclePlate, VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName, DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber, DocumentNumber: del.DocumentNumber,
@@ -94,24 +94,10 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
return nil, 0, err return nil, 0, err
} }
scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs)
}
if params.Search != "" { if params.Search != "" {
searchTerm := "%" + strings.TrimSpace(params.Search) + "%" searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id"). db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
@@ -130,28 +116,6 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
} }
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) { func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
if err != nil {
return nil, err
}
if scope.Restrict {
if len(scope.IDs) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
var count int64
if err := s.StockTransferRepo.DB().WithContext(c.Context()).
Table("stock_transfers").
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
Where("stock_transfers.id = ?", id).
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
Count(&count).Error; err != nil {
return nil, err
}
if count == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
}
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db) return s.withRelations(db)
@@ -160,8 +124,7 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id)) return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
} }
s.Log.Errorf("Failed to fetch transfer by ID %d: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id))
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data transfer")
} }
return transferPtr, nil return transferPtr, nil
@@ -179,8 +142,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
} }
s.Log.Errorf("Failed to fetch product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.SourceWarehouseID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID))
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengecek stok produk")
} }
if sourcePW.Quantity < product.ProductQty { if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
@@ -201,15 +163,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return nil, err return nil, err
} }
if destPfkID > 0 { projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) if err != nil {
if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
s.Log.Errorf("Failed to fetch project flock kandang by ID %d: %+v", destPfkID, err) }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") if projectFlockKandang.ClosedAt != nil {
} return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
if projectFlockKandang.ClosedAt != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
}
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
@@ -232,31 +191,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
for _, delivery := range req.Deliveries { for _, delivery := range req.Deliveries {
if delivery.SupplierID == 0 {
continue
}
if delivery.VehiclePlate == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Vehicle plate wajib diisi ketika supplier dipilih")
}
if delivery.DriverName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Driver name wajib diisi ketika supplier dipilih")
}
if delivery.DeliveryCost <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery cost harus lebih dari 0 ketika supplier dipilih")
}
if delivery.DeliveryCostPerItem <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery cost per item harus lebih dari 0 ketika supplier dipilih")
}
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil) supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
} }
s.Log.Errorf("Failed to fetch supplier by ID %d: %+v", delivery.SupplierID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID))
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data supplier")
} }
if supplier.Category != string(utils.SupplierCategoryBOP) { if supplier.Category != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
@@ -265,8 +205,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context()) movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
if err != nil { if err != nil {
s.Log.Errorf("Failed to generate movement number: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer")
} }
transferDate, _ := utils.ParseDateString(req.TransferDate) transferDate, _ := utils.ParseDateString(req.TransferDate)
@@ -289,7 +228,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx) stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx) stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)
productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx) productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
stocklogsRepoTx := s.StockLogsRepository.WithTx(tx)
if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil { if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil {
return err return err
@@ -307,16 +245,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
} }
s.Log.Errorf("Failed to fetch source product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.SourceWarehouseID, err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID))
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data stok gudang asal")
} }
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID( destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
) )
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch dest product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.DestinationWarehouseID, err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID))
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data stok gudang tujuan")
} }
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
ctx := c.Context() ctx := c.Context()
@@ -325,6 +261,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return err return err
} }
// Set ProjectFlockKandangId hanya jika ada kandang
var pfkID *uint var pfkID *uint
if projectFlockKandangID > 0 { if projectFlockKandangID > 0 {
pfkID = &projectFlockKandangID pfkID = &projectFlockKandangID
@@ -337,8 +274,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
ProjectFlockKandangId: pfkID, ProjectFlockKandangId: pfkID,
} }
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil { if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.DestinationWarehouseID, err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID))
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat data stok gudang tujuan")
} }
} }
@@ -364,16 +300,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
var deliveries []*entity.StockTransferDelivery var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries { for _, delivery := range req.Deliveries {
supplierId := func() *uint64 {
if delivery.SupplierID > 0 {
id := uint64(delivery.SupplierID)
return &id
}
return nil
}()
deliveries = append(deliveries, &entity.StockTransferDelivery{ deliveries = append(deliveries, &entity.StockTransferDelivery{
StockTransferId: entityTransfer.Id, StockTransferId: entityTransfer.Id,
SupplierId: supplierId, SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate, VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName, DriverName: delivery.DriverName,
ShippingCostItem: delivery.DeliveryCostPerItem, ShippingCostItem: delivery.DeliveryCostPerItem,
@@ -435,9 +364,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Files: documentFiles, Files: documentFiles,
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to upload document for delivery %d (delivery_id=%d, filename=%s): %+v", s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
deliveryIdx+1, delivery.Id, file.Filename, err) deliveryIdx+1, delivery.Id, file.Filename)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengunggah dokumen") return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", deliveryIdx+1, err))
} }
} }
} }
@@ -463,33 +392,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
"usage_qty": consumeResult.UsageQuantity, "usage_qty": consumeResult.UsageQuantity,
"pending_qty": consumeResult.PendingQuantity, "pending_qty": consumeResult.PendingQuantity,
}).Error; err != nil { }).Error; err != nil {
s.Log.Errorf("Failed to update tracking usage for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID))
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
}
stockLogDecrease := &entity.StockLog{
ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
CreatedBy: uint(actorID),
Increase: 0,
Decrease: product.ProductQty,
LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(detail.Id),
Notes: "",
}
stockLogs, err := s.StockLogsRepository.GetByProductWarehouse(c.Context(), uint(*detail.SourceProductWarehouseID), 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
stockLogDecrease.Stock -= latestStockLog.Stock - stockLogDecrease.Decrease
} else {
stockLogDecrease.Stock -= stockLogDecrease.Decrease
}
if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
} }
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
@@ -502,8 +405,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx, Tx: tx,
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to replenish stock for product_id=%d, pw_id=%d, qty=%.2f: %+v", product.ProductID, *detail.DestProductWarehouseID, product.ProductQty, err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err))
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menambah stok gudang tujuan")
} }
if err := tx.Model(&entity.StockTransferDetail{}). if err := tx.Model(&entity.StockTransferDetail{}).
@@ -511,42 +413,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"total_qty": replenishResult.AddedQuantity, "total_qty": replenishResult.AddedQuantity,
}).Error; err != nil { }).Error; err != nil {
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID))
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
}
stockLogIncrease := &entity.StockLog{
ProductWarehouseId: uint(*detail.DestProductWarehouseID),
CreatedBy: uint(actorID),
Increase: product.ProductQty,
Decrease: 0,
LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(detail.Id),
Notes: "",
}
stockLogs, err = s.StockLogsRepository.GetByProductWarehouse(c.Context(), uint(*detail.DestProductWarehouseID), 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
} else {
stockLogIncrease.Stock += stockLogIncrease.Increase
}
if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
} }
} }
if len(req.Deliveries) > 0 { if len(req.Deliveries) > 0 {
for _, delivery := range req.Deliveries { for _, delivery := range req.Deliveries {
// Skip adding to expensePayloads if SupplierID is 0 (optional)
if delivery.SupplierID == 0 {
continue
}
for _, prod := range delivery.Products { for _, prod := range delivery.Products {
detail := detailMap[uint64(prod.ProductID)] detail := detailMap[uint64(prod.ProductID)]
if detail == nil { if detail == nil {
@@ -575,10 +447,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}) })
if err != nil { if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err))
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
} }
result, err := s.GetOne(c, uint(entityTransfer.Id)) result, err := s.GetOne(c, uint(entityTransfer.Id))
@@ -588,8 +457,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if len(expensePayloads) > 0 { if len(expensePayloads) > 0 {
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil { if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
s.Log.Errorf("Failed to sync expense for transfer_id=%d, movement_number=%s: %+v", entityTransfer.Id, entityTransfer.MovementNumber, err) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi data expense. Silakan cek manual di module expense")
} }
} }
@@ -609,10 +477,10 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
} }
s.Log.Errorf("Failed to fetch warehouse by ID %d: %+v", warehouseID, err) return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID))
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
} }
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
if warehouse.KandangId == nil || *warehouse.KandangId == 0 { if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
return 0, nil return 0, nil
} }
@@ -622,8 +490,7 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId)) return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
} }
s.Log.Errorf("Failed to fetch active project flock kandang for kandang_id=%d: %+v", *warehouse.KandangId, err) return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif")
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
} }
return uint(projectFlockKandang.Id), nil return uint(projectFlockKandang.Id), nil
@@ -21,12 +21,12 @@ type TransferDeliveryProduct struct {
} }
type TransferDelivery struct { type TransferDelivery struct {
DeliveryCost float64 `json:"delivery_cost"` DeliveryCost float64 `json:"delivery_cost" validate:"required"`
DeliveryCostPerItem float64 `json:"delivery_cost_per_item"` DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"` DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"`
DriverName string `json:"driver_name"` DriverName string `json:"driver_name" validate:"required"`
VehiclePlate string `json:"vehicle_plate"` VehiclePlate string `json:"vehicle_plate" validate:"required"`
SupplierID uint `json:"supplier_id" ` SupplierID uint `json:"supplier_id" validate:"required"`
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"` Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
} }
+2 -4
View File
@@ -16,7 +16,6 @@ import (
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -33,7 +32,6 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
customerRepo := rCustomer.NewCustomerRepository(db) customerRepo := rCustomer.NewCustomerRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockLogRepo := rShared.NewStockLogRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
@@ -64,8 +62,8 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate) salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate) deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, fifoService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService) RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
@@ -15,14 +15,12 @@ type MarketingDeliveryProductRepository interface {
repository.BaseRepository[entity.MarketingDeliveryProduct] repository.BaseRepository[entity.MarketingDeliveryProduct]
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingPenjualanByCategory(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error
GetUsageQty(ctx context.Context, id uint) (float64, error) GetUsageQty(ctx context.Context, id uint) (float64, error)
ResetFifoFields(ctx context.Context, id uint) error ResetFifoFields(ctx context.Context, id uint) error
GetClosingPenjualanForAgeChickDataProduction(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
} }
type MarketingDeliveryProductRepositoryImpl struct { type MarketingDeliveryProductRepositoryImpl struct {
@@ -62,7 +60,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context
db := r.DB().WithContext(ctx). db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("marketing_delivery_products.delivery_date IS NOT NULL"). Where("marketing_delivery_products.delivery_date IS NOT NULL").
@@ -94,104 +91,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context
return deliveryProducts, nil return deliveryProducts, nil
} }
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanForAgeChickDataProduction(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("flags.name IN (?)", []string{
string(utils.FlagAyamAfkir),
string(utils.FlagAyamCulling),
string(utils.FlagPullet),
string(utils.FlagLayer),
}).
Where("marketing_delivery_products.delivery_date IS NOT NULL").
Distinct("marketing_delivery_products.*")
if projectFlockKandangID != nil {
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
}
db = db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Order("marketing_delivery_products.delivery_date DESC")
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanByCategory(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("marketing_delivery_products.delivery_date IS NOT NULL").
Distinct("marketing_delivery_products.*")
if projectFlockKandangID != nil {
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
}
if category == string(utils.ProjectFlockCategoryLaying) {
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagTelur),
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
})
} else {
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagDOC),
string(utils.FlagPullet),
string(utils.FlagLayer),
string(utils.FlagAyamAfkir),
string(utils.FlagAyamCulling),
string(utils.FlagAyamMati),
})
}
db = db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) { func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct var deliveryProducts []entity.MarketingDeliveryProduct
@@ -241,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
Where("marketing_delivery_products.delivery_date IS NOT NULL") Where("marketing_delivery_products.delivery_date IS NOT NULL")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil || filters.Search != "" || filters.MarketingType != "" { if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
} }
@@ -291,7 +190,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
} }
if filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil { if filters.AreaId > 0 || filters.LocationId > 0 {
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id") Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
@@ -302,22 +201,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
if filters.LocationId > 0 { if filters.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", filters.LocationId) db = db.Where("project_flocks.location_id = ?", filters.LocationId)
} }
if filters.AllowedAreaIDs != nil {
if len(filters.AllowedAreaIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Where("project_flocks.area_id IN ?", filters.AllowedAreaIDs)
}
}
if filters.AllowedLocationIDs != nil {
if len(filters.AllowedLocationIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Where("project_flocks.location_id IN ?", filters.AllowedLocationIDs)
}
}
} }
if filters.MarketingType != "" { if filters.MarketingType != "" {
@@ -10,10 +10,10 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware" m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
rShared "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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
@@ -34,7 +34,6 @@ type deliveryOrdersService struct {
MarketingRepo marketingRepo.MarketingRepository MarketingRepo marketingRepo.MarketingRepository
MarketingProductRepo marketingRepo.MarketingProductRepository MarketingProductRepo marketingRepo.MarketingProductRepository
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
StockLogRepo rShared.StockLogRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService FifoSvc commonSvc.FifoService
} }
@@ -43,7 +42,6 @@ func NewDeliveryOrdersService(
marketingRepo marketingRepo.MarketingRepository, marketingRepo marketingRepo.MarketingRepository,
marketingProductRepo marketingRepo.MarketingProductRepository, marketingProductRepo marketingRepo.MarketingProductRepository,
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
stockLogRepo rShared.StockLogRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService, fifoSvc commonSvc.FifoService,
validate *validator.Validate, validate *validator.Validate,
@@ -53,7 +51,6 @@ func NewDeliveryOrdersService(
MarketingRepo: marketingRepo, MarketingRepo: marketingRepo,
MarketingProductRepo: marketingProductRepo, MarketingProductRepo: marketingProductRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
StockLogRepo: stockLogRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc, FifoSvc: fifoSvc,
} }
@@ -70,10 +67,6 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB {
} }
func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil {
return nil, err
}
marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
@@ -98,11 +91,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
return nil, 0, err return nil, 0, err
} }
scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
@@ -114,23 +102,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.ProductWarehouse.Warehouse").
Preload("Products.DeliveryProduct") Preload("Products.DeliveryProduct")
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.Where(
`EXISTS (
SELECT 1
FROM marketing_products mp
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE mp.marketing_id = marketings.id
AND w.location_id IN ?
)`,
scope.IDs,
)
}
if params.MarketingId != 0 { if params.MarketingId != 0 {
return db.Where("id = ?", params.MarketingId) return db.Where("id = ?", params.MarketingId)
} }
@@ -159,9 +130,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
} }
func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err
}
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -201,10 +169,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
return nil, err return nil, err
} }
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), req.MarketingId); err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists},
); err != nil { ); err != nil {
@@ -283,6 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate itemDeliveryDate = &parsedDate
} }
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false isPakanOrOVK := false
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
@@ -293,13 +258,14 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
} }
} }
// Hitung total_weight dan total_price berdasarkan flag
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
var totalPrice float64 var totalPrice float64
if isPakanOrOVK { if isPakanOrOVK {
// PAKAN atau OVK: qty × unit_price
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
} else { } else {
// Produk lain: total_weight × unit_price
totalPrice = totalWeight * requestedProduct.UnitPrice totalPrice = totalWeight * requestedProduct.UnitPrice
} }
@@ -313,7 +279,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
if requestedProduct.Qty > 0 { if requestedProduct.Qty > 0 {
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil { if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
return err return err
} }
} }
@@ -355,22 +321,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return nil, err return nil, err
} }
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
if err != nil {
return nil, err
}
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
@@ -409,7 +366,6 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
} }
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
var itemDeliveryDate *time.Time var itemDeliveryDate *time.Time
if requestedProduct.DeliveryDate != "" { if requestedProduct.DeliveryDate != "" {
parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
@@ -421,8 +377,11 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
itemDeliveryDate = deliveryProduct.DeliveryDate itemDeliveryDate = deliveryProduct.DeliveryDate
} }
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false isPakanOrOVK := false
if foundMarketingProduct.ProductWarehouse.Id != 0 && foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true isPakanOrOVK = true
@@ -431,13 +390,14 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
} }
} }
// Hitung total_weight dan total_price berdasarkan flag
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
var totalPrice float64 var totalPrice float64
if isPakanOrOVK { if isPakanOrOVK {
// PAKAN atau OVK: qty × unit_price
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
} else { } else {
// Produk lain: total_weight × unit_price
totalPrice = totalWeight * requestedProduct.UnitPrice totalPrice = totalWeight * requestedProduct.UnitPrice
} }
@@ -452,13 +412,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
if requestedProduct.Qty != oldRequestedQty { if requestedProduct.Qty != oldRequestedQty {
if oldRequestedQty > 0 { if oldRequestedQty > 0 {
if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, actorID); err != nil { if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct); err != nil {
return err return err
} }
} }
if requestedProduct.Qty > 0 { if requestedProduct.Qty > 0 {
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil { if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
return err return err
} }
} }
@@ -483,7 +443,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return s.getMarketingWithDeliveries(c, id) return s.getMarketingWithDeliveries(c, id)
} }
func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error { func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64) error {
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
} }
@@ -501,43 +461,39 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
Tx: tx, Tx: tx,
}) })
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err2 != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
}
if pw == nil || pw.Quantity < requestedQty {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 {
if pw != nil {
return pw.Quantity
} else {
return 0
}
}(), requestedQty))
}
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
}
return nil
} }
if actorID > 0 && result.UsageQuantity > 0 { if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
decreaseLog := &entity.StockLog{ return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
Decrease: result.UsageQuantity,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease
} else {
decreaseLog.Stock -= decreaseLog.Decrease
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
} }
return nil return nil
} }
func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, actorID uint) error { func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct) error {
if deliveryProduct == nil || deliveryProduct.Id == 0 { if deliveryProduct == nil || deliveryProduct.Id == 0 {
return nil return nil
} }
@@ -568,28 +524,5 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
return err return err
} }
if actorID > 0 && currentUsage > 0 {
increaseLog := &entity.StockLog{
Increase: currentUsage,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase
} else {
increaseLog.Stock += increaseLog.Increase
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
}
return nil return nil
} }
@@ -19,7 +19,6 @@ import (
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -42,12 +41,11 @@ type salesOrdersService struct {
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
UserRepo userRepo.UserRepository UserRepo userRepo.UserRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
} }
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository, func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService { projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{ return &salesOrdersService{
Log: utils.Log, Log: utils.Log,
@@ -57,7 +55,6 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
UserRepo: userRepo, UserRepo: userRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
} }
@@ -73,10 +70,6 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
} }
func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err
}
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
@@ -115,9 +108,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
} }
for _, item := range req.MarketingProducts { for _, item := range req.MarketingProducts {
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
); err != nil { ); err != nil {
@@ -207,10 +197,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return nil, err return nil, err
} }
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -234,9 +220,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if len(req.MarketingProducts) > 0 { if len(req.MarketingProducts) > 0 {
for _, item := range req.MarketingProducts { for _, item := range req.MarketingProducts {
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
); err != nil { ); err != nil {
@@ -328,40 +311,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
} }
// Hitung total_weight dan total_price berdasarkan flag
totalWeight := rp.Qty * rp.AvgWeight totalWeight := rp.Qty * rp.AvgWeight
var totalPrice float64 var totalPrice float64
if isPakanOrOVK { if isPakanOrOVK {
// PAKAN atau OVK: qty × unit_price
totalPrice = rp.Qty * rp.UnitPrice totalPrice = rp.Qty * rp.UnitPrice
} else { } else {
// Produk lain: total_weight × unit_price
totalPrice = totalWeight * rp.UnitPrice totalPrice = totalWeight * rp.UnitPrice
} }
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
}
if err == nil && deliveryProduct.Id != 0 {
oldQty := old.Qty
newQty := rp.Qty
qtyDiff := newQty - oldQty
if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 {
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: rp.ProductWarehouseId,
Quantity: qtyDiff,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err))
}
}
}
updateBody := map[string]any{ updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId, "product_warehouse_id": rp.ProductWarehouseId,
"qty": rp.Qty, "qty": rp.Qty,
@@ -374,20 +334,25 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
} }
if deliveryProduct.Id == 0 { if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
mdp := &entity.MarketingDeliveryProduct{ if errors.Is(err, gorm.ErrRecordNotFound) {
MarketingProductId: old.Id,
UnitPrice: 0, mdp := &entity.MarketingDeliveryProduct{
TotalWeight: 0, MarketingProductId: old.Id,
AvgWeight: 0, UnitPrice: 0,
TotalPrice: 0, TotalWeight: 0,
DeliveryDate: nil, AvgWeight: 0,
VehicleNumber: rp.VehicleNumber, TotalPrice: 0,
UsageQty: 0, DeliveryDate: nil,
PendingQty: 0, VehicleNumber: rp.VehicleNumber,
} UsageQty: 0,
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { PendingQty: 0,
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") }
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
}
} else {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
} }
} }
} else { } else {
@@ -404,18 +369,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
} }
if err == nil && deliveryProduct.Id != 0 { if err == nil {
if deliveryProduct.DeliveryDate != nil { if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
}
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err))
} }
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
@@ -460,10 +417,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return err
}
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -491,19 +444,6 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
if len(marketing.Products) > 0 { if len(marketing.Products) > 0 {
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
if err == nil && len(deliveryProducts) > 0 {
for _, dp := range deliveryProducts {
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: dp.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err))
}
}
}
for _, product := range marketing.Products { for _, product := range marketing.Products {
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("marketing_product_id = ?", product.Id).Unscoped() return db.Where("marketing_product_id = ?", product.Id).Unscoped()
@@ -541,12 +481,6 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
return nil, err return nil, err
} }
for _, id := range req.ApprovableIds {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err
}
}
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -47,22 +47,16 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar
return nil, 0, err return nil, 0, err
} }
var scopeErr error
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db, scopeErr = m.ApplyAreaScope(c, db, "id")
if params.Search != "" { if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%") return db.Where("name ILIKE ?", "%"+params.Search+"%")
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
s.Log.Errorf("Failed to get areas: %+v", err) s.Log.Errorf("Failed to get areas: %+v", err)
return nil, 0, err return nil, 0, err
@@ -71,16 +65,7 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar
} }
func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) { func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) {
var scopeErr error area, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
area, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = m.ApplyAreaScope(c, db, "id")
return db
})
if scopeErr != nil {
return nil, scopeErr
}
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Area not found") return nil, fiber.NewError(fiber.StatusNotFound, "Area not found")
} }
@@ -156,22 +156,6 @@ func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
updateBody["type"] = typ updateBody["type"] = typ
} }
if req.Address != nil {
updateBody["address"] = *req.Address
}
if req.Phone != nil {
updateBody["phone"] = *req.Phone
}
if req.Email != nil {
updateBody["email"] = *req.Email
}
if req.AccountNumber != nil {
updateBody["account_number"] = *req.AccountNumber
}
if len(updateBody) == 0 { if len(updateBody) == 0 {
return s.GetOne(c, id) return s.GetOne(c, id)
} }
@@ -5,7 +5,6 @@ import (
"strings" "strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -44,61 +43,6 @@ func (s employeesService) withRelations(db *gorm.DB) *gorm.DB {
Where("employees.deleted_at IS NULL") Where("employees.deleted_at IS NULL")
} }
func (s employeesService) ensureEmployeeAccess(c *fiber.Ctx, employeeID uint) error {
if employeeID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id")
}
db := s.Repository.DB().WithContext(c.Context()).
Table("employees e").
Joins("JOIN employee_kandangs ek ON ek.employee_id = e.id").
Joins("JOIN kandangs k ON k.id = ek.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("e.id = ?", employeeID).
Where("e.deleted_at IS NULL")
scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if err != nil {
return err
}
var count int64
if err := scopedDB.Count(&count).Error; err != nil {
return err
}
if count == 0 {
return fiber.NewError(fiber.StatusNotFound, "Employees not found")
}
return nil
}
func (s employeesService) ensureKandangIDsAccess(c *fiber.Ctx, kandangIDs []uint) error {
if len(kandangIDs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
}
db := s.Repository.DB().WithContext(c.Context()).
Table("kandangs k").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("k.id IN ?", kandangIDs)
scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if err != nil {
return err
}
var count int64
if err := scopedDB.Count(&count).Error; err != nil {
return err
}
if count != int64(len(kandangIDs)) {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
return nil
}
func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
@@ -108,29 +52,17 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
Joins("JOIN kandangs k ON k.id = ek.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id")
var scopeErr error
db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "a.id")
if scopeErr != nil {
return db.Where("1 = 0")
}
if params.Search != "" { if params.Search != "" {
db = db.Where("employees.name ILIKE ?", "%"+params.Search+"%") db = db.Where("employees.name ILIKE ?", "%"+params.Search+"%")
} }
if params.KandangId != nil { if params.KandangId != nil {
db = db.Where("ek.kandang_id = ?", *params.KandangId) db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
Where("ek.kandang_id = ?", *params.KandangId)
} }
if params.IsActive != nil { if params.IsActive != nil {
db = db.Where("employees.is_active = ?", *params.IsActive) db = db.Where("employees.is_active = ?", *params.IsActive)
} }
return db. return db.Order("employees.created_at DESC").Order("employees.updated_at DESC")
Select("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at").
Group("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at").
Order("employees.created_at DESC").
Order("employees.updated_at DESC")
}) })
if err != nil { if err != nil {
@@ -141,9 +73,6 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
} }
func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) { func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) {
if err := s.ensureEmployeeAccess(c, id); err != nil {
return nil, err
}
employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations) employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found") return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found")
@@ -169,9 +98,6 @@ func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
} }
if err := s.ensureKandangIDsAccess(c, kandangIDs); err != nil {
return nil, err
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ?", strings.ToLower(name)) return db.Where("LOWER(name) = ?", strings.ToLower(name))
@@ -221,9 +147,6 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := s.ensureEmployeeAccess(c, id); err != nil {
return nil, err
}
updateBody := make(map[string]any) updateBody := make(map[string]any)
var ( var (
@@ -258,9 +181,6 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if len(ids) == 0 { if len(ids) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
} }
if err := s.ensureKandangIDsAccess(c, ids); err != nil {
return nil, err
}
kandangIDs = ids kandangIDs = ids
needKandangUpdate = true needKandangUpdate = true
@@ -314,9 +234,6 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
func (s employeesService) DeleteOne(c *fiber.Ctx, id uint) error { func (s employeesService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.ensureEmployeeAccess(c, id); err != nil {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Employees not found") return fiber.NewError(fiber.StatusNotFound, "Employees not found")
@@ -49,13 +49,10 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
return nil, 0, err return nil, 0, err
} }
var scopeErr error
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db, scopeErr = m.ApplyLocationScope(c, db, "kandangs.location_id")
if params.Search != "" { if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%") return db.Where("name ILIKE ?", "%"+params.Search+"%")
} }
@@ -68,9 +65,6 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
s.Log.Errorf("Failed to get kandangs: %+v", err) s.Log.Errorf("Failed to get kandangs: %+v", err)
return nil, 0, err return nil, 0, err
@@ -79,16 +73,7 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
} }
func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) { func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) {
var scopeErr error kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
kandang, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = m.ApplyLocationScope(c, db, "kandangs.location_id")
return db
})
if scopeErr != nil {
return nil, scopeErr
}
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
} }
@@ -103,9 +88,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureLocationAccess(c, s.Repository.DB(), req.LocationId); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil { if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check kandang name: %+v", err) s.Log.Errorf("Failed to check kandang name: %+v", err)
@@ -180,14 +162,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureKandangAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
if req.LocationId != nil {
if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil {
return nil, err
}
}
existing, err := s.Repository.GetByID(c.Context(), id, nil) existing, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -279,10 +253,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error { func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := m.EnsureKandangAccess(c, s.Repository.DB(), id); err != nil {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found") return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
@@ -47,13 +47,10 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
return nil, 0, err return nil, 0, err
} }
var scopeErr error
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
locations, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { locations, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db, scopeErr = m.ApplyLocationScope(c, db, "locations.id")
if params.Search != "" { if params.Search != "" {
db = db.Where("name ILIKE ?", "%"+params.Search+"%") db = db.Where("name ILIKE ?", "%"+params.Search+"%")
} }
@@ -63,9 +60,6 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
s.Log.Errorf("Failed to get locations: %+v", err) s.Log.Errorf("Failed to get locations: %+v", err)
return nil, 0, err return nil, 0, err
@@ -74,16 +68,7 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
} }
func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) { func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) {
var scopeErr error location, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
location, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = m.ApplyLocationScope(c, db, "locations.id")
return db
})
if scopeErr != nil {
return nil, scopeErr
}
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
} }
@@ -3,13 +3,11 @@ package controller
import ( import (
"math" "math"
"strconv" "strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -25,20 +23,12 @@ func NewWarehouseController(warehouseService service.WarehouseService) *Warehous
} }
func (u *WarehouseController) GetAll(c *fiber.Ctx) error { func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
excludeIDs, err := parseCommaSeparatedUint(c.Query("exclude_id", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
AreaId: c.QueryInt("area_id", 0), AreaId: c.QueryInt("area_id", 0),
LocationId: c.QueryInt("location_id", 0),
ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false), ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false),
TransferContext: c.Query(utils.TransferContextKey, ""),
ExcludeIDs: excludeIDs,
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -65,28 +55,6 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
}) })
} }
func parseCommaSeparatedUint(raw string) ([]uint, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
parts := strings.Split(raw, ",")
out := make([]uint, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
value, err := strconv.ParseUint(part, 10, 64)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid exclude_id")
}
out = append(out, uint(value))
}
return out, nil
}
func (u *WarehouseController) GetOne(c *fiber.Ctx) error { func (u *WarehouseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
@@ -48,29 +48,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
return nil, 0, err return nil, 0, err
} }
var scopeErr error
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
applyScope := true
if params.TransferContext == utils.TransferContextInventoryTransfer {
applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
}
if applyScope {
db, scopeErr = m.ApplyLocationAreaScope(c, db, "warehouses.location_id", "warehouses.area_id")
}
if params.Search != "" { if params.Search != "" {
db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%")
} }
if params.AreaId != 0 { if params.AreaId != 0 {
db = db.Where("area_id = ?", params.AreaId) db = db.Where("area_id = ?", params.AreaId)
} }
if params.LocationId != 0 {
db = db.Where("location_id = ?", params.LocationId)
}
if params.ActiveProjectFlockOnly { if params.ActiveProjectFlockOnly {
db = db.Where(` db = db.Where(`
EXISTS ( EXISTS (
@@ -88,15 +75,9 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
) )
`, "Aktif") `, "Aktif")
} }
if len(params.ExcludeIDs) > 0 {
db = db.Where("warehouses.id NOT IN ?", params.ExcludeIDs)
}
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
s.Log.Errorf("Failed to get warehouses: %+v", err) s.Log.Errorf("Failed to get warehouses: %+v", err)
return nil, 0, err return nil, 0, err
@@ -105,16 +86,7 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
} }
func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) {
var scopeErr error warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = m.ApplyLocationAreaScope(c, db, "warehouses.location_id", "warehouses.area_id")
return db
})
if scopeErr != nil {
return nil, scopeErr
}
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
} }
@@ -145,19 +117,6 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil { if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureAreaAccess(c, s.Repository.DB(), req.AreaId); err != nil {
return nil, err
}
if req.LocationId != nil {
if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil {
return nil, err
}
}
if req.KandangId != nil {
if err := m.EnsureKandangAccess(c, s.Repository.DB(), *req.KandangId); err != nil {
return nil, err
}
}
//? Check relation area, location, and kandang //? Check relation area, location, and kandang
if err := common.EnsureRelations(c.Context(), if err := common.EnsureRelations(c.Context(),
@@ -196,21 +155,6 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if req.AreaId != nil {
if err := m.EnsureAreaAccess(c, s.Repository.DB(), *req.AreaId); err != nil {
return nil, err
}
}
if req.LocationId != nil {
if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil {
return nil, err
}
}
if req.KandangId != nil {
if err := m.EnsureKandangAccess(c, s.Repository.DB(), *req.KandangId); err != nil {
return nil, err
}
}
existing, err := s.GetOne(c, id) existing, err := s.GetOne(c, id)
if err != nil { if err != nil {
@@ -301,10 +245,6 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error { func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := m.EnsureWarehouseAccess(c, s.Repository.DB(), id); err != nil {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
@@ -21,8 +21,5 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"` AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
ActiveProjectFlockOnly bool `query:"active_project_flock"` ActiveProjectFlockOnly bool `query:"active_project_flock"`
ExcludeIDs []uint `query:"-" validate:"omitempty,dive,gt=0"`
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
} }
@@ -645,18 +645,6 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID, CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d", chickin.Id), Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, chickin.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
decreaseLog.Stock = latestStockLog.Stock
decreaseLog.Stock -= decreaseLog.Decrease
} else {
decreaseLog.Stock -= decreaseLog.Decrease
}
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil) s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
} }
@@ -713,18 +701,6 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID, CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id), Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, chickin.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
increaseLog.Stock = latestStockLog.Stock
increaseLog.Stock += increaseLog.Increase
} else {
increaseLog.Stock += increaseLog.Increase
}
s.StockLogRepo.CreateOne(ctx, increaseLog, nil) s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
} }
@@ -88,14 +88,9 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer
return nil, 0, err return nil, 0, err
} }
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
projectFlockKandangs, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err)
@@ -111,28 +106,6 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer
} }
func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) {
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, nil, nil, err
}
if scope.Restrict {
if len(scope.IDs) == 0 {
return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
var count int64
if err := s.Repository.DB().WithContext(c.Context()).
Table("project_flock_kandangs").
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
Where("project_flock_kandangs.id = ?", id).
Where("project_flocks.location_id IN ?", scope.IDs).
Count(&count).Error; err != nil {
return nil, nil, nil, err
}
if count == 0 {
return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
}
projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) projectFlockKandang, err := s.Repository.GetByID(c.Context(), id)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
@@ -12,7 +12,6 @@ import (
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" 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" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -77,7 +76,6 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
query.Category = category query.Category = category
} }
query.TransferContext = c.Query(utils.TransferContextKey, "")
if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" { if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" {
ids, err := parseUintList(kandangRaw) ids, err := parseUintList(kandangRaw)
@@ -289,11 +287,6 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
} else { } else {
dtoResult.AvailableQuantity = population dtoResult.AvailableQuantity = population
} }
if chickinDate, err := u.ProjectflockService.GetProjectFlockKandangChickinDate(c, result.Id); err != nil {
return err
} else if chickinDate != nil {
dtoResult.ChickInDate = chickinDate
}
if warehouse, werr := u.ProjectflockService.GetWarehouseByKandangID(c, result.KandangId); werr != nil { if warehouse, werr := u.ProjectflockService.GetWarehouseByKandangID(c, result.KandangId); werr != nil {
return werr return werr
} else if warehouse != nil { } else if warehouse != nil {
@@ -1,8 +1,6 @@
package dto package dto
import ( import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
@@ -40,7 +38,6 @@ type ProjectFlockKandangDTO struct {
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
AvailableQuantity float64 `json:"available_quantity"` AvailableQuantity float64 `json:"available_quantity"`
Population *float64 `json:"population,omitempty"` Population *float64 `json:"population,omitempty"`
ChickInDate *time.Time `json:"chick_in_date,omitempty"`
} }
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
@@ -31,7 +31,6 @@ func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, p
Where("project_flock_id = ?", projectFlockID). Where("project_flock_id = ?", projectFlockID).
Preload("Nonstock"). Preload("Nonstock").
Preload("Nonstock.Uom"). Preload("Nonstock.Uom").
Preload("Nonstock.Flags").
Find(&budgets).Error Find(&budgets).Error
return budgets, err return budgets, err
} }
@@ -14,7 +14,6 @@ import (
type ProjectflockRepository interface { type ProjectflockRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error)
WithDefaultRelations() func(*gorm.DB) *gorm.DB WithDefaultRelations() func(*gorm.DB) *gorm.DB
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error)
@@ -49,19 +48,6 @@ func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offs
}) })
} }
func (r *ProjectflockRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.applyQueryFilters(r.WithDefaultRelations()(db), params)
if restrict {
if len(locationIDs) == 0 {
return db.Where("1 = 0")
}
db = db.Where("project_flocks.location_id IN ?", locationIDs)
}
return db
})
}
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db. return db.
@@ -20,7 +20,6 @@ type ProjectFlockKandangRepository interface {
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error)
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
@@ -86,7 +85,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont
var records []entity.ProjectFlockKandang var records []entity.ProjectFlockKandang
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Where("project_flock_id = ?", projectFlockID). Where("project_flock_id = ?", projectFlockID).
Preload("Kandang").
Find(&records).Error; err != nil { Find(&records).Error; err != nil {
return nil, err return nil, err
} }
@@ -198,104 +196,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
return records, total, nil return records, total, nil
} }
func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) {
var records []entity.ProjectFlockKandang
var total int64
query, ok := params.(*validation.Query)
q := r.db.WithContext(ctx).
Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\"").
Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"").
Preload("ProjectFlock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse")
if restrict {
if len(locationIDs) == 0 {
return []entity.ProjectFlockKandang{}, 0, nil
}
q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs)
}
if ok && query != nil && query.StepName != "" {
q = q.Where(`
EXISTS (
SELECT 1 FROM "approvals"
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
AND "approvals"."approvable_type" = ?
AND LOWER("approvals"."step_name") = LOWER(?)
AND "approvals"."id" IN (
SELECT "approvals"."id" FROM "approvals"
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
AND "approvals"."approvable_type" = ?
ORDER BY "approvals"."id" DESC
LIMIT 1
)
)
`, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS")
}
if ok && query != nil {
if query.Search != "" {
escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search)
q = q.Where(
r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%").
Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"),
)
}
if query.ProjectFlockId > 0 {
q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId)
}
if query.KandangId > 0 {
q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId)
}
if query.Category != "" {
q = q.Where("\"project_flocks\".\"category\" = ?", query.Category)
}
if query.AreaId > 0 {
q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId)
}
}
if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil {
return nil, 0, err
}
sortBy := "\"project_flock_kandangs\".\"created_at\" DESC"
if ok && query != nil && query.SortBy != "" {
sortOrder := "DESC"
if query.SortOrder == "ASC" {
sortOrder = "ASC"
}
switch query.SortBy {
case "created_at":
sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder
case "period":
sortBy = "\"project_flocks\".\"period\" " + sortOrder
}
}
if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: tx} return &projectFlockKandangRepositoryImpl{db: tx}
} }
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -43,7 +42,6 @@ type ProjectflockService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error)
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
@@ -117,20 +115,9 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
return nil, 0, nil, err return nil, 0, nil, err
} }
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, nil, err
}
if params.TransferContext == utils.TransferContextTransferToLaying {
if m.HasPermission(c, m.P_TransferToLaying_CreateOne) || m.HasPermission(c, m.P_TransferToLaying_UpdateOne) {
scope.Restrict = false
scope.IDs = nil
}
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectflocks: %+v", err) s.Log.Errorf("Failed to get projectflocks: %+v", err)
@@ -204,16 +191,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr
} }
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) {
scope, err := m.ResolveLocationScope(c, s.Repository.DB()) projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if err != nil {
return nil, nil, err
}
projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.Repository.WithDefaultRelations()(db)
db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id")
return db
})
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
@@ -481,35 +459,6 @@ func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, pr
return total, nil return total, nil
} }
func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error) {
if s.PopulationRepo == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
}
if projectFlockKandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}
populations, err := s.PopulationRepo.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch populations for project flock kandang %d: %+v", projectFlockKandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang chick in date")
}
var earliest *time.Time
for _, pop := range populations {
if pop.ProjectChickin == nil || pop.ProjectChickin.ChickInDate.IsZero() {
continue
}
chickinDate := pop.ProjectChickin.ChickInDate
if earliest == nil || chickinDate.Before(*earliest) {
copy := chickinDate
earliest = &copy
}
}
return earliest, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) { func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
idStr = strings.TrimSpace(idStr) idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr) projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
@@ -12,17 +12,16 @@ type Create struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
SortBy string `query:"sort_by" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
Period int `query:"period" validate:"omitempty,number,gt=0"` Period int `query:"period" validate:"omitempty,number,gt=0"`
Category string `query:"category" validate:"omitempty"` Category string `query:"category" validate:"omitempty"`
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=transfer_to_laying"`
} }
type Approve struct { type Approve struct {
@@ -3,8 +3,6 @@ package controller
import ( import (
"math" "math"
"strconv" "strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
@@ -84,16 +82,7 @@ func (u *RecordingController) GetNextDay(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
} }
recordTime := time.Now().UTC() nextDay, err := u.RecordingService.GetNextDay(c, uint(projectFlockID))
if recordDate := strings.TrimSpace(c.Query("record_date")); recordDate != "" {
parsed, err := time.Parse("2006-01-02", recordDate)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
}
recordTime = parsed.UTC()
}
nextDay, err := u.RecordingService.GetNextDay(c, uint(projectFlockID), recordTime)
if err != nil { if err != nil {
return err return err
} }
@@ -47,10 +47,8 @@ type RecordingRepository interface {
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (totalDepletion float64, err error)
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
@@ -475,17 +473,6 @@ func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.
return result, err return result, err
} }
func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var result float64
err := r.DB().WithContext(ctx).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangID).
Scan(&result).Error
return result, err
}
func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
// Body-weight tracking is removed; keep stub for report compatibility. // Body-weight tracking is removed; keep stub for report compatibility.
return 0, nil return 0, nil
@@ -622,23 +609,3 @@ func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectF
return result.TotalWeight, err return result.TotalWeight, err
} }
func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
if projectFlockKandangID == 0 {
return 0, nil
}
var result struct {
TotalWeight float64
}
err := r.DB().WithContext(ctx).
Table("project_flock_kandang_uniformity").
Select("COALESCE((mean_up / 1.10) * chick_qty_of_weight / 1000, 0) as total_weight").
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Order("id DESC").
Limit(1).
Scan(&result).Error
return result.TotalWeight, err
}
@@ -4,10 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -17,12 +13,15 @@ import (
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services" 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" 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" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording" recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording"
"math"
"strings"
"time"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -33,7 +32,7 @@ import (
type RecordingService interface { type RecordingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Recording, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.Recording, error)
GetNextDay(ctx *fiber.Ctx, projectFlockKandangId uint, recordTime time.Time) (int, error) GetNextDay(ctx *fiber.Ctx, projectFlockKandangId uint) (int, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
@@ -109,13 +108,6 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
return nil, 0, err return nil, 0, err
} }
var scopeErr error
if params.ProjectFlockKandangId != 0 {
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, params.ProjectFlockKandangId); err != nil {
return nil, 0, err
}
}
limit := params.Limit limit := params.Limit
if limit == 0 { if limit == 0 {
limit = 10 limit = 10
@@ -128,10 +120,6 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB { recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB {
db = s.Repository.WithRelations(db) db = s.Repository.WithRelations(db)
db = db.
Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id")
db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id")
if params.ProjectFlockKandangId != 0 { if params.ProjectFlockKandangId != 0 {
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
} }
@@ -139,9 +127,6 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC") return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
}) })
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
s.Log.Errorf("Failed to get recordings: %+v", err) s.Log.Errorf("Failed to get recordings: %+v", err)
return nil, 0, err return nil, 0, err
@@ -156,10 +141,6 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
} }
func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) { func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.Repository.WithRelations(db) return s.Repository.WithRelations(db)
}) })
@@ -179,18 +160,12 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
return recording, nil return recording, nil
} }
func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint, recordTime time.Time) (int, error) { func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) (int, error) {
if projectFlockKandangId == 0 { if projectFlockKandangId == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
} }
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, projectFlockKandangId); err != nil {
return 0, err
}
if recordTime.IsZero() { day, err := s.computeRecordingDay(c.Context(), projectFlockKandangId, time.Now().UTC())
recordTime = time.Now().UTC()
}
day, err := s.computeRecordingDay(c.Context(), projectFlockKandangId, recordTime)
if err != nil { if err != nil {
s.Log.Errorf("Failed to compute 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 0, err
@@ -203,9 +178,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil {
return nil, err
}
ctx := c.Context() ctx := c.Context()
recordTime := time.Now().UTC() recordTime := time.Now().UTC()
@@ -312,7 +284,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
depletionDesired := resetDepletionQuantitiesForFIFO(mappedDepletions, s.FifoSvc != nil)
if s.FifoSvc != nil && len(mappedDepletions) > 0 { if s.FifoSvc != nil && len(mappedDepletions) > 0 {
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId) sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
if err != nil { if err != nil {
@@ -327,7 +298,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err return err
} }
if s.FifoSvc != nil { if s.FifoSvc != nil {
applyDepletionDesiredQuantities(mappedDepletions, depletionDesired, true)
note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id) note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id)
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil { if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil {
return err return err
@@ -377,9 +347,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
return s.GetOne(c, id) return s.GetOne(c, id)
@@ -495,7 +462,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
depletionDesired := resetDepletionQuantitiesForFIFO(mappedDepletions, s.FifoSvc != nil)
if s.FifoSvc != nil && len(mappedDepletions) > 0 { if s.FifoSvc != nil && len(mappedDepletions) > 0 {
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId) sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
if err != nil { if err != nil {
@@ -511,7 +477,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
if s.FifoSvc != nil { if s.FifoSvc != nil {
applyDepletionDesiredQuantities(mappedDepletions, depletionDesired, true)
note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id)
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil { if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil {
return err return err
@@ -536,17 +501,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if egg.ProductWarehouseId == 0 || egg.Qty <= 0 { if egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
continue continue
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, egg.ProductWarehouseId, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
latestStockLog := &entity.StockLog{}
if len(stockLogs) > 0 {
latestStockLog = stockLogs[0]
} else {
latestStockLog.Stock = 0
}
logs = append(logs, &entity.StockLog{ logs = append(logs, &entity.StockLog{
ProductWarehouseId: egg.ProductWarehouseId, ProductWarehouseId: egg.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
@@ -554,7 +508,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
LoggableType: string(utils.StockLogTypeRecording), LoggableType: string(utils.StockLogTypeRecording),
LoggableId: recordingEntity.Id, LoggableId: recordingEntity.Id,
Notes: note, Notes: note,
Stock: latestStockLog.Stock - float64(egg.Qty),
}) })
} }
if len(logs) > 0 { if len(logs) > 0 {
@@ -673,11 +626,6 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
for _, id := range req.ApprovableIds {
if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
}
actionValue := strings.ToUpper(strings.TrimSpace(req.Action)) actionValue := strings.ToUpper(strings.TrimSpace(req.Action))
var action entity.ApprovalAction var action entity.ApprovalAction
@@ -755,15 +703,7 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
} }
func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
return err
}
ctx := c.Context() ctx := c.Context()
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return err
}
note := fmt.Sprintf("Recording-Delete#%d", id)
return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
oldDepletions, err := s.Repository.ListDepletions(tx, id) oldDepletions, err := s.Repository.ListDepletions(tx, id)
@@ -772,7 +712,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err return err
} }
if s.FifoSvc != nil { if s.FifoSvc != nil {
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions, note, actorID); err != nil { if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions, "", 0); err != nil {
return err return err
} }
} }
@@ -794,7 +734,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err return err
} }
if err := s.releaseRecordingStocks(ctx, tx, oldStocks, note, actorID); err != nil { if err := s.releaseRecordingStocks(ctx, tx, oldStocks, "", 0); err != nil {
return err return err
} }
@@ -802,10 +742,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err return err
} }
if err := s.logRecordingEggRollback(ctx, tx, oldEggs, note, actorID); err != nil {
return err
}
if err := s.Repository.WithTx(tx).DeleteOne(ctx, id); err != nil { if err := s.Repository.WithTx(tx).DeleteOne(ctx, id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Recording not found") return fiber.NewError(fiber.StatusNotFound, "Recording not found")
@@ -818,40 +754,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
}) })
} }
func (s *recordingService) logRecordingEggRollback(
ctx context.Context,
tx *gorm.DB,
eggs []entity.RecordingEgg,
note string,
actorID uint,
) error {
if len(eggs) == 0 || s.StockLogRepo == nil {
return nil
}
if strings.TrimSpace(note) == "" || actorID == 0 {
return nil
}
for _, egg := range eggs {
if egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
continue
}
log := &entity.StockLog{
ProductWarehouseId: egg.ProductWarehouseId,
CreatedBy: actorID,
Decrease: 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
}
// === Persistence Helpers === // === Persistence Helpers ===
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error { func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
@@ -950,18 +852,6 @@ func (s *recordingService) consumeRecordingStocks(
LoggableId: stock.RecordingId, LoggableId: stock.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock -= log.Decrease
} else {
log.Stock -= log.Decrease
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1029,18 +919,6 @@ func (s *recordingService) consumeRecordingDepletions(
LoggableId: depletion.RecordingId, LoggableId: depletion.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock -= log.Decrease
} else {
log.Stock -= log.Decrease
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1048,9 +926,6 @@ func (s *recordingService) consumeRecordingDepletions(
destDelta := depletion.Qty + depletion.PendingQty destDelta := depletion.Qty + depletion.PendingQty
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if depletion.ProductWarehouseId == sourceWarehouseID {
continue
}
log := &entity.StockLog{ log := &entity.StockLog{
ProductWarehouseId: depletion.ProductWarehouseId, ProductWarehouseId: depletion.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
@@ -1059,18 +934,6 @@ func (s *recordingService) consumeRecordingDepletions(
LoggableId: depletion.RecordingId, LoggableId: depletion.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock += log.Increase
} else {
log.Stock += log.Increase
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1131,18 +994,6 @@ func (s *recordingService) releaseRecordingStocks(
LoggableId: stock.RecordingId, LoggableId: stock.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock += log.Increase
} else {
log.Stock += log.Increase
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1205,18 +1056,6 @@ func (s *recordingService) releaseRecordingDepletions(
LoggableId: depletion.RecordingId, LoggableId: depletion.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock += log.Increase
} else {
log.Stock += log.Increase
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1224,9 +1063,6 @@ func (s *recordingService) releaseRecordingDepletions(
destDelta := depletion.Qty + depletion.PendingQty destDelta := depletion.Qty + depletion.PendingQty
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if depletion.ProductWarehouseId == sourceWarehouseID {
continue
}
log := &entity.StockLog{ log := &entity.StockLog{
ProductWarehouseId: depletion.ProductWarehouseId, ProductWarehouseId: depletion.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
@@ -1235,18 +1071,6 @@ func (s *recordingService) releaseRecordingDepletions(
LoggableId: depletion.RecordingId, LoggableId: depletion.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock -= log.Decrease
} else {
log.Stock -= log.Decrease
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1394,18 +1218,6 @@ func (s *recordingService) replenishRecordingEggs(
LoggableId: egg.RecordingId, LoggableId: egg.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, egg.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
log.Stock = latestStockLog.Stock
log.Stock += log.Increase
} else {
log.Stock += log.Increase
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
return err return err
} }
@@ -1420,11 +1232,6 @@ type desiredStock struct {
Pending float64 Pending float64
} }
type desiredDepletion struct {
Qty float64
Pending float64
}
func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock { func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock {
desired := make([]desiredStock, len(stocks)) desired := make([]desiredStock, len(stocks))
for i := range stocks { for i := range stocks {
@@ -1459,33 +1266,6 @@ func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desir
} }
} }
func resetDepletionQuantitiesForFIFO(depletions []entity.RecordingDepletion, enabled bool) []desiredDepletion {
desired := make([]desiredDepletion, len(depletions))
for i := range depletions {
desired[i].Qty = depletions[i].Qty
desired[i].Pending = depletions[i].PendingQty
if !enabled {
continue
}
depletions[i].Qty = 0
depletions[i].PendingQty = 0
}
return desired
}
func applyDepletionDesiredQuantities(depletions []entity.RecordingDepletion, desired []desiredDepletion, enabled bool) {
if !enabled {
return
}
for i := range depletions {
if i >= len(desired) {
break
}
depletions[i].Qty = desired[i].Qty
depletions[i].PendingQty = desired[i].Pending
}
}
func (s *recordingService) syncRecordingStocks( func (s *recordingService) syncRecordingStocks(
ctx context.Context, ctx context.Context,
tx *gorm.DB, tx *gorm.DB,
@@ -1577,7 +1357,6 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
} }
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
existingUsage := make(map[uint]float64) existingUsage := make(map[uint]float64)
for _, stock := range existing { for _, stock := range existing {
var usage float64 var usage float64
@@ -1789,8 +1568,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggMass float64 var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 { if remainingChick > 0 && totalEggWeightGrams > 0 {
// totalEggWeightGrams is in grams; egg mass is grams per hen. eggMass = (totalEggWeightGrams / remainingChick) / 1000
eggMass = totalEggWeightGrams / remainingChick
updates["egg_mass"] = eggMass updates["egg_mass"] = eggMass
recording.EggMass = &eggMass recording.EggMass = &eggMass
} else { } else {
@@ -9,7 +9,6 @@ import (
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -29,11 +28,9 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
StartDate: c.Query("start_date", ""), TransferDate: c.Query("transfer_date", ""),
EndDate: c.Query("end_date", ""), FlockSource: uint(c.QueryInt("flock_source", 0)),
FlockSource: utils.ParseQueryUintArray(c.Query("flock_source", "")), FlockDestination: uint(c.QueryInt("flock_destination", 0)),
FlockDestination: utils.ParseQueryUintArray(c.Query("flock_destination", "")),
Status: utils.ParseQueryArray(c.Query("status", "")),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -221,29 +218,3 @@ func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error
Data: resp, Data: resp,
}) })
} }
func (u *TransferLayingController) GetMaxTargetQtyPerKandang(c *fiber.Ctx) error {
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
kandangMaxTargetQty, err := u.TransferLayingService.GetMaxTargetQtyPerKandang(c, uint(projectFlockID))
if err != nil {
return err
}
kandangs := make([]dto.KandangMaxTargetQtyDTO, 0, len(kandangMaxTargetQty))
for pfkId, maxTargetQty := range kandangMaxTargetQty {
kandangs = append(kandangs, dto.ToKandangMaxTargetQtyDTO(pfkId, maxTargetQty))
}
resp := dto.ToMaxTargetQtyForTransferDTO(uint(projectFlockID), kandangs)
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get max target quantity successfully",
Data: resp,
})
}
@@ -5,9 +5,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -20,35 +17,60 @@ type TransferLayingRelationDTO struct {
Notes string `json:"notes"` Notes string `json:"notes"`
} }
type ProjectFlockKandangWithKandangDTO struct { type ProjectFlockSummaryDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
KandangId uint `json:"kandang_id"` FlockName string `json:"flock_name"`
ProjectFlockId uint `json:"project_flock_id"` Category string `json:"category"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` }
type ProductSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type ProductWarehouseSummaryDTO struct {
Product *ProductSummaryDTO `json:"product,omitempty"`
Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"`
}
type ProjectFlockKandangSummaryDTO struct {
Id uint `json:"id"`
Kandang *KandangSummaryDTO `json:"kandang,omitempty"`
}
type KandangSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
} }
type LayingTransferSourceDTO struct { type LayingTransferSourceDTO struct {
SourceProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"source_project_flock_kandang,omitempty"` SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"` Qty float64 `json:"qty"`
ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"` ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"` Note string `json:"note,omitempty"`
} }
type LayingTransferTargetDTO struct { type LayingTransferTargetDTO struct {
TargetProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"target_project_flock_kandang,omitempty"` TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"` Qty float64 `json:"qty"`
ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"` ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"` Note string `json:"note,omitempty"`
} }
type TransferLayingListDTO struct { type TransferLayingListDTO struct {
TransferLayingRelationDTO TransferLayingRelationDTO
FromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"from_project_flock,omitempty"` FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
ToProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"to_project_flock,omitempty"` ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
CreatedBy uint `json:"created_by"` CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
} }
type TransferLayingDetailDTO struct { type TransferLayingDetailDTO struct {
@@ -72,26 +94,70 @@ type AvailableQtyForTransferDTO struct {
Kandangs []KandangAvailableQtyDTO `json:"kandangs"` Kandangs []KandangAvailableQtyDTO `json:"kandangs"`
} }
// === Max Target Quantity DTOs ===
type KandangMaxTargetQtyDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
MaxTargetQty float64 `json:"max_target_qty"`
}
type MaxTargetQtyForTransferDTO struct {
ProjectFlockId uint `json:"project_flock_id"`
ProjectFlockKandangs []KandangMaxTargetQtyDTO `json:"project_flock_kandangs"`
}
// === Mapper Functions === // === Mapper Functions ===
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO { func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
return TransferLayingRelationDTO{ if pf == nil || pf.Id == 0 {
Id: e.Id, return nil
TransferNumber: e.TransferNumber, }
TransferDate: e.TransferDate,
Notes: e.Notes, return &ProjectFlockSummaryDTO{
Id: pf.Id,
FlockName: pf.FlockName,
Category: pf.Category,
}
}
func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO {
if pfk == nil || pfk.Id == 0 {
return nil
}
var kandang *KandangSummaryDTO
if pfk.Kandang.Id != 0 {
kandang = &KandangSummaryDTO{
Id: pfk.Kandang.Id,
Name: pfk.Kandang.Name,
}
}
return &ProjectFlockKandangSummaryDTO{
Id: pfk.Id,
Kandang: kandang,
}
}
func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO {
if product == nil || product.Id == 0 {
return nil
}
return &ProductSummaryDTO{
Id: product.Id,
Name: product.Name,
}
}
func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO {
if warehouse == nil || warehouse.Id == 0 {
return nil
}
return &WarehouseSummaryDTO{
Id: warehouse.Id,
Name: warehouse.Name,
Type: warehouse.Type,
}
}
func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO {
if pw == nil || pw.Id == 0 {
return nil
}
return &ProductWarehouseSummaryDTO{
Product: ToProductSummaryDTO(&pw.Product),
Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse),
} }
} }
@@ -106,29 +172,10 @@ func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransfe
displayQty = source.RequestedQty displayQty = source.RequestedQty
} }
var pfkDTO *ProjectFlockKandangWithKandangDTO
if source.SourceProjectFlockKandang != nil && source.SourceProjectFlockKandang.Id != 0 {
pfkDTO = &ProjectFlockKandangWithKandangDTO{
Id: source.SourceProjectFlockKandang.Id,
KandangId: source.SourceProjectFlockKandang.KandangId,
ProjectFlockId: source.SourceProjectFlockKandang.ProjectFlockId,
}
if source.SourceProjectFlockKandang.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(source.SourceProjectFlockKandang.Kandang)
pfkDTO.Kandang = &mapped
}
}
var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO
if source.ProductWarehouse != nil && source.ProductWarehouse.Id != 0 {
mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*source.ProductWarehouse)
pwDTO = &mapped
}
return LayingTransferSourceDTO{ return LayingTransferSourceDTO{
SourceProjectFlockKandang: pfkDTO, SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: displayQty, Qty: displayQty,
ProductWarehouse: pwDTO, ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note, Note: source.Note,
} }
} }
@@ -145,29 +192,10 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT
} }
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
var pfkDTO *ProjectFlockKandangWithKandangDTO
if target.TargetProjectFlockKandang != nil && target.TargetProjectFlockKandang.Id != 0 {
pfkDTO = &ProjectFlockKandangWithKandangDTO{
Id: target.TargetProjectFlockKandang.Id,
KandangId: target.TargetProjectFlockKandang.KandangId,
ProjectFlockId: target.TargetProjectFlockKandang.ProjectFlockId,
}
if target.TargetProjectFlockKandang.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(target.TargetProjectFlockKandang.Kandang)
pfkDTO.Kandang = &mapped
}
}
var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO
if target.ProductWarehouse != nil && target.ProductWarehouse.Id != 0 {
mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*target.ProductWarehouse)
pwDTO = &mapped
}
return LayingTransferTargetDTO{ return LayingTransferTargetDTO{
TargetProjectFlockKandang: pfkDTO, TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
Qty: target.TotalQty, Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity)
ProductWarehouse: pwDTO, ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
Note: target.Note, Note: target.Note,
} }
} }
@@ -183,6 +211,15 @@ func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingT
return result return result
} }
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
return TransferLayingRelationDTO{
Id: e.Id,
TransferNumber: e.TransferNumber,
TransferDate: e.TransferDate,
Notes: e.Notes,
}
}
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
var createdUser *userDTO.UserRelationDTO var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 { if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
@@ -190,52 +227,26 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
createdUser = &mapped createdUser = &mapped
} }
var approval *approvalDTO.ApprovalRelationDTO
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
approval = &mapped
}
// Build from project flock DTO
var fromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO
if e.FromProjectFlock != nil && e.FromProjectFlock.Id != 0 {
fromProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{
Id: e.FromProjectFlock.Id,
FlockName: e.FromProjectFlock.FlockName,
}
}
var toProjectFlock *projectFlockDTO.ProjectFlockRelationDTO
if e.ToProjectFlock != nil && e.ToProjectFlock.Id != 0 {
toProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{
Id: e.ToProjectFlock.Id,
FlockName: e.ToProjectFlock.FlockName,
}
}
return TransferLayingListDTO{ return TransferLayingListDTO{
TransferLayingRelationDTO: ToTransferLayingRelationDTO(e), TransferLayingRelationDTO: ToTransferLayingRelationDTO(e),
FromProjectFlock: fromProjectFlock, FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
ToProjectFlock: toProjectFlock, ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
CreatedBy: e.CreatedBy, CreatedBy: e.CreatedBy,
CreatedUser: createdUser, CreatedUser: createdUser,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
Approval: approval,
} }
} }
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
var latestApproval *approvalDTO.ApprovalRelationDTO var latestApproval *approvalDTO.ApprovalRelationDTO
// Prioritas: e.LatestApproval > approvals slice if e.LatestApproval != nil {
approvalToMap := e.LatestApproval mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
if approvalToMap == nil && len(approvals) > 0 {
approvalToMap = &approvals[len(approvals)-1]
}
if approvalToMap != nil {
mapped := approvalDTO.ToApprovalDTO(*approvalToMap)
latestApproval = &mapped latestApproval = &mapped
} else if len(approvals) > 0 {
// Fallback to approvals slice
latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1])
latestApproval = &latest
} }
return TransferLayingDetailDTO{ return TransferLayingDetailDTO{
@@ -249,14 +260,13 @@ func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Appro
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO { func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
var mappedApproval *approvalDTO.ApprovalRelationDTO var mappedApproval *approvalDTO.ApprovalRelationDTO
// Prioritas: e.LatestApproval > approval parameter // Prefer LatestApproval from entity
approvalToMap := e.LatestApproval if e.LatestApproval != nil && e.LatestApproval.Id != 0 {
if approvalToMap == nil && approval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
approvalToMap = approval mappedApproval = &mapped
} } else if approval != nil && approval.Id != 0 {
// Fallback to passed approval parameter
if approvalToMap != nil { mapped := approvalDTO.ToApprovalDTO(*approval)
mapped := approvalDTO.ToApprovalDTO(*approvalToMap)
mappedApproval = &mapped mappedApproval = &mapped
} }
@@ -275,17 +285,3 @@ func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingLis
} }
return result return result
} }
func ToKandangMaxTargetQtyDTO(pfkId uint, maxTargetQTY float64) KandangMaxTargetQtyDTO {
return KandangMaxTargetQtyDTO{
ProjectFlockKandangId: uint(pfkId),
MaxTargetQty: maxTargetQTY,
}
}
func ToMaxTargetQtyForTransferDTO(pfId uint, kandangs []KandangMaxTargetQtyDTO) MaxTargetQtyForTransferDTO {
return MaxTargetQtyForTransferDTO{
ProjectFlockId: pfId,
ProjectFlockKandangs: kandangs,
}
}
@@ -2,8 +2,6 @@ package repository
import ( import (
"context" "context"
"fmt"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -14,13 +12,6 @@ type TransferLayingRepository interface {
repository.BaseRepository[entity.LayingTransfer] repository.BaseRepository[entity.LayingTransfer]
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
// Tambah method baru untuk query dengan filter lengkap
GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error)
// Get sequence for movement number
GetNextMovementNumber(ctx context.Context) (int64, error)
GenerateMovementNumber(ctx context.Context) (string, error)
} }
type TransferLayingRepositoryImpl struct { type TransferLayingRepositoryImpl struct {
@@ -34,26 +25,6 @@ func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
db: db, db: db,
} }
} }
func (r *TransferLayingRepositoryImpl) GetNextMovementNumber(ctx context.Context) (int64, error) {
var seq int64
err := r.db.WithContext(ctx).Raw("SELECT nextval('transfer_laying_seq')").Scan(&seq).Error
if err != nil {
return 0, err
}
return seq, nil
}
func (r *TransferLayingRepositoryImpl) GenerateMovementNumber(ctx context.Context) (string, error) {
seq, err := r.GetNextMovementNumber(ctx)
if err != nil {
return "", err
}
movementNumber := fmt.Sprintf("TL-%05d", seq)
return movementNumber, nil
}
func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.LayingTransfer](ctx, r.db, id) return repository.Exists[entity.LayingTransfer](ctx, r.db, id)
} }
@@ -69,114 +40,3 @@ func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context,
} }
return &transfer, nil return &transfer, nil
} }
type GetAllFilterParams struct {
Search string
StartDate string
EndDate string
FlockSource []uint
FlockDestination []uint
Status []string
LocationIDs []uint
LocationRestrict bool
}
func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) {
var records []entity.LayingTransfer
var total int64
q := r.db.WithContext(ctx).Model(&entity.LayingTransfer{})
if params.Search != "" {
searchPattern := "%" + params.Search + "%"
q = q.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
searchPattern, searchPattern, searchPattern, searchPattern)
}
if params.StartDate != "" && params.EndDate != "" {
q = q.Where("transfer_date::date >= ?::date AND transfer_date::date <= ?::date",
params.StartDate, params.EndDate)
} else if params.StartDate != "" {
q = q.Where("transfer_date::date >= ?::date", params.StartDate)
} else if params.EndDate != "" {
q = q.Where("transfer_date::date <= ?::date", params.EndDate)
}
if len(params.FlockSource) > 0 {
q = q.Where("from_project_flock_id IN ?", params.FlockSource)
}
if len(params.FlockDestination) > 0 {
q = q.Where("to_project_flock_id IN ?", params.FlockDestination)
}
if len(params.Status) > 0 {
statusConditions := []string{}
statusValues := []interface{}{}
for _, status := range params.Status {
switch status {
case "PENDING":
statusConditions = append(statusConditions,
"NOT EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id)")
case "APPROVED":
statusConditions = append(statusConditions,
"EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'APPROVED' ORDER BY created_at DESC LIMIT 1)")
case "REJECTED":
statusConditions = append(statusConditions,
"EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'REJECTED' ORDER BY created_at DESC LIMIT 1)")
}
}
if len(statusConditions) > 0 {
q = q.Where("("+strings.Join(statusConditions, " OR ")+")", statusValues...)
}
}
if params.LocationRestrict {
if len(params.LocationIDs) == 0 {
q = q.Where("1 = 0")
} else {
q = q.Where(
`EXISTS (
SELECT 1 FROM project_flocks pf
WHERE pf.id = laying_transfers.from_project_flock_id
AND pf.location_id IN ?
) OR EXISTS (
SELECT 1 FROM project_flocks pf
WHERE pf.id = laying_transfers.to_project_flock_id
AND pf.location_id IN ?
)`,
params.LocationIDs, params.LocationIDs,
)
}
}
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
q = q.Offset(offset).Limit(limit).
Preload("FromProjectFlock").
Preload("ToProjectFlock").
Preload("CreatedUser").
Preload("Sources").
Preload("Sources.SourceProjectFlockKandang").
Preload("Sources.SourceProjectFlockKandang.Kandang").
Preload("Sources.ProductWarehouse").
Preload("Targets").
Preload("Targets.TargetProjectFlockKandang").
Preload("Targets.TargetProjectFlockKandang.Kandang").
Preload("Targets.ProductWarehouse").
Order("laying_transfers.created_at DESC")
if err := q.Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
@@ -21,12 +21,11 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
// route.Post("/approval", m.Auth(u), ctrl.Approval) // route.Post("/approval", m.Auth(u), ctrl.Approval)
route.Get("/", m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang)
} }
@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"strings" "strings"
"time" "time"
@@ -35,7 +34,6 @@ type TransferLayingService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error)
} }
type transferLayingService struct { type transferLayingService struct {
@@ -108,25 +106,37 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
return nil, 0, err return nil, 0, err
} }
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
filterParams := &repository.GetAllFilterParams{ transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
Search: params.Search, // Apply search and filters
StartDate: params.StartDate, if params.Search != "" {
EndDate: params.EndDate, searchPattern := "%" + params.Search + "%"
FlockSource: params.FlockSource, db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
FlockDestination: params.FlockDestination, Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
Status: params.Status, Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
LocationIDs: scope.IDs, searchPattern, searchPattern, searchPattern, searchPattern)
LocationRestrict: scope.Restrict, }
}
if params.TransferDate != "" {
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
}
if params.FlockSource > 0 {
db = db.Where("from_project_flock_id = ?", params.FlockSource)
}
if params.FlockDestination > 0 {
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
}
db = db.Order("created_at DESC")
db = s.withRelations(db)
return db
})
transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get transferLayings: %+v", err) s.Log.Errorf("Failed to get transferLayings: %+v", err)
return nil, 0, err return nil, 0, err
@@ -171,11 +181,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if !m.HasPermission(c, m.P_TransferToLaying_CreateOne) {
if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil {
return nil, err
}
}
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
if err != nil { if err != nil {
@@ -272,11 +277,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
} }
transferNumber, err := s.Repository.GenerateMovementNumber(c.Context()) transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
if err != nil {
s.Log.Errorf("Failed to generate movement number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer")
}
createBody := &entity.LayingTransfer{ createBody := &entity.LayingTransfer{
TransferNumber: transferNumber, TransferNumber: transferNumber,
@@ -408,11 +409,6 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if !m.HasPermission(c, m.P_TransferToLaying_UpdateOne) {
if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil {
return nil, err
}
}
existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return db.Preload("Sources.ProductWarehouse").Preload("Targets") return db.Preload("Sources.ProductWarehouse").Preload("Targets")
@@ -445,105 +441,15 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return nil, fiber.NewError(fiber.StatusBadRequest, "Target project flock not found") return nil, fiber.NewError(fiber.StatusBadRequest, "Target project flock not found")
} }
sourceKandangIDs := make([]uint, len(req.SourceKandangs))
for i, detail := range req.SourceKandangs {
sourceKandangIDs[i] = detail.ProjectFlockKandangId
}
if err := s.validateKandangOwnership(
c.Context(),
req.SourceProjectFlockId,
sourceKandangIDs,
); err != nil {
return nil, err
}
targetKandangIDs := make([]uint, len(req.TargetKandangs))
for i, detail := range req.TargetKandangs {
targetKandangIDs[i] = detail.ProjectFlockKandangId
}
if err := s.validateKandangOwnership(
c.Context(),
req.TargetProjectFlockId,
targetKandangIDs,
); err != nil {
return nil, err
}
transferDate, err := time.Parse("2006-01-02", req.TransferDate) transferDate, err := time.Parse("2006-01-02", req.TransferDate)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
} }
var totalSourceQty, totalTargetQty float64
sourceWarehouseMap := make(map[uint]uint)
for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 {
continue
}
totalSourceQty += sourceDetail.Quantity
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
if err != nil {
return nil, err
}
var totalPopulation float64
var productWarehouseId uint
for _, pop := range populations {
totalPopulation += pop.TotalQty
if productWarehouseId == 0 {
productWarehouseId = pop.ProductWarehouseId
}
}
if totalPopulation == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId))
}
if totalPopulation < sourceDetail.Quantity {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
}
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
}
for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 {
continue
}
totalTargetQty += targetDetail.Quantity
}
if totalSourceQty == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0")
}
if totalTargetQty == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
}
if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
}
// Ambil productWarehouseId pertama dari source yang valid (quantity > 0)
var firstProductWarehouseId uint
for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity > 0 {
if pwId, ok := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]; ok {
firstProductWarehouseId = pwId
break
}
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction) repoTx := s.Repository.WithTx(dbTransaction)
sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction) sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction)
targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction) targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction)
pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction)
// Hapus old sources dan targets // Hapus old sources dan targets
for _, oldSource := range existingTransfer.Sources { for _, oldSource := range existingTransfer.Sources {
@@ -567,11 +473,26 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
// Create new sources dengan pending quantity // Create new sources dengan pending quantity
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity == 0 { populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
continue if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations")
} }
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId))
}
var productWarehouseId uint
for _, pop := range populations {
if pop.ProductWarehouseId > 0 {
productWarehouseId = pop.ProductWarehouseId
break
}
}
if productWarehouseId == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no product warehouse", sourceDetail.ProjectFlockKandangId))
}
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: id, LayingTransferId: id,
@@ -586,18 +507,7 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
} }
} }
// Ambil product ID dari source warehouse pertama yang valid pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction)
var sourceProductID uint
if firstProductWarehouseId > 0 {
sourcePW, err := pwRepo.GetByID(c.Context(), firstProductWarehouseId, nil)
if err == nil {
sourceProductID = sourcePW.ProductId
}
}
if sourceProductID == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product from source warehouse")
}
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
@@ -613,6 +523,23 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
} }
// Ambil product ID dari source yang pertama (semua sources seharusnya product-nya sama)
var sourceProductID uint
if len(req.SourceKandangs) > 0 {
firstSourceKandangID := req.SourceKandangs[0].ProjectFlockKandangId
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), firstSourceKandangID)
if err == nil && len(populations) > 0 && populations[0].ProductWarehouseId > 0 {
sourcePW, err := pwRepo.GetByID(c.Context(), populations[0].ProductWarehouseId, nil)
if err == nil {
sourceProductID = sourcePW.ProductId
}
}
}
if sourceProductID == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product from source warehouse")
}
targetPW, err := pwRepo.FindByProductWarehouseAndPfk(c.Context(), sourceProductID, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId) targetPW, err := pwRepo.FindByProductWarehouseAndPfk(c.Context(), sourceProductID, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -657,9 +584,6 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
} }
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil {
return err
}
_, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { _, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return db.Preload("Sources.ProductWarehouse").Preload("Targets") return db.Preload("Sources.ProductWarehouse").Preload("Targets")
@@ -730,12 +654,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
} }
for _, approvableID := range approvableIDs {
if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), approvableID); err != nil {
return nil, err
}
}
step := utils.TransferToLayingStepPengajuan step := utils.TransferToLayingStepPengajuan
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
step = utils.TransferToLayingStepDisetujui step = utils.TransferToLayingStepDisetujui
@@ -744,10 +662,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction) repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
// Gunakan repo baru untuk transaction scope agar bisa akses method custom
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
for _, approvableID := range approvableIDs { for _, approvableID := range approvableIDs {
transfer, err := repoTx.GetByID(c.Context(), approvableID, nil) transfer, err := repoTx.GetByID(c.Context(), approvableID, nil)
@@ -782,28 +700,23 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer") return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer")
} }
// Hitung total quantity dari targets untuk di-consume dari sources
totalTargetQty := 0.0 totalTargetQty := 0.0
for _, target := range targets { for _, target := range targets {
totalTargetQty += target.TotalQty totalTargetQty += target.TotalQty
} }
totalSourceRequested := 0.0 // Consume dari laying_transfer_sources (Usable) - akan consume dari ProjectFlockPopulation (Stockable)
for _, source := range sources {
totalSourceRequested += source.RequestedQty
}
for _, source := range sources { for _, source := range sources {
if source.ProductWarehouseId == nil { if source.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID))
} }
sourceShare := (source.RequestedQty / totalSourceRequested) * totalTargetQty
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyTransferToLayingOut, UsableKey: fifo.UsableKeyTransferToLayingOut,
UsableID: source.Id, UsableID: source.Id,
ProductWarehouseID: *source.ProductWarehouseId, ProductWarehouseID: *source.ProductWarehouseId,
Quantity: sourceShare, Quantity: totalTargetQty,
AllowPending: false, AllowPending: false,
Tx: dbTransaction, Tx: dbTransaction,
}) })
@@ -817,52 +730,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
}, nil); err != nil { }, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
} }
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare)
for i, target := range targets {
roundedQty := math.Round(targetShares[i])
if roundedQty <= 0 {
continue
}
mappingAllocation := &entity.StockAllocation{
StockableType: fifo.UsableKeyTransferToLayingOut.String(),
StockableId: source.Id,
UsableType: fifo.StockableKeyTransferToLayingIn.String(),
UsableId: target.Id,
ProductWarehouseId: *source.ProductWarehouseId,
Qty: roundedQty,
Status: entity.StockAllocationStatusActive,
}
if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
}
}
stockLogDecrease := &entity.StockLog{
ProductWarehouseId: *source.ProductWarehouseId,
CreatedBy: actorID,
Increase: 0,
Decrease: sourceShare,
LoggableType: string(utils.StockLogTypeTransferLaying),
LoggableId: approvableID,
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
}
stockLogs, err := stockLogRepoTx.GetByProductWarehouse(c.Context(), *source.ProductWarehouseId, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
} else {
stockLogDecrease.Stock -= stockLogDecrease.Decrease
}
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
}
} }
for _, target := range targets { for _, target := range targets {
@@ -871,7 +738,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
} }
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
_, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLayingIn, StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id, StockableID: target.Id,
ProductWarehouseID: *target.ProductWarehouseId, ProductWarehouseID: *target.ProductWarehouseId,
@@ -884,35 +751,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
} }
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{ if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
"total_qty": target.TotalQty, "total_qty": replenishResult.AddedQuantity,
}, nil); err != nil { }, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty") return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
} }
stockLogIncrease := &entity.StockLog{
ProductWarehouseId: *target.ProductWarehouseId,
CreatedBy: actorID,
Increase: target.TotalQty,
Decrease: 0,
LoggableType: string(utils.StockLogTypeTransferLaying),
LoggableId: approvableID,
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
}
stockLogs, err := stockLogRepoTx.GetByProductWarehouse(c.Context(), *target.ProductWarehouseId, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
} else {
stockLogIncrease.Stock += stockLogIncrease.Increase
}
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
}
} }
} }
} }
@@ -959,6 +801,36 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
return err return err
} }
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil && existing != nil {
if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"qty": gorm.Expr("qty + ?", quantity)}, nil); err != nil {
return nil, err
}
return existing, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
newWarehouse := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
ProjectFlockKandangId: projectFlockKandangId,
Quantity: quantity,
}
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
return nil, err
}
return newWarehouse, nil
}
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
@@ -1016,70 +888,3 @@ func (s *transferLayingService) validateKandangOwnership(
return nil return nil
} }
func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
kandangMaxTargetQty := make(map[uint]float64)
for _, projectFlockKandang := range projectFlockKandangs {
capacity := projectFlockKandang.Kandang.Capacity
availableQty, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(
c.Context(),
projectFlockKandang.Id,
)
if err != nil {
return nil, err
}
kandangMaxTargetQty[projectFlockKandang.Id] = capacity - availableQty
if kandangMaxTargetQty[projectFlockKandang.Id] < 0 {
kandangMaxTargetQty[projectFlockKandang.Id] = 0
}
}
return kandangMaxTargetQty, nil
}
func distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 {
if len(targets) == 0 {
return []float64{}
}
targetShares := make([]float64, len(targets))
totalRounded := 0.0
for i, target := range targets {
targetShares[i] = (target.TotalQty / totalTargetQty) * sourceShare
totalRounded += math.Round(targetShares[i])
}
diff := sourceShare - totalRounded
if diff != 0 {
maxIdx := 0
maxDecimal := 0.0
for i, share := range targetShares {
decimal := share - math.Round(share)
if decimal > maxDecimal {
maxDecimal = decimal
maxIdx = i
}
}
targetShares[maxIdx] += diff
}
return targetShares
}
@@ -29,14 +29,12 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty"` Search string `query:"search" validate:"omitempty"`
StartDate string `query:"start_date" validate:"omitempty"` TransferDate string `query:"transfer_date" validate:"omitempty"`
EndDate string `query:"end_date" validate:"omitempty"` FlockSource uint `query:"flock_source" validate:"omitempty,number"`
FlockSource []uint `query:"flock_source" validate:"omitempty"` FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
FlockDestination []uint `query:"flock_destination" validate:"omitempty"`
Status []string `query:"status" validate:"omitempty"`
} }
type Approve struct { type Approve struct {
@@ -12,7 +12,7 @@ import (
type UniformityRepository interface { type UniformityRepository interface {
repository.BaseRepository[entity.ProjectFlockKandangUniformity] repository.BaseRepository[entity.ProjectFlockKandangUniformity]
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifiers ...func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandangUniformity, int64, error) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
WithDefaultRelations() func(*gorm.DB) *gorm.DB WithDefaultRelations() func(*gorm.DB) *gorm.DB
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
} }
@@ -27,15 +27,9 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository {
} }
} }
func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifiers ...func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandangUniformity, int64, error) { func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) return r.applyQueryFilters(r.WithDefaultRelations()(db), params)
for _, modifier := range modifiers {
if modifier != nil {
db = modifier(db)
}
}
return db
}) })
} }
@@ -87,20 +87,9 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
return nil, 0, err return nil, 0, err
} }
var scopeErr error
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params, func(db *gorm.DB) *gorm.DB { uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
db = db.
Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformity.project_flock_kandang_id").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id")
db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id")
return db
})
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil { if err != nil {
s.Log.Errorf("Failed to get uniformitys: %+v", err) s.Log.Errorf("Failed to get uniformitys: %+v", err)
return nil, 0, err return nil, 0, err
@@ -112,10 +101,6 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
} }
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) { func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
@@ -341,9 +326,6 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil {
return nil, err
}
if s.ProjectFlockKandangRepo == nil { if s.ProjectFlockKandangRepo == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available") return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available")
} }
@@ -393,7 +375,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
var latestWeek int var latestWeek int
if err := s.Repository.DB().WithContext(c.Context()). if err := s.Repository.DB().WithContext(c.Context()).
Model(&entity.ProjectFlockKandangUniformity{}). Model(&entity.ProjectFlockKandangUniformity{}).
Where("project_flock_kandang_id = ?", req.ProjectFlockKandangId). Where("project_flock_kandang_id = ? AND deleted_at IS NULL", req.ProjectFlockKandangId).
Select("COALESCE(MAX(week), 0)"). Select("COALESCE(MAX(week), 0)").
Scan(&latestWeek).Error; err != nil { Scan(&latestWeek).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
@@ -502,9 +484,6 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
updateBody := make(map[string]any) updateBody := make(map[string]any)
var uniformDate *time.Time var uniformDate *time.Time
@@ -720,10 +699,6 @@ func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint,
} }
func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error { func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
@@ -754,11 +729,6 @@ func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]en
if len(ids) == 0 { if len(ids) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
} }
for _, id := range ids {
if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
return nil, err
}
}
step := utils.UniformityStepPengajuan step := utils.UniformityStepPengajuan
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
@@ -45,7 +45,10 @@ type groupedItem struct {
projectFK *uint projectFK *uint
kandangID *uint kandangID *uint
totalPrice float64 totalPrice float64
poNumber string }
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
} }
type expenseBridge struct { type expenseBridge struct {
@@ -219,7 +222,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB { purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB {
return db. return db.
Preload("Items"). Preload("Items").
Preload("Items.Product").
Preload("Items.Warehouse"). Preload("Items.Warehouse").
Preload("Items.Warehouse.Kandang") Preload("Items.Warehouse.Kandang")
}) })
@@ -307,7 +309,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
// If supplier/date unchanged, update nonstock in place. // If supplier/date unchanged, update nonstock in place.
if oldSupplier == supplierID && oldDate.Equal(newDate) { if oldSupplier == supplierID && oldDate.Equal(newDate) {
note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
if err := b.db.WithContext(ctx). if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}). Model(&entity.ExpenseNonstock{}).
Where("id = ?", link.ExpenseNonstockID). Where("id = ?", link.ExpenseNonstockID).
@@ -338,7 +340,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
if err != nil { if err != nil {
return err return err
} }
note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
if err := b.db.WithContext(ctx). if err := b.db.WithContext(ctx).
Model(&entity.Expense{}). Model(&entity.Expense{}).
Where("id = ?", link.ExpenseID). Where("id = ?", link.ExpenseID).
@@ -390,7 +392,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
projectFK: projectFK, projectFK: projectFK,
kandangID: kandangID, kandangID: kandangID,
totalPrice: totalPrice, totalPrice: totalPrice,
poNumber: purchasePoNumber(purchase),
} }
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID) newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
@@ -409,7 +410,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
createdNonstockID = noteMap[payload.PurchaseItemID] createdNonstockID = noteMap[payload.PurchaseItemID]
} }
note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
updateBody := map[string]interface{}{ updateBody := map[string]interface{}{
"expense_id": expenseDetail.Id, "expense_id": expenseDetail.Id,
"qty": payload.ReceivedQty, "qty": payload.ReceivedQty,
@@ -482,7 +483,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
projectFK: projectFK, projectFK: projectFK,
kandangID: kandangID, kandangID: kandangID,
totalPrice: totalPrice, totalPrice: totalPrice,
poNumber: purchasePoNumber(purchase),
}) })
} }
@@ -679,14 +679,6 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil { Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
return err return err
} }
note := purchaseItemDisplayNote(gi.item, gi.payload.PurchaseItemID, gi.poNumber)
if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}).
Where("id = ?", expenseNonstockID).
Update("notes", note).Error; err != nil {
return err
}
} }
return nil return nil
@@ -717,22 +709,3 @@ func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 {
} }
return result return result
} }
func purchaseItemDisplayNote(item *entity.PurchaseItem, itemID uint, poNumber string) string {
poLabel := "PO"
if strings.TrimSpace(poNumber) != "" {
poLabel = strings.TrimSpace(poNumber)
}
productName := fmt.Sprintf("Item %d", itemID)
if item != nil && item.Product != nil && strings.TrimSpace(item.Product.Name) != "" {
productName = item.Product.Name
}
return fmt.Sprintf("%s (%s)", poLabel, productName)
}
func purchasePoNumber(purchase *entity.Purchase) string {
if purchase == nil || purchase.PoNumber == nil {
return ""
}
return *purchase.PoNumber
}

Some files were not shown because too many files have changed in this diff Show More