mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/Rekapitulasi-hutang-supplier
This commit is contained in:
@@ -1,13 +0,0 @@
|
|||||||
# .air.toml
|
|
||||||
root = "."
|
|
||||||
tmp_dir = "tmp"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
|
||||||
bin = "tmp/main"
|
|
||||||
full_bin = "APP_ENV=dev ./tmp/main"
|
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
|
||||||
exclude_dir = ["vendor", "tmp"]
|
|
||||||
|
|
||||||
[log]
|
|
||||||
time = true
|
|
||||||
+3
-1
@@ -13,7 +13,8 @@ bin/
|
|||||||
Makefile
|
Makefile
|
||||||
docker-compose.local.yml
|
docker-compose.local.yml
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
Dockerfile.local
|
Dockerfile
|
||||||
|
.gitlab-ci.yml
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
vendor
|
vendor
|
||||||
@@ -27,3 +28,4 @@ coverage/
|
|||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+164
-81
@@ -1,90 +1,173 @@
|
|||||||
stages:
|
stages:
|
||||||
|
- build
|
||||||
|
- migrate
|
||||||
- deploy
|
- deploy
|
||||||
|
- seed
|
||||||
|
|
||||||
deploy-dev:
|
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
|
stage: deploy
|
||||||
image: alpine:3.20
|
rules:
|
||||||
variables:
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
DEPLOY_APP: "LTI-MBUGROUP"
|
needs:
|
||||||
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
- job: migrate_staging
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
artifacts: false
|
||||||
GIT_DEPTH: "1"
|
- job: build_staging
|
||||||
|
artifacts: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
before_script:
|
cd "$DEPLOY_DIR"
|
||||||
- echo "🧰 Installing dependencies..."
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||||
- apk update && apk add --no-cache openssh git curl bash
|
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
|
||||||
# Setup SSH di runner
|
docker compose -f "$COMPOSE_FILE" pull
|
||||||
- mkdir -p ~/.ssh
|
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
docker image prune -f
|
||||||
- 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"
|
# 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
|
||||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
docker compose -f "$COMPOSE_FILE" run --rm seed
|
||||||
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
|
|
||||||
+29
-11
@@ -1,20 +1,38 @@
|
|||||||
FROM golang:1.23-alpine
|
# =========================
|
||||||
|
# Builder stage
|
||||||
|
# =========================
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
# Install dependensi dasar
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
RUN apk add --no-cache git curl bash build-base
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Air (pakai repo baru air-verse)
|
|
||||||
RUN go install github.com/air-verse/air@v1.52.3
|
|
||||||
|
|
||||||
WORKDIR /lti-api
|
|
||||||
|
|
||||||
# Cache dependencies
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Build API binary
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
|
||||||
|
|
||||||
|
# Build SEED binary (pastikan cmd/seed ada)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Runtime stage
|
||||||
|
# =========================
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
|
||||||
|
&& adduser -D -H -u 10001 appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/lti-api /app/lti-api
|
||||||
|
COPY --from=builder /app/lti-seed /app/lti-seed
|
||||||
|
|
||||||
|
USER appuser
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
CMD ["air", "-c", ".air.toml"]
|
CMD ["/app/lti-api"]
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
services:
|
|
||||||
postgresdb:
|
|
||||||
image: postgres:alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "${DB_PORT_HOST:-5542}:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER:-postgres}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
|
||||||
volumes:
|
|
||||||
- dbdata:/var/lib/postgresql/data
|
|
||||||
- ./internal/database/init:/docker-entrypoint-initdb.d
|
|
||||||
networks: [go-network]
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
|
|
||||||
]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${REDIS_PORT_HOST:-6381}:6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
networks: [go-network]
|
|
||||||
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.local
|
|
||||||
image: cosmtrek/air:v1.52.3
|
|
||||||
working_dir: /lti-api
|
|
||||||
volumes:
|
|
||||||
- .:/lti-api
|
|
||||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
|
||||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
|
||||||
command: air -c .air.toml
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
DB_HOST: postgresdb
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_USER: ${DB_USER:-postgres}
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
DB_NAME: ${DB_NAME:-db_lti_erp}
|
|
||||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
|
||||||
ports:
|
|
||||||
- "${APP_PORT:-8081}:8081"
|
|
||||||
depends_on:
|
|
||||||
postgresdb:
|
|
||||||
condition: service_healthy
|
|
||||||
networks: [go-network]
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
dbdata:
|
|
||||||
go-mod-cache:
|
|
||||||
go-build-cache:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
go-network:
|
|
||||||
name: lti-api_go-network
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
services:
|
|
||||||
dev-api-lti:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: dev-api-lti
|
|
||||||
working_dir: /lti-api
|
|
||||||
command: ["/bin/sh", "scripts/entrypoint.sh"]
|
|
||||||
ports:
|
|
||||||
- "8081:8081"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# override agar koneksi ke container internal
|
|
||||||
DB_HOST: dev-postgres-lti
|
|
||||||
DB_PORT: 5432
|
|
||||||
REDIS_URL: redis://dev-redis-lti:6379/0
|
|
||||||
volumes:
|
|
||||||
- .:/lti-api
|
|
||||||
- ./.air.toml:/lti-api/.air.toml:ro
|
|
||||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
|
||||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
|
||||||
depends_on:
|
|
||||||
- dev-postgres-lti
|
|
||||||
- dev-redis-lti
|
|
||||||
networks:
|
|
||||||
- lti-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "2.0"
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: "1.0"
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
dev-postgres-lti:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: dev-postgres-lti
|
|
||||||
restart: always
|
|
||||||
env_file:
|
|
||||||
- credential/.env.db
|
|
||||||
ports:
|
|
||||||
- "5433:5432"
|
|
||||||
volumes:
|
|
||||||
- dev-postgres-lti-data:/var/lib/postgresql/data
|
|
||||||
- ./credential:/docker-entrypoint-initdb.d:ro
|
|
||||||
networks:
|
|
||||||
- lti-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 5s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1.0"
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: "0.5"
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
dev-redis-lti:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: dev-redis-lti
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "6380:6379"
|
|
||||||
networks:
|
|
||||||
- lti-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "0.5"
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
cpus: "0.2"
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
networks:
|
|
||||||
lti-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
dev-postgres-lti-data:
|
|
||||||
@@ -78,6 +78,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetOverheadByProjectFlockKandang(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(pfkID)
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get overhead by project flock kandang successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||||
param := c.Params("projectFlockId")
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
@@ -108,12 +138,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -123,19 +148,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get closing penjualan successfully",
|
Message: "Get closing penjualan successfully",
|
||||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetPenjualanByProjectFlockKandang(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(pfkID)
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), &kandangID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing penjualan by project flock kandang successfully",
|
||||||
|
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
projectFlockID, err := strconv.Atoi(projectParam)
|
||||||
if err != nil {
|
if err != nil || projectFlockID <= 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
var projectFlockKandangID *uint
|
||||||
|
if kandangParam != "" {
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
kandangID := uint(pfkID)
|
||||||
|
projectFlockKandangID = &kandangID
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,10 +79,11 @@ type HppGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SummaryHpp struct {
|
type SummaryHpp struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Comparison `json:"-"`
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
Realization FinancialMetrics `json:"realization"`
|
||||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||||
|
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPurchasesSection struct {
|
type HppPurchasesSection struct {
|
||||||
@@ -246,11 +247,9 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
|
|||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
|
|
||||||
summary := SummaryHpp{
|
summary := SummaryHpp{
|
||||||
Label: label,
|
Label: label,
|
||||||
Comparison: ToComparison(
|
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
|
|
||||||
return PenjualanRealisasiResponseDTO{
|
return PenjualanRealisasiResponseDTO{
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
|
|||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
|
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
|
||||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||||
latestDateByNonstockID := make(map[uint]string)
|
latestDateByNonstockID := make(map[uint]string)
|
||||||
|
|
||||||
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
|||||||
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
|
||||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
budgetQty := budgets[i].Qty
|
||||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
budgetPrice := budgets[i].Price
|
||||||
|
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||||
|
|
||||||
|
// Budget division: per kandang view only
|
||||||
|
if isPerKandang && totalKandangCount > 0 {
|
||||||
|
budgetQty = budgetQty / float64(totalKandangCount)
|
||||||
|
budgetTotal = budgetTotal / float64(totalKandangCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range realizations {
|
for i := range realizations {
|
||||||
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
|||||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
}
|
}
|
||||||
|
|
||||||
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
qty := realizations[i].Qty
|
||||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||||
|
|
||||||
|
// Farm-level expense division
|
||||||
|
if realizations[i].ExpenseNonstock.Expense != nil &&
|
||||||
|
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
|
||||||
|
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
|
||||||
|
|
||||||
|
if len(projectFlockIDs) > 0 {
|
||||||
|
totalKandangInAllProjects := 0
|
||||||
|
for _, pfID := range projectFlockIDs {
|
||||||
|
if count, exists := projectFlockKandangCountMap[pfID]; exists {
|
||||||
|
totalKandangInAllProjects += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalKandangInAllProjects > 0 {
|
||||||
|
if isPerKandang {
|
||||||
|
qty = qty / float64(totalKandangInAllProjects)
|
||||||
|
totalAmount = totalAmount / float64(totalKandangInAllProjects)
|
||||||
|
} else {
|
||||||
|
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
|
||||||
|
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
|
||||||
|
perKandangQty := qty / float64(totalKandangInAllProjects)
|
||||||
|
|
||||||
|
qty = perKandangQty * float64(totalKandangCount)
|
||||||
|
totalAmount = perKandangAmount * float64(totalKandangCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].ActualQuantity += qty
|
||||||
|
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
|
||||||
|
|
||||||
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||||
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||||
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Helper Functions ===
|
func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
|
||||||
|
if projectFlockJSON == "" {
|
||||||
|
return []uint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlocks []uint
|
||||||
|
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
|
||||||
|
return []uint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectFlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
func countProjectFlocksInJSON(projectFlockJSON string) int {
|
||||||
|
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
|
||||||
|
if len(projectFlocks) == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return len(projectFlocks)
|
||||||
|
}
|
||||||
|
|
||||||
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||||
if nonstock != nil && nonstock.Id != 0 {
|
if nonstock != nil && nonstock.Id != 0 {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
|
|
||||||
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
||||||
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
|
||||||
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
||||||
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
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("/: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", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
||||||
@@ -32,4 +34,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
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/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
||||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -32,9 +33,9 @@ import (
|
|||||||
type ClosingService interface {
|
type ClosingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
||||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
|
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
|
||||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
||||||
@@ -46,6 +47,7 @@ type closingService struct {
|
|||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ClosingRepository
|
Repository repository.ClosingRepository
|
||||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
MarketingRepo marketingRepository.MarketingRepository
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
@@ -56,12 +58,13 @@ type closingService struct {
|
|||||||
RecordingRepo recordingRepository.RecordingRepository
|
RecordingRepo recordingRepository.RecordingRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
|
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
|
||||||
return &closingService{
|
return &closingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
ProjectFlockRepo: projectFlockRepo,
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
@@ -129,24 +132,9 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
|
|||||||
return projectFlock, nil
|
return projectFlock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
|
||||||
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
|
||||||
return db.
|
|
||||||
Preload("MarketingProduct").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
|
||||||
Preload("MarketingProduct.Marketing").
|
|
||||||
Preload("MarketingProduct.Marketing.Customer").
|
|
||||||
Order("marketing_delivery_products.delivery_date DESC")
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -154,16 +142,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
|
|||||||
return []entity.MarketingDeliveryProduct{}, nil
|
return []entity.MarketingDeliveryProduct{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi))
|
return realisasi, nil
|
||||||
for _, item := range realisasi {
|
|
||||||
|
|
||||||
if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 ||
|
|
||||||
item.UnitPrice != 0 || item.TotalPrice != 0 {
|
|
||||||
filtered = append(filtered, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||||
@@ -379,35 +358,90 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
|||||||
return statusProject, statusClosing, nil
|
return statusProject, statusClosing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
|
||||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
totalKandangCount := len(projectFlockKandangs)
|
||||||
|
|
||||||
|
// Build kandang count map for farm expense division
|
||||||
|
projectFlockKandangCountMap := make(map[uint]int)
|
||||||
|
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
|
||||||
|
|
||||||
|
involvedProjectFlocks := make(map[uint]bool)
|
||||||
|
for _, realization := range realizations {
|
||||||
|
if realization.ExpenseNonstock != nil &&
|
||||||
|
realization.ExpenseNonstock.Expense != nil &&
|
||||||
|
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
|
||||||
|
var projectFlockIDs []uint
|
||||||
|
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
|
||||||
|
for _, pfID := range projectFlockIDs {
|
||||||
|
if pfID != projectFlockID {
|
||||||
|
involvedProjectFlocks[pfID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pfID := range involvedProjectFlocks {
|
||||||
|
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
|
||||||
|
projectFlockKandangCountMap[pfID] = len(pfKandangs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalChickinQty float64
|
var totalChickinQty float64
|
||||||
for _, chickin := range chickins {
|
var totalDepletion float64
|
||||||
totalChickinQty += chickin.UsageQty
|
|
||||||
}
|
|
||||||
|
|
||||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
if projectFlockKandangID != nil {
|
||||||
if err != nil {
|
for _, chickin := range chickins {
|
||||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
if chickin.ProjectFlockKandangId == *projectFlockKandangID {
|
||||||
|
totalChickinQty += chickin.UsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var depletionResult float64
|
||||||
|
err = s.RecordingRepo.DB().WithContext(c.Context()).
|
||||||
|
Table("recording_depletions").
|
||||||
|
Select("COALESCE(SUM(recording_depletions.qty), 0)").
|
||||||
|
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
|
||||||
|
Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID).
|
||||||
|
Scan(&depletionResult).Error
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err)
|
||||||
|
} else {
|
||||||
|
totalDepletion = depletionResult
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
totalChickinQty += chickin.UsageQty
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
totalActualPopulation := totalChickinQty - totalDepletion
|
totalActualPopulation := totalChickinQty - totalDepletion
|
||||||
|
|
||||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
|
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,12 +98,14 @@ func (s dashboardService) buildPerformanceStatistics(ctx context.Context, params
|
|||||||
endDate := params.PeriodEnd
|
endDate := params.PeriodEnd
|
||||||
endExclusive := params.PeriodEndExclusive
|
endExclusive := params.PeriodEndExclusive
|
||||||
|
|
||||||
hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, filter, startDate, endExclusive, endDate, location)
|
globalStartDate, globalEndDate, globalEndExclusive := currentPeriodDates(location)
|
||||||
|
|
||||||
|
hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, globalStartDate, globalEndExclusive, globalEndDate, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, filter, endDate, location)
|
sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, globalEndDate, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -271,15 +273,15 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
|
|||||||
weekFeed := weeklyFeedMap[week]
|
weekFeed := weeklyFeedMap[week]
|
||||||
|
|
||||||
actFcr := 0.0
|
actFcr := 0.0
|
||||||
if weekFeed > 0 {
|
if weekEgg > 0 {
|
||||||
actFcr = weekEgg / weekFeed
|
actFcr = weekFeed / weekEgg
|
||||||
}
|
}
|
||||||
|
|
||||||
cumEgg += weekEgg
|
cumEgg += weekEgg
|
||||||
cumFeed += weekFeed
|
cumFeed += weekFeed
|
||||||
actFcrCum := 0.0
|
actFcrCum := 0.0
|
||||||
if cumFeed > 0 {
|
if cumEgg > 0 {
|
||||||
actFcrCum = cumEgg / cumFeed
|
actFcrCum = cumFeed / cumEgg
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyWeightDataset = append(bodyWeightDataset, map[string]interface{}{
|
bodyWeightDataset = append(bodyWeightDataset, map[string]interface{}{
|
||||||
@@ -357,10 +359,10 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
|
|||||||
},
|
},
|
||||||
"fcr": {
|
"fcr": {
|
||||||
Series: []dto.DashboardChartSeriesDTO{
|
Series: []dto.DashboardChartSeriesDTO{
|
||||||
{Id: "act_fcr", Label: "Act. FCR", Unit: "%"},
|
{Id: "act_fcr", Label: "Act. FCR", Unit: "kg/kg"},
|
||||||
{Id: "std_fcr", Label: "STD. FCR", Unit: "%"},
|
{Id: "std_fcr", Label: "STD. FCR", Unit: "kg/kg"},
|
||||||
{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "%"},
|
{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "kg/kg"},
|
||||||
{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "%"},
|
{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "kg/kg"},
|
||||||
},
|
},
|
||||||
Dataset: fcrDataset,
|
Dataset: fcrDataset,
|
||||||
},
|
},
|
||||||
@@ -843,12 +845,12 @@ func percentDelta(current, last float64) float64 {
|
|||||||
return (current - last) / last
|
return (current - last) / last
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
|
func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
|
||||||
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, filter)
|
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
totalCost, err := s.sumHppCost(ctx, filter, startDate, endExclusive)
|
totalCost, err := s.sumHppCost(ctx, nil, startDate, endExclusive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
@@ -859,11 +861,11 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *valida
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
|
lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
|
||||||
lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, filter)
|
lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
lastCost, err := s.sumHppCost(ctx, filter, lastMonthStart, lastMonthEndExclusive)
|
lastCost, err := s.sumHppCost(ctx, nil, lastMonthStart, lastMonthEndExclusive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
@@ -876,16 +878,16 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *valida
|
|||||||
return hppCurrent, hppLast, nil
|
return hppCurrent, hppLast, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s dashboardService) calculateSellingPrice(ctx context.Context, filter *validation.DashboardFilter, endDate time.Time, location *time.Location) (float64, float64, error) {
|
func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) {
|
||||||
startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
|
startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
|
||||||
currentEndExclusive := endDate.AddDate(0, 0, 1)
|
currentEndExclusive := endDate.AddDate(0, 0, 1)
|
||||||
|
|
||||||
currentAvg, err := s.avgSellingPrice(ctx, filter, startPrevMonth, currentEndExclusive)
|
currentAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, currentEndExclusive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAvg, err := s.avgSellingPrice(ctx, filter, startPrevMonth, endPrevMonthExclusive)
|
lastAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, endPrevMonthExclusive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
@@ -935,11 +937,11 @@ func (s dashboardService) fcrValue(ctx context.Context, filter *validation.Dashb
|
|||||||
}
|
}
|
||||||
feedUsageGrams := feedUsageToGrams(feedRows)
|
feedUsageGrams := feedUsageToGrams(feedRows)
|
||||||
|
|
||||||
if feedUsageGrams <= 0 {
|
if eggWeightGrams <= 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return eggWeightGrams / feedUsageGrams, nil
|
return feedUsageGrams / eggWeightGrams, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s dashboardService) mortalityValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
|
func (s dashboardService) mortalityValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
|
||||||
@@ -1027,3 +1029,11 @@ func monthRange(t time.Time, location *time.Location) (time.Time, time.Time) {
|
|||||||
endExclusive := start.AddDate(0, 1, 0)
|
endExclusive := start.AddDate(0, 1, 0)
|
||||||
return start, endExclusive
|
return start, endExclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func currentPeriodDates(location *time.Location) (time.Time, time.Time, time.Time) {
|
||||||
|
now := time.Now().In(location)
|
||||||
|
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
|
||||||
|
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
|
||||||
|
endExclusive := endDate.AddDate(0, 0, 1)
|
||||||
|
return startDate, endDate, endExclusive
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -15,6 +16,7 @@ type ExpenseRealizationRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||||
|
GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error)
|
||||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +57,40 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
|
|||||||
return realizations, err
|
return realizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) {
|
||||||
|
var realizations []entity.ExpenseRealization
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Preload("ExpenseNonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Flags").
|
||||||
|
Preload("ExpenseNonstock.Expense").
|
||||||
|
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||||
|
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")
|
||||||
|
|
||||||
|
if projectFlockKandangID != nil {
|
||||||
|
db = db.Where(`(
|
||||||
|
expense_nonstocks.project_flock_kandang_id = ? OR
|
||||||
|
(expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND
|
||||||
|
expense_nonstocks.project_flock_kandang_id IS NULL) OR
|
||||||
|
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
|
||||||
|
)`, *projectFlockKandangID, *projectFlockKandangID, fmt.Sprintf("[%d]", projectFlockID))
|
||||||
|
} else {
|
||||||
|
db = db.Where(`(
|
||||||
|
project_flock_kandangs.project_flock_id = ? OR
|
||||||
|
kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR
|
||||||
|
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
|
||||||
|
)`, projectFlockID, projectFlockID, fmt.Sprintf("[%d]", projectFlockID))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Find(&realizations).Error
|
||||||
|
return realizations, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
||||||
var realizations []entity.ExpenseRealization
|
var realizations []entity.ExpenseRealization
|
||||||
var total int64
|
var total int64
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
type MarketingDeliveryProductRepository interface {
|
type MarketingDeliveryProductRepository interface {
|
||||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||||
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
|
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
|
||||||
@@ -53,6 +54,43 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
|
|||||||
return deliveryProducts, nil
|
return deliveryProducts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||||
|
Distinct("marketing_delivery_products.*")
|
||||||
|
|
||||||
|
if projectFlockKandangID != nil {
|
||||||
|
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
|
||||||
|
}
|
||||||
|
|
||||||
|
db = db.
|
||||||
|
Preload("MarketingProduct").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||||
|
Preload("MarketingProduct.Marketing").
|
||||||
|
Preload("MarketingProduct.Marketing.Customer").
|
||||||
|
Order("marketing_delivery_products.delivery_date DESC")
|
||||||
|
|
||||||
|
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
@@ -99,13 +137,14 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
|
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
|
||||||
}).
|
}).
|
||||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
|
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||||
|
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||||
|
|
||||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" {
|
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")
|
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.ProductId > 0 || filters.Search != "" {
|
if filters.ProductId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||||
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +178,29 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filters.MarketingType != "" {
|
||||||
|
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
|
Group("marketing_delivery_products.id")
|
||||||
|
|
||||||
|
switch filters.MarketingType {
|
||||||
|
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{
|
||||||
|
string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah),
|
||||||
|
string(utils.FlagTelurPutih), string(utils.FlagTelurRetak),
|
||||||
|
})
|
||||||
|
case "trading":
|
||||||
|
db = db.Where("flags.name IN (?)", []string{
|
||||||
|
string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia),
|
||||||
|
string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
|
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
|
||||||
if filters.FilterBy == "so_date" {
|
if filters.FilterBy == "so_date" {
|
||||||
if filters.StartDate != "" {
|
if filters.StartDate != "" {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
||||||
@@ -38,6 +39,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
@@ -88,6 +90,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
kandangRepo,
|
kandangRepo,
|
||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
productWarehouseRepo,
|
productWarehouseRepo,
|
||||||
|
productRepo,
|
||||||
projectFlockRepo,
|
projectFlockRepo,
|
||||||
projectflockkandangrepo,
|
projectflockkandangrepo,
|
||||||
projectflockpopulationrepo,
|
projectflockpopulationrepo,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
|
||||||
@@ -44,6 +45,7 @@ type chickinService struct {
|
|||||||
KandangRepo KandangRepo.KandangRepository
|
KandangRepo KandangRepo.KandangRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
|
ProductRepo rProduct.ProductRepository
|
||||||
ProjectFlockRepo rProjectFlock.ProjectflockRepository
|
ProjectFlockRepo rProjectFlock.ProjectflockRepository
|
||||||
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
||||||
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||||
@@ -52,7 +54,7 @@ type chickinService struct {
|
|||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
|
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
|
||||||
return &chickinService{
|
return &chickinService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -60,6 +62,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
|
|||||||
KandangRepo: kandangRepo,
|
KandangRepo: kandangRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
ProductRepo: productRepo,
|
||||||
ProjectFlockRepo: projectFlockRepo,
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
ProjectflockKandangRepo: projectflockkandangRepo,
|
ProjectflockKandangRepo: projectflockkandangRepo,
|
||||||
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
||||||
@@ -99,7 +102,6 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get chickins: %+v", err)
|
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return chickins, total, nil
|
return chickins, total, nil
|
||||||
@@ -347,7 +349,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to update chickin: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +381,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
warehouseDeltas := make(map[uint]float64)
|
warehouseDeltas := make(map[uint]float64)
|
||||||
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
||||||
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,6 +449,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
|
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
if _, err := approvalSvc.CreateApproval(
|
if _, err := approvalSvc.CreateApproval(
|
||||||
@@ -479,39 +480,55 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
|
|
||||||
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
|
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
|
||||||
|
|
||||||
|
var targetFlag utils.FlagType
|
||||||
if category == string(utils.ProjectFlockCategoryGrowing) {
|
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
targetFlag = utils.FlagPullet
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
|
||||||
}
|
|
||||||
|
|
||||||
pfkID := approvableID
|
|
||||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
|
||||||
}
|
|
||||||
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
|
||||||
}
|
|
||||||
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
targetFlag = utils.FlagLayer
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
}
|
||||||
}
|
if populationExists {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pfkID := approvableID
|
sourcePW, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
|
return db.Preload("Product.Flags")
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for chickin %d", chickin.Id))
|
||||||
}
|
}
|
||||||
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
if err := s.autoAddFlagToProduct(c.Context(), dbTransaction, sourcePW.Product.Id, targetFlag); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to auto-add flag to product %d", sourcePW.Product.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
population := &entity.ProjectFlockPopulation{
|
||||||
|
ProjectChickinId: chickin.Id,
|
||||||
|
ProductWarehouseId: sourcePW.Id,
|
||||||
|
TotalQty: 0,
|
||||||
|
TotalUsedQty: 0,
|
||||||
|
Notes: chickin.Notes,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
}
|
||||||
|
if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chickinRepoTx.PatchOne(c.Context(), chickin.Id, map[string]any{
|
||||||
|
"pending_usage_qty": 0,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset pending usage qty for chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,7 +551,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
warehouseDeltas := make(map[uint]float64)
|
warehouseDeltas := make(map[uint]float64)
|
||||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||||
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
|
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,104 +584,35 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
|
// 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 {
|
||||||
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
if s.ProductRepo == nil {
|
||||||
if err == nil && len(products) > 0 {
|
return nil
|
||||||
existingPW := &products[0]
|
|
||||||
|
|
||||||
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
|
|
||||||
existingPW.ProjectFlockKandangId = projectFlockKandangId
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return existingPW, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
|
currentFlags, err := s.ProductRepo.GetFlags(ctx, productID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
|
return fmt.Errorf("failed to get product flags: %w", err)
|
||||||
}
|
|
||||||
if product == nil {
|
|
||||||
return nil, fmt.Errorf("no %s product found in system", categoryCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newPW := &entity.ProductWarehouse{
|
hasTargetFlag := false
|
||||||
ProductId: product.Id,
|
currentFlagNames := make([]string, 0, len(currentFlags))
|
||||||
WarehouseId: warehouseId,
|
for _, flag := range currentFlags {
|
||||||
ProjectFlockKandangId: projectFlockKandangId,
|
currentFlagNames = append(currentFlagNames, flag.Name)
|
||||||
Quantity: 0,
|
if flag.Name == string(targetFlag) {
|
||||||
|
hasTargetFlag = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
if hasTargetFlag {
|
||||||
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPW, nil
|
newFlags := append(currentFlagNames, string(targetFlag))
|
||||||
}
|
if err := s.ProductRepo.SyncFlags(ctx, tx, productID, newFlags); err != nil {
|
||||||
|
return fmt.Errorf("failed to sync flags: %w", err)
|
||||||
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
|
|
||||||
|
|
||||||
if targetPW == nil || targetPW.Id == 0 {
|
|
||||||
return fmt.Errorf("invalid target product warehouse")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
|
||||||
chickinRepoTx := s.Repository.WithTx(dbTransaction)
|
|
||||||
|
|
||||||
var totalQuantityAdded float64
|
|
||||||
|
|
||||||
for _, chickin := range chickins {
|
|
||||||
|
|
||||||
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if populationExists {
|
|
||||||
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
quantityToConvert := chickin.UsageQty
|
|
||||||
|
|
||||||
population := &entity.ProjectFlockPopulation{
|
|
||||||
ProjectChickinId: chickin.Id,
|
|
||||||
ProductWarehouseId: targetPW.Id,
|
|
||||||
TotalQty: 0, // Will be set by FIFO Replenish
|
|
||||||
TotalUsedQty: 0,
|
|
||||||
Notes: chickin.Notes,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
}
|
|
||||||
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset PendingUsageQty to 0 since population has been created
|
|
||||||
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
|
|
||||||
"pending_usage_qty": 0,
|
|
||||||
}, nil); err != nil {
|
|
||||||
return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replenish stock to target ProductWarehouse based on source flag
|
|
||||||
// StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID
|
|
||||||
if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
totalQuantityAdded += quantityToConvert
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
|
|
||||||
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
|
|
||||||
// - DOC → replenish ke PULLET
|
|
||||||
// - PULLET → replenish ke LAYER
|
|
||||||
// - LAYER → tidak perlu replenish (sudah final)
|
|
||||||
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,9 +621,6 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f",
|
|
||||||
chickin.Id, chickin.ProductWarehouseId, desiredQty)
|
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||||
UsableKey: chickinUsableKey,
|
UsableKey: chickinUsableKey,
|
||||||
UsableID: chickin.Id,
|
UsableID: chickin.Id,
|
||||||
@@ -686,13 +630,9 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
|
|
||||||
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
|
||||||
|
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -706,10 +646,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
||||||
}
|
}
|
||||||
if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil {
|
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
|
||||||
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -720,93 +657,17 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||||
return db.Preload("Product.Flags")
|
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||||
|
StockableID: population.Id,
|
||||||
|
ProductWarehouseID: targetPW.Id,
|
||||||
|
Quantity: chickin.UsageQty,
|
||||||
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if sourcePW == nil || sourcePW.Product.Id == 0 {
|
|
||||||
return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceFlags := sourcePW.Product.Flags
|
|
||||||
if len(sourceFlags) == 0 {
|
|
||||||
s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hasDoc := false
|
|
||||||
hasPullet := false
|
|
||||||
hasLayer := false
|
|
||||||
for _, flag := range sourceFlags {
|
|
||||||
flagName := utils.FlagType(flag.Name)
|
|
||||||
if flagName == utils.FlagDOC {
|
|
||||||
hasDoc = true
|
|
||||||
} else if flagName == utils.FlagPullet {
|
|
||||||
hasPullet = true
|
|
||||||
} else if flagName == utils.FlagLayer {
|
|
||||||
hasLayer = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasDoc && hasPullet && hasLayer {
|
|
||||||
s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id)
|
|
||||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
|
||||||
StockableID: population.Id,
|
|
||||||
ProductWarehouseID: sourcePW.Id,
|
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LAYER only - no replenish needed
|
|
||||||
if hasLayer && !hasDoc && !hasPullet {
|
|
||||||
s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasDoc && !hasPullet && !hasLayer {
|
|
||||||
s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id)
|
|
||||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
|
||||||
StockableID: population.Id,
|
|
||||||
ProductWarehouseID: targetPW.Id,
|
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasPullet && !hasDoc && !hasLayer {
|
|
||||||
s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id)
|
|
||||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
|
||||||
StockableID: population.Id,
|
|
||||||
ProductWarehouseID: targetPW.Id,
|
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other combinations (e.g., DOC + PULLET without LAYER) - skip for now
|
|
||||||
s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +686,6 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
UsableID: chickin.Id,
|
UsableID: chickin.Id,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,9 +702,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||||
}
|
}
|
||||||
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
|
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
|
||||||
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
|||||||
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
||||||
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
||||||
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
||||||
|
MarketingType: ctx.Query("marketing_type", ""),
|
||||||
FilterBy: ctx.Query("filter_by", ""),
|
FilterBy: ctx.Query("filter_by", ""),
|
||||||
StartDate: ctx.Query("start_date", ""),
|
StartDate: ctx.Query("start_date", ""),
|
||||||
EndDate: ctx.Query("end_date", ""),
|
EndDate: ctx.Query("end_date", ""),
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -22,7 +26,7 @@ type RepportMarketingItemDTO struct {
|
|||||||
DoNumber string `json:"do_number"`
|
DoNumber string `json:"do_number"`
|
||||||
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
|
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
|
||||||
VehicleNumber string `json:"vehicle_number"`
|
VehicleNumber string `json:"vehicle_number"`
|
||||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
Product *ProductRelationDTOFixed `json:"product,omitempty"`
|
||||||
MarketingType string `json:"marketing_type"`
|
MarketingType string `json:"marketing_type"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
AverageWeightKg float64 `json:"average_weight_kg"`
|
AverageWeightKg float64 `json:"average_weight_kg"`
|
||||||
@@ -46,6 +50,12 @@ type RepportMarketingResponseDTO struct {
|
|||||||
Total *Summary `json:"total,omitempty"`
|
Total *Summary `json:"total,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductRelationDTOFixed struct {
|
||||||
|
productDTO.ProductRelationDTO
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
|
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
|
||||||
soDate := time.Time{}
|
soDate := time.Time{}
|
||||||
agingDays := 0
|
agingDays := 0
|
||||||
@@ -106,7 +116,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
|
|||||||
|
|
||||||
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
|
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
|
||||||
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
||||||
item.Product = &mapped
|
item.Product = newProductRelationDTOFixedPtr(&mapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
@@ -139,7 +149,7 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
||||||
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
hasAyam, hasTelur, hasTrading := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||||
|
|
||||||
if hasAyam {
|
if hasAyam {
|
||||||
return "ayam"
|
return "ayam"
|
||||||
@@ -147,12 +157,15 @@ func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
|||||||
if hasTelur {
|
if hasTelur {
|
||||||
return "telur"
|
return "telur"
|
||||||
}
|
}
|
||||||
return "trading"
|
if hasTrading {
|
||||||
|
return "trading"
|
||||||
|
}
|
||||||
|
return "trading" // default to trading if no flags found
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
|
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool) {
|
||||||
if len(flags) == 0 {
|
if len(flags) == 0 {
|
||||||
return false, false
|
return false, false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, flag := range flags {
|
for _, flag := range flags {
|
||||||
@@ -167,13 +180,18 @@ func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
|
|||||||
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
|
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
|
||||||
hasTelur = true
|
hasTelur = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ft == utils.FlagOVK || ft == utils.FlagObat || ft == utils.FlagVitamin || ft == utils.FlagKimia ||
|
||||||
|
ft == utils.FlagPakan || ft == utils.FlagPreStarter || ft == utils.FlagStarter || ft == utils.FlagFinisher {
|
||||||
|
hasTrading = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasAyam, hasTelur
|
return hasAyam, hasTelur, hasTrading
|
||||||
}
|
}
|
||||||
|
|
||||||
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
|
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
|
||||||
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
hasAyam, hasTelur, _ := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||||
|
|
||||||
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
|
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
|
||||||
return hasAyam
|
return hasAyam
|
||||||
@@ -259,3 +277,39 @@ func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPr
|
|||||||
Total: total,
|
Total: total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newProductRelationDTOFixedPtr(original *productDTO.ProductRelationDTO) *ProductRelationDTOFixed {
|
||||||
|
if original == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fixed := ProductRelationDTOFixed{
|
||||||
|
ProductRelationDTO: *original,
|
||||||
|
ProductPrice: original.ProductPrice,
|
||||||
|
SellingPrice: original.SellingPrice,
|
||||||
|
}
|
||||||
|
return &fixed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
SellingPrice *float64 `json:"selling_price"`
|
||||||
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
|
Flags *[]string `json:"flags,omitempty"`
|
||||||
|
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||||
|
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(&Alias{
|
||||||
|
Id: p.ProductRelationDTO.Id,
|
||||||
|
Name: p.ProductRelationDTO.Name,
|
||||||
|
ProductPrice: p.ProductPrice,
|
||||||
|
SellingPrice: p.SellingPrice,
|
||||||
|
Uom: p.ProductRelationDTO.Uom,
|
||||||
|
Flags: p.ProductRelationDTO.Flags,
|
||||||
|
ProductCategory: p.ProductRelationDTO.ProductCategory,
|
||||||
|
Suppliers: p.ProductRelationDTO.Suppliers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type MarketingQuery struct {
|
|||||||
ProductId int64 `query:"product_id" validate:"omitempty"`
|
ProductId int64 `query:"product_id" validate:"omitempty"`
|
||||||
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
||||||
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
||||||
|
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
|
||||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
|
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
|
||||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
|||||||
Reference in New Issue
Block a user