Compare commits

..

1 Commits

Author SHA1 Message Date
MacBook Air M1 348eb05fc3 tes new account commit 2026-01-14 14:35:33 +07:00
138 changed files with 2628 additions and 7161 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ main
bin/
*.exe
*.out
.air.toml
Makefile
docker-compose.local.yml
docker-compose.yaml
+87 -18
View File
@@ -1,21 +1,90 @@
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "development"'
- if: '$CI_COMMIT_BRANCH == "staging"'
- if: '$CI_COMMIT_BRANCH == "production"'
- when: never
stages:
- deploy
include:
- local: "ci/development.yml"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "development"'
deploy-dev:
stage: deploy
image: alpine:3.20
variables:
DEPLOY_APP: "LTI-MBUGROUP"
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: "1"
- local: "ci/staging.yml"
rules:
- if: '$CI_COMMIT_BRANCH == "staging"'
before_script:
- echo "🧰 Installing dependencies..."
- apk update && apk add --no-cache openssh git curl bash
- local: "ci/production.yml"
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
# Setup SSH di runner
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
set -e
cd /home/devops/docker/deployment/development/lti-api
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
# Fetch/reset pakai SSH
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
git reset --hard origin/development
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
"; then
STATUS='success';
else
STATUS='failed';
fi;
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
if [ "$STATUS" = "success" ]; then
COLOR=3066993;
TITLE="✅ Deployment API Succeeded";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
else
COLOR=15158332;
TITLE="❌ Deployment API Failed Gaes";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
fi;
echo "{
\"username\": \"CI Bot\",
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESC\",
\"color\": $COLOR,
\"fields\": [
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
]
}]
}" > payload.json;
echo "📡 Sending notification to Discord...";
curl -sS -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL";
only:
- development
environment:
name: development
+1 -1
View File
@@ -106,7 +106,7 @@ internal/
## ✨ Author
IT Development PT Mitra Berlian Unggas Group
IT Development PT Mitra Berlian Unggas Groups
## 📃 License
-90
View File
@@ -1,90 +0,0 @@
stages:
- deploy
deploy-dev:
stage: deploy
image: alpine:3.20
variables:
DEPLOY_APP: "LTI-MBUGROUP"
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: "1"
before_script:
- echo "🧰 Installing dependencies..."
- apk update && apk add --no-cache openssh git curl bash
# Setup SSH di runner
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
set -e
cd /home/devops/docker/deployment/development/lti-api
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
# Fetch/reset pakai SSH
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
git reset --hard origin/development
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
"; then
STATUS='success';
else
STATUS='failed';
fi;
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
if [ "$STATUS" = "success" ]; then
COLOR=3066993;
TITLE="✅ Deployment API Succeeded";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
else
COLOR=15158332;
TITLE="❌ Deployment API Failed Gaes";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
fi;
echo "{
\"username\": \"CI Bot\",
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESC\",
\"color\": $COLOR,
\"fields\": [
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
]
}]
}" > payload.json;
echo "📡 Sending notification to Discord...";
curl -sS -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL";
only:
- development
environment:
name: development
-131
View File
@@ -1,131 +0,0 @@
stages:
- build
# - migrate
- deploy
- seed
default:
tags:
- self-hosted-prod
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
when: always
- when: never
variables:
DOCKER_BUILDKIT: "1"
IMAGE_TAG: "production_${CI_COMMIT_SHORT_SHA}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:production_latest"
DEPLOY_DIR: "/opt/deploy/lti"
COMPOSE_FILE: "docker-compose.yaml"
# =========================
# BUILD (AUTO)
# =========================
build_production:
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
echo "✅ Build image: $IMAGE_NAME"
docker build -t "$IMAGE_NAME" -f Dockerfile .
echo "✅ Push image: $IMAGE_NAME"
docker push "$IMAGE_NAME"
echo "✅ Tag latest: $IMAGE_LATEST"
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
docker push "$IMAGE_LATEST"
# =========================
# MIGRATE (PRODUCTION - MANUAL)
# =========================
#migrate_production:
# stage: migrate
# rules:
# - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
# when: manual
# allow_failure: false
# needs:
# - job: build_production
# artifacts: false
# script: |
# set -e
# cd /opt/deploy/lti
# test -f .env || (echo "❌ .env not found" && exit 1)
# set -a
# . ./.env
# set +a
# Validasi env wajib
# : "${DB_HOST:?DB_HOST not set}"
# : "${DB_PORT:?DB_PORT not set}"
# : "${DB_USER:?DB_USER not set}"
# : "${DB_PASSWORD:?DB_PASSWORD not set}"
# : "${DB_NAME:?DB_NAME not set}"
# DB_SSLMODE="${DB_SSLMODE:-require}"
# export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
# echo "✅ Running migrations (production)..."
# docker run --rm \
# -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
# migrate/migrate:v4.15.2 \
# -path=/migrations -database "$DATABASE_URL" up
# =========================
# DEPLOY (AUTO)
# =========================
deploy_production:
stage: deploy
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
needs:
# - job: migrate_production
# artifacts: false
- job: build_production
artifacts: false
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
docker compose -f "$COMPOSE_FILE" pull
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
docker image prune -f
# =========================
# SEED (MANUAL)
# =========================
seed_production:
stage: seed
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
when: manual
script: |
set -e
cd /opt/deploy/lti
test -f .env || (echo "❌ .env not found" && exit 1)
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
docker compose --env-file .env pull seed
docker compose --env-file .env run --rm seed
-173
View File
@@ -1,173 +0,0 @@
stages:
- build
- migrate
- deploy
- seed
default:
tags:
- self-hosted-stg
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
when: always
- when: never
variables:
DOCKER_BUILDKIT: "1"
IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
DEPLOY_DIR: "/opt/deploy/stg-lti-api"
COMPOSE_FILE: "docker-compose.yaml"
# =========================
# BUILD (AUTO)
# =========================
build_staging:
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
echo "✅ Build image: $IMAGE_NAME"
docker build -t "$IMAGE_NAME" -f Dockerfile .
echo "✅ Push image: $IMAGE_NAME"
docker push "$IMAGE_NAME"
echo "✅ Tag latest: $IMAGE_LATEST"
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
docker push "$IMAGE_LATEST"
# =========================
# MIGRATE (AUTO)
# =========================
migrate_staging:
stage: migrate
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs:
- job: build_staging
artifacts: false
script: |
set -e
echo "✅ Running migrations (staging) ..."
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
# ✅ load env dari server
set -a
. ./.env
set +a
# ✅ validasi
test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1)
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 "✅ DATABASE_URL=$DATABASE_URL"
# ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!)
echo "✅ Ensuring postgres & redis running ..."
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"
# =========================
# DEPLOY (AUTO)
# =========================
deploy_staging:
stage: deploy
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs:
- job: migrate_staging
artifacts: false
- job: build_staging
artifacts: false
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
docker compose -f "$COMPOSE_FILE" pull
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
docker image prune -f
# =========================
# SEED (MANUAL)
# =========================
seed_staging:
stage: seed
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs:
- job: deploy_staging
artifacts: false
when: manual
allow_failure: false
script: |
set -e
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
test -f .env || (echo "❌ .env not found" && exit 1)
docker compose -f "$COMPOSE_FILE" pull seed || true
docker compose -f "$COMPOSE_FILE" run --rm seed%
@@ -1,309 +0,0 @@
package repository
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
type HppCostRepository interface {
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetBudgetCostByProjectFlockId(ctx context.Context, projectFlockId uint) (float64, error)
GetExpedisionCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
GetOvkUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetPulletCost(ctx context.Context, projectFlockKandangId uint) (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)
GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error)
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
}
type HppRepositoryImpl struct {
db *gorm.DB
}
func NewHppCostRepository(db *gorm.DB) HppCostRepository {
return &HppRepositoryImpl{db: db}
}
func (r *HppRepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) {
var ids []uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Select("id").
Where("project_flock_id = ?", projectFlockId).
Scan(&ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
func (r *HppRepositoryImpl) GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select("COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0)").
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetBudgetCostByProjectFlockId(ctx context.Context, projectFlockId uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_budgets AS pb").
Select("COALESCE(SUM(pb.qty * pb.price), 0)").
Where("pb.project_flock_id = ?", projectFlockId).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetExpedisionCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("expense_nonstocks AS en").
Select("COALESCE(SUM(er.qty * er.price), 0)").
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Joins("JOIN flags AS f ON f.flagable_id = en.nonstock_id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Where("en.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagEkspedisi).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
if date == nil {
now := time.Now()
date = &now
}
var total float64
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Where("f.name = ?", utils.FlagPakan).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
if date == nil {
now := time.Now()
date = &now
}
flags := []utils.FlagType{
utils.FlagOVK,
utils.FlagObat,
utils.FlagVitamin,
utils.FlagKimia,
}
var total float64
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Where("f.name IN ?", flags).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select("COALESCE(SUM(pc.usage_qty), 0)").
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) {
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableTransferIn := fifo.StockableKeyStockTransferIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
COALESCE(SUM(pc.usage_qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0)
ELSE 0
END), 0)`,
stockablePurchase, stockableTransferIn).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id", usableProjectChickin).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN stock_allocations AS tsa ON tsa.usable_type = ? AND tsa.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa.stockable_type = ?", stockableTransferIn, stockableTransferIn, stockablePurchase).
Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa.stockable_id").
Where("pc.project_flock_kandang_id = ?", projectFlockKandangId).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) {
// if date == nil {
// now := time.Now()
// date = &now
// }
var totals struct {
TotalPieces float64
TotalWeightKg float64
}
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0)AS total_weight_kg").
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Scan(&totals).Error
if err != nil {
return 0, 0, err
}
return totals.TotalPieces, totals.TotalWeightKg, nil
}
func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(
ctx context.Context,
projectFlockKandangIDs []uint,
startDate *time.Time,
endDate *time.Time,
) (float64, float64, error) {
if endDate == nil {
now := time.Now()
endDate = &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 {
TotalPieces float64
TotalWeight float64
}
err := r.db.WithContext(ctx).
Table("(?) AS x", subQuery).
Select(`
COALESCE(SUM(x.mdp_usage_qty), 0) AS total_pieces,
COALESCE(SUM(x.mdp_weight), 0) AS total_weight
`).
Scan(&totals).Error
if err != nil {
return 0, 0, err
}
return totals.TotalPieces, totals.TotalWeight, nil
}
func (r *HppRepositoryImpl) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) {
var projectFlockID uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Select("project_flock_id").
Where("id = ?", projectFlockKandangId).
Scan(&projectFlockID).Error
if err != nil {
return 0, err
}
return projectFlockID, nil
}
func (r *HppRepositoryImpl) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) {
var summary struct {
ProjectFlockID uint
TotalQty float64
}
err := r.db.WithContext(ctx).
Table("laying_transfer_targets AS ltt").
Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty").
Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id").
Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId).
Group("lt.from_project_flock_id").
Scan(&summary).Error
if err != nil {
return 0, 0, err
}
return summary.ProjectFlockID, summary.TotalQty, nil
}
@@ -25,7 +25,6 @@ type FifoService interface {
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error
}
type fifoService struct {
@@ -96,15 +95,6 @@ type StockReplenishRequest struct {
Tx *gorm.DB
}
type StockAdjustRequest struct {
StockableKey fifo.StockableKey
StockableID uint
ProductWarehouseID uint
Quantity float64
Note *string
Tx *gorm.DB
}
type PendingResolution struct {
UsableKey fifo.UsableKey
UsableID uint
@@ -147,37 +137,6 @@ type StockReleaseRequest struct {
Reason *string
Tx *gorm.DB
}
func (s *fifoService) AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
return errors.New("stockable key and id are required")
}
if req.ProductWarehouseID == 0 {
return errors.New("product warehouse id is required")
}
if req.Quantity == 0 {
return nil
}
if req.Quantity > 0 {
return errors.New("quantity must be negative")
}
cfg, ok := fifo.Stockable(req.StockableKey)
if !ok {
return fmt.Errorf("stockable %q is not registered", req.StockableKey)
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
return err
}
return s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
req.ProductWarehouseID: req.Quantity,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
})
})
}
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
@@ -1,272 +0,0 @@
package service
import (
"context"
"math"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
)
type HppService interface {
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error)
GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error)
GetBudgetKandangLaying(projectFlockKandangId uint, endDate *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)
}
type HppCostResponse struct {
Estimation HppCostDetail `json:"estimation"`
Real HppCostDetail `json:"real"`
}
type HppCostDetail struct {
HargaKg float64 `json:"harga_kg"`
HargaButir float64 `json:"harga_butir"`
Total float64 `json:"total"`
Kg float64 `json:"kg"`
Butir float64 `json:"butir"`
}
type hppService struct {
hppRepo commonRepo.HppCostRepository
}
func NewHppService(hppRepo commonRepo.HppCostRepository) HppService {
return &hppService{hppRepo: hppRepo}
}
func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
if date == nil {
now := time.Now()
date = &now
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, err
}
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour)
depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer)
if err != nil {
return nil, err
}
return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
}
func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) {
if date == nil {
now := time.Now()
date = &now
}
if s.hppRepo == nil {
return 0, nil
}
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return 0, err
}
docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs)
if err != nil {
return 0, err
}
budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID)
if err != nil {
return 0, err
}
expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs)
if err != nil {
return 0, err
}
feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date)
if err != nil {
return 0, err
}
ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date)
if err != nil {
return 0, err
}
return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil
}
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) {
// if date == nil {
// now := time.Now()
// date = &now
// }
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
if err != nil {
return 0, err
}
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return 0, err
}
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return 0, err
}
costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId})
if err != nil {
return 0, err
}
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil
}
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
// if date == nil {
// now := time.Now()
// date = &now
// }
if s.hppRepo == nil {
return 0, nil
}
projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil {
return 0, err
}
projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId)
if err != nil {
return 0, err
}
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate)
if err != nil {
return 0, err
}
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return 0, err
}
totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId)
if err != nil {
return 0, err
}
if eggProduksiPiecesFlock == 0 {
return 0, nil
}
return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil
}
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
// if endDate == nil {
// now := time.Now()
// endDate = &now
// }
if s.hppRepo == nil {
return 0, nil
}
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
if err != nil {
return 0, err
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return 0, err
}
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil {
return 0, err
}
if totalPopulationFlockGrowing == 0 {
return 0, nil
}
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate)
if err != nil {
return 0, err
}
return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil
}
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
if s.hppRepo == nil {
return &HppCostResponse{}, nil
}
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return nil, err
}
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil {
return nil, err
}
estimation := HppCostDetail{
Total: totalProductionCost,
Kg: estimWeightKg,
Butir: estimPieces,
}
if estimWeightKg > 0 {
estimation.HargaKg = roundToTwoDecimals(totalProductionCost / estimWeightKg)
}
if estimPieces > 0 {
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
}
real := HppCostDetail{
Total: totalProductionCost,
Kg: realWeightKg,
Butir: realPieces,
}
if realWeightKg > 0 {
real.HargaKg = roundToTwoDecimals(totalProductionCost / realWeightKg)
}
if realPieces > 0 {
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
}
return &HppCostResponse{
Estimation: estimation,
Real: real,
}, nil
}
func roundToTwoDecimals(value float64) float64 {
return math.Round(value*100) / 100
}
@@ -1,3 +0,0 @@
ALTER TABLE recording_eggs
DROP COLUMN IF EXISTS total_used,
DROP COLUMN IF EXISTS total_qty;
@@ -1,7 +0,0 @@
ALTER TABLE recording_eggs
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
UPDATE recording_eggs
SET total_qty = qty
WHERE total_qty = 0;
@@ -1,3 +0,0 @@
-- Rollback: add price back to nonstock_suppliers
ALTER TABLE nonstock_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
@@ -1,3 +0,0 @@
-- Migration: remove price from nonstock_suppliers
ALTER TABLE nonstock_suppliers
DROP COLUMN IF EXISTS price;
@@ -1,4 +0,0 @@
-- Rollback: Remove requested_qty column from laying_transfer_sources table
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS requested_qty;
@@ -1,9 +0,0 @@
-- Add requested_qty column to laying_transfer_sources table
-- This field stores the quantity requested by user during create/update
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
ALTER TABLE laying_transfer_sources
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
-- Add comment for documentation
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
@@ -1,3 +0,0 @@
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS source_product_warehouse_id;
@@ -1,17 +0,0 @@
ALTER TABLE recording_depletions
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
UPDATE recording_depletions rd
SET source_product_warehouse_id = src.product_warehouse_id
FROM recordings r
JOIN LATERAL (
SELECT pfp.product_warehouse_id
FROM project_chickins pc
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
ORDER BY pfp.created_at ASC, pfp.id ASC
LIMIT 1
) AS src ON true
WHERE r.id = rd.recording_id
AND rd.source_product_warehouse_id IS NULL;
@@ -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,2 +0,0 @@
ALTER TABLE stock_logs
DROP COLUMN stock;
@@ -1,19 +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;
+21 -9
View File
@@ -2,16 +2,28 @@ package entities
import "time"
// AdjustmentStock tracks FIFO allocation for stock adjustments
// - For INCREASE adjustments (Stockable): Tracks stock added to warehouse
// - For DECREASE adjustments (Usable): Tracks stock consumed from warehouse
type AdjustmentStock struct {
Id uint `gorm:"primaryKey"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
TotalQty float64 `gorm:"column:total_qty;default:0"`
TotalUsed float64 `gorm:"column:total_used;default:0"`
UsageQty float64 `gorm:"column:usage_qty;default:0"`
PendingQty float64 `gorm:"column:pending_qty;default:0"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
Id uint `gorm:"primaryKey"`
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
// === FIFO FIELDS FOR INCREASE ADJUSTMENT (Stockable) ===
// Tracks stock added to warehouse via adjustment INCREASE
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot quantity available
TotalUsed float64 `gorm:"column:total_used;default:0"` // Quantity already used from this lot
// === FIFO FIELDS FOR DECREASE ADJUSTMENT (Usable) ===
// Tracks stock consumed from warehouse via adjustment DECREASE
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual quantity consumed
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Pending quantity (waiting for stock)
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
// Relations
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"`
}
@@ -11,7 +11,6 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""`
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Note string `gorm:"type:text"`
+1
View File
@@ -5,6 +5,7 @@ import "time"
type NonstockSupplier struct {
NonstockId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
+4 -6
View File
@@ -1,12 +1,10 @@
package entities
type RecordingDepletion struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
Qty float64 `gorm:"column:qty;not null"`
PendingQty float64 `gorm:"column:pending_qty"`
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty float64 `gorm:"column:qty;not null"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
-2
View File
@@ -7,8 +7,6 @@ type RecordingEgg struct {
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty int `gorm:"column:qty;not null"`
TotalQty float64 `gorm:"column:total_qty"`
TotalUsed float64 `gorm:"column:total_used"`
Weight *float64 `gorm:"column:weight"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
-1
View File
@@ -9,7 +9,6 @@ type StockLog struct {
Increase float64 `gorm:"column:increase;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"`
LoggableId uint `gorm:"column:loggable_id;not null"`
+2 -16
View File
@@ -1,9 +1,8 @@
package middleware
const (
const(
P_DashboardGetAll = "lti.dashboard.list"
)
// project-flock
const (
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
@@ -52,7 +51,6 @@ const (
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
)
const (
@@ -153,7 +151,7 @@ const (
P_ProductsCreateOne = "lti.master.products.create"
P_ProductsUpdateOne = "lti.master.products.update"
P_ProductsDeleteOne = "lti.master.products.delete"
P_SuppliersGetAll = "lti.master.suppliers.list"
P_SuppliersGetOne = "lti.master.suppliers.detail"
P_SuppliersCreateOne = "lti.master.suppliers.create"
@@ -240,15 +238,3 @@ const (
P_UserGetAll = "lti.users.list"
P_UserGetOne = "lti.users.detail"
)
// daily-checklist
const (
P_DailyChecklistDashboardList = "lti.daily_checklist.dashboard.list"
P_DailyChecklistCreateOne = "lti.daily_checklist.create"
P_DailyChecklistGetAll = "lti.daily_checklist.list"
P_DailyChecklistGetOne = "lti.daily_checklist.detail"
P_DailyChecklistReports = "lti.daily_checklist.reports"
P_DailyChecklistEmployee = "lti.daily_checklist.master_data.employee"
P_DailyChecklistActivity = "lti.daily_checklist.master_data.activity"
P_DailyChecklistActivityConfig = "lti.daily_checklist.master_data.configuration"
)
@@ -14,45 +14,22 @@ import (
)
type ClosingController struct {
ClosingService service.ClosingService
SapronakService service.SapronakService
ClosingKeuanganService service.ClosingKeuanganService
ClosingService service.ClosingService
SapronakService service.SapronakService
}
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
return &ClosingController{
ClosingService: closingService,
SapronakService: sapronakService,
ClosingKeuanganService: closingKeuanganService,
ClosingService: closingService,
SapronakService: sapronakService,
}
}
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
var projectStatus *int
if raw := c.Query("project_status"); raw != "" {
statusValue, err := strconv.Atoi(raw)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_status")
}
projectStatus = &statusValue
}
var locationID *uint
if raw := c.Query("location_id"); raw != "" {
locationValue, err := strconv.Atoi(raw)
if err != nil || locationValue <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
}
locationUint := uint(locationValue)
locationID = &locationUint
}
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
ProjectStatus: projectStatus,
LocationID: locationID,
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
@@ -181,7 +158,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(result),
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
})
}
@@ -211,7 +188,7 @@ func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) erro
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan by project flock kandang successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(result),
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
})
}
@@ -257,10 +234,9 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
@@ -299,45 +275,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
})
}
func (u *ClosingController) GetClosingSapronakSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Search: c.Query("search"),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved closing report (sapronak summary) successfully",
Data: result,
})
}
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
flag := c.Query("flag", "")
@@ -401,7 +338,7 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
if err != nil {
return err
}
@@ -415,34 +352,6 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
})
}
func (u *ClosingController) GetClosingKeuanganByKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing keuangan by kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
+20 -20
View File
@@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct {
}
type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"`
EggMass float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeight float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"`
HenHouseAct float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"`
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd *float64 `json:"hen_day_std,omitempty"`
EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
}
type ClosingSalesGroupDTO struct {
@@ -1,31 +1,59 @@
package dto
import (
"slices"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type ClosingHPPCode string
// === CONSTANTS ===
const (
HPPCodePakan ClosingHPPCode = "PAKAN"
HPPCodeOVK ClosingHPPCode = "OVK"
HPPCodeDOC ClosingHPPCode = "DOC"
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
HPPGroupPengeluaran = "HPP dan Pengeluaran"
HPPGroupBahanBaku = "HPP dan Bahan Baku"
HPPLabelOverhead = "Pengeluaran Overhead"
HPPLabelEkspedisi = "Beban Ekspedisi"
HPPSummaryLabel = "HPP"
PLSalesTypeChicken = "Penjualan Ayam Besar"
PLSalesTypeEgg = "Penjualan Telur"
PLItemTypeSapronak = "Pembelian Sapronak"
PLItemTypeOverhead = "Pengeluaran Overhead"
PLItemTypeEkspedisi = "Beban Ekspedisi"
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
PLSummaryLabelSubTotal = "SUB TOTAL"
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
PurchaseLabelPrefix = "Pembelian "
)
type ClosingProfitLossCode string
// === CONTEXT STRUCTS ===
const (
PLCodeSales ClosingProfitLossCode = "SALES"
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
)
type CalculationContext struct {
TotalPopulation float64
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
TotalWeightSold float64
ActualPopulation float64
}
type ClosingKeuanganInput struct {
ProjectFlockCategory string
PurchaseItems []entities.PurchaseItem
Budgets []entities.ProjectBudget
Realizations []entities.ExpenseRealization
DeliveryProducts []entities.MarketingDeliveryProduct
Chickins []entities.ProjectChickin
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
}
// === BASE METRICS ===
type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"`
@@ -33,58 +61,75 @@ type FinancialMetrics struct {
Amount float64 `json:"amount"`
}
type HPPItem struct {
ID uint `json:"id"`
Category string `json:"category"`
Code string `json:"code"`
Label string `json:"label"`
type Comparison struct {
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
}
type HPPSummary struct {
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
// === HPP PURCHASES PACKAGE ===
type HppItem struct {
Type string `json:"type"`
Comparison
}
type HPPSection struct {
Items []HPPItem `json:"items"`
Summary HPPSummary `json:"summary"`
type HppGroup struct {
GroupName string `json:"group_name"`
Data []HppItem `json:"data"`
}
type ProfitLossItem struct {
Code string `json:"code"`
Label string `json:"label"`
Type string `json:"type"`
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
type SummaryHpp struct {
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
}
type ProfitLossSummary struct {
GrossProfit FinancialMetrics `json:"gross_profit"`
SubTotal FinancialMetrics `json:"sub_total"`
NetProfit FinancialMetrics `json:"net_profit"`
type HppPurchasesSection struct {
Hpp []HppGroup `json:"hpp"`
SummaryHpp SummaryHpp `json:"summary_hpp"`
}
// === PROFIT LOSS PACKAGE ===
type PLItem struct {
Type string `json:"type"`
FinancialMetrics
}
type PLSummaryItem struct {
Label string `json:"label"`
FinancialMetrics
}
type PLSummaryGroup struct {
GrossProfit PLSummaryItem `json:"gross_profit"`
SubTotal PLSummaryItem `json:"sub_total"`
NetProfit PLSummaryItem `json:"net_profit"`
}
type ProfitLossData struct {
Penjualan []PLItem `json:"penjualan"`
Pembelian []PLItem `json:"pembelian"`
Overhead PLItem `json:"overhead"`
Ekspedisi PLItem `json:"ekspedisi"`
Summary PLSummaryGroup `json:"summary"`
}
type ProfitLossSection struct {
Items []ProfitLossItem `json:"items"`
Summary ProfitLossSummary `json:"summary"`
Data ProfitLossData `json:"data"`
}
type ClosingKeuanganData struct {
HPP HPPSection `json:"hpp"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
type MetricsCalculator struct {
TotalPopulation float64
ActualPopulation float64
TotalWeightProduced float64
// === RESPONSE DTO (ROOT) ===
type ReportResponse struct {
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// === MAPPER FUNCTIONS ===
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{
RpPerBird: rpPerBird,
@@ -93,133 +138,451 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
}
}
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
return HPPItem{
ID: id,
Category: category,
Code: code,
Label: label,
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
return Comparison{
Budgeting: budgeting,
Realization: realization,
}
}
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
return HPPSummary{
Label: label,
Budgeting: budgeting,
Realization: realization,
EggBudgeting: eggBudgeting,
EggRealization: eggRealization,
}
// === HPP PENGELUARAN (from Purchase Items) ===
func getFlagLabel(flagType utils.FlagType) string {
return PurchaseLabelPrefix + string(flagType)
}
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
return HPPSection{
Items: items,
Summary: summary,
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
flags := []utils.FlagType{
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
}
}
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
return ProfitLossItem{
Code: code,
Label: label,
Type: itemType,
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
}
}
items := []HppItem{}
seenFlags := make(map[utils.FlagType]bool)
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
return ProfitLossSummary{
GrossProfit: grossProfit,
SubTotal: subTotal,
NetProfit: netProfit,
}
}
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
return ProfitLossSection{
Items: items,
Summary: summary,
}
}
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
return ClosingKeuanganData{
HPP: hpp,
ProfitLoss: profitLoss,
}
}
func (mc *MetricsCalculator) CalculateMetrics(amount float64) (rpPerBird, rpPerKg float64) {
if mc.ActualPopulation > 0 {
rpPerBird = amount / mc.ActualPopulation
}
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 {
for _, item := range purchaseItems {
if item.Product == nil || len(item.Product.Flags) == 0 {
continue
}
if pf.ShouldIncludeProduct(delivery.MarketingProduct.ProductWarehouse.Product) {
filtered = append(filtered, delivery)
for _, flag := range item.Product.Flags {
flagType := utils.FlagType(flag.Name)
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
amount := sumPurchasesByFlag(purchaseItems, flagType)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
items = append(items, HppItem{
Type: getFlagLabel(flagType),
Comparison: ToComparison(
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
),
})
seenFlags[flagType] = true
}
}
}
return filtered
return items
}
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelOverhead,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
),
}
}
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelEkspedisi,
Comparison: ToComparison(
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
),
}
}
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
items := []HppItem{}
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
realizationAmount := getOperationalExpenses(realizations)
if budgetAmount > 0 || realizationAmount > 0 {
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
}
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
return HppGroup{
GroupName: HPPGroupBahanBaku,
Data: items,
}
}
// === HPP SUMMARY ===
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
purchaseTotal := sumPurchaseTotal(purchaseItems)
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
totalBudget := purchaseTotal + budgetTotal
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{
Label: label,
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
}
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
summary.EggBudgeting = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: budgetEggRpPerKg,
Amount: totalBudget,
}
summary.EggRealization = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: realizationEggRpPerKg,
Amount: totalRealization,
}
}
return summary
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: HPPGroupPengeluaran,
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
},
ToHppBahanBakuGroup(budgets, realizations, ctx),
}
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
return HppPurchasesSection{
Hpp: hppGroups,
SummaryHpp: summaryHpp,
}
}
// === PROFIT & LOSS ===
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
return PLItem{
Type: itemType,
FinancialMetrics: metrics,
}
}
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
return PLSummaryItem{
Label: label,
FinancialMetrics: metrics,
}
}
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
}
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
for _, item := range items {
totalAmount += item.Amount
totalPerBird += item.RpPerBird
}
return
}
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
}
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
items := []PLItem{}
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
} else {
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
}
return items
}
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
purchaseAmount := sumPurchaseTotal(purchases)
return []PLItem{
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
}
}
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
realizationAmount := getOperationalExpenses(realizations)
return []PLItem{
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
}
}
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
return []PLItem{
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
}
}
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
grossProfit := totalPenjualan - totalPembelian
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
totalOtherExpenses := totalOverhead + totalEkspedisi
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
netProfit := grossProfit - totalOtherExpenses
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
return PLSummaryGroup{
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
}
}
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
return ProfitLossData{
Penjualan: penjualanItems,
Pembelian: pembelianItems,
Overhead: totalOverhead,
Ekspedisi: totalEkspedisi,
Summary: summary,
}
}
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
return ProfitLossSection{
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
}
}
func aggregatePLItems(items []PLItem, label string) PLItem {
totalAmount, totalPerBird := sumPLItems(items)
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
}
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
return ReportResponse{
HppPurchases: hppPurchases,
ProfitLoss: profitLoss,
}
}
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
var totalPopulation float64
var totalWeightSold float64
for _, chickin := range input.Chickins {
totalPopulation += chickin.UsageQty
}
for _, delivery := range input.DeliveryProducts {
totalWeightSold += delivery.TotalWeight
}
ctx := CalculationContext{
TotalPopulation: totalPopulation,
TotalWeightProduced: input.TotalWeightProduced,
TotalEggWeightKg: input.TotalEggWeightKg,
TotalDepletion: input.TotalDepletion,
TotalWeightSold: totalWeightSold,
ActualPopulation: totalPopulation - input.TotalDepletion,
}
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
overheadItems := ToOverheadItems(input.Realizations, ctx)
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
return ToReportResponse(hppSection, plSection)
}
// === HELPER FUNCTIONS ===
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
if totalPopulation > 0 {
rpPerBird = amount / totalPopulation
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
return rpPerBird, rpPerKg
}
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
for _, flag := range flags {
if strings.ToUpper(flag.Name) == string(flagType) {
return true
}
}
return false
}
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
return func(item *entities.PurchaseItem) bool {
if item.Product == nil || len(item.Product.Flags) == 0 {
return false
}
return hasProductFlag(item.Product.Flags, flagType)
}
}
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
return func(realization *entities.ExpenseRealization) bool {
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
return false
}
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
}
}
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
hasFlag := filterRealizationByNonstockFlag(flagType)
return func(realization *entities.ExpenseRealization) bool {
return !hasFlag(realization)
}
}
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
amount := 0.0
for i := range items {
if filter(&items[i]) {
amount += extractor(&items[i])
}
}
return amount
}
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
}
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
}
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
}
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
}
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
}
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
}
func isChickenProductFlag(flagType utils.FlagType) bool {
switch flagType {
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
return true
}
return false
}
func isEggProductFlag(flagType utils.FlagType) bool {
switch flagType {
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
utils.FlagTelurPutih, utils.FlagTelurRetak:
return true
}
return false
}
func getSalesTypeFromProductFlags(product *entities.Product) string {
if product == nil || len(product.Flags) == 0 {
return PLSalesTypeChicken
}
for _, flag := range product.Flags {
flagType := utils.FlagType(strings.ToUpper(flag.Name))
if isEggProductFlag(flagType) {
return PLSalesTypeEgg
}
if isChickenProductFlag(flagType) {
return PLSalesTypeChicken
}
}
return PLSalesTypeChicken
}
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
categorized := make(map[string][]entities.MarketingDeliveryProduct)
for _, delivery := range deliveries {
product := delivery.MarketingProduct.ProductWarehouse.Product
salesType := getSalesTypeFromProductFlags(&product)
categorized[salesType] = append(categorized[salesType], delivery)
}
return categorized
}
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
amount := 0.0
for _, delivery := range deliveries {
amount += delivery.TotalPrice
}
return amount
}
@@ -8,54 +8,34 @@ import (
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === Response DTO ===
type SalesDTO struct {
Id uint `json:"id"`
RealizationDate time.Time `json:"realization_date"`
Age int `json:"age"`
Week int `json:"week"`
DoNumber string `json:"do_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AvgWeight float64 `json:"avg_weight"`
SalesPrice float64 `json:"sales_price"`
TotalSalesPrice float64 `json:"total_sales_price"`
ActualPrice float64 `json:"actual_price"`
TotalActualPrice float64 `json:"total_actual_price"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
}
type SummaryDTO struct {
TotalSalesPrice float64 `json:"total_sales_price"`
AvgSalesPrice float64 `json:"avg_sales_price"`
TotalActualPrice float64 `json:"total_actual_price"`
AvgActualPrice float64 `json:"avg_actual_price"`
Id uint `json:"id"`
RealizationDate time.Time `json:"realization_date"`
Age int `json:"age"`
DoNumber string `json:"do_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AvgWeight float64 `json:"avg_weight"`
Price float64 `json:"price"`
TotalPrice float64 `json:"total_price"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
PaymentStatus string `json:"payment_status"`
}
type PenjualanRealisasiResponseDTO struct {
Sales []SalesDTO `json:"sales"`
Summary SummaryDTO `json:"summary"`
Sales []SalesDTO `json:"sales"`
}
// === Mapper Functions ===
func ToSalesDTO(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, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
var product *productDTO.ProductRelationDTO
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
@@ -83,42 +63,19 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
return SalesDTO{
Id: e.Id,
RealizationDate: realizationDate,
Age: ageInDay,
Week: ageInWeeks,
DoNumber: doNumber,
Product: product,
Customer: customer,
Qty: e.UsageQty,
Weight: e.TotalWeight,
AvgWeight: e.AvgWeight,
SalesPrice: e.MarketingProduct.UnitPrice,
TotalSalesPrice: e.MarketingProduct.TotalPrice,
ActualPrice: e.UnitPrice,
TotalActualPrice: e.TotalPrice,
Kandang: kandang,
}
}
func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO {
var totalSalesPrice, totalActualPrice, sumSales, sumActual float64
count := len(e)
for _, item := range e {
totalSalesPrice += item.MarketingProduct.TotalPrice
totalActualPrice += item.TotalPrice
sumSales += item.MarketingProduct.UnitPrice
sumActual += item.UnitPrice
}
return SummaryDTO{
TotalSalesPrice: totalSalesPrice,
TotalActualPrice: totalActualPrice,
AvgSalesPrice: sumSales / float64(count),
AvgActualPrice: sumActual / float64(count),
Id: e.Id,
RealizationDate: realizationDate,
Age: age,
DoNumber: doNumber,
Product: product,
Customer: customer,
Qty: e.UsageQty,
Weight: e.TotalWeight,
AvgWeight: e.AvgWeight,
Price: e.UnitPrice,
TotalPrice: e.TotalPrice,
Kandang: kandang,
PaymentStatus: "Paid",
}
}
@@ -130,36 +87,29 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
return result
}
func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
return PenjualanRealisasiResponseDTO{
Sales: ToSalesDTOs(e),
Summary: ToSummaryDto(e),
Sales: ToSalesDTOs(e),
}
}
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 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
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
if len(realisasi) > 0 {
for _, item := range realisasi {
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
}
}
}
return 0
}
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0
}
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
for _, chickin := range projectFlockKandang.Chickins {
@@ -168,20 +118,7 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de
}
}
diff := deliveryDate.Sub(earliestChickinDate)
ageInDays := int(diff.Hours() / 24)
var ageInWeeks int
if ageInDays <= 0 {
ageInWeeks = 0
} else {
if category == string(utils.ProjectFlockCategoryLaying) {
ageInDays = ageInDays + 119
ageInWeeks = ((ageInDays - 1) / 7) + 1
} else {
ageInWeeks = ((ageInDays - 1) / 7) + 1
}
}
return ageInDays, ageInWeeks
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
ageInWeeks := ageInDays / 7
return ageInWeeks
}
@@ -114,17 +114,6 @@ type ClosingSapronakDTO struct {
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
}
type ClosingSapronakSummaryItemDTO struct {
Category string `json:"category"`
TotalQty int64 `json:"total_qty"`
Uom UomSummaryDTO `json:"uom"`
}
type UomSummaryDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
@@ -196,11 +185,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
}
for idx, item := range group.Items {
refKey := strings.TrimSpace(item.NoReferensi)
productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey)
if refKey == "" {
productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal))
}
productKey := strings.ToUpper(flagKey + "|" + item.ProductName)
baseRow := SapronakCategoryRowDTO{
ID: idx + 1,
Date: formatDate(item.Tanggal),
@@ -216,51 +201,18 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk
if item.Tanggal != nil {
row.Date = formatDate(item.Tanggal)
}
if row.UnitPrice == 0 {
if item.QtyMasuk > 0 && item.Nilai > 0 {
row.UnitPrice = item.Nilai / item.QtyMasuk
} else if item.Harga > 0 {
row.UnitPrice = item.Harga
}
}
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
row.TotalAmount += item.Nilai
case "pemakaian", "adjustment keluar":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price
case "mutasi keluar", "penjualan":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
case "mutasi keluar":
row.QtyOut += item.QtyKeluar
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
default:
row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai
if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn
}
}
if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn
}
}
@@ -281,8 +233,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
total += r.TotalAmount
}
avg := 0.0
if qtyUsed > 0 {
avg = total / qtyUsed
if qtyIn > 0 {
avg = total / qtyIn
}
cat.Total = SapronakCategoryTotalDTO{
Label: label,
+1 -4
View File
@@ -39,13 +39,10 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
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)
closingKeuanganService := sClosing.NewClosingKeuanganService(projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo, hppService, hppCostRepo)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
ClosingRoutes(router, userService, closingService, sapronakService)
}
@@ -17,7 +17,6 @@ import (
type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
@@ -32,10 +31,9 @@ type ClosingRepository interface {
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, 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)
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
}
@@ -62,18 +60,10 @@ type SapronakRow struct {
DestinationWarehouse string `gorm:"column:destination_warehouse"`
Destination string `gorm:"column:destination"`
Quantity float64 `gorm:"column:quantity"`
UnitID uint `gorm:"column:unit_id"`
Unit string `gorm:"column:unit"`
Notes string `gorm:"column:notes"`
}
type SapronakSummaryRow struct {
Category string `gorm:"column:category"`
TotalQty int64 `gorm:"column:total_qty"`
UomID uint `gorm:"column:uom_id"`
UomName string `gorm:"column:uom_name"`
}
type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"`
@@ -85,7 +75,6 @@ type SapronakQueryParams struct {
ProjectFlockKandangIDs []uint
Limit int
Offset int
Search string
}
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
@@ -121,36 +110,14 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
unionSQL := strings.Join(unionParts, " UNION ALL ")
search := strings.TrimSpace(params.Search)
searchClause := ""
var searchArgs []any
if search != "" {
searchClause = `
WHERE (
reference_number ILIKE ?
OR product_name ILIKE ?
OR product_category ILIKE ?
OR source_warehouse ILIKE ?
OR destination_warehouse ILIKE ?
OR CAST(quantity AS TEXT) ILIKE ?
OR unit ILIKE ?
OR notes ILIKE ?
OR transaction_type ILIKE ?
)`
like := "%" + search + "%"
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
}
var totalResults int64
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
countArgs := append(append([]any{}, args...), searchArgs...)
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
return nil, 0, err
}
dataArgs := append(append([]any{}, args...), searchArgs...)
dataArgs = append(dataArgs, params.Limit, params.Offset)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
var rows []SapronakRow
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
@@ -160,79 +127,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil
}
func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
db := r.DB().WithContext(ctx)
var (
unionParts []string
args []any
)
switch params.Type {
case validation.SapronakTypeIncoming:
if len(params.WarehouseIDs) == 0 {
return []SapronakSummaryRow{}, nil
}
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
args = append(args, params.WarehouseIDs)
}
if len(params.ProjectFlockKandangIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
args = append(args, params.ProjectFlockKandangIDs)
}
if len(unionParts) == 0 {
return []SapronakSummaryRow{}, nil
}
default:
return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
}
unionSQL := strings.Join(unionParts, " UNION ALL ")
search := strings.TrimSpace(params.Search)
searchClause := ""
var searchArgs []any
if search != "" {
searchClause = `
WHERE (
reference_number ILIKE ?
OR product_name ILIKE ?
OR product_category ILIKE ?
OR source_warehouse ILIKE ?
OR destination_warehouse ILIKE ?
OR CAST(quantity AS TEXT) ILIKE ?
OR unit ILIKE ?
OR notes ILIKE ?
OR transaction_type ILIKE ?
)`
like := "%" + search + "%"
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
}
querySQL := fmt.Sprintf(`
SELECT
product_category AS category,
CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
unit_id AS uom_id,
unit AS uom_name
FROM (%s) AS combined%s
GROUP BY product_category, unit_id, unit
ORDER BY product_category ASC, unit ASC
`, unionSQL, searchClause)
queryArgs := append(append([]any{}, args...), searchArgs...)
var rows []SapronakSummaryRow
if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
@@ -424,7 +318,6 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo
Joins("JOIN suppliers s ON s.id = e.supplier_id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("e.category = ?", "BOP").
Where("e.realization_date IS NOT NULL").
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
@@ -486,7 +379,6 @@ SELECT
w.name AS destination_warehouse,
'' AS destination,
pi.total_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
COALESCE(p.notes, '') AS notes
FROM purchase_items pi
@@ -535,7 +427,6 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination,
std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
'Stock Refill' AS notes
FROM stock_transfer_details std
@@ -582,10 +473,9 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
COALESCE(fw.name, '') AS source_warehouse,
COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination,
'' AS destination_warehouse,
COALESCE(tw.name, '') AS destination,
std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
'Transfer to other unit' AS notes
FROM stock_transfer_details std
@@ -632,27 +522,18 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
w.name AS source_warehouse,
COALESCE(c.name, '') AS destination_warehouse,
'' AS destination,
'' AS destination_warehouse,
'RETAIL CUSTOMER' AS destination,
mp.qty AS quantity,
u.id AS unit_id,
u.name AS unit,
m.notes AS notes
FROM marketing_products mp
JOIN marketings m ON m.id = mp.marketing_id
LEFT JOIN customers c ON c.id = m.customer_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.project_flock_kandang_id IN ?
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
)
`
)
@@ -709,23 +590,6 @@ var (
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 {
m := make(map[uint][]SapronakDetailRow)
for _, row := range rows {
@@ -762,12 +626,11 @@ func (r *ClosingRepositoryImpl) usageQuery(
COALESCE(p.product_price, 0) AS default_price
`)
db = applyJoins(db, joins...)
db = db.
return db.
Joins("JOIN product_warehouses pw ON "+pwJoinCond).
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...)
db = r.joinSapronakProductFlag(db, "p")
return db
}
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
@@ -798,10 +661,10 @@ func (r *ClosingRepositoryImpl) detailQuery(
db := r.withCtx(ctx).
Table(table).
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 = r.joinSapronakProductFlag(db, "p")
return db.Select(selectSQL).Where(where, args...)
}
@@ -891,84 +754,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,
sl.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 stock_logs sl ON sl.id = ast.stock_log_id").
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, sl.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 {
db := r.withCtx(ctx).
return r.withCtx(ctx).
Table("purchase_items AS pi").
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 flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL")
return r.joinSapronakProductFlag(db, "p")
}
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
@@ -1039,10 +834,10 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui
`).
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_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")
db = applyJoins(db, joins...)
db = r.joinSapronakProductFlag(db, "p")
if err := db.
Where("sl.loggable_type = ?", logType).
@@ -1094,196 +889,143 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
}
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
incomingQuery := r.withCtx(ctx).
Table("stock_transfer_details AS std").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
COALESCE(std.total_qty, 0) AS qty_in,
0 AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN warehouses fw ON fw.id = st.from_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 products p ON p.id = std.product_id").
Where("w.kandang_id = ?", kandangID).
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll)
incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
incoming, err := scanAndGroupDetails(incomingQuery)
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
if err != nil {
return nil, nil, err
}
incomingLayingQuery := r.withCtx(ctx).
Table("laying_transfer_targets AS ltt").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
lt.transfer_date::timestamp AS date,
COALESCE(lt.transfer_number, '') AS reference,
COALESCE(ltt.total_qty, 0) AS qty_in,
0 AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.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 products p ON p.id = pw.product_id").
Where("w.kandang_id = ?", kandangID).
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll)
incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p")
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
if err != nil {
return nil, nil, err
}
for pid, rows := range incomingLaying {
incoming[pid] = append(incoming[pid], rows...)
}
outgoingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.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("JOIN products p ON p.id = std.product_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll).
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)
if err != nil {
return nil, nil, err
}
outgoingLayingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
lt.transfer_date::timestamp AS date,
COALESCE(lt.transfer_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.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").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll).
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)
if err != nil {
return nil, nil, err
}
for pid, rows := range outgoingLaying {
outgoing[pid] = append(outgoing[pid], rows...)
}
return incoming, outgoing, nil
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
return ref
}
return fmt.Sprintf("TRF-%d", row.ID)
})
return in, out, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) {
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(mdp.delivery_date, mdp.created_at) AS date,
COALESCE(m.so_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
`).
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
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 = sa.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
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")
type ActualUsageCostRow struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
FlagName string `gorm:"column:flag_name"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
AveragePrice float64 `gorm:"column:average_price"`
}
query = r.joinSapronakProductFlag(query, "p")
sales, err := scanAndGroupDetails(query)
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) {
if projectFlockID == 0 {
return []ActualUsageCostRow{}, nil
}
db := r.DB().WithContext(ctx)
// Get all project flock kandang IDs for this project flock
var pfkIDs []uint
err := db.Table("project_flock_kandangs").
Where("project_flock_id = ?", projectFlockID).
Pluck("id", &pfkIDs).Error
if err != nil {
return nil, err
}
nonFifoQuery := r.withCtx(ctx).
Table("marketing_delivery_products AS mdp").
if len(pfkIDs) == 0 {
return []ActualUsageCostRow{}, nil
}
var rows []ActualUsageCostRow
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
recordingQuery := db.
Table("recordings AS r").
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")
COALESCE(f.name, tf.name) AS flag_name,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) AS total_qty,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0) AS total_price,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) AS qty_divisor,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0) / NULLIF(COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0), 0) AS average_price`,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey).
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?",
"recording_stocks", entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id IN ?", pfkIDs).
Where("r.deleted_at IS NULL").
Group("pw.product_id, p.name, COALESCE(f.name, tf.name)")
nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p")
nonFifoSales, err := scanAndGroupDetails(nonFifoQuery)
if err != nil {
if err := recordingQuery.Scan(&rows).Error; err != nil {
return nil, err
}
for pid, rows := range nonFifoSales {
sales[pid] = append(sales[pid], rows...)
chickinQuery := db.
Table("project_chickins AS pc").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag_name,
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
`).
Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id").
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("pc.project_flock_kandang_id IN ?", pfkIDs).
Where("pc.usage_qty > 0").
Group("pw.product_id, p.name, f.name")
var chickinRows []ActualUsageCostRow
if err := chickinQuery.Scan(&chickinRows).Error; err != nil {
return nil, err
}
return sales, nil
rows = append(rows, chickinRows...)
return rows, nil
}
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
+2 -4
View File
@@ -9,8 +9,8 @@ import (
"github.com/gofiber/fiber/v2"
)
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
ctrl := controller.NewClosingController(s, sapronakSvc)
route := v1.Group("/closings")
route.Use(m.Auth(u))
@@ -30,11 +30,9 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang)
}
@@ -40,7 +40,7 @@ type ClosingService interface {
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
}
@@ -99,31 +99,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
}
offset := (params.Page - 1) * params.Limit
statusFilter := ""
if params.ProjectStatus != nil {
switch *params.ProjectStatus {
case 1:
statusFilter = "Pengajuan"
case 2:
statusFilter = "Aktif"
}
}
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withClosingRelations(db)
if params.LocationID != nil {
db = db.Where("location_id = ?", *params.LocationID)
}
if statusFilter != "" {
latestApprovalSubQuery := s.Repository.DB().
WithContext(c.Context()).
Table("approvals").
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
Where("approvable_type = ?", utils.ApprovalWorkflowProjectFlock.String()).
Order("approvable_id, id DESC")
db = db.Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
Where("LOWER(latest_approval.step_name) = LOWER(?)", statusFilter)
}
if params.Search != "" {
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
}
@@ -162,12 +140,7 @@ 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) {
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, err
}
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID, projectFlock.Category)
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil {
return nil, err
}
@@ -364,10 +337,8 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
}
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
@@ -381,7 +352,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
ProjectFlockKandangIDs: projectFlockKandangIDs,
Limit: params.Limit,
Offset: offset,
Search: params.Search,
})
if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
@@ -416,74 +386,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
return items, totalResults, nil
}
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if params == nil {
params = &validation.ClosingSapronakQuery{}
}
if err := s.Validate.Struct(params); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
if err != nil {
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")
}
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
}
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
Type: params.Type,
WarehouseIDs: warehouseIDs,
ProjectFlockKandangIDs: projectFlockKandangIDs,
Search: params.Search,
})
if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
}
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, dto.ClosingSapronakSummaryItemDTO{
Category: row.Category,
TotalQty: row.TotalQty,
Uom: dto.UomSummaryDTO{
ID: row.UomID,
Name: row.UomName,
},
})
}
return items, nil
}
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
var kandangIDs []uint
db := s.Repository.DB().WithContext(ctx)
@@ -516,11 +418,14 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
return ids, nil
}
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
var ids []uint
query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID)
if kandangID != nil {
query = query.Where("kandang_id = ?", *kandangID)
}
err := query.Order("id ASC").Pluck("id", &ids).Error
if err != nil {
return nil, err
@@ -672,6 +577,82 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl
return &result, nil
}
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
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")
}
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
}
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
}
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product")
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
}
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
}
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
input := dto.ClosingKeuanganInput{
ProjectFlockCategory: projectFlock.Category,
PurchaseItems: purchaseItems,
Budgets: budgets,
Realizations: realizations,
DeliveryProducts: deliveryProducts,
Chickins: chickins,
TotalWeightProduced: totalWeightProduced,
TotalEggWeightKg: totalEggWeightKg,
TotalDepletion: totalDepletion,
}
report := dto.ToClosingKeuanganReport(input)
return &report, nil
}
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
@@ -709,16 +690,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
var projectFlockKandangIDs []uint
if kandangID != nil && *kandangID > 0 {
projectFlockKandangIDs = []uint{*kandangID}
} else {
var err error
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
if len(projectFlockKandangIDs) == 0 {
@@ -748,10 +723,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
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)
if err != nil {
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
@@ -961,19 +932,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
if !isGrowing {
if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = henDayAct
performance.HenDayAct = &henDayAct
}
if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = henHouseAct
performance.HenHouseAct = &henHouseAct
}
if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = eggWeight
performance.EggWeight = &eggWeight
}
if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg
performance.EggMass = eggMass
performance.EggMass = &eggMass
}
}
performance.DeffFcr = performance.FcrStd - performance.FcrAct
@@ -983,16 +954,16 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
}
if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil {
performance.HendayStd = *productionStandardDetail.TargetHenDayProduction
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
}
if productionStandardDetail.TargetHenHouseProduction != nil {
performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
}
if productionStandardDetail.TargetEggWeight != nil {
performance.EggWeightStd = *productionStandardDetail.TargetEggWeight
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
}
if productionStandardDetail.TargetEggMass != nil {
performance.EggMassStd = *productionStandardDetail.TargetEggMass
performance.EggMassStd = productionStandardDetail.TargetEggMass
}
}
}
@@ -1131,3 +1102,53 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
return closest.Mortality, closest.FcrNumber
}
func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem {
if len(actualUsageRows) == 0 {
return []entity.PurchaseItem{}
}
// Collect all product IDs
productIDs := make([]uint, len(actualUsageRows))
for i, row := range actualUsageRows {
productIDs[i] = row.ProductID
}
// Fetch products with flags from repository
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs)
if err != nil {
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
products = []entity.Product{}
}
// Create product map
productMap := make(map[uint]*entity.Product)
for i := range products {
productMap[products[i].Id] = &products[i]
}
// Convert to pseudo purchase items
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
for _, row := range actualUsageRows {
product := productMap[row.ProductID]
// Skip if product not found
if product == nil {
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
continue
}
purchaseItem := entity.PurchaseItem{
Id: 0, // Pseudo item, no ID
ProductId: row.ProductID,
TotalQty: row.TotalQty,
TotalPrice: row.TotalPrice,
Price: row.AveragePrice,
Product: product,
}
purchaseItems = append(purchaseItems, purchaseItem)
}
return purchaseItems
}
@@ -1,506 +0,0 @@
package service
import (
"errors"
"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"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/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"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
// ClosingKeuanganService handles closing keuangan business logic
type ClosingKeuanganService interface {
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID 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
TotalSalesAmount float64
}
type closingKeuanganService struct {
Log *logrus.Logger
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
RecordingRepo recordingRepository.RecordingRepository
HppSvc commonSvc.HppService
HppRepo commonRepo.HppCostRepository
}
func NewClosingKeuanganService(
projectFlockRepo projectflockRepository.ProjectflockRepository,
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
chickinRepo chickinRepository.ProjectChickinRepository,
recordingRepo recordingRepository.RecordingRepository,
hppSvc commonSvc.HppService,
hppRepo commonRepo.HppCostRepository,
) ClosingKeuanganService {
return &closingKeuanganService{
Log: utils.Log,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
HppSvc: hppSvc,
HppRepo: hppRepo,
}
}
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs)
}
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
if projectFlockKandang.ProjectFlockId != projectFlockID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
projectFlockKandangs := []entity.ProjectFlockKandang{*projectFlockKandang}
return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs)
}
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
var projectFlockKandangID *uint
if isPerKandang {
kandangID := projectFlockKandangs[0].Id
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
costs.FeedCost, err = s.HppRepo.GetFeedUsageCost(c.Context(), projectFlockKandangIDs, nil)
if err != nil {
costs.FeedCost = 0
}
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 {
for _, projectFlockKandang := range projectFlockKandangs {
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 {
data.TotalDepletion = 0
}
if projectFlockKandangID != nil {
data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
}
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.GetClosingPenjualan(c.Context(), projectFlock.Id, projectFlockKandangID, projectFlock.Category)
} else {
deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, nil, projectFlock.Category)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data penjualan")
}
for _, delivery := range deliveryProducts {
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue
}
data.TotalWeightSold += delivery.TotalWeight
data.TotalSalesAmount += delivery.TotalPrice
}
return data, nil
}
func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection {
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
totalWeightProduced := production.TotalWeightProduced
totalEggWeightKg := production.TotalEggWeightKg
weightForCalculation := totalWeightProduced
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
weightForCalculation = totalEggWeightKg
}
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if weightForCalculation > 0 {
rpPerKg = amount / weightForCalculation
}
return
}
createHPPItem := func(id uint, category, code, label string, budgetAmount, realizationAmount float64) dto.HPPItem {
budgetRpPerBird, budgetRpPerKg := calculateMetrics(budgetAmount)
realizationRpPerBird, realizationRpPerKg := calculateMetrics(realizationAmount)
return dto.ToHPPItem(
id,
category,
code,
label,
dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
)
}
hppItems := []dto.HPPItem{}
hppItems = append(hppItems, createHPPItem(1, "purchase", string(dto.HPPCodePakan), "Pembelian Pakan", costs.FeedCost, costs.FeedCost))
hppItems = append(hppItems, createHPPItem(2, "purchase", string(dto.HPPCodeOVK), "Pembelian OVK", costs.OvkCost, costs.OvkCost))
docCode := string(dto.HPPCodeDOC)
docLabel := "Pembelian DOC"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
docCode = string(dto.HPPCodeDepresiasi)
docLabel = "Depresiasi"
}
hppItems = append(hppItems, createHPPItem(3, "purchase", docCode, docLabel, costs.ChickenCost, costs.ChickenCost))
hppItems = append(hppItems, createHPPItem(4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", costs.BudgetOperational, costs.RealizationOperational))
hppItems = append(hppItems, createHPPItem(5, "overhead", string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", costs.ExpeditionCost, costs.ExpeditionCost))
totalBudgetHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.BudgetOperational + costs.ExpeditionCost
totalRealizationHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.RealizationOperational + costs.ExpeditionCost
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
var eggBudgeting, eggRealization *dto.FinancialMetrics
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
accumulateEggMetrics := func(metrics **dto.FinancialMetrics, amount, rpPerKg float64) {
if *metrics == nil {
*metrics = &dto.FinancialMetrics{
RpPerBird: 0,
RpPerKg: rpPerKg,
Amount: amount,
}
} else {
(*metrics).Amount += amount
if totalEggWeightKg > 0 {
(*metrics).RpPerKg = (*metrics).Amount / totalEggWeightKg
}
}
}
for _, projectFlockKandang := range projectFlockKandangs {
hppResponse, err := s.HppSvc.CalculateHppCost(projectFlockKandang.Id, nil)
if err == nil {
accumulateEggMetrics(&eggBudgeting, hppResponse.Estimation.Total, hppResponse.Estimation.HargaKg)
accumulateEggMetrics(&eggRealization, hppResponse.Real.Total, hppResponse.Real.HargaKg)
}
}
}
hppSummary := dto.ToHPPSummary(
"HPP",
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
eggBudgeting,
eggRealization,
)
return dto.ToHPPSection(hppItems, hppSummary)
}
func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection {
totalPopulationIn := production.TotalPopulationIn
totalWeightProduced := production.TotalWeightProduced
totalEggWeightKg := production.TotalEggWeightKg
totalSalesAmount := production.TotalSalesAmount
totalWeightSold := production.TotalWeightSold
weightForSales := totalWeightSold
weightForCalculation := totalWeightProduced
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
weightForSales = totalWeightSold
weightForCalculation = totalEggWeightKg
}
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if totalPopulationIn > 0 {
rpPerBird = amount / totalPopulationIn
}
if weightForSales > 0 {
rpPerKg = amount / weightForSales
}
return
}
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if weightForCalculation > 0 {
rpPerKg = amount / weightForCalculation
}
return
}
plItems := []dto.ProfitLossItem{}
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
salesLabel := "Penjualan Ayam"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
salesLabel = "Penjualan Telur"
}
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSales),
salesLabel,
"income",
salesRpPerBird,
salesRpPerKg,
totalSalesAmount,
))
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost
_, sapronakRpPerKg := calculateMetrics(totalSapronakAmount)
sapronakRpPerBird := 0.0
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} {
rpPerBird, _ := calculateMetrics(amount)
sapronakRpPerBird += rpPerBird
}
sapronakLabel := "Pengeluaran Sapronak"
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSapronak),
sapronakLabel,
"purchase",
sapronakRpPerBird,
sapronakRpPerKg,
totalSapronakAmount,
))
overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational)
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeOverhead),
"Overhead",
"overhead",
overheadRpPerBird,
overheadRpPerKg,
costs.RealizationOperational,
))
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost)
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeEkspedisi),
"Ekspedisi",
"overhead",
ekspedisiRpPerBird,
ekspedisiRpPerKg,
costs.ExpeditionCost,
))
costOfGoodsSold := costs.ChickenCost + costs.FeedCost + costs.OvkCost
costOfGoodsSoldRpPerBird := sapronakRpPerBird
costOfGoodsSoldRpPerKg := sapronakRpPerKg
grossProfit := totalSalesAmount - costOfGoodsSold
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
grossProfitRpPerKg := salesRpPerKg - costOfGoodsSoldRpPerKg
totalOperatingExpenses := costs.RealizationOperational + costs.ExpeditionCost
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRpPerBird
totalOperatingExpensesRpPerKg := overheadRpPerKg + ekspedisiRpPerKg
netProfit := grossProfit - totalOperatingExpenses
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
netProfitRpPerKg := grossProfitRpPerKg - totalOperatingExpensesRpPerKg
plSummary := dto.ToProfitLossSummary(
dto.ToFinancialMetrics(grossProfitRpPerBird, grossProfitRpPerKg, grossProfit),
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, totalOperatingExpensesRpPerKg, totalOperatingExpenses),
dto.ToFinancialMetrics(netProfitRpPerBird, netProfitRpPerKg, netProfit),
)
return dto.ToProfitLossSection(plItems, plSummary)
}
func containsFlag(flags []entity.Flag, name string) bool {
for _, flag := range flags {
if flag.Name == name {
return true
}
}
return false
}
@@ -2,8 +2,8 @@ package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -112,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
}
// We no longer filter by date for closing sapronak report; pass nil pointers.
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
if err != nil {
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
@@ -126,6 +126,8 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
KandangName: pfk.Kandang.Name,
Period: pfk.Period,
Status: status,
StartDate: nil,
EndDate: nil,
TotalIncomingValue: totalIncoming,
TotalUsageValue: totalUsage,
Items: items,
@@ -263,7 +265,6 @@ type sapronakDetailMaps struct {
AdjOutgoing map[uint][]dto.SapronakDetailDTO
TransferIn map[uint][]dto.SapronakDetailDTO
TransferOut map[uint][]dto.SapronakDetailDTO
SalesOut map[uint][]dto.SapronakDetailDTO
}
func buildSapronakDetails(
@@ -273,7 +274,6 @@ func buildSapronakDetails(
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
transferInRows map[uint][]repository.SapronakDetailRow,
transferOutRows map[uint][]repository.SapronakDetailRow,
salesOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps {
result := sapronakDetailMaps{
Incoming: make(map[uint][]dto.SapronakDetailDTO),
@@ -282,7 +282,6 @@ func buildSapronakDetails(
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
SalesOut: make(map[uint][]dto.SapronakDetailDTO),
}
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
@@ -315,12 +314,11 @@ func buildSapronakDetails(
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
return result
}
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
// For sapronak closing report we intentionally ignore date range
// and aggregate all historical transactions for the kandang/project.
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
@@ -347,14 +345,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil {
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)
if err != nil {
return nil, nil, 0, 0, err
@@ -363,10 +353,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil {
return nil, nil, 0, 0, err
}
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.Id)
if err != nil {
return nil, nil, 0, 0, err
}
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
matchesFlag := func(f string) bool {
@@ -379,34 +365,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
return candidate == filterFlag
}
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
seen := make(map[string]struct{})
for pid, rows := range src {
for _, d := range rows {
dateKey := ""
if d.Tanggal != nil {
dateKey = d.Tanggal.Format("2006-01-02")
}
qtyKey := d.QtyMasuk
if qtyKey == 0 {
qtyKey = d.QtyKeluar
}
ref := strings.TrimSpace(d.NoReferensi)
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
if ref == "" {
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result[pid] = append(result[pid], d)
}
}
return result
}
// For project flocks with category GROWING, pullet usage from chickin
// should not be counted yet. Only when category is LAYING we allow
@@ -445,17 +403,13 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
}
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
incomingDetails := detailMaps.Incoming
usageDetails := detailMaps.Usage
adjIncoming := detailMaps.AdjIncoming
adjOutgoing := detailMaps.AdjOutgoing
transIncoming := detailMaps.TransferIn
transOutgoing := detailMaps.TransferOut
salesOutgoing := detailMaps.SalesOut
transIncoming = dedupTransfers(transIncoming)
transOutgoing = dedupTransfers(transOutgoing)
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
if g, ok := groupMap[flag]; ok {
@@ -465,22 +419,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
return groupMap[flag]
}
resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if flag == "" && len(details) > 0 {
flag = details[0].Flag
}
if name == "" && len(details) > 0 {
name = details[0].ProductName
}
return flag, name
}
for _, row := range incoming {
if !matchesFlag(row.Flag) {
continue
@@ -616,18 +554,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range incomingDetails {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai
@@ -636,18 +575,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range adjIncoming {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai
@@ -656,18 +596,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range usageDetails {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
@@ -675,18 +616,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range adjOutgoing {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
@@ -694,18 +636,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range transIncoming {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai
@@ -714,37 +657,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range transOutgoing {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
}
}
for productID, details := range salesOutgoing {
flag, name := resolveFlagName(productID, details)
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
@@ -9,11 +9,9 @@ type Update struct {
}
type Query struct {
Page int `query:"page" 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,max=50"`
ProjectStatus *int `query:"project_status" validate:"omitempty,oneof=1 2"`
LocationID *uint `query:"location_id" validate:"omitempty,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"`
Search string `query:"search" validate:"omitempty,max=50"`
}
const (
@@ -26,5 +24,4 @@ type ClosingSapronakQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
}
@@ -83,7 +83,7 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"KANDANG",
},
"stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT", "MARKETING", "CHICKIN", "PURCHASE", "RECORDING"},
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
"transaction_types": []string{"INCREASE", "DECREASE"},
},
"supplier_categories": []string{
+14 -14
View File
@@ -15,49 +15,49 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
route := v1.Group("/daily-checklists")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_DailyChecklistGetAll), ctrl.GetAll)
route.Get("/report", m.RequirePermissions(m.P_DailyChecklistReports), ctrl.GetReport)
route.Get("/", ctrl.GetAll)
route.Get("/report", ctrl.GetReport)
route.Get("/summary", m.RequirePermissions(m.P_DailyChecklistDashboardList), ctrl.GetSummary)
route.Get("/summary", ctrl.GetSummary)
// route.Get("/report", ctrl.GetReport)
route.Get("/report", ctrl.GetReport)
// upsert daily checklist
route.Post("/", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateOne)
route.Post("/", ctrl.CreateOne)
// get detail data daily checklist by id
route.Get("/relation/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistGetOne), ctrl.GetOne)
route.Get("/relation/:idDailyChecklist", ctrl.GetOne)
// get phases by daily checklist id
route.Get("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetPhaseByIdChecklist)
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist)
// create task
/*
ketika add phase
*/
route.Post("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateDailyChecklistPhase)
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
// create assigment
/*
ketika add ABK
*/
route.Post("/assignment/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateAssignment)
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
// remove assignment
/*
ketika remove ABK
*/
route.Delete("/:idDailyChecklist/assignments/:idEmployee", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.RemoveAssignment)
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
//get all tasks
route.Get("/tasks", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetAllTasks)
route.Get("/tasks", ctrl.GetAllTasks)
// update assignment
/*
ketika check dan uncheck tugas oleh ABK
*/
route.Post("/assignment", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateAssignment)
route.Post("/assignment", ctrl.UpdateAssignment)
route.Patch("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateOne)
route.Delete("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.DeleteOne)
route.Patch("/:idDailyChecklist", ctrl.UpdateOne)
route.Delete("/:idDailyChecklist", ctrl.DeleteOne)
}
@@ -52,7 +52,7 @@ type SummaryQuery struct {
type ReportQuery struct {
Page int `query:"page" validate:"required,number,min=1,gt=0"`
Limit int `query:"limit" validate:"required,number,min=1,gt=0"`
Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"`
Month int `query:"bulan" validate:"required,number,min=1,max=12"`
Year int `query:"tahun" validate:"required,number,min=1900"`
AreaID *uint `query:"area_id" validate:"omitempty"`
+2 -5
View File
@@ -5,8 +5,6 @@ import (
"github.com/gofiber/fiber/v2"
"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"
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) {
dashboardRepo := rDashboard.NewDashboardRepository(db)
hppCostRepo := commonRepo.NewHppCostRepository(db)
userRepo := rUser.NewUserRepository(db)
hppSvc := commonService.NewHppService(hppCostRepo)
dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate, hppSvc)
dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
DashboardRoutes(router, userService, dashboardService)
}
@@ -21,7 +21,6 @@ type DashboardRepository interface {
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)
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)
GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error)
GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error)
@@ -285,7 +285,7 @@ func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Contex
db := r.DB().WithContext(ctx).
Table("recording_eggs AS re").
Select("COALESCE(SUM(re.weight * 1000), 0)").
Select("COALESCE(SUM(re.qty * re.weight), 0)").
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").
@@ -309,27 +309,6 @@ func (r *DashboardRepositoryImpl) SumEggProductionWeightKg(ctx context.Context,
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) {
var rows []FeedUsageByUom
@@ -574,7 +553,7 @@ func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context
var rows []ComparisonWeeklyMetric
db := r.DB().WithContext(ctx).
Table("recordings AS r").
Select(fmt.Sprintf(`(CASE WHEN r.day IS NULL OR r.day <= 0 THEN 1 ELSE ((r.day - 1) / 7 + 1) END) AS week,
Select(fmt.Sprintf(`((r.day - 1) / 7 + 1) AS week,
%s AS series_id,
COALESCE(AVG(%s), 0) AS value`, seriesExpr, metricExpr)).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
@@ -582,7 +561,8 @@ func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
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)
@@ -668,7 +648,7 @@ func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, s
Table("recording_eggs AS re").
Select(`
((r.day - 1) / 7 + 1) AS week,
COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`).
COALESCE(SUM(re.qty * re.weight), 0) AS egg_weight_grams`).
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").
@@ -10,7 +10,6 @@ import (
"strings"
"time"
commonService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
@@ -28,15 +27,13 @@ type dashboardService struct {
Log *logrus.Logger
Validate *validator.Validate
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{
Log: utils.Log,
Validate: validate,
Repository: repo,
HppSvc: hppSvc,
}
}
@@ -595,13 +592,13 @@ func buildAggregateComparisonPercent(weeks []int, seriesRows []repository.Compar
count++
}
if count == 0 {
continue
}
if result[week] == nil {
result[week] = map[uint]float64{}
}
if count == 0 {
result[week][series.Id] = 0
continue
}
result[week][series.Id] = sum / count
}
}
@@ -849,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) {
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)
if err != nil {
return 0, 0, err
@@ -896,37 +878,6 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, end
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) {
startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
currentEndExclusive := endDate.AddDate(0, 0, 1)
@@ -70,8 +70,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Contex
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
Where("expenses.realization_date IS NOT NULL").
Where("expenses.category = ?", "BOP")
Where("expenses.realization_date IS NOT NULL")
if projectFlockKandangID != nil {
db = db.Where(`(
@@ -396,10 +396,6 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["supplier_id"] = *req.SupplierID
}
if req.Notes != nil {
updateBody["notes"] = *req.Notes
}
if req.LocationID != nil {
locationID := uint(*req.LocationID)
updateBody["location_id"] = locationID
@@ -572,28 +568,20 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
if *latestApproval.Action != entity.ApprovalActionUpdated {
approvalAction := entity.ApprovalActionUpdated
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
if previousStep < utils.ExpenseStepPengajuan {
previousStep = utils.ExpenseStepPengajuan
}
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowExpense,
id,
previousStep,
utils.ExpenseStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
}
}
if s.DocumentSvc != nil && len(req.Documents) > 0 {
@@ -31,7 +31,6 @@ type Update struct {
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
}
@@ -20,7 +20,7 @@ type InitialRelationDTO struct {
InitialBalanceType string `json:"initial_balance_type"`
InitialBalanceTypeLabel string `json:"initial_balance_type_label"`
Party Party `json:"party"`
Bank *bankDTO.BankRelationDTO `json:"bank"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
Direction string `json:"direction"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
@@ -128,12 +128,11 @@ func partyFromInitial(e entity.Payment) Party {
return party
}
func bankFromInitial(e entity.Payment) *bankDTO.BankRelationDTO {
func bankFromInitial(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return nil
return bankDTO.BankRelationDTO{}
}
bank := bankDTO.ToBankRelationDTO(e.BankWarehouse)
return &bank
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromInitial(e entity.Payment) userDTO.UserRelationDTO {
@@ -162,7 +161,7 @@ func initialBalanceLabel(balanceType string) string {
}
func initialBalanceTypeFromPayment(e entity.Payment) string {
if e.Nominal < 0 {
if strings.EqualFold(e.Direction, "OUT") || e.Nominal < 0 {
return "NEGATIVE"
}
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) {
normalizeOptionalBankId(&req.BankId)
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -125,7 +124,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
Direction: directionForInitialType(party, balanceType),
Direction: directionForInitialType(balanceType),
Nominal: signedNominal(balanceType, req.Nominal),
Notes: req.Note,
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) {
normalizeOptionalBankId(&req.BankId)
if err := s.Validate.Struct(req); err != nil {
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
requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil
var existing *entity.Payment
var resolvedPartyType string
var resolvedPartyId uint
if requiresVerification {
current, err := s.Repository.GetByID(c.Context(), id, nil)
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")
}
existing = current
resolvedPartyType = existing.PartyType
resolvedPartyId = existing.PartyId
}
if req.PartyType != nil || req.PartyId != nil {
partyType := existing.PartyType
partyId := existing.PartyId
if req.PartyType != nil {
normalized, err := normalizePartyType(*req.PartyType)
if err != nil {
return nil, err
}
resolvedPartyType = normalized
updateBody["party_type"] = resolvedPartyType
partyType = normalized
updateBody["party_type"] = partyType
}
if req.PartyId != nil {
resolvedPartyId = *req.PartyId
updateBody["party_id"] = resolvedPartyId
partyId = *req.PartyId
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
}
}
@@ -241,11 +238,8 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
nominal = *req.Nominal
}
updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
updateBody["direction"] = directionForInitialType(balanceType)
updateBody["nominal"] = signedNominal(balanceType, nominal)
} else if req.PartyType != nil {
balanceType := balanceTypeFromPayment(existing)
updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
}
if len(updateBody) == 0 {
@@ -268,7 +262,7 @@ func isInitialTransaction(transactionType string) bool {
}
func balanceTypeFromPayment(payment *entity.Payment) string {
if payment.Nominal < 0 {
if strings.EqualFold(payment.Direction, "OUT") || payment.Nominal < 0 {
return "NEGATIVE"
}
return "POSITIVE"
@@ -292,24 +286,11 @@ func normalizeInitialBalanceType(balanceType string) (string, error) {
}
}
func directionForInitialType(partyType string, balanceType string) string {
switch utils.PaymentParty(strings.ToUpper(strings.TrimSpace(partyType))) {
case utils.PaymentPartySupplier:
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"
func directionForInitialType(balanceType string) string {
if strings.EqualFold(balanceType, "NEGATIVE") {
return "OUT"
}
return "IN"
}
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},
)
}
func normalizeOptionalBankId(bankId **uint) {
if bankId == nil || *bankId == nil {
return
}
if **bankId == 0 {
*bankId = nil
}
}
@@ -3,7 +3,7 @@ package validation
type Create struct {
PartyType string `json:"party_type" validate:"required_strict,max=50"`
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"`
InitialBalanceType string `json:"initial_balance_type" validate:"required_strict,oneof=NEGATIVE POSITIVE"`
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,
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
Direction: directionForInjectionNominal(req.Nominal),
Direction: "IN",
Nominal: req.Nominal,
Notes: req.Notes,
CreatedBy: actorID,
@@ -186,7 +186,6 @@ func (s injectionService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
if req.Nominal != nil {
updateBody["nominal"] = *req.Nominal
updateBody["direction"] = directionForInjectionNominal(*req.Nominal)
}
if req.Notes != nil {
updateBody["notes"] = *req.Notes
@@ -211,13 +210,6 @@ func isInjectionTransaction(transactionType string) bool {
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) {
sequence, err := s.Repository.NextPaymentSequence(ctx)
if err != nil {
@@ -3,14 +3,14 @@ package validation
type Create struct {
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
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"`
}
type Update struct {
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
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"`
}
@@ -3,7 +3,6 @@ package controller
import (
"math"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto"
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 {
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{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
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", ""),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
@@ -21,7 +21,7 @@ type TransactionRelationDTO struct {
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank *bankDTO.BankRelationDTO `json:"bank"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
@@ -37,7 +37,7 @@ type TransactionListDTO struct {
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank *bankDTO.BankRelationDTO `json:"bank"`
Bank bankDTO.BankRelationDTO `json:"bank"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
@@ -151,12 +151,11 @@ func partyFromPayment(e entity.Payment) Party {
return party
}
func bankFromPayment(e entity.Payment) *bankDTO.BankRelationDTO {
func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return nil
return bankDTO.BankRelationDTO{}
}
bank := bankDTO.ToBankRelationDTO(e.BankWarehouse)
return &bank
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"strings"
"time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
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
}
startDate, endDate, err := parseTransactionDateRange(params.StartDate, params.EndDate)
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transactions, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
db = db.Where(
return db.Where(
`LOWER(payment_code) LIKE ? OR
LOWER(COALESCE(reference_number, '')) 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,
)
}
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)
return db.Order("payment_date DESC").Order("created_at DESC")
})
if err != nil {
@@ -208,47 +173,3 @@ func (s transactionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
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 {
Page int `query:"page" 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,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"`
Page int `query:"page" 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,max=50"`
}
@@ -100,24 +100,26 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
}
}
func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
return AdjustmentRelationDTO{
Id: e.Id,
Note: "",
Increase: e.TotalQty,
Decrease: e.UsageQty,
Note: e.Notes,
Increase: e.Increase,
Decrease: e.Decrease,
ProductWarehouseId: e.ProductWarehouseId,
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
}
}
func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO {
func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
var createdUser *userDTO.UserRelationDTO
// Get created user from StockLog
if e.StockLog != nil && e.StockLog.CreatedUser != nil {
mapped := userDTO.ToUserRelationDTO(*e.StockLog.CreatedUser)
createdUser = &mapped
if e.CreatedUser != nil {
createdUser = &userDTO.UserRelationDTO{
Id: e.CreatedUser.Id,
IdUser: e.CreatedUser.IdUser,
Email: e.CreatedUser.Email,
Name: e.CreatedUser.Name,
}
}
return AdjustmentListDTO{
@@ -127,8 +129,9 @@ func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO {
}
}
func ToAdjustmentDetailDTO(e *entity.AdjustmentStock) AdjustmentDetailDTO {
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
return AdjustmentDetailDTO{
AdjustmentListDTO: ToAdjustmentListDTO(e),
// UpdatedAt: e.UpdatedAt,
}
}
@@ -9,7 +9,7 @@ import (
type AdjustmentStockRepository interface {
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
DB() *gorm.DB
}
@@ -30,13 +30,11 @@ func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *ent
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
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
err := q.First(&record, id).Error
err := r.db.WithContext(ctx).
Where("stock_log_id = ?", stockLogID).
First(&record).Error
if err != nil {
return nil, err
}
@@ -25,9 +25,9 @@ import (
)
type AdjustmentService interface {
Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.AdjustmentStock, error)
AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error)
Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error)
AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error)
}
type adjustmentService struct {
@@ -70,11 +70,13 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Preload("StockLog.CreatedUser")
Preload("CreatedUser")
}
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations)
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) {
stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory")
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
@@ -83,10 +85,14 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentSto
return nil, err
}
return adjustmentStock, nil
if stockLog.LoggableType != string(utils.StockLogTypeAdjustment) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
return stockLog, nil
}
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error) {
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -105,13 +111,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if req.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
}
transactionType := strings.ToUpper(req.TransactionType)
if transactionType != string(utils.StockLogTransactionTypeIncrease) && transactionType != string(utils.StockLogTransactionTypeDecrease) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
}
var createdAdjustmentStockId uint
var createdLogId uint
var projectFlockKandangID *uint
pfkID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
@@ -146,8 +151,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return nil, err
}
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
productWarehouse, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
@@ -162,50 +166,31 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
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) {
afterQuantity += req.Quantity
newLog.Increase = req.Quantity
newLog.Stock += newLog.Increase
newLog.Increase = afterQuantity
} else {
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, "Insufficient stock for adjustment")
}
afterQuantity -= req.Quantity
newLog.Decrease = req.Quantity
newLog.Stock -= newLog.Decrease
newLog.Decrease = afterQuantity
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log: %+v", err)
return err
}
adjustmentStock := &entity.AdjustmentStock{
StockLogId: newLog.Id,
ProductWarehouseId: productWarehouse.Id,
}
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
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) {
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
@@ -227,7 +212,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
UsableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity,
AllowPending: false,
AllowPending: false, // Don't allow pending for adjustment
Tx: tx,
})
if err != nil {
@@ -235,26 +220,24 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
}
}
// 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
createdLogId = newLog.Id
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
var fiberErr *fiber.Error
if errors.As(err, &fiberErr) {
return nil, err
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
}
return s.GetOne(c, createdAdjustmentStockId)
return s.GetOne(c, createdLogId)
}
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
@@ -283,15 +266,13 @@ func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context,
return uint(projectFlockKandang.Id), nil
}
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
if err := s.Validate.Struct(query); err != nil {
return nil, 0, err
}
offset := (query.Page - 1) * query.Limit
var isProductsExist bool
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
if err != nil {
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
}
@@ -299,8 +280,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
isProductsExist, err = s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
@@ -309,45 +289,28 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
var adjustmentStocks []entity.AdjustmentStock
var total int64
stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB {
q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}).
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Preload("StockLog.CreatedUser")
db = s.withRelations(db)
if query.ProductID > 0 {
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id").
Where("product_warehouses.product_id = ?", query.ProductID)
}
db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment))
if query.WarehouseID > 0 {
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id").
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
}
if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
}
db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
if query.TransactionType != "" {
q = q.Joins("JOIN stock_logs ON stock_logs.loggable_type = ? AND stock_logs.loggable_id = adjustment_stocks.id", "ADJUSTMENT").
Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType))
}
if err = q.Count(&total).Error; err != nil {
s.Log.Errorf("Failed to get adjustments: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
}
err = q.Offset(offset).Limit(query.Limit).Order("created_at DESC").Find(&adjustmentStocks).Error
return db.Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get adjustments: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
}
result := make([]*entity.AdjustmentStock, len(adjustmentStocks))
for i := range adjustmentStocks {
result[i] = &adjustmentStocks[i]
result := make([]*entity.StockLog, len(stockLogs))
for i, v := range stockLogs {
result[i] = &v
}
return result, total, nil
@@ -5,7 +5,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
)
// === DTO Structs ===
@@ -17,29 +16,60 @@ type ProductWarehouseRelationDTO struct {
Quantity float64 `json:"quantity"`
}
type ProductWarehousNestedDTO struct {
Id uint `json:"id"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
}
type ProductWarehouseListDTO struct {
ProductWarehouseRelationDTO
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"`
CreatedUser *UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"`
CreatedUser *UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserRelationDTO struct {
Id uint `json:"id"`
Username string `json:"username"`
}
type ProductWarehouseDetailDTO struct {
ProductWarehouseListDTO
}
type ProductWarehousNestedDTO struct {
Id uint `json:"id"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
// Nested DTOs for relations
type ProductRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Sku string `json:"sku"`
Flags []string `json:"flags"`
}
type UserRelationDTO struct {
Id uint `json:"id"`
Username string `json:"username"`
type WarehouseRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Kandang *KandangRelationDTO `json:"kandang,omitempty"`
Location *LocationRelationDTO `json:"location,omitempty"`
Area *AreaRelationDTO `json:"area,omitempty"`
}
type KandangRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProjectFlockKandangRelationDTO struct {
@@ -66,28 +96,65 @@ func ToProductWarehouseRelationDTO(e entity.ProductWarehouse) ProductWarehouseRe
}
}
func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO {
product := productDTO.ToProductRelationDTO(e.Product)
return ProductWarehousNestedDTO{
Id: e.Id,
Product: &product,
Warehouse: &WarehouseRelationDTO{
Id: e.Warehouse.Id,
Name: e.Warehouse.Name,
},
}
}
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
dto := ProductWarehouseListDTO{
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
// CreatedAt: e.CreatedAt,
// UpdatedAt: e.UpdatedAt,
}
// Map Product relation jika ada
if e.Product.Id != 0 {
product := productDTO.ToProductRelationDTO(e.Product)
// Create a copy with flock name appended if exists
// Tambahkan flock name ke product name jika ada project flock
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
productCopy := product
productCopy.Name = product.Name + " (" + e.ProjectFlockKandang.ProjectFlock.FlockName + ")"
dto.Product = &productCopy
} else {
dto.Product = &product
product.Name = product.Name + " (" + e.ProjectFlockKandang.ProjectFlock.FlockName + ")"
}
dto.Product = &product
}
// Map Warehouse relation jika ada
if e.Warehouse.Id != 0 {
warehouse := warehouseDTO.ToWarehouseRelationDTO(e.Warehouse)
warehouse := WarehouseRelationDTO{
Id: e.Warehouse.Id,
Name: e.Warehouse.Name,
}
// Map Kandang jika ada
if e.Warehouse.Kandang != nil && e.Warehouse.Kandang.Id != 0 {
warehouse.Kandang = &KandangRelationDTO{
Id: e.Warehouse.Kandang.Id,
Name: e.Warehouse.Kandang.Name,
}
}
// Map Location jika ada
if e.Warehouse.Location != nil && e.Warehouse.Location.Id != 0 {
warehouse.Location = &LocationRelationDTO{
Id: e.Warehouse.Location.Id,
Name: e.Warehouse.Location.Name,
}
}
if e.Warehouse.Area.Id != 0 {
warehouse.Area = &AreaRelationDTO{
Id: e.Warehouse.Area.Id,
Name: e.Warehouse.Area.Name,
}
}
dto.Warehouse = &warehouse
}
@@ -101,6 +168,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
Period: e.ProjectFlockKandang.Period,
}
// Map ProjectFlock jika ada
if e.ProjectFlockKandang.ProjectFlock.Id != 0 {
pfkDTO.ProjectFlock = &ProjectFlockRelationDTO{
Id: e.ProjectFlockKandang.ProjectFlock.Id,
@@ -111,6 +179,15 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
dto.ProjectFlockKandang = pfkDTO
}
// Map CreatedUser relation jika ada
// if e.CreatedUser.Id != 0 {
// user := UserRelationDTO{
// Id: e.CreatedUser.Id,
// Username: e.CreatedUser.Name,
// }
// dto.CreatedUser = &user
// }
return dto
}
@@ -128,13 +205,23 @@ func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDeta
}
}
func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO {
product := productDTO.ToProductRelationDTO(e.Product)
warehouse := warehouseDTO.ToWarehouseRelationDTO(e.Warehouse)
return ProductWarehousNestedDTO{
Id: e.Id,
Product: &product,
Warehouse: &warehouse,
func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO {
return KandangRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToLocationRelationDTO(e entity.Location) LocationRelationDTO {
return LocationRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToAreaRelationDTO(e entity.Area) AreaRelationDTO {
return AreaRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
@@ -3,14 +3,15 @@ package service
import (
"errors"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
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"
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -39,7 +40,6 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Product.Flags").
Preload("Product").
Preload("Product.Uom").
Preload("Warehouse").
Preload("Warehouse.Location").
Preload("Warehouse.Area").
@@ -4,17 +4,20 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
type TransferRelationDTO struct {
Id uint64 `json:"id"`
MovementNumber string `json:"movement_number"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductSimpleDTO struct {
@@ -22,6 +25,16 @@ type ProductSimpleDTO struct {
Name string `json:"name"`
}
type AreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type SupplierSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
@@ -35,6 +48,13 @@ type DocumentDTO struct {
Size float64 `json:"size"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct {
TransferRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
@@ -77,19 +97,16 @@ type TransferDeliveryItemDTO struct {
}
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse)
sourceWarehouse = &mapped
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
}
var destinationWarehouse *warehouseDTO.WarehouseRelationDTO
var destinationWarehouse *WarehouseDetailDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
destinationWarehouse = &mapped
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
}
return TransferRelationDTO{
Id: e.Id,
MovementNumber: e.MovementNumber,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse,
@@ -97,6 +114,38 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
}
}
func toAreaDTO(a *entity.Area) *AreaDTO {
if a == nil {
return nil
}
return &AreaDTO{
Id: a.Id,
Name: a.Name,
}
}
func toLocationDTO(l *entity.Location) *LocationDTO {
if l == nil {
return nil
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
if w == nil {
return nil
}
return &WarehouseDetailDTO{
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area),
}
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil {
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
if err != nil {
return "", err
}
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq)
movementNumber := fmt.Sprintf("ST-%05d", seq)
return movementNumber, nil
}
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
route := v1.Group("/transfers")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
}
@@ -99,11 +99,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if 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").
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
searchTerm, searchTerm, searchTerm)
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -122,10 +118,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
})
if err != nil {
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, "Transfer not found")
}
s.Log.Errorf("Failed to fetch transfer by ID %d: %+v", id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data transfer")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
}
return transferPtr, nil
@@ -141,13 +136,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
)
if err != nil {
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("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
}
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, "Gagal mengecek stok produk")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
}
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 cukup", product.ProductID))
}
pwIDs = append(pwIDs, sourcePW.Id)
}
@@ -165,15 +159,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return nil, err
}
if destPfkID > 0 {
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
if err != nil {
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")))
}
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
}
if projectFlockKandang.ClosedAt != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
}
actorID, err := m.ActorIDFromContext(c)
@@ -201,18 +192,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if errors.Is(err, gorm.ErrRecordNotFound) {
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, "Gagal mengambil data supplier")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
}
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 dengan ID %d bukan kategori BOP", delivery.SupplierID))
}
}
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
if err != nil {
s.Log.Errorf("Failed to generate movement number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
}
transferDate, _ := utils.ParseDateString(req.TransferDate)
@@ -235,7 +224,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)
productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
stocklogsRepoTx := s.StockLogsRepository.WithTx(tx)
if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil {
return err
@@ -251,18 +239,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
)
if err != nil {
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("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
}
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, "Gagal mengambil data stok gudang asal")
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
}
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
)
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, "Gagal mengambil data stok gudang tujuan")
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
}
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx := c.Context()
@@ -270,21 +256,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil {
return err
}
var pfkID *uint
if projectFlockKandangID > 0 {
pfkID = &projectFlockKandangID
}
destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
ProjectFlockKandangId: pfkID,
ProjectFlockKandangId: &projectFlockKandangID,
}
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, "Gagal membuat data stok gudang tujuan")
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
}
}
@@ -330,7 +309,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
for _, prod := range item.Products {
detail, ok := detailMap[uint64(prod.ProductID)]
if !ok {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
}
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
StockTransferDeliveryId: delivery.Id,
@@ -374,9 +353,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Files: documentFiles,
})
if err != nil {
s.Log.Errorf("Failed to upload document for delivery %d (delivery_id=%d, filename=%s): %+v",
deliveryIdx+1, delivery.Id, file.Filename, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengunggah dokumen")
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
deliveryIdx+1, delivery.Id, file.Filename)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", deliveryIdx+1, err))
}
}
}
@@ -393,7 +372,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
}
if err := tx.Model(&entity.StockTransferDetail{}).
@@ -402,21 +381,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
"usage_qty": consumeResult.UsageQuantity,
"pending_qty": consumeResult.PendingQuantity,
}).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, "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: "",
}
if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
return fmt.Errorf("gagal update usage tracking: %w", err)
}
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
@@ -429,8 +394,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx,
})
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, "Gagal menambah stok gudang tujuan")
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
}
if err := tx.Model(&entity.StockTransferDetail{}).
@@ -438,21 +402,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Updates(map[string]interface{}{
"total_qty": replenishResult.AddedQuantity,
}).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, "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: "",
}
if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
return fmt.Errorf("gagal update total tracking: %w", err)
}
}
@@ -486,10 +436,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
}
result, err := s.GetOne(c, uint(entityTransfer.Id))
@@ -499,8 +446,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if len(expensePayloads) > 0 {
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, "Gagal sinkronisasi data expense. Silakan cek manual di module expense")
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
}
}
@@ -514,27 +461,32 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
}
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
return nil
}
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
}
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
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, "Gagal mengambil data gudang")
}
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
return 0, nil
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
}
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
if err != nil {
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("Kandang %d belum memiliki project flock aktif", *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")
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
}
return uint(projectFlockKandang.Id), nil
@@ -140,21 +140,12 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
if actorID == 0 {
actorID = 1
}
approvalRepo := commonRepo.NewApprovalRepository(b.db)
svc := commonSvc.NewApprovalService(approvalRepo)
action := entity.ApprovalActionCreated
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
action := entity.ApprovalActionUpdated
for id := range expenseIDs {
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
}
return nil
}
@@ -9,7 +9,6 @@ import (
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -69,10 +68,10 @@ type DeliveryItemDTO struct {
}
type DeliveryGroupDTO struct {
DoNumber string `json:"do_number"`
DeliveryDate *time.Time `json:"delivery_date"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
Deliveries []DeliveryItemDTO `json:"deliveries"`
DoNumber string `json:"do_number"`
DeliveryDate *time.Time `json:"delivery_date"`
Warehouse *productwarehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
Deliveries []DeliveryItemDTO `json:"deliveries"`
}
type DeliveryMarketingProductDTO struct {
@@ -287,7 +286,7 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
if !exists {
group = &DeliveryGroupDTO{
DeliveryDate: product.DeliveryDate,
Warehouse: &warehouseDTO.WarehouseRelationDTO{
Warehouse: &productwarehouseDTO.WarehouseRelationDTO{
Id: warehouseId,
Name: warehouseName,
},
+1 -3
View File
@@ -16,7 +16,6 @@ import (
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/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"
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"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)
customerRepo := rCustomer.NewCustomerRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockLogRepo := rShared.NewStockLogRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
@@ -65,7 +63,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
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)
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
@@ -14,7 +14,7 @@ import (
type MarketingDeliveryProductRepository interface {
repository.BaseRepository[entity.MarketingDeliveryProduct]
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error)
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingId(ctx context.Context, marketingId 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)
@@ -54,14 +54,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) {
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(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("marketing_delivery_products.delivery_date IS NOT NULL").
@@ -71,25 +69,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context
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").
@@ -161,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
Where("marketing_delivery_products.delivery_date IS NOT NULL")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" {
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
}
@@ -169,30 +148,18 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
}
if filters.WarehouseId > 0 || filters.Search != "" {
if filters.WarehouseId > 0 {
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
}
if filters.Search != "" {
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
}
if filters.Search != "" {
searchPattern := "%" + filters.Search + "%"
db = db.Where(`(
marketing_delivery_products.vehicle_number ILIKE ? OR
customers.name ILIKE ? OR
warehouses.name ILIKE ? OR
products.name ILIKE ? OR
sales_users.name ILIKE ? OR
CONCAT(
marketings.so_number,
'-',
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
'-',
COALESCE(product_warehouses.warehouse_id::text, '')
) ILIKE ?
)`,
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
searchPattern, searchPattern, searchPattern, searchPattern)
}
if filters.CustomerId > 0 {
@@ -211,19 +178,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
}
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").
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
if filters.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
}
if filters.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
}
}
if filters.MarketingType != "" {
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Group("marketing_delivery_products.id")
@@ -232,6 +186,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
case "ayam":
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),
})
case "telur":
db = db.Where("flags.name IN (?)", []string{
@@ -246,12 +201,8 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
}
}
if filters.StartDate != "" || filters.EndDate != "" {
filterBy := filters.FilterBy
if filterBy == "" {
filterBy = "so_date"
}
if filterBy == "so_date" {
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
if filters.FilterBy == "so_date" {
if filters.StartDate != "" {
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("marketings.so_date >= ?", startDate)
@@ -263,7 +214,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("marketings.so_date < ?", nextDate)
}
}
} else if filterBy == "realization_date" {
} else if filters.FilterBy == "realization_date" {
if filters.StartDate != "" {
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate)
@@ -26,10 +26,7 @@ func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository {
func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) {
var products []entity.MarketingProduct
if err := r.DB().WithContext(ctx).
Preload("ProductWarehouse.Product.Flags").
Where("marketing_id = ?", marketingID).
Find(&products).Error; err != nil {
if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil {
return nil, err
}
if len(products) == 0 {
@@ -14,7 +14,6 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
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/fifo"
@@ -35,7 +34,6 @@ type deliveryOrdersService struct {
MarketingRepo marketingRepo.MarketingRepository
MarketingProductRepo marketingRepo.MarketingProductRepository
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
StockLogRepo rShared.StockLogRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
}
@@ -44,7 +42,6 @@ func NewDeliveryOrdersService(
marketingRepo marketingRepo.MarketingRepository,
marketingProductRepo marketingRepo.MarketingProductRepository,
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
stockLogRepo rShared.StockLogRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate,
@@ -54,7 +51,6 @@ func NewDeliveryOrdersService(
MarketingRepo: marketingRepo,
MarketingProductRepo: marketingProductRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
StockLogRepo: stockLogRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
}
@@ -251,25 +247,9 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate
}
isPakanOrOVK := false
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
var totalPrice float64
if isPakanOrOVK {
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
} else {
totalPrice = totalWeight * requestedProduct.UnitPrice
}
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
@@ -281,7 +261,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
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
}
}
@@ -329,12 +309,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
@@ -386,26 +361,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
var totalPrice float64
if isPakanOrOVK {
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
} else {
totalPrice = totalWeight * requestedProduct.UnitPrice
}
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
@@ -418,13 +376,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
if requestedProduct.Qty != oldRequestedQty {
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
}
}
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
}
}
@@ -449,7 +407,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
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 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
}
@@ -469,31 +427,6 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
if err == nil && result.UsageQuantity > 0 {
if actorID > 0 {
decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: "",
}
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.Stock -= decreaseLog.Decrease
} else {
decreaseLog.Stock = 0
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
}
}
if err != nil {
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
@@ -502,42 +435,12 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
}
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))
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")
}
if actorID > 0 {
decreaseLog := &entity.StockLog{
Decrease: requestedQty,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: "",
}
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.Stock -= decreaseLog.Decrease
} else {
decreaseLog.Stock = 0
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
}
return nil
}
@@ -548,7 +451,7 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
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 {
return nil
}
@@ -575,29 +478,6 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
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: "",
}
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.Stock += increaseLog.Increase
} else {
increaseLog.Stock = 0
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
}
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
}
@@ -292,32 +292,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
// Get product untuk cek flag PAKAN atau OVK
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
if err != nil {
return err
}
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false
if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 {
for _, flag := range productWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
var totalPrice float64
if isPakanOrOVK {
totalPrice = rp.Qty * rp.UnitPrice
} else {
totalPrice = totalWeight * rp.UnitPrice
}
totalPrice := rp.UnitPrice * rp.Qty
updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId,
@@ -615,34 +592,9 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
// Get product untuk cek flag PAKAN atau OVK
productWarehouse, err := s.ProductWarehouseRepo.GetByID(ctx, rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
if err != nil {
return err
}
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false
if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 {
for _, flag := range productWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
var totalPrice float64
if isPakanOrOVK {
// PAKAN atau OVK: qty × unit_price
totalPrice = rp.Qty * rp.UnitPrice
} else {
// Produk lain: total_weight × unit_price
totalPrice = totalWeight * rp.UnitPrice
}
totalPrice := rp.UnitPrice * rp.Qty
marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId,
@@ -15,9 +15,9 @@ func ConfigChecklistRoutes(v1 fiber.Router, u user.UserService, s configChecklis
route := v1.Group("/config-checklists")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -76,9 +76,6 @@ func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if req.PercentageThresholdBad > req.PercentageThresholdEnough {
return nil, fiber.NewError(fiber.StatusBadRequest, "percentage_threshold_bad cannot be greater than percentage_threshold_enough")
}
date, err := time.Parse("2006-01-02", req.Date)
if err != nil {
@@ -103,11 +100,6 @@ func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update,
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if req.PercentageThresholdBad != nil && req.PercentageThresholdEnough != nil {
if *req.PercentageThresholdBad > *req.PercentageThresholdEnough {
return nil, fiber.NewError(fiber.StatusBadRequest, "percentage_threshold_bad cannot be greater than percentage_threshold_enough")
}
}
updateBody := make(map[string]any)
@@ -14,7 +14,6 @@ type CustomerRelationDTO struct {
Name string `json:"name"`
Type string `json:"type"`
AccountNumber string `json:"account_number"`
Address string `json:"address,omitempty"`
Balance float64 `json:"balance"`
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
}
@@ -53,8 +52,6 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO {
Name: e.Name,
Type: e.Type,
AccountNumber: e.AccountNumber,
Address: e.Address,
Balance: e.Balance,
Pic: pic,
}
}
@@ -156,22 +156,6 @@ func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
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 {
return s.GetOne(c, id)
}
+5 -5
View File
@@ -15,9 +15,9 @@ func EmployeesRoutes(v1 fiber.Router, u user.UserService, s employees.EmployeesS
route := v1.Group("/employees")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -37,6 +37,7 @@ type NonstockSupplierDTO struct {
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
Price float64 `json:"price"`
}
// === Mapper Functions ===
@@ -120,6 +121,7 @@ func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []NonstockSuppl
Name: relation.Supplier.Name,
Alias: relation.Supplier.Alias,
Category: relation.Supplier.Category,
Price: relation.Price,
})
}
@@ -61,20 +61,30 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
return err
}
existingMap := make(map[uint]struct{}, len(existing))
existingMap := make(map[uint]entity.NonstockSupplier, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierId] = struct{}{}
existingMap[rel.SupplierId] = rel
}
incomingMap := make(map[uint]struct{}, len(suppliers))
for _, rel := range suppliers {
incomingMap[rel.SupplierId] = struct{}{}
if _, exists := existingMap[rel.SupplierId]; exists {
if existingRel, exists := existingMap[rel.SupplierId]; exists {
if existingRel.Price != rel.Price {
if err := db.WithContext(ctx).
Model(&entity.NonstockSupplier{}).
Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierId).
Update("price", rel.Price).
Error; err != nil {
return err
}
}
continue
}
record := entity.NonstockSupplier{
NonstockId: nonstockID,
SupplierId: rel.SupplierId,
Price: rel.Price,
}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
@@ -115,18 +115,19 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
supplierLinks []entity.NonstockSupplier
supplierIDs []uint
)
if len(req.SupplierIDs) > 0 {
seen := make(map[uint]struct{}, len(req.SupplierIDs))
supplierLinks = make([]entity.NonstockSupplier, 0, len(req.SupplierIDs))
supplierIDs = make([]uint, 0, len(req.SupplierIDs))
for _, supplierID := range req.SupplierIDs {
if _, exists := seen[supplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplierID))
if len(req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(req.Suppliers))
supplierLinks = make([]entity.NonstockSupplier, 0, len(req.Suppliers))
supplierIDs = make([]uint, 0, len(req.Suppliers))
for _, supplier := range req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplierID)
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
SupplierId: supplierID,
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
@@ -211,20 +212,21 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
var supplierLinks []entity.NonstockSupplier
var supplierUpdate bool
if req.SupplierIDs != nil {
if req.Suppliers != nil {
supplierUpdate = true
if len(*req.SupplierIDs) > 0 {
seen := make(map[uint]struct{}, len(*req.SupplierIDs))
supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.SupplierIDs))
supplierIDs := make([]uint, 0, len(*req.SupplierIDs))
for _, supplierID := range *req.SupplierIDs {
if _, exists := seen[supplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplierID))
if len(*req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(*req.Suppliers))
supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.Suppliers))
supplierIDs := make([]uint, 0, len(*req.Suppliers))
for _, supplier := range *req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplierID)
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
SupplierId: supplierID,
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
@@ -1,17 +1,22 @@
package validation
type SupplierPrice struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
Price float64 `json:"price" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags" validate:"dive,max=50"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flags []string `json:"flags" validate:"dive,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
type Query struct {
@@ -15,9 +15,9 @@ func PhaseActivityRoutes(v1 fiber.Router, u user.UserService, s phaseActivity.Ph
route := v1.Group("/phase-activities")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -110,17 +110,6 @@ func (s *phaseActivityService) CreateOne(c *fiber.Ctx, req *validation.Create) (
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
}
existing, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("phase_id = ? AND name = ? AND time_type = ?", phase.Id, name, timeType)
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to check phaseActivity uniqueness: %+v", err)
return nil, err
}
if existing != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase activity with same name and time_type already exists")
}
createBody := &entity.PhaseActivity{
PhaseId: phase.Id,
Name: name,
@@ -15,7 +15,7 @@ type Update struct {
type Query struct {
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,max=50"`
PhaseIDs string `query:"phase_ids" validate:"omitempty"`
}
+5 -5
View File
@@ -15,9 +15,9 @@ func PhasesRoutes(v1 fiber.Router, u user.UserService, s phases.PhasesService) {
route := v1.Group("/phases")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -28,7 +28,6 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
Category: c.Query("category", ""),
Flag: c.Query("flag", ""),
}
if query.Page < 1 || query.Limit < 1 {
@@ -10,6 +10,7 @@ import (
type SupplierNonstockDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
@@ -42,6 +43,7 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst
result = append(result, SupplierNonstockDTO{
Id: Nonstock.Id,
Name: Nonstock.Name,
Price: relation.Price,
Uom: uomRef,
Flags: flags,
})
@@ -47,10 +47,6 @@ func (s supplierService) withRelations(db *gorm.DB) *gorm.DB {
Preload("NonstockSuppliers.Nonstock.Flags")
}
func (s supplierService) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -67,7 +63,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
offset := (params.Page - 1) * params.Limit
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withListRelations(db)
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
}
@@ -76,23 +72,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
}
if params.Flag != "" {
flag := strings.ToUpper(params.Flag)
db = db.Where(`
EXISTS (
SELECT 1
FROM nonstock_suppliers nsup
JOIN nonstocks n ON n.id = nsup.nonstock_id
JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?
WHERE nsup.supplier_id = suppliers.id
AND UPPER(f.name) = ?
)`,
entity.FlagableTypeNonstock,
flag,
)
}
return db.Order("suppliers.created_at DESC").Order("suppliers.updated_at DESC")
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
@@ -32,8 +32,7 @@ type Update struct {
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1"`
Flag string `query:"flag" validate:"omitempty"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
Category string `query:"category" validate:"omitempty,max=50"`
}
@@ -28,7 +28,6 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
AreaId: c.QueryInt("area_id", 0),
LocationId: c.QueryInt("location_id", 0),
ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false),
}
@@ -58,9 +58,6 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
if params.AreaId != 0 {
db = db.Where("area_id = ?", params.AreaId)
}
if params.LocationId != 0 {
db = db.Where("location_id = ?", params.LocationId)
}
if params.ActiveProjectFlockOnly {
db = db.Where(`
EXISTS (
@@ -113,11 +110,7 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
}
typ := strings.ToUpper(req.Type)
createValidationOpts := WarehouseTypeValidationOptions{
LocationProvided: req.LocationId != nil,
KandangProvided: req.KandangId != nil,
}
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil {
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
return nil, err
}
@@ -215,22 +208,9 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
finalKandangId = req.KandangId
}
originalLocationId := finalLocationId
originalKandangId := finalKandangId
updateValidationOpts := WarehouseTypeValidationOptions{
AutoClear: true,
LocationProvided: req.LocationId != nil,
KandangProvided: req.KandangId != nil,
}
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, &finalLocationId, &finalKandangId, updateValidationOpts); err != nil {
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
return nil, err
}
if originalLocationId != finalLocationId {
updateBody["location_id"] = nil
}
if originalKandangId != finalKandangId {
updateBody["kandang_id"] = nil
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
@@ -258,65 +238,47 @@ func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil
}
type WarehouseTypeValidationOptions struct {
AutoClear bool
LocationProvided bool
KandangProvided bool
}
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID **uint, kandangID **uint, opts WarehouseTypeValidationOptions) error {
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
switch utils.WarehouseType(typ) {
case utils.WarehouseTypeArea:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
}
if locationID != nil && *locationID != nil {
if opts.AutoClear && !opts.LocationProvided {
*locationID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
if locationID != nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
if kandangID != nil && *kandangID != nil {
if opts.AutoClear && !opts.KandangProvided {
*kandangID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
}
return nil
case utils.WarehouseTypeLokasi:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
}
if locationID == nil || *locationID == nil {
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
}
if **locationID == 0 {
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
}
if kandangID != nil && *kandangID != nil {
if opts.AutoClear && !opts.KandangProvided {
*kandangID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
}
return nil
case utils.WarehouseTypeKandang:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
}
if locationID == nil || *locationID == nil {
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
}
if **locationID == 0 {
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
}
if kandangID == nil || *kandangID == nil {
if kandangID == nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
}
if **kandangID == 0 {
if *kandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
}
return nil
@@ -21,6 +21,5 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
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"`
}
@@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
newChikins = append(newChikins, newChickin)
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId)
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId))
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId))
}
availableQty := productWarehouse.Quantity - totalPopulationQty
@@ -584,6 +584,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return updated, nil
}
// autoAddFlagToProduct adds target flag to product if not already present (idempotent)
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
if s.ProductRepo == nil {
return nil
@@ -645,17 +646,6 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID,
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 = 0
}
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
}
@@ -712,17 +702,6 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID,
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 = 0
}
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
}
@@ -1,7 +1,6 @@
package dto
import (
"strconv"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -54,7 +53,6 @@ type ProjectFlockKandangListDTO struct {
ProjectFlockKandangRelationDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
NameWithPeriod string `json:"name_with_period"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
@@ -106,7 +104,6 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -129,16 +126,6 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
return &mapped
}
func toNameWithPeriod(kandang entity.Kandang, period int) string {
if kandang.Name == "" {
return ""
}
if period == 0 {
return kandang.Name
}
return kandang.Name + " Period " + strconv.Itoa(period)
}
func toApprovalDTOSelector(
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
approval := selector(e)
@@ -160,7 +147,6 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -7,7 +7,6 @@ import (
"strconv"
"strings"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
@@ -279,27 +278,14 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
if err != nil {
return err
}
_ = availableStock
dtoResult := dto.ToProjectFlockKandangDTO(*result)
if population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id); err != nil {
return err
} else {
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 {
return werr
} else if warehouse != nil {
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
dtoResult.Warehouse = &mapped
}
dtoResult.AvailableQuantity = float64(availableStock)
if withPopulation {
population := dtoResult.AvailableQuantity
population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id)
if err != nil {
return err
}
dtoResult.Population = &population
}

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