mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88bdf70994 | |||
| d54911f8b4 | |||
| 1edd071a8a | |||
| fb565ef728 | |||
| 0d585a99a6 | |||
| b1b50c3c01 | |||
| c085888ca9 | |||
| 774c9b1d58 | |||
| 12ed9cd753 | |||
| 4ab1553340 | |||
| 3fa50b6344 | |||
| 80424dee17 | |||
| a9b33eaf28 | |||
| 551534d02a | |||
| 202a8ffc66 | |||
| 6bc5e7d293 | |||
| 2e0827dec5 | |||
| 87973a6c9f | |||
| 1ca6c6a104 | |||
| 78a45b11e7 | |||
| 58b29501c0 | |||
| bac0361df5 | |||
| 645a97b460 | |||
| eb2479cc41 | |||
| 04ec8560a7 | |||
| 12240a9e2d | |||
| f2a46843c8 | |||
| 06e92d1c77 | |||
| d7ed768d14 | |||
| f04cbd24bd | |||
| ec4b849778 | |||
| 132e043597 | |||
| e99af36796 | |||
| f6bdb17699 | |||
| 8f7fc622f6 | |||
| 30f5ed417c | |||
| 1d726afa6f | |||
| 96ba947952 | |||
| ad0504f49e | |||
| 0b708cd57b | |||
| 32153f02b8 | |||
| cf37822a07 | |||
| 7fb6c3c7bf | |||
| fd689919b0 | |||
| 2ad0c17fbe | |||
| e8c7b3f2a8 | |||
| ce09c2473c | |||
| 3493d1d7b2 | |||
| 9fff954857 | |||
| c7c1e4b335 | |||
| 5dbe9bb989 | |||
| e555cfa950 | |||
| 32a8557a3b | |||
| 1818f5a295 | |||
| a73b44808f | |||
| 69d9137e78 | |||
| e32b231c6c | |||
| ca6d0b160b | |||
| e8a89f0f17 | |||
| d96a12776a | |||
| adabb4e035 | |||
| 16a0b848bc | |||
| c2d2701d72 | |||
| 894efa7aa5 | |||
| d0625e7d21 | |||
| 67ecdbc1dd | |||
| d50ab7cc97 | |||
| ad3bb0e29a | |||
| dd4dcc1c39 | |||
| aa4da68680 | |||
| 95965cb26a | |||
| e4e17f16f9 | |||
| edd77c5265 | |||
| dab692a0c1 | |||
| e6244dea8a | |||
| d1e4bf060e | |||
| fc06b3e4db | |||
| cf42e8c130 | |||
| 8ff97cb647 | |||
| 1b7ce3c62c | |||
| 21de17b18a | |||
| 2aaaab91f7 | |||
| 4227152979 | |||
| ec020ac17c | |||
| adc30ad5cd | |||
| 9fb5395469 | |||
| bc771660be | |||
| b615570036 | |||
| c793c3cf9a | |||
| b3e0410f5a | |||
| a882d5a687 | |||
| c0848b6d2d | |||
| b240478ed5 | |||
| 3052497fc0 | |||
| 71c62c5e02 | |||
| 768961d7d6 | |||
| 8cd9627a51 | |||
| 378d633ea4 | |||
| fb193fc61f | |||
| af7aabdec8 | |||
| bac36b4f00 | |||
| 687d02313b | |||
| 7d3602d829 | |||
| 533e9aca6f | |||
| dcfb5e10b4 | |||
| fbeccf4cdc | |||
| f2ae2cc731 | |||
| 409652c15e | |||
| 9d611b7492 | |||
| f13e4f907c | |||
| a5af469865 | |||
| 3f4d6c630a | |||
| cad15bcd78 | |||
| e0ff6e6d79 | |||
| 9f1c153841 | |||
| dbf72c7248 | |||
| 894fa0b22a | |||
| d680196919 | |||
| cfbe431222 | |||
| 4c434899aa | |||
| 7fd90f3268 | |||
| d26c2dba3f | |||
| f8415ea15d | |||
| 64fe845128 |
+18
-87
@@ -1,90 +1,21 @@
|
|||||||
stages:
|
workflow:
|
||||||
- deploy
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
- when: never
|
||||||
|
|
||||||
deploy-dev:
|
include:
|
||||||
stage: deploy
|
- local: "ci/development.yml"
|
||||||
image: alpine:3.20
|
rules:
|
||||||
variables:
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
DEPLOY_APP: "LTI-MBUGROUP"
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
|
||||||
GIT_DEPTH: "1"
|
|
||||||
|
|
||||||
before_script:
|
- local: "ci/staging.yml"
|
||||||
- echo "🧰 Installing dependencies..."
|
rules:
|
||||||
- apk update && apk add --no-cache openssh git curl bash
|
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||||
|
|
||||||
# Setup SSH di runner
|
- local: "ci/production.yml"
|
||||||
- mkdir -p ~/.ssh
|
rules:
|
||||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
- chmod 600 ~/.ssh/id_rsa
|
|
||||||
- eval "$(ssh-agent -s)"
|
|
||||||
- ssh-add ~/.ssh/id_rsa
|
|
||||||
|
|
||||||
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
|
||||||
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
|
||||||
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
script:
|
|
||||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
|
||||||
|
|
||||||
- >
|
|
||||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd /home/devops/docker/deployment/development/lti-api
|
|
||||||
|
|
||||||
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
|
||||||
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
|
||||||
|
|
||||||
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
# Fetch/reset pakai SSH
|
|
||||||
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
|
||||||
git reset --hard origin/development
|
|
||||||
|
|
||||||
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
|
||||||
"; then
|
|
||||||
STATUS='success';
|
|
||||||
else
|
|
||||||
STATUS='failed';
|
|
||||||
fi;
|
|
||||||
|
|
||||||
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
|
||||||
|
|
||||||
if [ "$STATUS" = "success" ]; then
|
|
||||||
COLOR=3066993;
|
|
||||||
TITLE="✅ Deployment API Succeeded";
|
|
||||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
|
||||||
else
|
|
||||||
COLOR=15158332;
|
|
||||||
TITLE="❌ Deployment API Failed Gaes";
|
|
||||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
|
||||||
fi;
|
|
||||||
|
|
||||||
echo "{
|
|
||||||
\"username\": \"CI Bot\",
|
|
||||||
\"embeds\": [{
|
|
||||||
\"title\": \"$TITLE\",
|
|
||||||
\"description\": \"$DESC\",
|
|
||||||
\"color\": $COLOR,
|
|
||||||
\"fields\": [
|
|
||||||
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
|
||||||
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
|
||||||
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
|
||||||
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}" > payload.json;
|
|
||||||
|
|
||||||
echo "📡 Sending notification to Discord...";
|
|
||||||
curl -sS -H "Content-Type: application/json" \
|
|
||||||
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
|
||||||
|
|
||||||
only:
|
|
||||||
- development
|
|
||||||
|
|
||||||
environment:
|
|
||||||
name: development
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
stages:
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
deploy-dev:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine:3.20
|
||||||
|
variables:
|
||||||
|
DEPLOY_APP: "LTI-MBUGROUP"
|
||||||
|
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
GIT_DEPTH: "1"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- echo "🧰 Installing dependencies..."
|
||||||
|
- apk update && apk add --no-cache openssh git curl bash
|
||||||
|
|
||||||
|
# Setup SSH di runner
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||||
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
|
- eval "$(ssh-agent -s)"
|
||||||
|
- ssh-add ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||||
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
|
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
script:
|
||||||
|
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||||
|
|
||||||
|
- >
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd /home/devops/docker/deployment/development/lti-api
|
||||||
|
|
||||||
|
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
||||||
|
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
||||||
|
|
||||||
|
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Fetch/reset pakai SSH
|
||||||
|
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
||||||
|
git reset --hard origin/development
|
||||||
|
|
||||||
|
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||||
|
"; then
|
||||||
|
STATUS='success';
|
||||||
|
else
|
||||||
|
STATUS='failed';
|
||||||
|
fi;
|
||||||
|
|
||||||
|
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
||||||
|
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
COLOR=3066993;
|
||||||
|
TITLE="✅ Deployment API Succeeded";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
||||||
|
else
|
||||||
|
COLOR=15158332;
|
||||||
|
TITLE="❌ Deployment API Failed Gaes";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
||||||
|
fi;
|
||||||
|
|
||||||
|
echo "{
|
||||||
|
\"username\": \"CI Bot\",
|
||||||
|
\"embeds\": [{
|
||||||
|
\"title\": \"$TITLE\",
|
||||||
|
\"description\": \"$DESC\",
|
||||||
|
\"color\": $COLOR,
|
||||||
|
\"fields\": [
|
||||||
|
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
||||||
|
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
||||||
|
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
||||||
|
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}" > payload.json;
|
||||||
|
|
||||||
|
echo "📡 Sending notification to Discord...";
|
||||||
|
curl -sS -H "Content-Type: application/json" \
|
||||||
|
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||||
|
|
||||||
|
only:
|
||||||
|
- development
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
stages:
|
||||||
|
- build
|
||||||
|
# - migrate
|
||||||
|
- deploy
|
||||||
|
- seed
|
||||||
|
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- self-hosted-prod
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||||
|
when: always
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
|
||||||
|
IMAGE_TAG: "production_${CI_COMMIT_SHORT_SHA}"
|
||||||
|
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||||
|
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:production_latest"
|
||||||
|
|
||||||
|
DEPLOY_DIR: "/opt/deploy/lti"
|
||||||
|
COMPOSE_FILE: "docker-compose.yaml"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# BUILD (AUTO)
|
||||||
|
# =========================
|
||||||
|
build_production:
|
||||||
|
stage: build
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
|
echo "✅ Build image: $IMAGE_NAME"
|
||||||
|
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
||||||
|
|
||||||
|
echo "✅ Push image: $IMAGE_NAME"
|
||||||
|
docker push "$IMAGE_NAME"
|
||||||
|
|
||||||
|
echo "✅ Tag latest: $IMAGE_LATEST"
|
||||||
|
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||||
|
docker push "$IMAGE_LATEST"
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# MIGRATE (PRODUCTION - MANUAL)
|
||||||
|
# =========================
|
||||||
|
#migrate_production:
|
||||||
|
# stage: migrate
|
||||||
|
# rules:
|
||||||
|
# - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||||
|
# when: manual
|
||||||
|
# allow_failure: false
|
||||||
|
# needs:
|
||||||
|
# - job: build_production
|
||||||
|
# artifacts: false
|
||||||
|
# script: |
|
||||||
|
# set -e
|
||||||
|
# cd /opt/deploy/lti
|
||||||
|
# test -f .env || (echo "❌ .env not found" && exit 1)
|
||||||
|
|
||||||
|
# set -a
|
||||||
|
# . ./.env
|
||||||
|
# set +a
|
||||||
|
|
||||||
|
# Validasi env wajib
|
||||||
|
# : "${DB_HOST:?DB_HOST not set}"
|
||||||
|
# : "${DB_PORT:?DB_PORT not set}"
|
||||||
|
# : "${DB_USER:?DB_USER not set}"
|
||||||
|
# : "${DB_PASSWORD:?DB_PASSWORD not set}"
|
||||||
|
# : "${DB_NAME:?DB_NAME not set}"
|
||||||
|
|
||||||
|
# DB_SSLMODE="${DB_SSLMODE:-require}"
|
||||||
|
# export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
|
||||||
|
|
||||||
|
# echo "✅ Running migrations (production)..."
|
||||||
|
# docker run --rm \
|
||||||
|
# -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
|
||||||
|
# migrate/migrate:v4.15.2 \
|
||||||
|
# -path=/migrations -database "$DATABASE_URL" up
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DEPLOY (AUTO)
|
||||||
|
# =========================
|
||||||
|
deploy_production:
|
||||||
|
stage: deploy
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||||
|
needs:
|
||||||
|
# - job: migrate_production
|
||||||
|
# artifacts: false
|
||||||
|
- job: build_production
|
||||||
|
artifacts: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" pull
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SEED (MANUAL)
|
||||||
|
# =========================
|
||||||
|
seed_production:
|
||||||
|
stage: seed
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
when: manual
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd /opt/deploy/lti
|
||||||
|
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||||
|
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
|
docker compose --env-file .env pull seed
|
||||||
|
docker compose --env-file .env run --rm seed
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- migrate
|
||||||
|
- deploy
|
||||||
|
- seed
|
||||||
|
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- self-hosted-stg
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
when: always
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
|
||||||
|
IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
|
||||||
|
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||||
|
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
|
||||||
|
|
||||||
|
DEPLOY_DIR: "/opt/deploy/stg-lti-api"
|
||||||
|
COMPOSE_FILE: "docker-compose.yaml"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# BUILD (AUTO)
|
||||||
|
# =========================
|
||||||
|
build_staging:
|
||||||
|
stage: build
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
|
echo "✅ Build image: $IMAGE_NAME"
|
||||||
|
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
||||||
|
|
||||||
|
echo "✅ Push image: $IMAGE_NAME"
|
||||||
|
docker push "$IMAGE_NAME"
|
||||||
|
|
||||||
|
echo "✅ Tag latest: $IMAGE_LATEST"
|
||||||
|
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||||
|
docker push "$IMAGE_LATEST"
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# MIGRATE (AUTO)
|
||||||
|
# =========================
|
||||||
|
migrate_staging:
|
||||||
|
stage: migrate
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
needs:
|
||||||
|
- job: build_staging
|
||||||
|
artifacts: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
echo "✅ Running migrations (staging) ..."
|
||||||
|
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
|
||||||
|
# ✅ load env dari server
|
||||||
|
set -a
|
||||||
|
. ./.env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
# ✅ validasi
|
||||||
|
test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1)
|
||||||
|
test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1)
|
||||||
|
test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1)
|
||||||
|
test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1)
|
||||||
|
test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1)
|
||||||
|
|
||||||
|
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}"
|
||||||
|
echo "✅ DATABASE_URL=$DATABASE_URL"
|
||||||
|
|
||||||
|
# ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!)
|
||||||
|
echo "✅ Ensuring postgres & redis running ..."
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true
|
||||||
|
|
||||||
|
# ✅ Ambil network key dari compose
|
||||||
|
COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')"
|
||||||
|
echo "✅ Compose network key: $COMPOSE_NETWORK_KEY"
|
||||||
|
|
||||||
|
# ✅ Cari network name yang dipakai docker
|
||||||
|
NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)"
|
||||||
|
test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1)
|
||||||
|
|
||||||
|
echo "✅ Docker network detected: $NETWORK_NAME"
|
||||||
|
|
||||||
|
# ✅ Migrations dari repo (CI workspace)
|
||||||
|
echo "✅ Checking migrations from repo..."
|
||||||
|
ls -lah "$CI_PROJECT_DIR/internal/database/migrations"
|
||||||
|
|
||||||
|
echo "✅ Running migrations via migrate/migrate container"
|
||||||
|
set +e
|
||||||
|
out=$(docker run --rm \
|
||||||
|
--network "$NETWORK_NAME" \
|
||||||
|
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
|
||||||
|
migrate/migrate:v4.15.2 \
|
||||||
|
-path=/migrations -database "$DATABASE_URL" up 2>&1)
|
||||||
|
code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "$out"
|
||||||
|
|
||||||
|
# ✅ Handle no change dengan benar (tidak false-success)
|
||||||
|
if echo "$out" | grep -qi "no change"; then
|
||||||
|
echo "✅ No change (already up to date)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $code -ne 0 ]; then
|
||||||
|
echo "❌ Migration failed with exit code $code"
|
||||||
|
exit $code
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Migration applied successfully"
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DEPLOY (AUTO)
|
||||||
|
# =========================
|
||||||
|
deploy_staging:
|
||||||
|
stage: deploy
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
needs:
|
||||||
|
- job: migrate_staging
|
||||||
|
artifacts: false
|
||||||
|
- job: build_staging
|
||||||
|
artifacts: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" pull
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SEED (MANUAL)
|
||||||
|
# =========================
|
||||||
|
seed_staging:
|
||||||
|
stage: seed
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
needs:
|
||||||
|
- job: deploy_staging
|
||||||
|
artifacts: false
|
||||||
|
when: manual
|
||||||
|
allow_failure: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
|
||||||
|
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" pull seed || true
|
||||||
|
docker compose -f "$COMPOSE_FILE" run --rm seed%
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HppCostRepository interface {
|
||||||
|
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
|
||||||
|
GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
|
GetBudgetCostByProjectFlockId(ctx context.Context, projectFlockId uint) (float64, error)
|
||||||
|
GetExpedisionCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
|
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
|
||||||
|
GetOvkUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
|
||||||
|
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
|
GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error)
|
||||||
|
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
|
||||||
|
GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
|
||||||
|
GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error)
|
||||||
|
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHppCostRepository(db *gorm.DB) HppCostRepository {
|
||||||
|
return &HppRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) {
|
||||||
|
var ids []uint
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Select("id").
|
||||||
|
Where("project_flock_id = ?", projectFlockId).
|
||||||
|
Scan(&ids).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_chickins AS pc").
|
||||||
|
Select("COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0)").
|
||||||
|
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String()).
|
||||||
|
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||||
|
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetBudgetCostByProjectFlockId(ctx context.Context, projectFlockId uint) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_budgets AS pb").
|
||||||
|
Select("COALESCE(SUM(pb.qty * pb.price), 0)").
|
||||||
|
Where("pb.project_flock_id = ?", projectFlockId).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetExpedisionCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("expense_nonstocks AS en").
|
||||||
|
Select("COALESCE(SUM(er.qty * er.price), 0)").
|
||||||
|
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = en.nonstock_id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||||
|
Where("en.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Where("f.name = ?", utils.FlagEkspedisi).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
|
||||||
|
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
|
||||||
|
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||||
|
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Where("r.record_datetime <= ?", *date).
|
||||||
|
Where("f.name = ?", utils.FlagPakan).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := []utils.FlagType{
|
||||||
|
utils.FlagOVK,
|
||||||
|
utils.FlagObat,
|
||||||
|
utils.FlagVitamin,
|
||||||
|
utils.FlagKimia,
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
|
||||||
|
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
|
||||||
|
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||||
|
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Where("r.record_datetime <= ?", *date).
|
||||||
|
Where("f.name IN ?", flags).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_chickins AS pc").
|
||||||
|
Select("COALESCE(SUM(pc.usage_qty), 0)").
|
||||||
|
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) {
|
||||||
|
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
|
||||||
|
stockableTransferIn := fifo.StockableKeyStockTransferIn.String()
|
||||||
|
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_chickins AS pc").
|
||||||
|
Select(`
|
||||||
|
COALESCE(SUM(pc.usage_qty * CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END), 0)`,
|
||||||
|
stockablePurchase, stockableTransferIn).
|
||||||
|
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id", usableProjectChickin).
|
||||||
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
||||||
|
Joins("LEFT JOIN stock_allocations AS tsa ON tsa.usable_type = ? AND tsa.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa.stockable_type = ?", stockableTransferIn, stockableTransferIn, stockablePurchase).
|
||||||
|
Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa.stockable_id").
|
||||||
|
Where("pc.project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
var totals struct {
|
||||||
|
TotalPieces float64
|
||||||
|
TotalWeightKg float64
|
||||||
|
}
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0)AS total_weight_kg").
|
||||||
|
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
|
||||||
|
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Where("r.record_datetime <= ?", *date).
|
||||||
|
Scan(&totals).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return totals.TotalPieces, totals.TotalWeightKg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
var totals struct {
|
||||||
|
TotalPieces float64
|
||||||
|
TotalWeight float64
|
||||||
|
}
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select("COALESCE(SUM(mdp.usage_qty), 0) AS total_pieces, COALESCE(SUM(mdp.total_weight), 0) AS total_weight").
|
||||||
|
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
|
||||||
|
Joins("JOIN stock_allocations AS sa ON sa.stockable_type = ? AND sa.stockable_id = re.id AND sa.usable_type = ?", fifo.StockableKeyRecordingEgg.String(), fifo.UsableKeyMarketingDelivery.String()).
|
||||||
|
Joins("JOIN marketing_delivery_products AS mdp ON mdp.id = sa.usable_id").
|
||||||
|
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||||
|
Where("r.record_datetime <= ?", *date).
|
||||||
|
Scan(&totals).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return totals.TotalPieces, totals.TotalWeight, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) {
|
||||||
|
var projectFlockID uint
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Select("project_flock_id").
|
||||||
|
Where("id = ?", projectFlockKandangId).
|
||||||
|
Scan(&projectFlockID).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectFlockID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppRepositoryImpl) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) {
|
||||||
|
var summary struct {
|
||||||
|
ProjectFlockID uint
|
||||||
|
TotalQty float64
|
||||||
|
}
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("laying_transfer_targets AS ltt").
|
||||||
|
Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty").
|
||||||
|
Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id").
|
||||||
|
Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
|
Group("lt.from_project_flock_id").
|
||||||
|
Scan(&summary).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary.ProjectFlockID, summary.TotalQty, nil
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ type FifoService interface {
|
|||||||
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
|
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
|
||||||
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
|
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
|
||||||
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
|
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
|
||||||
|
AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type fifoService struct {
|
type fifoService struct {
|
||||||
@@ -95,6 +96,15 @@ type StockReplenishRequest struct {
|
|||||||
Tx *gorm.DB
|
Tx *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StockAdjustRequest struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
StockableID uint
|
||||||
|
ProductWarehouseID uint
|
||||||
|
Quantity float64
|
||||||
|
Note *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
type PendingResolution struct {
|
type PendingResolution struct {
|
||||||
UsableKey fifo.UsableKey
|
UsableKey fifo.UsableKey
|
||||||
UsableID uint
|
UsableID uint
|
||||||
@@ -137,6 +147,37 @@ type StockReleaseRequest struct {
|
|||||||
Reason *string
|
Reason *string
|
||||||
Tx *gorm.DB
|
Tx *gorm.DB
|
||||||
}
|
}
|
||||||
|
func (s *fifoService) AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error {
|
||||||
|
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
||||||
|
return errors.New("stockable key and id are required")
|
||||||
|
}
|
||||||
|
if req.ProductWarehouseID == 0 {
|
||||||
|
return errors.New("product warehouse id is required")
|
||||||
|
}
|
||||||
|
if req.Quantity == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if req.Quantity > 0 {
|
||||||
|
return errors.New("quantity must be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := fifo.Stockable(req.StockableKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("stockable %q is not registered", req.StockableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
req.ProductWarehouseID: req.Quantity,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
|
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
|
||||||
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HppService interface {
|
||||||
|
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
|
||||||
|
GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error)
|
||||||
|
GetTotalProductionCost(projectFlockKandangId uint, date *time.Time, totalDepresiasiGrowing float64) (float64, error)
|
||||||
|
GetBudgetKandangLaying(projectFlockKandangId uint, date *time.Time) (float64, error)
|
||||||
|
GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error)
|
||||||
|
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppCostResponse struct {
|
||||||
|
Estimation HppCostDetail `json:"estimation"`
|
||||||
|
Real HppCostDetail `json:"real"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppCostDetail struct {
|
||||||
|
HargaKg float64 `json:"harga_kg"`
|
||||||
|
HargaButir float64 `json:"harga_butir"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Kg float64 `json:"kg"`
|
||||||
|
Butir float64 `json:"butir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hppService struct {
|
||||||
|
hppRepo commonRepo.HppCostRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHppService(hppRepo commonRepo.HppCostRepository) HppService {
|
||||||
|
return &hppService{hppRepo: hppRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, date, depresiasiTransfer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, date)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, date *time.Time, depresiasiTransfer float64) (float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, date *time.Time) (float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if eggProduksiPiecesFlock == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if totalPopulationFlockGrowing == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, date)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return &HppCostResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
estimation := HppCostDetail{
|
||||||
|
Total: totalProductionCost,
|
||||||
|
Kg: estimWeightKg,
|
||||||
|
Butir: estimPieces,
|
||||||
|
}
|
||||||
|
if estimWeightKg > 0 {
|
||||||
|
estimation.HargaKg = roundToTwoDecimals(totalProductionCost / estimWeightKg)
|
||||||
|
}
|
||||||
|
if estimPieces > 0 {
|
||||||
|
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
|
||||||
|
}
|
||||||
|
|
||||||
|
real := HppCostDetail{
|
||||||
|
Total: totalProductionCost,
|
||||||
|
Kg: realWeightKg,
|
||||||
|
Butir: realPieces,
|
||||||
|
}
|
||||||
|
if realWeightKg > 0 {
|
||||||
|
real.HargaKg = roundToTwoDecimals(totalProductionCost / realWeightKg)
|
||||||
|
}
|
||||||
|
if realPieces > 0 {
|
||||||
|
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppCostResponse{
|
||||||
|
Estimation: estimation,
|
||||||
|
Real: real,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundToTwoDecimals(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
-- Rollback: Remove requested_qty column from laying_transfer_sources table
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
DROP COLUMN IF EXISTS requested_qty;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
-- Add requested_qty column to laying_transfer_sources table
|
||||||
|
-- This field stores the quantity requested by user during create/update
|
||||||
|
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
-- Add comment for documentation
|
||||||
|
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE recording_depletions
|
||||||
|
DROP COLUMN IF EXISTS pending_qty,
|
||||||
|
DROP COLUMN IF EXISTS source_product_warehouse_id;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
ALTER TABLE recording_depletions
|
||||||
|
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
|
||||||
|
|
||||||
|
UPDATE recording_depletions rd
|
||||||
|
SET source_product_warehouse_id = src.product_warehouse_id
|
||||||
|
FROM recordings r
|
||||||
|
JOIN LATERAL (
|
||||||
|
SELECT pfp.product_warehouse_id
|
||||||
|
FROM project_chickins pc
|
||||||
|
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
|
||||||
|
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
||||||
|
ORDER BY pfp.created_at ASC, pfp.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
) AS src ON true
|
||||||
|
WHERE r.id = rd.recording_id
|
||||||
|
AND rd.source_product_warehouse_id IS NULL;
|
||||||
@@ -2,28 +2,17 @@ package entities
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// AdjustmentStock tracks FIFO allocation for stock adjustments
|
|
||||||
// - For INCREASE adjustments (Stockable): Tracks stock added to warehouse
|
|
||||||
// - For DECREASE adjustments (Usable): Tracks stock consumed from warehouse
|
|
||||||
type AdjustmentStock struct {
|
type AdjustmentStock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
|
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty;default:0"`
|
||||||
|
TotalUsed float64 `gorm:"column:total_used;default:0"`
|
||||||
|
UsageQty float64 `gorm:"column:usage_qty;default:0"`
|
||||||
|
PendingQty float64 `gorm:"column:pending_qty;default:0"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
|
||||||
|
|
||||||
// === FIFO FIELDS FOR INCREASE ADJUSTMENT (Stockable) ===
|
|
||||||
// Tracks stock added to warehouse via adjustment INCREASE
|
|
||||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot quantity available
|
|
||||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Quantity already used from this lot
|
|
||||||
|
|
||||||
// === FIFO FIELDS FOR DECREASE ADJUSTMENT (Usable) ===
|
|
||||||
// Tracks stock consumed from warehouse via adjustment DECREASE
|
|
||||||
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual quantity consumed
|
|
||||||
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Pending quantity (waiting for stock)
|
|
||||||
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
|
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
|
||||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type LayingTransferSource struct {
|
|||||||
LayingTransferId uint `gorm:"index;not null"`
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
SourceProjectFlockKandangId uint `gorm:"not null"`
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint `gorm:""`
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
|
||||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
type RecordingDepletion struct {
|
type RecordingDepletion struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
Qty float64 `gorm:"column:qty;not null"`
|
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
|
||||||
|
Qty float64 `gorm:"column:qty;not null"`
|
||||||
|
PendingQty float64 `gorm:"column:pending_qty"`
|
||||||
|
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -28,10 +28,31 @@ func NewClosingController(closingService service.ClosingService, sapronakService
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
||||||
|
var projectStatus *int
|
||||||
|
if raw := c.Query("project_status"); raw != "" {
|
||||||
|
statusValue, err := strconv.Atoi(raw)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_status")
|
||||||
|
}
|
||||||
|
projectStatus = &statusValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var locationID *uint
|
||||||
|
if raw := c.Query("location_id"); raw != "" {
|
||||||
|
locationValue, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || locationValue <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
|
||||||
|
}
|
||||||
|
locationUint := uint(locationValue)
|
||||||
|
locationID = &locationUint
|
||||||
|
}
|
||||||
|
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
|
ProjectStatus: projectStatus,
|
||||||
|
LocationID: locationID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
@@ -160,7 +181,7 @@ 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(uint(projectFlockID), result),
|
Data: dto.ToPenjualanRealisasiResponseDTO(result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +211,7 @@ func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) erro
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get closing penjualan by project flock kandang successfully",
|
Message: "Get closing penjualan by project flock kandang successfully",
|
||||||
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
Data: dto.ToPenjualanRealisasiResponseDTO(result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,9 +257,10 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := &validation.ClosingSapronakQuery{
|
query := &validation.ClosingSapronakQuery{
|
||||||
Type: strings.ToLower(c.Query("type")),
|
Type: strings.ToLower(c.Query("type")),
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search"),
|
||||||
}
|
}
|
||||||
if raw := c.Query("kandang_id"); raw != "" {
|
if raw := c.Query("kandang_id"); raw != "" {
|
||||||
kandangInt, convErr := strconv.Atoi(raw)
|
kandangInt, convErr := strconv.Atoi(raw)
|
||||||
@@ -277,6 +299,45 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetClosingSapronakSummary(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &validation.ClosingSapronakQuery{
|
||||||
|
Type: strings.ToLower(c.Query("type")),
|
||||||
|
Search: c.Query("search"),
|
||||||
|
}
|
||||||
|
if raw := c.Query("kandang_id"); raw != "" {
|
||||||
|
kandangInt, convErr := strconv.Atoi(raw)
|
||||||
|
if convErr != nil || kandangInt <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
kandangUint := uint(kandangInt)
|
||||||
|
query.KandangID = &kandangUint
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved closing report (sapronak summary) successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
flag := c.Query("flag", "")
|
flag := c.Query("flag", "")
|
||||||
|
|||||||
@@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClosingPerformanceDTO struct {
|
type ClosingPerformanceDTO struct {
|
||||||
Depletion float64 `json:"depletion"`
|
Depletion float64 `json:"depletion"`
|
||||||
Age float64 `json:"age_day"`
|
Age float64 `json:"age_day"`
|
||||||
MortalityStd float64 `json:"mor_std"`
|
MortalityStd float64 `json:"mor_std"`
|
||||||
MortalityAct float64 `json:"mor_act"`
|
MortalityAct float64 `json:"mor_act"`
|
||||||
DeffMortality float64 `json:"mor_diff"`
|
DeffMortality float64 `json:"mor_diff"`
|
||||||
FcrStd float64 `json:"fcr_std"`
|
FcrStd float64 `json:"fcr_std"`
|
||||||
FcrAct float64 `json:"fcr_act"`
|
FcrAct float64 `json:"fcr_act"`
|
||||||
DeffFcr float64 `json:"fcr_diff"`
|
DeffFcr float64 `json:"fcr_diff"`
|
||||||
AwgAct float64 `json:"awg_act"`
|
AwgAct float64 `json:"awg_act"`
|
||||||
AwgStd float64 `json:"awg_std"`
|
AwgStd float64 `json:"awg_std"`
|
||||||
FeedIntake float64 `json:"feed_intake"`
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
FeedIntakeStd float64 `json:"feed_intake_std"`
|
FeedIntakeStd float64 `json:"feed_intake_std"`
|
||||||
HenDayAct *float64 `json:"hen_day_act,omitempty"`
|
HenDayAct float64 `json:"hen_day_act,omitempty"`
|
||||||
HendayStd float64 `json:"hen_day_std"`
|
HendayStd float64 `json:"hen_day_std"`
|
||||||
EggMass *float64 `json:"egg_mass,omitempty"`
|
EggMass float64 `json:"egg_mass,omitempty"`
|
||||||
EggMassStd float64 `json:"egg_mass_std"`
|
EggMassStd float64 `json:"egg_mass_std"`
|
||||||
EggWeight *float64 `json:"egg_weight,omitempty"`
|
EggWeight float64 `json:"egg_weight,omitempty"`
|
||||||
EggWeightStd float64 `json:"egg_weight_std"`
|
EggWeightStd float64 `json:"egg_weight_std"`
|
||||||
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
HenHouseAct float64 `json:"hen_housed_act,omitempty"`
|
||||||
HenHouseStd float64 `json:"hen_housed_std"`
|
HenHouseStd float64 `json:"hen_housed_std"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingSalesGroupDTO struct {
|
type ClosingSalesGroupDTO struct {
|
||||||
|
|||||||
@@ -12,30 +12,39 @@ import (
|
|||||||
|
|
||||||
// === Response DTO ===
|
// === Response DTO ===
|
||||||
type SalesDTO struct {
|
type SalesDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
RealizationDate time.Time `json:"realization_date"`
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
Age int `json:"age"`
|
Age int `json:"age"`
|
||||||
DoNumber string `json:"do_number"`
|
Week int `json:"week"`
|
||||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
DoNumber string `json:"do_number"`
|
||||||
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
Qty float64 `json:"qty"`
|
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||||
Weight float64 `json:"weight"`
|
Qty float64 `json:"qty"`
|
||||||
AvgWeight float64 `json:"avg_weight"`
|
Weight float64 `json:"weight"`
|
||||||
Price float64 `json:"price"`
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
SalesPrice float64 `json:"sales_price"`
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
TotalSalesPrice float64 `json:"total_sales_price"`
|
||||||
PaymentStatus string `json:"payment_status"`
|
ActualPrice float64 `json:"actual_price"`
|
||||||
|
TotalActualPrice float64 `json:"total_actual_price"`
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
}
|
||||||
|
type SummaryDTO struct {
|
||||||
|
TotalSalesPrice float64 `json:"total_sales_price"`
|
||||||
|
AvgSalesPrice float64 `json:"avg_sales_price"`
|
||||||
|
TotalActualPrice float64 `json:"total_actual_price"`
|
||||||
|
AvgActualPrice float64 `json:"avg_actual_price"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PenjualanRealisasiResponseDTO struct {
|
type PenjualanRealisasiResponseDTO struct {
|
||||||
Sales []SalesDTO `json:"sales"`
|
Sales []SalesDTO `json:"sales"`
|
||||||
|
Summary SummaryDTO `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||||
|
|
||||||
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
|
ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
|
||||||
|
|
||||||
var product *productDTO.ProductRelationDTO
|
var product *productDTO.ProductRelationDTO
|
||||||
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||||
@@ -63,19 +72,42 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|||||||
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
||||||
|
|
||||||
return SalesDTO{
|
return SalesDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
RealizationDate: realizationDate,
|
RealizationDate: realizationDate,
|
||||||
Age: age,
|
Age: ageInDay,
|
||||||
DoNumber: doNumber,
|
Week: ageInWeeks,
|
||||||
Product: product,
|
DoNumber: doNumber,
|
||||||
Customer: customer,
|
Product: product,
|
||||||
Qty: e.UsageQty,
|
Customer: customer,
|
||||||
Weight: e.TotalWeight,
|
Qty: e.UsageQty,
|
||||||
AvgWeight: e.AvgWeight,
|
Weight: e.TotalWeight,
|
||||||
Price: e.UnitPrice,
|
AvgWeight: e.AvgWeight,
|
||||||
TotalPrice: e.TotalPrice,
|
SalesPrice: e.MarketingProduct.UnitPrice,
|
||||||
Kandang: kandang,
|
TotalSalesPrice: e.MarketingProduct.TotalPrice,
|
||||||
PaymentStatus: "Paid",
|
ActualPrice: e.UnitPrice,
|
||||||
|
TotalActualPrice: e.TotalPrice,
|
||||||
|
Kandang: kandang,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO {
|
||||||
|
|
||||||
|
var totalSalesPrice, totalActualPrice, sumSales, sumActual float64
|
||||||
|
count := len(e)
|
||||||
|
|
||||||
|
for _, item := range e {
|
||||||
|
totalSalesPrice += item.MarketingProduct.TotalPrice
|
||||||
|
totalActualPrice += item.TotalPrice
|
||||||
|
sumSales += item.MarketingProduct.UnitPrice
|
||||||
|
sumActual += item.UnitPrice
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return SummaryDTO{
|
||||||
|
TotalSalesPrice: totalSalesPrice,
|
||||||
|
TotalActualPrice: totalActualPrice,
|
||||||
|
AvgSalesPrice: sumSales / float64(count),
|
||||||
|
AvgActualPrice: sumActual / float64(count),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,28 +119,16 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
|
|
||||||
return PenjualanRealisasiResponseDTO{
|
return PenjualanRealisasiResponseDTO{
|
||||||
|
Sales: ToSalesDTOs(e),
|
||||||
Sales: ToSalesDTOs(e),
|
Summary: ToSummaryDto(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
|
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) (int, int) {
|
||||||
if len(realisasi) > 0 {
|
|
||||||
for _, item := range realisasi {
|
|
||||||
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
|
||||||
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
|
||||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
||||||
return 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
||||||
@@ -118,7 +138,16 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
diff := deliveryDate.Sub(earliestChickinDate)
|
||||||
ageInWeeks := ageInDays / 7
|
ageInDays := int(diff.Hours() / 24)
|
||||||
return ageInWeeks
|
|
||||||
|
var ageInWeeks int
|
||||||
|
if ageInDays <= 0 {
|
||||||
|
ageInWeeks = 0
|
||||||
|
} else {
|
||||||
|
|
||||||
|
ageInWeeks = ((ageInDays - 1) / 7) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return ageInDays, ageInWeeks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ type ClosingSapronakDTO struct {
|
|||||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakSummaryItemDTO struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
TotalQty int64 `json:"total_qty"`
|
||||||
|
Uom UomSummaryDTO `json:"uom"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UomSummaryDTO struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||||
|
|
||||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
@@ -201,18 +212,48 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
switch strings.ToLower(item.JenisTransaksi) {
|
switch strings.ToLower(item.JenisTransaksi) {
|
||||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||||
row.QtyIn += item.QtyMasuk
|
row.QtyIn += item.QtyMasuk
|
||||||
row.TotalAmount += item.Nilai
|
if row.UnitPrice == 0 {
|
||||||
|
if item.QtyMasuk > 0 && item.Nilai > 0 {
|
||||||
|
row.UnitPrice = item.Nilai / item.QtyMasuk
|
||||||
|
} else if item.Harga > 0 {
|
||||||
|
row.UnitPrice = item.Harga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
|
||||||
|
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
||||||
|
if strings.HasPrefix(ref, "TL-") {
|
||||||
|
row.Notes = "TRANSFER LAYING"
|
||||||
|
} else if strings.HasPrefix(ref, "ST-") {
|
||||||
|
row.Notes = "TRANSFER STOCK"
|
||||||
|
}
|
||||||
|
}
|
||||||
case "pemakaian", "adjustment keluar":
|
case "pemakaian", "adjustment keluar":
|
||||||
|
price := row.UnitPrice
|
||||||
|
if price == 0 {
|
||||||
|
price = item.Harga
|
||||||
|
}
|
||||||
row.QtyUsed += item.QtyKeluar
|
row.QtyUsed += item.QtyKeluar
|
||||||
case "mutasi keluar":
|
row.TotalAmount += item.QtyKeluar * price
|
||||||
|
case "mutasi keluar", "penjualan":
|
||||||
|
price := row.UnitPrice
|
||||||
|
if price == 0 {
|
||||||
|
price = item.Harga
|
||||||
|
}
|
||||||
row.QtyOut += item.QtyKeluar
|
row.QtyOut += item.QtyKeluar
|
||||||
|
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
|
||||||
|
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
||||||
|
if strings.HasPrefix(ref, "TL-") {
|
||||||
|
row.Notes = "TRANSFER LAYING"
|
||||||
|
} else if strings.HasPrefix(ref, "ST-") {
|
||||||
|
row.Notes = "TRANSFER STOCK"
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
row.QtyIn += item.QtyMasuk
|
row.QtyIn += item.QtyMasuk
|
||||||
row.TotalAmount += item.Nilai
|
row.TotalAmount += item.Nilai
|
||||||
}
|
if row.QtyIn > 0 {
|
||||||
|
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||||
if row.QtyIn > 0 {
|
}
|
||||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,8 +274,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
total += r.TotalAmount
|
total += r.TotalAmount
|
||||||
}
|
}
|
||||||
avg := 0.0
|
avg := 0.0
|
||||||
if qtyIn > 0 {
|
if qtyUsed > 0 {
|
||||||
avg = total / qtyIn
|
avg = total / qtyUsed
|
||||||
}
|
}
|
||||||
cat.Total = SapronakCategoryTotalDTO{
|
cat.Total = SapronakCategoryTotalDTO{
|
||||||
Label: label,
|
Label: label,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
type ClosingRepository interface {
|
type ClosingRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
|
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
|
||||||
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
||||||
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
@@ -33,6 +34,7 @@ type ClosingRepository interface {
|
|||||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +61,18 @@ type SapronakRow struct {
|
|||||||
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||||
Destination string `gorm:"column:destination"`
|
Destination string `gorm:"column:destination"`
|
||||||
Quantity float64 `gorm:"column:quantity"`
|
Quantity float64 `gorm:"column:quantity"`
|
||||||
|
UnitID uint `gorm:"column:unit_id"`
|
||||||
Unit string `gorm:"column:unit"`
|
Unit string `gorm:"column:unit"`
|
||||||
Notes string `gorm:"column:notes"`
|
Notes string `gorm:"column:notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SapronakSummaryRow struct {
|
||||||
|
Category string `gorm:"column:category"`
|
||||||
|
TotalQty int64 `gorm:"column:total_qty"`
|
||||||
|
UomID uint `gorm:"column:uom_id"`
|
||||||
|
UomName string `gorm:"column:uom_name"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExpeditionHPPRow struct {
|
type ExpeditionHPPRow struct {
|
||||||
SupplierName string `gorm:"column:supplier_name"`
|
SupplierName string `gorm:"column:supplier_name"`
|
||||||
TotalAmount float64 `gorm:"column:total_amount"`
|
TotalAmount float64 `gorm:"column:total_amount"`
|
||||||
@@ -74,6 +84,7 @@ type SapronakQueryParams struct {
|
|||||||
ProjectFlockKandangIDs []uint
|
ProjectFlockKandangIDs []uint
|
||||||
Limit int
|
Limit int
|
||||||
Offset int
|
Offset int
|
||||||
|
Search string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||||
@@ -109,14 +120,36 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
|||||||
|
|
||||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||||
|
|
||||||
|
search := strings.TrimSpace(params.Search)
|
||||||
|
searchClause := ""
|
||||||
|
var searchArgs []any
|
||||||
|
if search != "" {
|
||||||
|
searchClause = `
|
||||||
|
WHERE (
|
||||||
|
reference_number ILIKE ?
|
||||||
|
OR product_name ILIKE ?
|
||||||
|
OR product_category ILIKE ?
|
||||||
|
OR source_warehouse ILIKE ?
|
||||||
|
OR destination_warehouse ILIKE ?
|
||||||
|
OR CAST(quantity AS TEXT) ILIKE ?
|
||||||
|
OR unit ILIKE ?
|
||||||
|
OR notes ILIKE ?
|
||||||
|
OR transaction_type ILIKE ?
|
||||||
|
)`
|
||||||
|
like := "%" + search + "%"
|
||||||
|
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
|
||||||
|
}
|
||||||
|
|
||||||
var totalResults int64
|
var totalResults int64
|
||||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
|
||||||
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
countArgs := append(append([]any{}, args...), searchArgs...)
|
||||||
|
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
dataArgs := append(append([]any{}, args...), searchArgs...)
|
||||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
dataArgs = append(dataArgs, params.Limit, params.Offset)
|
||||||
|
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
|
||||||
|
|
||||||
var rows []SapronakRow
|
var rows []SapronakRow
|
||||||
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||||
@@ -126,6 +159,79 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
|||||||
return rows, totalResults, nil
|
return rows, totalResults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
unionParts []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
switch params.Type {
|
||||||
|
case validation.SapronakTypeIncoming:
|
||||||
|
if len(params.WarehouseIDs) == 0 {
|
||||||
|
return []SapronakSummaryRow{}, nil
|
||||||
|
}
|
||||||
|
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||||
|
case validation.SapronakTypeOutgoing:
|
||||||
|
if len(params.WarehouseIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs)
|
||||||
|
}
|
||||||
|
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||||
|
args = append(args, params.ProjectFlockKandangIDs)
|
||||||
|
}
|
||||||
|
if len(unionParts) == 0 {
|
||||||
|
return []SapronakSummaryRow{}, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||||
|
|
||||||
|
search := strings.TrimSpace(params.Search)
|
||||||
|
searchClause := ""
|
||||||
|
var searchArgs []any
|
||||||
|
if search != "" {
|
||||||
|
searchClause = `
|
||||||
|
WHERE (
|
||||||
|
reference_number ILIKE ?
|
||||||
|
OR product_name ILIKE ?
|
||||||
|
OR product_category ILIKE ?
|
||||||
|
OR source_warehouse ILIKE ?
|
||||||
|
OR destination_warehouse ILIKE ?
|
||||||
|
OR CAST(quantity AS TEXT) ILIKE ?
|
||||||
|
OR unit ILIKE ?
|
||||||
|
OR notes ILIKE ?
|
||||||
|
OR transaction_type ILIKE ?
|
||||||
|
)`
|
||||||
|
like := "%" + search + "%"
|
||||||
|
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
querySQL := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
product_category AS category,
|
||||||
|
CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
|
||||||
|
unit_id AS uom_id,
|
||||||
|
unit AS uom_name
|
||||||
|
FROM (%s) AS combined%s
|
||||||
|
GROUP BY product_category, unit_id, unit
|
||||||
|
ORDER BY product_category ASC, unit ASC
|
||||||
|
`, unionSQL, searchClause)
|
||||||
|
queryArgs := append(append([]any{}, args...), searchArgs...)
|
||||||
|
|
||||||
|
var rows []SapronakSummaryRow
|
||||||
|
if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
return 0, 0, nil
|
return 0, 0, nil
|
||||||
@@ -379,6 +485,7 @@ SELECT
|
|||||||
w.name AS destination_warehouse,
|
w.name AS destination_warehouse,
|
||||||
'' AS destination,
|
'' AS destination,
|
||||||
pi.total_qty AS quantity,
|
pi.total_qty AS quantity,
|
||||||
|
u.id AS unit_id,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
COALESCE(p.notes, '') AS notes
|
COALESCE(p.notes, '') AS notes
|
||||||
FROM purchase_items pi
|
FROM purchase_items pi
|
||||||
@@ -427,6 +534,7 @@ SELECT
|
|||||||
COALESCE(tw.name, '') AS destination_warehouse,
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
'' AS destination,
|
'' AS destination,
|
||||||
std.usage_qty AS quantity,
|
std.usage_qty AS quantity,
|
||||||
|
u.id AS unit_id,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
'Stock Refill' AS notes
|
'Stock Refill' AS notes
|
||||||
FROM stock_transfer_details std
|
FROM stock_transfer_details std
|
||||||
@@ -476,6 +584,7 @@ SELECT
|
|||||||
COALESCE(tw.name, '') AS destination_warehouse,
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
'' AS destination,
|
'' AS destination,
|
||||||
std.usage_qty AS quantity,
|
std.usage_qty AS quantity,
|
||||||
|
u.id AS unit_id,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
'Transfer to other unit' AS notes
|
'Transfer to other unit' AS notes
|
||||||
FROM stock_transfer_details std
|
FROM stock_transfer_details std
|
||||||
@@ -522,18 +631,27 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
w.name AS source_warehouse,
|
w.name AS source_warehouse,
|
||||||
'RETAIL CUSTOMER' AS destination_warehouse,
|
COALESCE(c.name, '') AS destination_warehouse,
|
||||||
'' AS destination,
|
'' AS destination,
|
||||||
mp.qty AS quantity,
|
mp.qty AS quantity,
|
||||||
|
u.id AS unit_id,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
m.notes AS notes
|
m.notes AS notes
|
||||||
FROM marketing_products mp
|
FROM marketing_products mp
|
||||||
JOIN marketings m ON m.id = mp.marketing_id
|
JOIN marketings m ON m.id = mp.marketing_id
|
||||||
|
LEFT JOIN customers c ON c.id = m.customer_id
|
||||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
JOIN products prod ON prod.id = pw.product_id
|
JOIN products prod ON prod.id = pw.product_id
|
||||||
JOIN uoms u ON u.id = prod.uom_id
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
WHERE pw.project_flock_kandang_id IN ?
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_id = pw.product_id
|
||||||
|
AND f.flagable_type = 'products'
|
||||||
|
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
|
||||||
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -902,17 +1020,50 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
|||||||
COALESCE(p.product_price, 0) AS price
|
COALESCE(p.product_price, 0) AS price
|
||||||
`).
|
`).
|
||||||
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||||
|
Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id").
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
|
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
Joins("JOIN products p ON p.id = std.product_id").
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Where("w.kandang_id = ?", kandangID).
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
|
||||||
Where("f.name IN ?", sapronakFlagsAll)
|
Where("f.name IN ?", sapronakFlagsAll)
|
||||||
incoming, err := scanAndGroupDetails(incomingQuery)
|
incoming, err := scanAndGroupDetails(incomingQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
incomingLayingQuery := r.withCtx(ctx).
|
||||||
|
Table("laying_transfer_targets AS ltt").
|
||||||
|
Select(`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
lt.transfer_date::timestamp AS date,
|
||||||
|
COALESCE(lt.transfer_number, '') AS reference,
|
||||||
|
COALESCE(ltt.total_qty, 0) AS qty_in,
|
||||||
|
0 AS qty_out,
|
||||||
|
COALESCE(p.product_price, 0) AS price
|
||||||
|
`).
|
||||||
|
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||||
|
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
|
||||||
|
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll)
|
||||||
|
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for pid, rows := range incomingLaying {
|
||||||
|
incoming[pid] = append(incoming[pid], rows...)
|
||||||
|
}
|
||||||
|
|
||||||
outgoingQuery := r.withCtx(ctx).
|
outgoingQuery := r.withCtx(ctx).
|
||||||
Table("stock_allocations AS sa").
|
Table("stock_allocations AS sa").
|
||||||
Select(`
|
Select(`
|
||||||
@@ -929,10 +1080,13 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
|||||||
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
|
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
||||||
Joins("JOIN products p ON p.id = std.product_id").
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||||
Where("w.kandang_id = ?", kandangID).
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
||||||
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||||
@@ -940,9 +1094,71 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outgoingLayingQuery := r.withCtx(ctx).
|
||||||
|
Table("stock_allocations AS sa").
|
||||||
|
Select(`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
lt.transfer_date::timestamp AS date,
|
||||||
|
COALESCE(lt.transfer_number, '') AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||||
|
COALESCE(p.product_price, 0) AS price
|
||||||
|
`).
|
||||||
|
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
||||||
|
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
||||||
|
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
|
||||||
|
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
|
||||||
|
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for pid, rows := range outgoingLaying {
|
||||||
|
outgoing[pid] = append(outgoing[pid], rows...)
|
||||||
|
}
|
||||||
|
|
||||||
return incoming, outgoing, nil
|
return incoming, outgoing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
query := r.withCtx(ctx).
|
||||||
|
Table("stock_allocations AS sa").
|
||||||
|
Select(`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(mdp.delivery_date, mdp.created_at) AS date,
|
||||||
|
COALESCE(m.so_number, '') AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||||
|
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
|
||||||
|
`).
|
||||||
|
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
|
||||||
|
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||||
|
Joins("JOIN marketings m ON m.id = mp.marketing_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
|
||||||
|
|
||||||
|
return scanAndGroupDetails(query)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||||
if len(productIDs) == 0 {
|
if len(productIDs) == 0 {
|
||||||
return []entity.Product{}, nil
|
return []entity.Product{}, nil
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
route.Get("/:project_flock_id/: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)
|
||||||
|
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
|
||||||
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
route.Get("/: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)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type ClosingService interface {
|
|||||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
||||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
|
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
|
||||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +99,31 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
|
|||||||
}
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
statusFilter := ""
|
||||||
|
if params.ProjectStatus != nil {
|
||||||
|
switch *params.ProjectStatus {
|
||||||
|
case 1:
|
||||||
|
statusFilter = "Pengajuan"
|
||||||
|
case 2:
|
||||||
|
statusFilter = "Aktif"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withClosingRelations(db)
|
db = s.withClosingRelations(db)
|
||||||
|
if params.LocationID != nil {
|
||||||
|
db = db.Where("location_id = ?", *params.LocationID)
|
||||||
|
}
|
||||||
|
if statusFilter != "" {
|
||||||
|
latestApprovalSubQuery := s.Repository.DB().
|
||||||
|
WithContext(c.Context()).
|
||||||
|
Table("approvals").
|
||||||
|
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
|
||||||
|
Where("approvable_type = ?", utils.ApprovalWorkflowProjectFlock.String()).
|
||||||
|
Order("approvable_id, id DESC")
|
||||||
|
db = db.Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
|
||||||
|
Where("LOWER(latest_approval.step_name) = LOWER(?)", statusFilter)
|
||||||
|
}
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
|
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
@@ -353,6 +376,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||||
Limit: params.Limit,
|
Limit: params.Limit,
|
||||||
Offset: offset,
|
Offset: offset,
|
||||||
|
Search: params.Search,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||||
@@ -387,6 +411,74 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
return items, totalResults, nil
|
return items, totalResults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if params == nil {
|
||||||
|
params = &validation.ClosingSapronakQuery{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlockKandangIDs []uint
|
||||||
|
if params.KandangID != nil && *params.KandangID > 0 {
|
||||||
|
projectFlockKandangIDs = []uint{*params.KandangID}
|
||||||
|
} else if params.Type == validation.SapronakTypeOutgoing {
|
||||||
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
|
||||||
|
Type: params.Type,
|
||||||
|
WarehouseIDs: warehouseIDs,
|
||||||
|
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||||
|
Search: params.Search,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
items = append(items, dto.ClosingSapronakSummaryItemDTO{
|
||||||
|
Category: row.Category,
|
||||||
|
TotalQty: row.TotalQty,
|
||||||
|
Uom: dto.UomSummaryDTO{
|
||||||
|
ID: row.UomID,
|
||||||
|
Name: row.UomName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
var kandangIDs []uint
|
var kandangIDs []uint
|
||||||
db := s.Repository.DB().WithContext(ctx)
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
@@ -860,19 +952,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
if !isGrowing {
|
if !isGrowing {
|
||||||
if targetAverages.HenDayCount > 0 {
|
if targetAverages.HenDayCount > 0 {
|
||||||
henDayAct := targetAverages.HenDayAvg
|
henDayAct := targetAverages.HenDayAvg
|
||||||
performance.HenDayAct = &henDayAct
|
performance.HenDayAct = henDayAct
|
||||||
}
|
}
|
||||||
if targetAverages.HenHouseCount > 0 {
|
if targetAverages.HenHouseCount > 0 {
|
||||||
henHouseAct := targetAverages.HenHouseAvg
|
henHouseAct := targetAverages.HenHouseAvg
|
||||||
performance.HenHouseAct = &henHouseAct
|
performance.HenHouseAct = henHouseAct
|
||||||
}
|
}
|
||||||
if targetAverages.EggWeightCount > 0 {
|
if targetAverages.EggWeightCount > 0 {
|
||||||
eggWeight := targetAverages.EggWeightAvg
|
eggWeight := targetAverages.EggWeightAvg
|
||||||
performance.EggWeight = &eggWeight
|
performance.EggWeight = eggWeight
|
||||||
}
|
}
|
||||||
if targetAverages.EggMassCount > 0 {
|
if targetAverages.EggMassCount > 0 {
|
||||||
eggMass := targetAverages.EggMassAvg
|
eggMass := targetAverages.EggMassAvg
|
||||||
performance.EggMass = &eggMass
|
performance.EggMass = eggMass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
||||||
@@ -1030,4 +1122,3 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
|
|||||||
|
|
||||||
return closest.Mortality, closest.FcrNumber
|
return closest.Mortality, closest.FcrNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -111,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
@@ -262,6 +263,7 @@ type sapronakDetailMaps struct {
|
|||||||
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||||
TransferIn map[uint][]dto.SapronakDetailDTO
|
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||||
TransferOut map[uint][]dto.SapronakDetailDTO
|
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||||
|
SalesOut map[uint][]dto.SapronakDetailDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSapronakDetails(
|
func buildSapronakDetails(
|
||||||
@@ -271,6 +273,7 @@ func buildSapronakDetails(
|
|||||||
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||||
transferInRows map[uint][]repository.SapronakDetailRow,
|
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||||
transferOutRows map[uint][]repository.SapronakDetailRow,
|
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
salesOutRows map[uint][]repository.SapronakDetailRow,
|
||||||
) sapronakDetailMaps {
|
) sapronakDetailMaps {
|
||||||
result := sapronakDetailMaps{
|
result := sapronakDetailMaps{
|
||||||
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
@@ -279,6 +282,7 @@ func buildSapronakDetails(
|
|||||||
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
SalesOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
}
|
}
|
||||||
|
|
||||||
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||||
@@ -311,6 +315,7 @@ func buildSapronakDetails(
|
|||||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||||
|
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -350,6 +355,10 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, 0, err
|
return nil, nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||||
matchesFlag := func(f string) bool {
|
matchesFlag := func(f string) bool {
|
||||||
@@ -362,6 +371,34 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
return candidate == filterFlag
|
return candidate == filterFlag
|
||||||
}
|
}
|
||||||
|
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
|
||||||
|
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for pid, rows := range src {
|
||||||
|
for _, d := range rows {
|
||||||
|
dateKey := ""
|
||||||
|
if d.Tanggal != nil {
|
||||||
|
dateKey = d.Tanggal.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
qtyKey := d.QtyMasuk
|
||||||
|
if qtyKey == 0 {
|
||||||
|
qtyKey = d.QtyKeluar
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := strings.TrimSpace(d.NoReferensi)
|
||||||
|
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
|
||||||
|
if ref == "" {
|
||||||
|
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
|
||||||
|
}
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
result[pid] = append(result[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// For project flocks with category GROWING, pullet usage from chickin
|
// For project flocks with category GROWING, pullet usage from chickin
|
||||||
// should not be counted yet. Only when category is LAYING we allow
|
// should not be counted yet. Only when category is LAYING we allow
|
||||||
@@ -400,13 +437,17 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||||
}
|
}
|
||||||
|
|
||||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
|
||||||
incomingDetails := detailMaps.Incoming
|
incomingDetails := detailMaps.Incoming
|
||||||
usageDetails := detailMaps.Usage
|
usageDetails := detailMaps.Usage
|
||||||
adjIncoming := detailMaps.AdjIncoming
|
adjIncoming := detailMaps.AdjIncoming
|
||||||
adjOutgoing := detailMaps.AdjOutgoing
|
adjOutgoing := detailMaps.AdjOutgoing
|
||||||
transIncoming := detailMaps.TransferIn
|
transIncoming := detailMaps.TransferIn
|
||||||
transOutgoing := detailMaps.TransferOut
|
transOutgoing := detailMaps.TransferOut
|
||||||
|
salesOutgoing := detailMaps.SalesOut
|
||||||
|
|
||||||
|
transIncoming = dedupTransfers(transIncoming)
|
||||||
|
transOutgoing = dedupTransfers(transOutgoing)
|
||||||
|
|
||||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
if g, ok := groupMap[flag]; ok {
|
if g, ok := groupMap[flag]; ok {
|
||||||
@@ -683,6 +724,25 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for productID, details := range salesOutgoing {
|
||||||
|
flag, name := resolveFlagName(productID, details)
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
for _, g := range groupMap {
|
for _, g := range groupMap {
|
||||||
groups = append(groups, *g)
|
groups = append(groups, *g)
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
ProjectStatus *int `query:"project_status" validate:"omitempty,oneof=1 2"`
|
||||||
|
LocationID *uint `query:"location_id" validate:"omitempty,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,4 +26,5 @@ type ClosingSapronakQuery struct {
|
|||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=100"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
|||||||
"KANDANG",
|
"KANDANG",
|
||||||
},
|
},
|
||||||
"stock_log": map[string][]string{
|
"stock_log": map[string][]string{
|
||||||
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
|
"log_types": []string{"TRANSFER", "ADJUSTMENT", "MARKETING", "CHICKIN", "PURCHASE", "RECORDING"},
|
||||||
"transaction_types": []string{"INCREASE", "DECREASE"},
|
"transaction_types": []string{"INCREASE", "DECREASE"},
|
||||||
},
|
},
|
||||||
"supplier_categories": []string{
|
"supplier_categories": []string{
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type SummaryQuery struct {
|
|||||||
|
|
||||||
type ReportQuery struct {
|
type ReportQuery struct {
|
||||||
Page int `query:"page" validate:"required,number,min=1,gt=0"`
|
Page int `query:"page" validate:"required,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"required,number,min=1,gt=0"`
|
||||||
Month int `query:"bulan" validate:"required,number,min=1,max=12"`
|
Month int `query:"bulan" validate:"required,number,min=1,max=12"`
|
||||||
Year int `query:"tahun" validate:"required,number,min=1900"`
|
Year int `query:"tahun" validate:"required,number,min=1900"`
|
||||||
AreaID *uint `query:"area_id" validate:"omitempty"`
|
AreaID *uint `query:"area_id" validate:"omitempty"`
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Contex
|
|||||||
|
|
||||||
db := r.DB().WithContext(ctx).
|
db := r.DB().WithContext(ctx).
|
||||||
Table("recording_eggs AS re").
|
Table("recording_eggs AS re").
|
||||||
Select("COALESCE(SUM(re.qty * re.weight), 0)").
|
Select("COALESCE(SUM(re.weight * 1000), 0)").
|
||||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
@@ -648,7 +648,7 @@ func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, s
|
|||||||
Table("recording_eggs AS re").
|
Table("recording_eggs AS re").
|
||||||
Select(`
|
Select(`
|
||||||
((r.day - 1) / 7 + 1) AS week,
|
((r.day - 1) / 7 + 1) AS week,
|
||||||
COALESCE(SUM(re.qty * re.weight), 0) AS egg_weight_grams`).
|
COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`).
|
||||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Contex
|
|||||||
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_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 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").
|
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
|
||||||
Where("expenses.realization_date IS NOT NULL")
|
Where("expenses.realization_date IS NOT NULL").
|
||||||
|
Where("expenses.category = ?", "BOP")
|
||||||
|
|
||||||
if projectFlockKandangID != nil {
|
if projectFlockKandangID != nil {
|
||||||
db = db.Where(`(
|
db = db.Where(`(
|
||||||
|
|||||||
@@ -396,6 +396,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["supplier_id"] = *req.SupplierID
|
updateBody["supplier_id"] = *req.SupplierID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Notes != nil {
|
||||||
|
updateBody["notes"] = *req.Notes
|
||||||
|
}
|
||||||
|
|
||||||
if req.LocationID != nil {
|
if req.LocationID != nil {
|
||||||
locationID := uint(*req.LocationID)
|
locationID := uint(*req.LocationID)
|
||||||
updateBody["location_id"] = locationID
|
updateBody["location_id"] = locationID
|
||||||
@@ -568,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
}
|
}
|
||||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
|
||||||
|
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionUpdated
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
|
|
||||||
|
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
|
||||||
|
|
||||||
|
if previousStep < utils.ExpenseStepPengajuan {
|
||||||
|
previousStep = utils.ExpenseStepPengajuan
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowExpense,
|
utils.ApprovalWorkflowExpense,
|
||||||
id,
|
id,
|
||||||
utils.ExpenseStepPengajuan,
|
previousStep,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Update struct {
|
|||||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
||||||
|
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,38 +100,42 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
|
||||||
return AdjustmentRelationDTO{
|
return AdjustmentRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Note: e.Notes,
|
Note: e.StockLog.Notes,
|
||||||
Increase: e.Increase,
|
Increase: e.TotalQty,
|
||||||
Decrease: e.Decrease,
|
Decrease: e.UsageQty,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
|
func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
var createdUser *userDTO.UserRelationDTO
|
||||||
if e.CreatedUser != nil {
|
if e.StockLog != nil && e.StockLog.CreatedUser != nil {
|
||||||
createdUser = &userDTO.UserRelationDTO{
|
createdUser = &userDTO.UserRelationDTO{
|
||||||
Id: e.CreatedUser.Id,
|
Id: e.StockLog.CreatedUser.Id,
|
||||||
IdUser: e.CreatedUser.IdUser,
|
IdUser: e.StockLog.CreatedUser.IdUser,
|
||||||
Email: e.CreatedUser.Email,
|
Email: e.StockLog.CreatedUser.Email,
|
||||||
Name: e.CreatedUser.Name,
|
Name: e.StockLog.CreatedUser.Name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdAt := time.Time{}
|
||||||
|
if e.StockLog != nil {
|
||||||
|
createdAt = e.StockLog.CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
return AdjustmentListDTO{
|
return AdjustmentListDTO{
|
||||||
AdjustmentRelationDTO: ToAdjustmentRelationDTO(e),
|
AdjustmentRelationDTO: ToAdjustmentRelationDTO(e),
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: createdAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
func ToAdjustmentDetailDTO(e *entity.AdjustmentStock) AdjustmentDetailDTO {
|
||||||
return AdjustmentDetailDTO{
|
return AdjustmentDetailDTO{
|
||||||
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
||||||
// UpdatedAt: e.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *ent
|
|||||||
func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) {
|
func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) {
|
||||||
var record entity.AdjustmentStock
|
var record entity.AdjustmentStock
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("StockLog").
|
||||||
|
Preload("StockLog.ProductWarehouse").
|
||||||
|
Preload("StockLog.ProductWarehouse.Product").
|
||||||
|
Preload("StockLog.ProductWarehouse.Warehouse").
|
||||||
|
Preload("StockLog.CreatedUser").
|
||||||
|
Preload("ProductWarehouse").
|
||||||
|
Preload("ProductWarehouse.Product").
|
||||||
|
Preload("ProductWarehouse.Warehouse").
|
||||||
Where("stock_log_id = ?", stockLogID).
|
Where("stock_log_id = ?", stockLogID).
|
||||||
First(&record).Error
|
First(&record).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AdjustmentService interface {
|
type AdjustmentService interface {
|
||||||
Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error)
|
Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.AdjustmentStock, error)
|
||||||
AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error)
|
AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type adjustmentService struct {
|
type adjustmentService struct {
|
||||||
@@ -73,10 +73,8 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("CreatedUser")
|
Preload("CreatedUser")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) {
|
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
|
||||||
stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
adjustmentStock, err := s.AdjustmentStockRepository.GetByStockLogID(c.Context(), id)
|
||||||
return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory")
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||||
@@ -85,14 +83,10 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if stockLog.LoggableType != string(utils.StockLogTypeAdjustment) {
|
return adjustmentStock, nil
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return stockLog, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) {
|
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -111,12 +105,13 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
if req.Quantity <= 0 {
|
if req.Quantity <= 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionType := strings.ToUpper(req.TransactionType)
|
transactionType := strings.ToUpper(req.TransactionType)
|
||||||
if transactionType != string(utils.StockLogTransactionTypeIncrease) && transactionType != string(utils.StockLogTransactionTypeDecrease) {
|
if transactionType != string(utils.StockLogTransactionTypeIncrease) && transactionType != string(utils.StockLogTransactionTypeDecrease) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
|
||||||
}
|
}
|
||||||
|
|
||||||
var createdLogId uint
|
var createdAdjustmentStockId uint
|
||||||
|
|
||||||
var projectFlockKandangID *uint
|
var projectFlockKandangID *uint
|
||||||
pfkID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
|
pfkID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
|
||||||
@@ -151,7 +146,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
|
||||||
|
productWarehouse, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get product warehouse: %+v", err)
|
s.Log.Errorf("Failed to get product warehouse: %+v", err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
@@ -171,14 +167,14 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
newLog.Increase = afterQuantity
|
newLog.Increase = afterQuantity
|
||||||
} else {
|
} else {
|
||||||
if productWarehouse.Quantity < req.Quantity {
|
if productWarehouse.Quantity < req.Quantity {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Current: %.2f, Requested: %.2f", productWarehouse.Quantity, req.Quantity))
|
||||||
}
|
}
|
||||||
afterQuantity -= req.Quantity
|
afterQuantity -= req.Quantity
|
||||||
newLog.Decrease = afterQuantity
|
newLog.Decrease = afterQuantity
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create stock log: %+v", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +183,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
}
|
}
|
||||||
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +208,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
UsableID: adjustmentStock.Id,
|
UsableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
AllowPending: false, // Don't allow pending for adjustment
|
AllowPending: false,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -220,24 +216,27 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
// LEGACY: Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||||
|
|
||||||
productWarehouse.Quantity = afterQuantity
|
productWarehouse.Quantity = afterQuantity
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
createdLogId = newLog.Id
|
createdAdjustmentStockId = adjustmentStock.Id
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
||||||
|
var fiberErr *fiber.Error
|
||||||
|
if errors.As(err, &fiberErr) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, createdLogId)
|
return s.GetOne(c, createdAdjustmentStockId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
@@ -266,13 +265,15 @@ func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context,
|
|||||||
return uint(projectFlockKandang.Id), nil
|
return uint(projectFlockKandang.Id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
|
||||||
if err := s.Validate.Struct(query); err != nil {
|
if err := s.Validate.Struct(query); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
offset := (query.Page - 1) * query.Limit
|
offset := (query.Page - 1) * query.Limit
|
||||||
|
|
||||||
|
var isProductsExist bool
|
||||||
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
|
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||||
}
|
}
|
||||||
@@ -280,7 +281,8 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
|
isProductsExist, err = s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to check product existence: %+v", err)
|
s.Log.Errorf("Failed to check product existence: %+v", err)
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
|
||||||
@@ -289,28 +291,51 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB {
|
var adjustmentStocks []entity.AdjustmentStock
|
||||||
|
var total int64
|
||||||
|
|
||||||
db = s.withRelations(db)
|
q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}).
|
||||||
|
Preload("StockLog").
|
||||||
|
Preload("StockLog.ProductWarehouse").
|
||||||
|
Preload("StockLog.ProductWarehouse.Product").
|
||||||
|
Preload("StockLog.ProductWarehouse.Warehouse").
|
||||||
|
Preload("StockLog.CreatedUser").
|
||||||
|
Preload("ProductWarehouse").
|
||||||
|
Preload("ProductWarehouse.Product").
|
||||||
|
Preload("ProductWarehouse.Warehouse")
|
||||||
|
|
||||||
db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment))
|
if query.ProductID > 0 {
|
||||||
|
q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
|
||||||
|
Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
||||||
|
Where("product_warehouses.product_id = ?", query.ProductID)
|
||||||
|
}
|
||||||
|
|
||||||
if query.TransactionType != "" {
|
if query.WarehouseID > 0 {
|
||||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
|
||||||
}
|
Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
||||||
db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
|
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
return db.Order("created_at DESC")
|
if query.TransactionType != "" {
|
||||||
})
|
q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
|
||||||
|
Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = q.Count(&total).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to get adjustments: %+v", err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.Offset(offset).Limit(query.Limit).Order("created_at DESC").Find(&adjustmentStocks).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get adjustments: %+v", err)
|
s.Log.Errorf("Failed to get adjustments: %+v", err)
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]*entity.StockLog, len(stockLogs))
|
result := make([]*entity.AdjustmentStock, len(adjustmentStocks))
|
||||||
for i, v := range stockLogs {
|
for i := range adjustmentStocks {
|
||||||
result[i] = &v
|
result[i] = &adjustmentStocks[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, total, nil
|
return result, total, nil
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -16,60 +17,29 @@ type ProductWarehouseRelationDTO struct {
|
|||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehousNestedDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
|
||||||
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductWarehouseListDTO struct {
|
type ProductWarehouseListDTO struct {
|
||||||
ProductWarehouseRelationDTO
|
ProductWarehouseRelationDTO
|
||||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
|
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
||||||
ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"`
|
ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"`
|
||||||
CreatedUser *UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
|
||||||
|
|
||||||
type UserRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseDetailDTO struct {
|
type ProductWarehouseDetailDTO struct {
|
||||||
ProductWarehouseListDTO
|
ProductWarehouseListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested DTOs for relations
|
type ProductWarehousNestedDTO struct {
|
||||||
type ProductRelationDTO struct {
|
Id uint `json:"id"`
|
||||||
Id uint `json:"id"`
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
Name string `json:"name"`
|
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
||||||
Sku string `json:"sku"`
|
|
||||||
Flags []string `json:"flags"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseRelationDTO struct {
|
type UserRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Username string `json:"username"`
|
||||||
Kandang *KandangRelationDTO `json:"kandang,omitempty"`
|
|
||||||
Location *LocationRelationDTO `json:"location,omitempty"`
|
|
||||||
Area *AreaRelationDTO `json:"area,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type KandangRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocationRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AreaRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockKandangRelationDTO struct {
|
type ProjectFlockKandangRelationDTO struct {
|
||||||
@@ -96,65 +66,28 @@ func ToProductWarehouseRelationDTO(e entity.ProductWarehouse) ProductWarehouseRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO {
|
|
||||||
product := productDTO.ToProductRelationDTO(e.Product)
|
|
||||||
|
|
||||||
return ProductWarehousNestedDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
Product: &product,
|
|
||||||
Warehouse: &WarehouseRelationDTO{
|
|
||||||
Id: e.Warehouse.Id,
|
|
||||||
Name: e.Warehouse.Name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
||||||
dto := ProductWarehouseListDTO{
|
dto := ProductWarehouseListDTO{
|
||||||
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
|
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
|
||||||
// CreatedAt: e.CreatedAt,
|
|
||||||
// UpdatedAt: e.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Product relation jika ada
|
// Map Product relation jika ada
|
||||||
if e.Product.Id != 0 {
|
if e.Product.Id != 0 {
|
||||||
product := productDTO.ToProductRelationDTO(e.Product)
|
product := productDTO.ToProductRelationDTO(e.Product)
|
||||||
|
|
||||||
// Tambahkan flock name ke product name jika ada project flock
|
// Create a copy with flock name appended if exists
|
||||||
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
product.Name = product.Name + " (" + e.ProjectFlockKandang.ProjectFlock.FlockName + ")"
|
productCopy := product
|
||||||
|
productCopy.Name = product.Name + " (" + e.ProjectFlockKandang.ProjectFlock.FlockName + ")"
|
||||||
|
dto.Product = &productCopy
|
||||||
|
} else {
|
||||||
|
dto.Product = &product
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Product = &product
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Warehouse relation jika ada
|
// Map Warehouse relation jika ada
|
||||||
if e.Warehouse.Id != 0 {
|
if e.Warehouse.Id != 0 {
|
||||||
warehouse := WarehouseRelationDTO{
|
warehouse := warehouseDTO.ToWarehouseRelationDTO(e.Warehouse)
|
||||||
Id: e.Warehouse.Id,
|
|
||||||
Name: e.Warehouse.Name,
|
|
||||||
}
|
|
||||||
// Map Kandang jika ada
|
|
||||||
if e.Warehouse.Kandang != nil && e.Warehouse.Kandang.Id != 0 {
|
|
||||||
warehouse.Kandang = &KandangRelationDTO{
|
|
||||||
Id: e.Warehouse.Kandang.Id,
|
|
||||||
Name: e.Warehouse.Kandang.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Map Location jika ada
|
|
||||||
if e.Warehouse.Location != nil && e.Warehouse.Location.Id != 0 {
|
|
||||||
warehouse.Location = &LocationRelationDTO{
|
|
||||||
Id: e.Warehouse.Location.Id,
|
|
||||||
Name: e.Warehouse.Location.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Warehouse.Area.Id != 0 {
|
|
||||||
warehouse.Area = &AreaRelationDTO{
|
|
||||||
Id: e.Warehouse.Area.Id,
|
|
||||||
Name: e.Warehouse.Area.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.Warehouse = &warehouse
|
dto.Warehouse = &warehouse
|
||||||
}
|
}
|
||||||
@@ -168,7 +101,6 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
|||||||
Period: e.ProjectFlockKandang.Period,
|
Period: e.ProjectFlockKandang.Period,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map ProjectFlock jika ada
|
|
||||||
if e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
if e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
pfkDTO.ProjectFlock = &ProjectFlockRelationDTO{
|
pfkDTO.ProjectFlock = &ProjectFlockRelationDTO{
|
||||||
Id: e.ProjectFlockKandang.ProjectFlock.Id,
|
Id: e.ProjectFlockKandang.ProjectFlock.Id,
|
||||||
@@ -179,15 +111,6 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
|||||||
dto.ProjectFlockKandang = pfkDTO
|
dto.ProjectFlockKandang = pfkDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map CreatedUser relation jika ada
|
|
||||||
// if e.CreatedUser.Id != 0 {
|
|
||||||
// user := UserRelationDTO{
|
|
||||||
// Id: e.CreatedUser.Id,
|
|
||||||
// Username: e.CreatedUser.Name,
|
|
||||||
// }
|
|
||||||
// dto.CreatedUser = &user
|
|
||||||
// }
|
|
||||||
|
|
||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,23 +128,13 @@ func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDeta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO {
|
func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO {
|
||||||
return KandangRelationDTO{
|
product := productDTO.ToProductRelationDTO(e.Product)
|
||||||
Id: e.Id,
|
warehouse := warehouseDTO.ToWarehouseRelationDTO(e.Warehouse)
|
||||||
Name: e.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToLocationRelationDTO(e entity.Location) LocationRelationDTO {
|
return ProductWarehousNestedDTO{
|
||||||
return LocationRelationDTO{
|
Id: e.Id,
|
||||||
Id: e.Id,
|
Product: &product,
|
||||||
Name: e.Name,
|
Warehouse: &warehouse,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToAreaRelationDTO(e entity.Area) AreaRelationDTO {
|
|
||||||
return AreaRelationDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
Name: e.Name,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-13
@@ -86,24 +86,14 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWareho
|
|||||||
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
|
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
|
||||||
var productWarehouse entity.ProductWarehouse
|
var productWarehouse entity.ProductWarehouse
|
||||||
|
|
||||||
|
// Cari product warehouse dengan project_flock_kandang yang masih aktif (belum closed)
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT NULL", productId, warehouseId).
|
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||||
|
Where("product_id = ? AND warehouse_id = ? AND (project_flock_kandang_id IS NULL OR project_flock_kandangs.closed_at IS NULL)", productId, warehouseId).
|
||||||
Order("id DESC").
|
Order("id DESC").
|
||||||
Preload("ProjectFlockKandang").
|
Preload("ProjectFlockKandang").
|
||||||
First(&productWarehouse).Error
|
First(&productWarehouse).Error
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
|
|
||||||
if productWarehouse.ProjectFlockKandang.ClosedAt == nil {
|
|
||||||
return &productWarehouse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.DB().WithContext(ctx).
|
|
||||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NULL", productId, warehouseId).
|
|
||||||
First(&productWarehouse).Error
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
||||||
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +39,7 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
return db.
|
return db.
|
||||||
Preload("Product.Flags").
|
Preload("Product.Flags").
|
||||||
Preload("Product").
|
Preload("Product").
|
||||||
|
Preload("Product.Uom").
|
||||||
Preload("Warehouse").
|
Preload("Warehouse").
|
||||||
Preload("Warehouse.Location").
|
Preload("Warehouse.Location").
|
||||||
Preload("Warehouse.Area").
|
Preload("Warehouse.Area").
|
||||||
|
|||||||
@@ -4,20 +4,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferRelationDTO struct {
|
type TransferRelationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
TransferReason string `json:"transfer_reason"`
|
MovementNumber string `json:"movement_number"`
|
||||||
TransferDate string `json:"transfer_date"`
|
TransferReason string `json:"transfer_reason"`
|
||||||
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
|
TransferDate string `json:"transfer_date"`
|
||||||
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
|
||||||
}
|
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
|
||||||
|
|
||||||
type WarehouseSimpleDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductSimpleDTO struct {
|
type ProductSimpleDTO struct {
|
||||||
@@ -25,16 +22,6 @@ type ProductSimpleDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AreaDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SupplierSimpleDTO struct {
|
type SupplierSimpleDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -48,13 +35,6 @@ type DocumentDTO struct {
|
|||||||
Size float64 `json:"size"`
|
Size float64 `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseDetailDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Location *LocationDTO `json:"location"`
|
|
||||||
Area *AreaDTO `json:"area"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransferListDTO struct {
|
type TransferListDTO struct {
|
||||||
TransferRelationDTO
|
TransferRelationDTO
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
@@ -97,16 +77,19 @@ type TransferDeliveryItemDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||||
var sourceWarehouse *WarehouseDetailDTO
|
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
|
||||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||||
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse)
|
||||||
|
sourceWarehouse = &mapped
|
||||||
}
|
}
|
||||||
var destinationWarehouse *WarehouseDetailDTO
|
var destinationWarehouse *warehouseDTO.WarehouseRelationDTO
|
||||||
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||||
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
|
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
|
||||||
|
destinationWarehouse = &mapped
|
||||||
}
|
}
|
||||||
return TransferRelationDTO{
|
return TransferRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
|
MovementNumber: e.MovementNumber,
|
||||||
TransferReason: e.Reason,
|
TransferReason: e.Reason,
|
||||||
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
||||||
SourceWarehouse: sourceWarehouse,
|
SourceWarehouse: sourceWarehouse,
|
||||||
@@ -114,38 +97,6 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAreaDTO(a *entity.Area) *AreaDTO {
|
|
||||||
if a == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &AreaDTO{
|
|
||||||
Id: a.Id,
|
|
||||||
Name: a.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toLocationDTO(l *entity.Location) *LocationDTO {
|
|
||||||
if l == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &LocationDTO{
|
|
||||||
Id: l.Id,
|
|
||||||
Name: l.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
|
||||||
if w == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &WarehouseDetailDTO{
|
|
||||||
Id: w.Id,
|
|
||||||
Name: w.Name,
|
|
||||||
Location: toLocationDTO(w.Location),
|
|
||||||
Area: toAreaDTO(&w.Area),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
var createdUser *userDTO.UserRelationDTO
|
||||||
if e.CreatedUser != nil {
|
if e.CreatedUser != nil {
|
||||||
|
|||||||
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
movementNumber := fmt.Sprintf("ST-%05d", seq)
|
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq)
|
||||||
return movementNumber, nil
|
return movementNumber, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
|
|||||||
route := v1.Group("/transfers")
|
route := v1.Group("/transfers")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||||
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
|
||||||
|
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
|
||||||
|
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
|
||||||
|
searchTerm, searchTerm, searchTerm)
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -118,9 +122,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
return transferPtr, nil
|
return transferPtr, nil
|
||||||
@@ -136,12 +140,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID))
|
||||||
}
|
}
|
||||||
if sourcePW.Quantity < product.ProductQty {
|
if sourcePW.Quantity < product.ProductQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
|
||||||
}
|
}
|
||||||
pwIDs = append(pwIDs, sourcePW.Id)
|
pwIDs = append(pwIDs, sourcePW.Id)
|
||||||
}
|
}
|
||||||
@@ -161,10 +165,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
|
|
||||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
|
||||||
}
|
}
|
||||||
if projectFlockKandang.ClosedAt != nil {
|
if projectFlockKandang.ClosedAt != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
@@ -192,16 +196,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID))
|
||||||
}
|
}
|
||||||
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer")
|
||||||
}
|
}
|
||||||
|
|
||||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||||
@@ -239,16 +243,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID))
|
||||||
}
|
}
|
||||||
|
|
||||||
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
||||||
}
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -256,14 +260,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set ProjectFlockKandangId hanya jika ada kandang
|
||||||
|
var pfkID *uint
|
||||||
|
if projectFlockKandangID > 0 {
|
||||||
|
pfkID = &projectFlockKandangID
|
||||||
|
}
|
||||||
|
|
||||||
destPW = &entity.ProductWarehouse{
|
destPW = &entity.ProductWarehouse{
|
||||||
ProductId: uint(product.ProductID),
|
ProductId: uint(product.ProductID),
|
||||||
WarehouseId: uint(req.DestinationWarehouseID),
|
WarehouseId: uint(req.DestinationWarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
ProjectFlockKandangId: &projectFlockKandangID,
|
ProjectFlockKandangId: pfkID,
|
||||||
}
|
}
|
||||||
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +320,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
for _, prod := range item.Products {
|
for _, prod := range item.Products {
|
||||||
detail, ok := detailMap[uint64(prod.ProductID)]
|
detail, ok := detailMap[uint64(prod.ProductID)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
|
||||||
}
|
}
|
||||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||||
StockTransferDeliveryId: delivery.Id,
|
StockTransferDeliveryId: delivery.Id,
|
||||||
@@ -372,7 +383,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
@@ -381,7 +392,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
"usage_qty": consumeResult.UsageQuantity,
|
"usage_qty": consumeResult.UsageQuantity,
|
||||||
"pending_qty": consumeResult.PendingQuantity,
|
"pending_qty": consumeResult.PendingQuantity,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return fmt.Errorf("gagal update usage tracking: %w", err)
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID))
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||||
@@ -394,7 +405,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
@@ -402,7 +413,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"total_qty": replenishResult.AddedQuantity,
|
"total_qty": replenishResult.AddedQuantity,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return fmt.Errorf("gagal update total tracking: %w", err)
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +447,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||||
@@ -446,8 +457,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
|
|
||||||
if len(expensePayloads) > 0 {
|
if len(expensePayloads) > 0 {
|
||||||
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
||||||
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,32 +471,26 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
|
|||||||
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
|
|
||||||
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
}
|
}
|
||||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
|
||||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
|
||||||
}
|
}
|
||||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif")
|
||||||
}
|
}
|
||||||
|
|
||||||
return uint(projectFlockKandang.Id), nil
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
|||||||
@@ -140,12 +140,21 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
|
|||||||
if actorID == 0 {
|
if actorID == 0 {
|
||||||
actorID = 1
|
actorID = 1
|
||||||
}
|
}
|
||||||
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
||||||
action := entity.ApprovalActionUpdated
|
svc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
action := entity.ApprovalActionCreated
|
||||||
|
|
||||||
for id := range expenseIDs {
|
for id := range expenseIDs {
|
||||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if latestApproval == nil {
|
||||||
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,10 +69,10 @@ type DeliveryItemDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DeliveryGroupDTO struct {
|
type DeliveryGroupDTO struct {
|
||||||
DoNumber string `json:"do_number"`
|
DoNumber string `json:"do_number"`
|
||||||
DeliveryDate *time.Time `json:"delivery_date"`
|
DeliveryDate *time.Time `json:"delivery_date"`
|
||||||
Warehouse *productwarehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
||||||
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeliveryMarketingProductDTO struct {
|
type DeliveryMarketingProductDTO struct {
|
||||||
@@ -286,7 +287,7 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
|
|||||||
if !exists {
|
if !exists {
|
||||||
group = &DeliveryGroupDTO{
|
group = &DeliveryGroupDTO{
|
||||||
DeliveryDate: product.DeliveryDate,
|
DeliveryDate: product.DeliveryDate,
|
||||||
Warehouse: &productwarehouseDTO.WarehouseRelationDTO{
|
Warehouse: &warehouseDTO.WarehouseRelationDTO{
|
||||||
Id: warehouseId,
|
Id: warehouseId,
|
||||||
Name: warehouseName,
|
Name: warehouseName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||||
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||||
|
|
||||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||||
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,18 +148,30 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.WarehouseId > 0 {
|
if filters.WarehouseId > 0 || filters.Search != "" {
|
||||||
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.Search != "" {
|
if filters.Search != "" {
|
||||||
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
|
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
|
||||||
}
|
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
|
||||||
|
|
||||||
if filters.Search != "" {
|
|
||||||
searchPattern := "%" + filters.Search + "%"
|
searchPattern := "%" + filters.Search + "%"
|
||||||
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
|
db = db.Where(`(
|
||||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
marketing_delivery_products.vehicle_number ILIKE ? OR
|
||||||
|
customers.name ILIKE ? OR
|
||||||
|
warehouses.name ILIKE ? OR
|
||||||
|
products.name ILIKE ? OR
|
||||||
|
sales_users.name ILIKE ? OR
|
||||||
|
CONCAT(
|
||||||
|
marketings.so_number,
|
||||||
|
'-',
|
||||||
|
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
|
||||||
|
'-',
|
||||||
|
COALESCE(product_warehouses.warehouse_id::text, '')
|
||||||
|
) ILIKE ?
|
||||||
|
)`,
|
||||||
|
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.CustomerId > 0 {
|
if filters.CustomerId > 0 {
|
||||||
@@ -178,6 +190,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filters.AreaId > 0 || filters.LocationId > 0 {
|
||||||
|
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||||
|
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
|
||||||
|
|
||||||
|
if filters.AreaId > 0 {
|
||||||
|
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.LocationId > 0 {
|
||||||
|
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if filters.MarketingType != "" {
|
if filters.MarketingType != "" {
|
||||||
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
Group("marketing_delivery_products.id")
|
Group("marketing_delivery_products.id")
|
||||||
@@ -186,7 +211,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
case "ayam":
|
case "ayam":
|
||||||
db = db.Where("flags.name IN (?)", []string{
|
db = db.Where("flags.name IN (?)", []string{
|
||||||
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
|
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
|
||||||
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
|
|
||||||
})
|
})
|
||||||
case "telur":
|
case "telur":
|
||||||
db = db.Where("flags.name IN (?)", []string{
|
db = db.Where("flags.name IN (?)", []string{
|
||||||
@@ -201,8 +225,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
|
if filters.StartDate != "" || filters.EndDate != "" {
|
||||||
if filters.FilterBy == "so_date" {
|
filterBy := filters.FilterBy
|
||||||
|
if filterBy == "" {
|
||||||
|
filterBy = "so_date"
|
||||||
|
}
|
||||||
|
if filterBy == "so_date" {
|
||||||
if filters.StartDate != "" {
|
if filters.StartDate != "" {
|
||||||
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
|
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
db = db.Where("marketings.so_date >= ?", startDate)
|
db = db.Where("marketings.so_date >= ?", startDate)
|
||||||
@@ -214,7 +242,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
db = db.Where("marketings.so_date < ?", nextDate)
|
db = db.Where("marketings.so_date < ?", nextDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if filters.FilterBy == "realization_date" {
|
} else if filterBy == "realization_date" {
|
||||||
if filters.StartDate != "" {
|
if filters.StartDate != "" {
|
||||||
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
|
if startDate, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate)
|
db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate)
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository {
|
|||||||
|
|
||||||
func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) {
|
func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) {
|
||||||
var products []entity.MarketingProduct
|
var products []entity.MarketingProduct
|
||||||
if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil {
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Preload("ProductWarehouse.Product.Flags").
|
||||||
|
Where("marketing_id = ?", marketingID).
|
||||||
|
Find(&products).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(products) == 0 {
|
if len(products) == 0 {
|
||||||
|
|||||||
@@ -247,9 +247,27 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
|||||||
itemDeliveryDate = &parsedDate
|
itemDeliveryDate = &parsedDate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Cek apakah product punya flag PAKAN atau OVK
|
||||||
|
isPakanOrOVK := false
|
||||||
|
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
|
||||||
|
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
|
||||||
|
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
|
||||||
|
isPakanOrOVK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung total_weight dan total_price berdasarkan flag
|
||||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||||
totalPrice := requestedProduct.UnitPrice * totalWeight
|
var totalPrice float64
|
||||||
|
if isPakanOrOVK {
|
||||||
|
// PAKAN atau OVK: qty × unit_price
|
||||||
|
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
|
||||||
|
} else {
|
||||||
|
// Produk lain: total_weight × unit_price
|
||||||
|
totalPrice = totalWeight * requestedProduct.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
@@ -361,9 +379,27 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
|||||||
|
|
||||||
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Cek apakah product punya flag PAKAN atau OVK
|
||||||
|
isPakanOrOVK := false
|
||||||
|
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
|
||||||
|
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
|
||||||
|
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
|
||||||
|
isPakanOrOVK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung total_weight dan total_price berdasarkan flag
|
||||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||||
totalPrice := requestedProduct.UnitPrice * totalWeight
|
var totalPrice float64
|
||||||
|
if isPakanOrOVK {
|
||||||
|
// PAKAN atau OVK: qty × unit_price
|
||||||
|
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
|
||||||
|
} else {
|
||||||
|
// Produk lain: total_weight × unit_price
|
||||||
|
totalPrice = totalWeight * requestedProduct.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
@@ -435,7 +471,13 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pw == nil || pw.Quantity < requestedQty {
|
if pw == nil || pw.Quantity < requestedQty {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 {
|
||||||
|
if pw != nil {
|
||||||
|
return pw.Quantity
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}(), requestedQty))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
|
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
|
||||||
|
|||||||
@@ -292,9 +292,35 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
for _, rp := range req.MarketingProducts {
|
for _, rp := range req.MarketingProducts {
|
||||||
if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
|
if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
|
||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Get product untuk cek flag PAKAN atau OVK
|
||||||
|
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Product.Flags")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah product punya flag PAKAN atau OVK
|
||||||
|
isPakanOrOVK := false
|
||||||
|
if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 {
|
||||||
|
for _, flag := range productWarehouse.Product.Flags {
|
||||||
|
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
|
||||||
|
isPakanOrOVK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung total_weight dan total_price berdasarkan flag
|
||||||
totalWeight := rp.Qty * rp.AvgWeight
|
totalWeight := rp.Qty * rp.AvgWeight
|
||||||
totalPrice := rp.UnitPrice * totalWeight
|
var totalPrice float64
|
||||||
|
if isPakanOrOVK {
|
||||||
|
// PAKAN atau OVK: qty × unit_price
|
||||||
|
totalPrice = rp.Qty * rp.UnitPrice
|
||||||
|
} else {
|
||||||
|
// Produk lain: total_weight × unit_price
|
||||||
|
totalPrice = totalWeight * rp.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
updateBody := map[string]any{
|
updateBody := map[string]any{
|
||||||
"product_warehouse_id": rp.ProductWarehouseId,
|
"product_warehouse_id": rp.ProductWarehouseId,
|
||||||
@@ -592,9 +618,34 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
|||||||
|
|
||||||
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
|
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
|
||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Get product untuk cek flag PAKAN atau OVK
|
||||||
|
productWarehouse, err := s.ProductWarehouseRepo.GetByID(ctx, rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Product.Flags")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah product punya flag PAKAN atau OVK
|
||||||
|
isPakanOrOVK := false
|
||||||
|
if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 {
|
||||||
|
for _, flag := range productWarehouse.Product.Flags {
|
||||||
|
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
|
||||||
|
isPakanOrOVK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
totalWeight := rp.Qty * rp.AvgWeight
|
totalWeight := rp.Qty * rp.AvgWeight
|
||||||
totalPrice := rp.UnitPrice * totalWeight
|
var totalPrice float64
|
||||||
|
if isPakanOrOVK {
|
||||||
|
// PAKAN atau OVK: qty × unit_price
|
||||||
|
totalPrice = rp.Qty * rp.UnitPrice
|
||||||
|
} else {
|
||||||
|
// Produk lain: total_weight × unit_price
|
||||||
|
totalPrice = totalWeight * rp.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
marketingProduct := &entity.MarketingProduct{
|
marketingProduct := &entity.MarketingProduct{
|
||||||
MarketingId: marketingId,
|
MarketingId: marketingId,
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if req.PercentageThresholdBad > req.PercentageThresholdEnough {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "percentage_threshold_bad cannot be greater than percentage_threshold_enough")
|
||||||
|
}
|
||||||
|
|
||||||
date, err := time.Parse("2006-01-02", req.Date)
|
date, err := time.Parse("2006-01-02", req.Date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,6 +103,11 @@ func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
|||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if req.PercentageThresholdBad != nil && req.PercentageThresholdEnough != nil {
|
||||||
|
if *req.PercentageThresholdBad > *req.PercentageThresholdEnough {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "percentage_threshold_bad cannot be greater than percentage_threshold_enough")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,17 @@ func (s *phaseActivityService) CreateOne(c *fiber.Ctx, req *validation.Create) (
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existing, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("phase_id = ? AND name = ? AND time_type = ?", phase.Id, name, timeType)
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to check phaseActivity uniqueness: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "phase activity with same name and time_type already exists")
|
||||||
|
}
|
||||||
|
|
||||||
createBody := &entity.PhaseActivity{
|
createBody := &entity.PhaseActivity{
|
||||||
PhaseId: phase.Id,
|
PhaseId: phase.Id,
|
||||||
Name: name,
|
Name: name,
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
|
|
||||||
newChikins = append(newChikins, newChickin)
|
newChikins = append(newChikins, newChickin)
|
||||||
|
|
||||||
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||||
}
|
}
|
||||||
|
|
||||||
availableQty := productWarehouse.Quantity - totalPopulationQty
|
availableQty := productWarehouse.Quantity - totalPopulationQty
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -53,6 +54,7 @@ type ProjectFlockKandangListDTO struct {
|
|||||||
ProjectFlockKandangRelationDTO
|
ProjectFlockKandangRelationDTO
|
||||||
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
NameWithPeriod string `json:"name_with_period"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||||
@@ -104,6 +106,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
|
|||||||
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
||||||
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||||
Kandang: toKandangRelation(e.Kandang),
|
Kandang: toKandangRelation(e.Kandang),
|
||||||
|
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||||
@@ -126,6 +129,16 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
|
|||||||
return &mapped
|
return &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toNameWithPeriod(kandang entity.Kandang, period int) string {
|
||||||
|
if kandang.Name == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if period == 0 {
|
||||||
|
return kandang.Name
|
||||||
|
}
|
||||||
|
return kandang.Name + " Period " + strconv.Itoa(period)
|
||||||
|
}
|
||||||
|
|
||||||
func toApprovalDTOSelector(
|
func toApprovalDTOSelector(
|
||||||
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
||||||
approval := selector(e)
|
approval := selector(e)
|
||||||
@@ -147,6 +160,7 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
|
|||||||
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
||||||
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||||
Kandang: toKandangRelation(e.Kandang),
|
Kandang: toKandangRelation(e.Kandang),
|
||||||
|
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||||
|
|||||||
+22
-1
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -16,6 +17,7 @@ type ProjectFlockPopulationRepository interface {
|
|||||||
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
|
GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error)
|
||||||
|
|
||||||
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
@@ -111,7 +113,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c
|
|||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockPopulation{}).
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
Where("product_warehouse_id = ?", productWarehouseID).
|
Where("product_warehouse_id = ?", productWarehouseID).
|
||||||
Select("COALESCE(SUM(total_qty), 0)").
|
Select("COALESCE(SUM(total_qty - total_used_qty), 0)").
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -135,3 +137,22 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
|
|||||||
}
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("project_flock_populations").
|
||||||
|
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||||
|
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||||
|
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if total < 0 {
|
||||||
|
total = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(math.Round(total)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
|||||||
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||||
|
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search"),
|
||||||
}
|
}
|
||||||
if projectFlockID > 0 {
|
if projectFlockID > 0 {
|
||||||
query.ProjectFlockKandangId = uint(projectFlockID)
|
query.ProjectFlockKandangId = uint(projectFlockID)
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type RecordingProjectFlockDTO struct {
|
type RecordingProjectFlockDTO struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
FlockName string `json:"flock_name"`
|
FlockName string `json:"flock_name"`
|
||||||
ProjectFlockCategory string `json:"project_flock_category"`
|
ProjectFlockCategory string `json:"project_flock_category"`
|
||||||
Period int `json:"period"`
|
Period int `json:"period"`
|
||||||
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
||||||
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
||||||
TotalChickQty float64 `json:"total_chick_qty"`
|
TotalChickQty float64 `json:"total_chick_qty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingProductionStandardDTO struct {
|
type RecordingProductionStandardDTO struct {
|
||||||
@@ -53,6 +53,13 @@ type RecordingLocationDTO struct {
|
|||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecordingKandangDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Capacity float64 `json:"capacity"`
|
||||||
|
}
|
||||||
|
|
||||||
type RecordingWarehouseDTO struct {
|
type RecordingWarehouseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -82,12 +89,14 @@ type RecordingListDTO struct {
|
|||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
|
||||||
|
Location *RecordingLocationDTO `json:"location,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingDetailDTO struct {
|
type RecordingDetailDTO struct {
|
||||||
RecordingListDTO
|
RecordingListDTO
|
||||||
ProductCategory string `json:"product_category"`
|
ProductCategory string `json:"product_category"`
|
||||||
|
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
||||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||||
Stocks []RecordingStockDTO `json:"stocks"`
|
Stocks []RecordingStockDTO `json:"stocks"`
|
||||||
Eggs []RecordingEggDTO `json:"eggs"`
|
Eggs []RecordingEggDTO `json:"eggs"`
|
||||||
@@ -133,10 +142,11 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
|||||||
|
|
||||||
return RecordingDetailDTO{
|
return RecordingDetailDTO{
|
||||||
RecordingListDTO: listDTO,
|
RecordingListDTO: listDTO,
|
||||||
ProductCategory: recordingProductCategory(e),
|
ProductCategory: recordingProductCategory(e),
|
||||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
Warehouse: recordingWarehouseDTO(e),
|
||||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||||
|
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +212,8 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
|
|||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
Warehouse: recordingWarehouseDTO(e),
|
Kandang: recordingKandangDTO(e),
|
||||||
|
Location: recordingKandangLocationDTO(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,20 +225,20 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return RecordingRelationDTO{
|
return RecordingRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlock: toRecordingProjectFlockDTO(e),
|
ProjectFlock: toRecordingProjectFlockDTO(e),
|
||||||
RecordDatetime: e.RecordDatetime,
|
RecordDatetime: e.RecordDatetime,
|
||||||
Day: intValue(e.Day),
|
Day: intValue(e.Day),
|
||||||
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||||
CumDepletionRate: floatValue(e.CumDepletionRate),
|
CumDepletionRate: floatValue(e.CumDepletionRate),
|
||||||
CumIntake: intValue(e.CumIntake),
|
CumIntake: intValue(e.CumIntake),
|
||||||
FcrValue: floatValue(e.FcrValue),
|
FcrValue: floatValue(e.FcrValue),
|
||||||
HenDay: floatValue(e.HenDay),
|
HenDay: floatValue(e.HenDay),
|
||||||
HenHouse: floatValue(e.HenHouse),
|
HenHouse: floatValue(e.HenHouse),
|
||||||
FeedIntake: floatValue(e.FeedIntake),
|
FeedIntake: floatValue(e.FeedIntake),
|
||||||
EggMass: floatValue(e.EggMass),
|
EggMass: floatValue(e.EggMass),
|
||||||
EggWeight: floatValue(e.EggWeight),
|
EggWeight: floatValue(e.EggWeight),
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +332,34 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
|
|||||||
return mapWarehouseDTO(&pw.Warehouse)
|
return mapWarehouseDTO(&pw.Warehouse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordingKandangDTO(e entity.Recording) *RecordingKandangDTO {
|
||||||
|
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
kandang := e.ProjectFlockKandang.Kandang
|
||||||
|
return &RecordingKandangDTO{
|
||||||
|
Id: kandang.Id,
|
||||||
|
Name: kandang.Name,
|
||||||
|
Status: kandang.Status,
|
||||||
|
Capacity: kandang.Capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordingKandangLocationDTO(e entity.Recording) *RecordingLocationDTO {
|
||||||
|
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
location := e.ProjectFlockKandang.Kandang.Location
|
||||||
|
if location.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &RecordingLocationDTO{
|
||||||
|
Id: location.Id,
|
||||||
|
Name: location.Name,
|
||||||
|
Address: location.Address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
|
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
|
||||||
if len(e.Stocks) > 0 {
|
if len(e.Stocks) > 0 {
|
||||||
pw := e.Stocks[0].ProductWarehouse
|
pw := e.Stocks[0].ProductWarehouse
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
|
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||||
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||||
@@ -74,6 +76,28 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
|
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
|
Key: fifo.UsableKeyRecordingDepletion,
|
||||||
|
Table: "recording_depletions",
|
||||||
|
Columns: fifo.UsableColumns{
|
||||||
|
ID: "id",
|
||||||
|
ProductWarehouseID: "source_product_warehouse_id",
|
||||||
|
UsageQuantity: "qty",
|
||||||
|
PendingQuantity: "pending_qty",
|
||||||
|
CreatedAt: "id",
|
||||||
|
},
|
||||||
|
ExcludedStockables: []fifo.StockableKey{
|
||||||
|
fifo.StockableKeyTransferToLayingIn,
|
||||||
|
fifo.StockableKeyStockTransferIn,
|
||||||
|
fifo.StockableKeyAdjustmentIn,
|
||||||
|
fifo.StockableKeyPurchaseItems,
|
||||||
|
fifo.StockableKeyRecordingEgg,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||||
|
panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
@@ -91,6 +115,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
approvalRepo,
|
approvalRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
fifoService,
|
fifoService,
|
||||||
|
stockLogRepo,
|
||||||
productionStandardService,
|
productionStandardService,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type RecordingRepository interface {
|
|||||||
repository.BaseRepository[entity.Recording]
|
repository.BaseRepository[entity.Recording]
|
||||||
|
|
||||||
WithRelations(db *gorm.DB) *gorm.DB
|
WithRelations(db *gorm.DB) *gorm.DB
|
||||||
|
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
|
||||||
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ type RecordingRepository interface {
|
|||||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
||||||
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
|
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
|
||||||
|
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
|
||||||
|
|
||||||
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||||
@@ -84,6 +86,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
Preload("ProjectFlockKandang").
|
Preload("ProjectFlockKandang").
|
||||||
Preload("ProjectFlockKandang.Kandang").
|
Preload("ProjectFlockKandang.Kandang").
|
||||||
|
Preload("ProjectFlockKandang.Kandang.Location").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock").
|
Preload("ProjectFlockKandang.ProjectFlock").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||||
@@ -107,6 +110,42 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
|
||||||
|
if normalized == "" {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
likeQuery := "%" + normalized + "%"
|
||||||
|
subQuery := db.Session(&gorm.Session{NewDB: true}).
|
||||||
|
Table("recordings").
|
||||||
|
Select("recordings.id").
|
||||||
|
Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
|
||||||
|
Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||||
|
Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("LEFT JOIN locations l ON l.id = k.location_id").
|
||||||
|
Joins("LEFT JOIN recording_stocks rs ON rs.recording_id = recordings.id").
|
||||||
|
Joins("LEFT JOIN recording_depletions rd ON rd.recording_id = recordings.id").
|
||||||
|
Joins("LEFT JOIN recording_eggs re ON re.recording_id = recordings.id").
|
||||||
|
Joins("LEFT JOIN product_warehouses pws ON pws.id = rs.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN product_warehouses pwd ON pwd.id = rd.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN product_warehouses pwe ON pwe.id = re.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses ws ON ws.id = pws.warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses wd ON wd.id = pwd.warehouse_id").
|
||||||
|
Joins("LEFT JOIN warehouses we ON we.id = pwe.warehouse_id").
|
||||||
|
Where(`
|
||||||
|
LOWER(pf.flock_name) LIKE ?
|
||||||
|
OR LOWER(k.name) LIKE ?
|
||||||
|
OR LOWER(l.name) LIKE ?
|
||||||
|
OR LOWER(l.address) LIKE ?
|
||||||
|
OR LOWER(ws.name) LIKE ?
|
||||||
|
OR LOWER(wd.name) LIKE ?
|
||||||
|
OR LOWER(we.name) LIKE ?`,
|
||||||
|
likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery,
|
||||||
|
)
|
||||||
|
return db.Where("recordings.id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
||||||
if projectFlockKandangId == 0 {
|
if projectFlockKandangId == 0 {
|
||||||
return nil, errors.New("project_flock_kandang_id is required")
|
return nil, errors.New("project_flock_kandang_id is required")
|
||||||
@@ -132,6 +171,7 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
|
|||||||
var days []int
|
var days []int
|
||||||
if err := tx.Model(&entity.Recording{}).
|
if err := tx.Model(&entity.Recording{}).
|
||||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
Where("day IS NOT NULL").
|
Where("day IS NOT NULL").
|
||||||
Pluck("day", &days).Error; err != nil {
|
Pluck("day", &days).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -167,6 +207,12 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error {
|
||||||
|
return tx.Model(&entity.RecordingDepletion{}).
|
||||||
|
Where("id = ?", depletionID).
|
||||||
|
Update("pending_qty", pendingQty).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -322,38 +368,25 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
var rows []struct {
|
var result struct {
|
||||||
TotalQty float64
|
TotalQty float64
|
||||||
UomName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.
|
if err := tx.
|
||||||
Table("recording_stocks").
|
Table("recording_stocks").
|
||||||
Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
Select("COALESCE(SUM(COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0)), 0) AS total_qty").
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
|
||||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||||
Where("recording_stocks.recording_id = ?", recordingID).
|
Where("recording_stocks.recording_id = ?", recordingID).
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&result).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var total float64
|
if result.TotalQty <= 0 {
|
||||||
for _, row := range rows {
|
return 0, nil
|
||||||
if row.TotalQty <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch strings.TrimSpace(row.UomName) {
|
|
||||||
case "kilogram", "kg", "kilograms", "kilo":
|
|
||||||
total += row.TotalQty * 1000
|
|
||||||
case "gram", "g", "grams":
|
|
||||||
total += row.TotalQty
|
|
||||||
default:
|
|
||||||
total += row.TotalQty
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return total, nil
|
return result.TotalQty * 1000, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
|
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
|
||||||
@@ -367,7 +400,7 @@ func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordin
|
|||||||
}
|
}
|
||||||
err = tx.
|
err = tx.
|
||||||
Table("recording_eggs").
|
Table("recording_eggs").
|
||||||
Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(recording_eggs.qty * COALESCE(recording_eggs.weight, 0)), 0) AS total_weight_grams").
|
Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(COALESCE(recording_eggs.weight, 0) * 1000), 0) AS total_weight_grams").
|
||||||
Where("recording_eggs.recording_id = ?", recordingID).
|
Where("recording_eggs.recording_id = ?", recordingID).
|
||||||
Scan(&result).Error
|
Scan(&result).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -453,7 +486,7 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
|
|||||||
var result float64
|
var result float64
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Table("recording_eggs").
|
Table("recording_eggs").
|
||||||
Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000").
|
Select("COALESCE(SUM(recording_eggs.weight), 0)").
|
||||||
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
|
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
|
||||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
|
||||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -39,11 +40,12 @@ type RecordingService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RecordingFIFOIntegrationService interface {
|
type RecordingFIFOIntegrationService interface {
|
||||||
ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
|
ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error
|
||||||
ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
|
ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
|
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
|
||||||
|
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
|
||||||
|
|
||||||
type recordingService struct {
|
type recordingService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
@@ -56,6 +58,7 @@ type recordingService struct {
|
|||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecordingService(
|
func NewRecordingService(
|
||||||
@@ -66,6 +69,7 @@ func NewRecordingService(
|
|||||||
approvalRepo commonRepo.ApprovalRepository,
|
approvalRepo commonRepo.ApprovalRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoSvc commonSvc.FifoService,
|
||||||
|
stockLogRepo rStockLogs.StockLogRepository,
|
||||||
productionStandardSvc sProductionStandard.ProductionStandardService,
|
productionStandardSvc sProductionStandard.ProductionStandardService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) RecordingService {
|
) RecordingService {
|
||||||
@@ -80,6 +84,7 @@ func NewRecordingService(
|
|||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
ProductionStandardSvc: productionStandardSvc,
|
ProductionStandardSvc: productionStandardSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
|
StockLogRepo: stockLogRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +92,14 @@ func NewRecordingFIFOIntegrationService(
|
|||||||
repo repository.RecordingRepository,
|
repo repository.RecordingRepository,
|
||||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoSvc commonSvc.FifoService,
|
||||||
|
stockLogRepo rStockLogs.StockLogRepository,
|
||||||
) RecordingFIFOIntegrationService {
|
) RecordingFIFOIntegrationService {
|
||||||
return &recordingService{
|
return &recordingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
|
StockLogRepo: stockLogRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +123,8 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
if params.ProjectFlockKandangId != 0 {
|
if params.ProjectFlockKandangId != 0 {
|
||||||
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
|
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
|
||||||
}
|
}
|
||||||
return db.Order("record_datetime DESC").Order("created_at DESC")
|
db = s.Repository.ApplySearchFilters(db, params.Search)
|
||||||
|
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -157,14 +165,13 @@ func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) (
|
|||||||
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
db := s.Repository.DB().WithContext(c.Context())
|
day, err := s.computeRecordingDay(c.Context(), projectFlockKandangId, time.Now().UTC())
|
||||||
next, err := s.Repository.GenerateNextDay(db, projectFlockKandangId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to compute next recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
|
s.Log.Errorf("Failed to compute recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return next, nil
|
return day, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) {
|
func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) {
|
||||||
@@ -206,12 +213,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
day, err := s.computeRecordingDay(ctx, pfk.Id, recordTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
}
|
}
|
||||||
if isLaying && len(req.Eggs) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -222,13 +231,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
var createdRecording entity.Recording
|
var createdRecording entity.Recording
|
||||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if s.ProductionStandardSvc != nil {
|
if s.ProductionStandardSvc != nil {
|
||||||
if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, nextDay); err != nil {
|
if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, day); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +246,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Recording for this project flock today already exists")
|
return fiber.NewError(fiber.StatusBadRequest, "Recording for this project flock today already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
day := nextDay
|
|
||||||
createdRecording = entity.Recording{
|
createdRecording = entity.Recording{
|
||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
RecordDatetime: recordTime,
|
RecordDatetime: recordTime,
|
||||||
@@ -275,15 +278,31 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
|
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
|
||||||
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id)
|
||||||
|
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
|
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
|
||||||
|
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
|
||||||
|
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range mappedDepletions {
|
||||||
|
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if s.FifoSvc != nil {
|
||||||
|
note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id)
|
||||||
|
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
|
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
|
||||||
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
||||||
@@ -291,17 +310,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if s.FifoSvc != nil {
|
if s.FifoSvc != nil {
|
||||||
if err := s.replenishRecordingEggs(ctx, tx, mappedEggs); err != nil {
|
note := fmt.Sprintf("Recording-Create#%d", createdRecording.Id)
|
||||||
|
if err := s.replenishRecordingEggs(ctx, tx, mappedEggs, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var warehouseDeltas map[uint]float64
|
var warehouseDeltas map[uint]float64
|
||||||
if s.FifoSvc != nil {
|
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)
|
||||||
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil)
|
|
||||||
} else {
|
|
||||||
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)
|
|
||||||
}
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -337,6 +353,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var recordingEntity *entity.Recording
|
var recordingEntity *entity.Recording
|
||||||
var updatedRecording *entity.Recording
|
var updatedRecording *entity.Recording
|
||||||
@@ -407,9 +427,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
}
|
}
|
||||||
if isLaying && len(req.Eggs) == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
@@ -425,23 +442,47 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil {
|
note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id)
|
||||||
|
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasDepletionChanges {
|
if hasDepletionChanges {
|
||||||
|
if s.FifoSvc != nil {
|
||||||
|
note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id)
|
||||||
|
if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions, note, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
|
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
|
||||||
|
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
|
||||||
|
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range mappedDepletions {
|
||||||
|
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to update depletions: %+v", err)
|
s.Log.Errorf("Failed to update depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.FifoSvc != nil {
|
||||||
|
note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id)
|
||||||
|
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions, note, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -453,6 +494,28 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
if err := ensureRecordingEggsUnused(existingEggs); err != nil {
|
if err := ensureRecordingEggsUnused(existingEggs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if s.StockLogRepo != nil {
|
||||||
|
note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id)
|
||||||
|
logs := make([]*entity.StockLog, 0, len(existingEggs))
|
||||||
|
for _, egg := range existingEggs {
|
||||||
|
if egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logs = append(logs, &entity.StockLog{
|
||||||
|
ProductWarehouseId: egg.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Decrease: float64(egg.Qty),
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: recordingEntity.Id,
|
||||||
|
Notes: note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(logs) > 0 {
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateMany(ctx, logs, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, nil)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, nil)); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -471,7 +534,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.FifoSvc != nil {
|
if s.FifoSvc != nil {
|
||||||
if err := s.replenishRecordingEggs(ctx, tx, mappedEggs); err != nil {
|
note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id)
|
||||||
|
if err := s.replenishRecordingEggs(ctx, tx, mappedEggs, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -647,6 +711,11 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
s.Log.Errorf("Failed to list depletions before delete: %+v", err)
|
s.Log.Errorf("Failed to list depletions before delete: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if s.FifoSvc != nil {
|
||||||
|
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions, "", 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
oldEggs, err := s.Repository.ListEggs(tx, id)
|
oldEggs, err := s.Repository.ListEggs(tx, id)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -665,7 +734,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.releaseRecordingStocks(ctx, tx, oldStocks); err != nil {
|
if err := s.releaseRecordingStocks(ctx, tx, oldStocks, "", 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,10 +793,19 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (s *recordingService) consumeRecordingStocks(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
stocks []entity.RecordingStock,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
if len(stocks) == 0 || s.FifoSvc == nil {
|
if len(stocks) == 0 || s.FifoSvc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
|
return errors.New("stock log repository is not available")
|
||||||
|
}
|
||||||
|
|
||||||
for _, stock := range stocks {
|
for _, stock := range stocks {
|
||||||
if stock.Id == 0 {
|
if stock.Id == 0 {
|
||||||
@@ -760,19 +838,134 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logDecrease := result.UsageQuantity
|
||||||
|
if result.PendingQuantity > 0 {
|
||||||
|
logDecrease += result.PendingQuantity
|
||||||
|
}
|
||||||
|
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: stock.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Decrease: logDecrease,
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: stock.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (s *recordingService) consumeRecordingDepletions(
|
||||||
return s.consumeRecordingStocks(ctx, tx, stocks)
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
depletions []entity.RecordingDepletion,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
|
if len(depletions) == 0 || s.FifoSvc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
|
return errors.New("stock log repository is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, depletion := range depletions {
|
||||||
|
if depletion.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWarehouseID := uint(0)
|
||||||
|
if depletion.SourceProductWarehouseId != nil {
|
||||||
|
sourceWarehouseID = *depletion.SourceProductWarehouseId
|
||||||
|
}
|
||||||
|
if sourceWarehouseID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
|
||||||
|
}
|
||||||
|
|
||||||
|
desired := depletion.Qty + depletion.PendingQty
|
||||||
|
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||||
|
UsableKey: recordingDepletionUsableKey,
|
||||||
|
UsableID: depletion.Id,
|
||||||
|
ProductWarehouseID: sourceWarehouseID,
|
||||||
|
Quantity: desired,
|
||||||
|
AllowPending: false,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logDecrease := result.UsageQuantity
|
||||||
|
if result.PendingQuantity > 0 {
|
||||||
|
logDecrease += result.PendingQuantity
|
||||||
|
}
|
||||||
|
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: sourceWarehouseID,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Decrease: logDecrease,
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: depletion.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destDelta := depletion.Qty + depletion.PendingQty
|
||||||
|
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: depletion.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: destDelta,
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: depletion.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (s *recordingService) ConsumeRecordingStocks(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
stocks []entity.RecordingStock,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
|
return s.consumeRecordingStocks(ctx, tx, stocks, note, actorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) releaseRecordingStocks(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
stocks []entity.RecordingStock,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
if len(stocks) == 0 || s.FifoSvc == nil {
|
if len(stocks) == 0 || s.FifoSvc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
|
return errors.New("stock log repository is not available")
|
||||||
|
}
|
||||||
|
|
||||||
for _, stock := range stocks {
|
for _, stock := range stocks {
|
||||||
if stock.Id == 0 {
|
if stock.Id == 0 {
|
||||||
@@ -791,13 +984,166 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
|
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if stock.UsageQty != nil && *stock.UsageQty > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: stock.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: *stock.UsageQty,
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: stock.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (s *recordingService) releaseRecordingDepletions(
|
||||||
return s.releaseRecordingStocks(ctx, tx, stocks)
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
depletions []entity.RecordingDepletion,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
|
if len(depletions) == 0 || s.FifoSvc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
|
return errors.New("stock log repository is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, depletion := range depletions {
|
||||||
|
if depletion.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWarehouseID := uint(0)
|
||||||
|
if depletion.SourceProductWarehouseId != nil {
|
||||||
|
sourceWarehouseID = *depletion.SourceProductWarehouseId
|
||||||
|
}
|
||||||
|
if sourceWarehouseID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||||
|
UsableKey: recordingDepletionUsableKey,
|
||||||
|
UsableID: depletion.Id,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logIncrease := depletion.Qty
|
||||||
|
if depletion.PendingQty > 0 {
|
||||||
|
logIncrease += depletion.PendingQty
|
||||||
|
}
|
||||||
|
if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: sourceWarehouseID,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: logIncrease,
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: depletion.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destDelta := depletion.Qty + depletion.PendingQty
|
||||||
|
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: depletion.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Decrease: destDelta,
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: depletion.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) ReleaseRecordingStocks(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
stocks []entity.RecordingStock,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
|
return s.releaseRecordingStocks(ctx, tx, stocks, note, actorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||||
|
}
|
||||||
|
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
|
||||||
|
}
|
||||||
|
for _, pop := range populations {
|
||||||
|
if pop.ProductWarehouseId > 0 && pop.TotalQty > 0 {
|
||||||
|
return pop.ProductWarehouseId, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pop := range populations {
|
||||||
|
if pop.ProductWarehouseId > 0 {
|
||||||
|
return pop.ProductWarehouseId, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) computeRecordingDay(ctx context.Context, projectFlockKandangID uint, recordTime time.Time) (int, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
|
||||||
|
}
|
||||||
|
|
||||||
|
var chickinDate time.Time
|
||||||
|
for _, pop := range populations {
|
||||||
|
if pop.ProjectChickin == nil || pop.ProjectChickin.ChickInDate.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if chickinDate.IsZero() || pop.ProjectChickin.ChickInDate.Before(chickinDate) {
|
||||||
|
chickinDate = pop.ProjectChickin.ChickInDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chickinDate.IsZero() {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Tanggal chick in tidak ditemukan")
|
||||||
|
}
|
||||||
|
|
||||||
|
chickinDay := time.Date(chickinDate.Year(), chickinDate.Month(), chickinDate.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
recordDay := time.Date(recordTime.Year(), recordTime.Month(), recordTime.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
diff := int(recordDay.Sub(chickinDay).Hours() / 24)
|
||||||
|
if diff < 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Record date tidak boleh sebelum tanggal chick in")
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff + 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildWarehouseDeltas(
|
func buildWarehouseDeltas(
|
||||||
@@ -834,27 +1180,48 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context,
|
|||||||
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) replenishRecordingEggs(ctx context.Context, tx *gorm.DB, eggs []entity.RecordingEgg) error {
|
func (s *recordingService) replenishRecordingEggs(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
eggs []entity.RecordingEgg,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
if len(eggs) == 0 || s.FifoSvc == nil {
|
if len(eggs) == 0 || s.FifoSvc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
|
return errors.New("stock log repository is not available")
|
||||||
|
}
|
||||||
|
|
||||||
for _, egg := range eggs {
|
for _, egg := range eggs {
|
||||||
if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
|
if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
note := fmt.Sprintf("Recording egg #%d", egg.Id)
|
|
||||||
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||||
StockableKey: fifo.StockableKeyRecordingEgg,
|
StockableKey: fifo.StockableKeyRecordingEgg,
|
||||||
StockableID: egg.Id,
|
StockableID: egg.Id,
|
||||||
ProductWarehouseID: egg.ProductWarehouseId,
|
ProductWarehouseID: egg.ProductWarehouseId,
|
||||||
Quantity: float64(egg.Qty),
|
Quantity: float64(egg.Qty),
|
||||||
Note: ¬e,
|
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err)
|
s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: egg.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: float64(egg.Qty),
|
||||||
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
|
LoggableId: egg.RecordingId,
|
||||||
|
Notes: note,
|
||||||
|
}
|
||||||
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -905,6 +1272,8 @@ func (s *recordingService) syncRecordingStocks(
|
|||||||
recordingID uint,
|
recordingID uint,
|
||||||
existing []entity.RecordingStock,
|
existing []entity.RecordingStock,
|
||||||
incoming []validation.Stock,
|
incoming []validation.Stock,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
) error {
|
) error {
|
||||||
if s.FifoSvc == nil {
|
if s.FifoSvc == nil {
|
||||||
if err := s.Repository.DeleteStocks(tx, recordingID); err != nil {
|
if err := s.Repository.DeleteStocks(tx, recordingID); err != nil {
|
||||||
@@ -941,10 +1310,8 @@ func (s *recordingService) syncRecordingStocks(
|
|||||||
|
|
||||||
desired := item.Qty
|
desired := item.Qty
|
||||||
stock.UsageQty = &desired
|
stock.UsageQty = &desired
|
||||||
if item.PendingQty != nil {
|
zero := 0.0
|
||||||
pending := *item.PendingQty
|
stock.PendingQty = &zero
|
||||||
stock.PendingQty = &pending
|
|
||||||
}
|
|
||||||
stocksToConsume = append(stocksToConsume, stock)
|
stocksToConsume = append(stocksToConsume, stock)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,7 +1320,7 @@ func (s *recordingService) syncRecordingStocks(
|
|||||||
leftovers = append(leftovers, list...)
|
leftovers = append(leftovers, list...)
|
||||||
}
|
}
|
||||||
if len(leftovers) > 0 {
|
if len(leftovers) > 0 {
|
||||||
if err := s.releaseRecordingStocks(ctx, tx, leftovers); err != nil {
|
if err := s.releaseRecordingStocks(ctx, tx, leftovers, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ids := make([]uint, 0, len(leftovers))
|
ids := make([]uint, 0, len(leftovers))
|
||||||
@@ -972,7 +1339,7 @@ func (s *recordingService) syncRecordingStocks(
|
|||||||
if len(stocksToConsume) == 0 {
|
if len(stocksToConsume) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return s.consumeRecordingStocks(ctx, tx, stocksToConsume)
|
return s.consumeRecordingStocks(ctx, tx, stocksToConsume, note, actorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type eggTotals struct {
|
type eggTotals struct {
|
||||||
@@ -990,43 +1357,20 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
|
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
|
||||||
hasPending := false
|
|
||||||
for _, item := range incoming {
|
|
||||||
if item.PendingQty != nil {
|
|
||||||
hasPending = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUsage := make(map[uint]float64)
|
existingUsage := make(map[uint]float64)
|
||||||
existingTotal := make(map[uint]float64)
|
|
||||||
for _, stock := range existing {
|
for _, stock := range existing {
|
||||||
var usage float64
|
var usage float64
|
||||||
var pending float64
|
|
||||||
if stock.UsageQty != nil {
|
if stock.UsageQty != nil {
|
||||||
usage = *stock.UsageQty
|
usage = *stock.UsageQty
|
||||||
}
|
}
|
||||||
if stock.PendingQty != nil {
|
|
||||||
pending = *stock.PendingQty
|
|
||||||
}
|
|
||||||
existingUsage[stock.ProductWarehouseId] += usage
|
existingUsage[stock.ProductWarehouseId] += usage
|
||||||
existingTotal[stock.ProductWarehouseId] += usage + pending
|
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingUsage := make(map[uint]float64)
|
incomingUsage := make(map[uint]float64)
|
||||||
incomingTotal := make(map[uint]float64)
|
|
||||||
for _, item := range incoming {
|
for _, item := range incoming {
|
||||||
var pending float64
|
|
||||||
if item.PendingQty != nil {
|
|
||||||
pending = *item.PendingQty
|
|
||||||
}
|
|
||||||
incomingUsage[item.ProductWarehouseId] += item.Qty
|
incomingUsage[item.ProductWarehouseId] += item.Qty
|
||||||
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasPending {
|
|
||||||
return floatMapsMatch(existingTotal, incomingTotal)
|
|
||||||
}
|
|
||||||
return floatMapsMatch(existingUsage, incomingUsage)
|
return floatMapsMatch(existingUsage, incomingUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1053,7 +1397,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
|
|||||||
}
|
}
|
||||||
current := existingTotals[egg.ProductWarehouseId]
|
current := existingTotals[egg.ProductWarehouseId]
|
||||||
current.Qty += egg.Qty
|
current.Qty += egg.Qty
|
||||||
current.Weight += float64(egg.Qty) * weight
|
current.Weight += weight
|
||||||
existingTotals[egg.ProductWarehouseId] = current
|
existingTotals[egg.ProductWarehouseId] = current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,7 +1409,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
|
|||||||
}
|
}
|
||||||
current := incomingTotals[egg.ProductWarehouseId]
|
current := incomingTotals[egg.ProductWarehouseId]
|
||||||
current.Qty += egg.Qty
|
current.Qty += egg.Qty
|
||||||
current.Weight += float64(egg.Qty) * weight
|
current.Weight += weight
|
||||||
incomingTotals[egg.ProductWarehouseId] = current
|
incomingTotals[egg.ProductWarehouseId] = current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1224,7 +1568,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var eggMass float64
|
var eggMass float64
|
||||||
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
||||||
eggMass = (totalEggWeightGrams / remainingChick) * 1000
|
eggMass = (totalEggWeightGrams / remainingChick) / 1000
|
||||||
updates["egg_mass"] = eggMass
|
updates["egg_mass"] = eggMass
|
||||||
recording.EggMass = &eggMass
|
recording.EggMass = &eggMass
|
||||||
} else {
|
} else {
|
||||||
@@ -1234,7 +1578,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var eggWeight float64
|
var eggWeight float64
|
||||||
if totalEggQty > 0 && totalEggWeightGrams > 0 {
|
if totalEggQty > 0 && totalEggWeightGrams > 0 {
|
||||||
eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
|
eggWeight = totalEggWeightGrams / totalEggQty
|
||||||
updates["egg_weight"] = eggWeight
|
updates["egg_weight"] = eggWeight
|
||||||
recording.EggWeight = &eggWeight
|
recording.EggWeight = &eggWeight
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package validation
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Stock struct {
|
Stock struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||||
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Depletion struct {
|
Depletion struct {
|
||||||
@@ -20,23 +19,24 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Stocks []Stock `json:"stocks" validate:"dive"`
|
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||||
Depletions []Depletion `json:"depletions" validate:"dive"`
|
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||||
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type Approve struct {
|
||||||
|
|||||||
+7
-4
@@ -25,8 +25,12 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
|
|||||||
|
|
||||||
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
TransferDate: c.Query("transfer_date", ""),
|
||||||
|
FlockSource: uint(c.QueryInt("flock_source", 0)),
|
||||||
|
FlockDestination: uint(c.QueryInt("flock_destination", 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
@@ -66,7 +70,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id))
|
result, approval, err := u.TransferLayingService.GetOne(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -179,7 +183,6 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
||||||
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -162,9 +162,19 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
||||||
|
// Tampilkan requested qty sebelum approve, consumed qty setelah approve
|
||||||
|
var displayQty float64
|
||||||
|
if source.UsageQty > 0 {
|
||||||
|
// Sudah di-approve dan di-consume, tampilkan actual consumed quantity
|
||||||
|
displayQty = source.UsageQty
|
||||||
|
} else {
|
||||||
|
// Belum di-approve, tampilkan requested quantity
|
||||||
|
displayQty = source.RequestedQty
|
||||||
|
}
|
||||||
|
|
||||||
return LayingTransferSourceDTO{
|
return LayingTransferSourceDTO{
|
||||||
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
||||||
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
|
Qty: displayQty,
|
||||||
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
||||||
Note: source.Note,
|
Note: source.Note,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ import (
|
|||||||
|
|
||||||
type TransferLayingService interface {
|
type TransferLayingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
|
||||||
GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
|
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
@@ -110,8 +109,31 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
// Apply search and filters
|
||||||
|
if params.Search != "" {
|
||||||
|
searchPattern := "%" + params.Search + "%"
|
||||||
|
db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
|
||||||
|
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
|
||||||
|
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
|
||||||
|
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.TransferDate != "" {
|
||||||
|
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.FlockSource > 0 {
|
||||||
|
db = db.Where("from_project_flock_id = ?", params.FlockSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.FlockDestination > 0 {
|
||||||
|
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
|
||||||
|
}
|
||||||
|
|
||||||
db = db.Order("created_at DESC")
|
db = db.Order("created_at DESC")
|
||||||
|
|
||||||
|
db = s.withRelations(db)
|
||||||
|
|
||||||
return db
|
return db
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,14 +155,15 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
return transferLayings, total, nil
|
return transferLayings, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
|
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
|
||||||
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
return nil, nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
|
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||||
@@ -151,15 +174,6 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran
|
|||||||
transferLaying.LatestApproval = latestApproval
|
transferLaying.LatestApproval = latestApproval
|
||||||
}
|
}
|
||||||
|
|
||||||
return transferLaying, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
|
|
||||||
transferLaying, err := s.GetOne(c, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return transferLaying, transferLaying.LatestApproval, nil
|
return transferLaying, transferLaying.LatestApproval, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +230,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
for _, sourceDetail := range req.SourceKandangs {
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
if sourceDetail.Quantity <= 0 {
|
if sourceDetail.Quantity <= 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
|
continue
|
||||||
}
|
}
|
||||||
totalSourceQty += sourceDetail.Quantity
|
totalSourceQty += sourceDetail.Quantity
|
||||||
|
|
||||||
@@ -247,11 +261,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
for _, targetDetail := range req.TargetKandangs {
|
for _, targetDetail := range req.TargetKandangs {
|
||||||
if targetDetail.Quantity <= 0 {
|
if targetDetail.Quantity <= 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
|
continue
|
||||||
}
|
}
|
||||||
totalTargetQty += targetDetail.Quantity
|
totalTargetQty += targetDetail.Quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalSourceQty == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0")
|
||||||
|
}
|
||||||
|
if totalTargetQty == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
|
||||||
|
}
|
||||||
|
|
||||||
if totalSourceQty != totalTargetQty {
|
if totalSourceQty != totalTargetQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
|
||||||
}
|
}
|
||||||
@@ -279,11 +300,16 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, sourceDetail := range req.SourceKandangs {
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
|
if sourceDetail.Quantity == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
||||||
|
|
||||||
source := entity.LayingTransferSource{
|
source := entity.LayingTransferSource{
|
||||||
LayingTransferId: createBody.Id,
|
LayingTransferId: createBody.Id,
|
||||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||||
|
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
|
||||||
UsageQty: 0,
|
UsageQty: 0,
|
||||||
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
||||||
ProductWarehouseId: &productWarehouseId,
|
ProductWarehouseId: &productWarehouseId,
|
||||||
@@ -295,6 +321,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, targetDetail := range req.TargetKandangs {
|
for _, targetDetail := range req.TargetKandangs {
|
||||||
|
if targetDetail.Quantity == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -368,7 +397,12 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, createBody.Id)
|
laying_transfer, _, err := s.GetOne(c, createBody.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return laying_transfer, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
||||||
@@ -463,8 +497,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
|||||||
source := entity.LayingTransferSource{
|
source := entity.LayingTransferSource{
|
||||||
LayingTransferId: id,
|
LayingTransferId: id,
|
||||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||||
|
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
|
||||||
UsageQty: 0,
|
UsageQty: 0,
|
||||||
PendingUsageQty: sourceDetail.Quantity,
|
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
||||||
ProductWarehouseId: &productWarehouseId,
|
ProductWarehouseId: &productWarehouseId,
|
||||||
}
|
}
|
||||||
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
|
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
|
||||||
@@ -543,7 +578,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, id)
|
layingTransfer, _, err := s.GetOne(c, id)
|
||||||
|
|
||||||
|
return layingTransfer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
@@ -700,7 +737,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber)
|
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||||
StockableID: target.Id,
|
StockableID: target.Id,
|
||||||
@@ -734,7 +771,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
|
|
||||||
updated := make([]entity.LayingTransfer, 0, len(approvableIDs))
|
updated := make([]entity.LayingTransfer, 0, len(approvableIDs))
|
||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
transfer, err := s.GetOne(c, approvableID)
|
transfer, _, err := s.GetOne(c, approvableID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -814,15 +851,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
|
|||||||
|
|
||||||
kandangAvailableQty := make(map[uint]float64)
|
kandangAvailableQty := make(map[uint]float64)
|
||||||
for _, kandang := range kandangs {
|
for _, kandang := range kandangs {
|
||||||
|
// Gunakan fungsi repository yang sama dengan recording service
|
||||||
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
|
totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err)
|
s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err)
|
||||||
kandangAvailableQty[kandang.Id] = 0
|
kandangAvailableQty[kandang.Id] = 0
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
kandangAvailableQty[kandang.Id] = totalQty
|
kandangAvailableQty[kandang.Id] = totalAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
return pf, kandangAvailableQty, nil
|
return pf, kandangAvailableQty, nil
|
||||||
|
|||||||
+8
-4
@@ -2,12 +2,12 @@ package validation
|
|||||||
|
|
||||||
type SourceKandangDetail struct {
|
type SourceKandangDetail struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TargetKandangDetail struct {
|
type TargetKandangDetail struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
@@ -29,8 +29,12 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty"`
|
||||||
|
TransferDate string `query:"transfer_date" validate:"omitempty"`
|
||||||
|
FlockSource uint `query:"flock_source" validate:"omitempty,number"`
|
||||||
|
FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type Approve struct {
|
||||||
|
|||||||
@@ -345,7 +345,52 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil {
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
category := strings.TrimSpace(pfk.ProjectFlock.Category)
|
||||||
|
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
|
||||||
|
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
|
||||||
|
if strings.TrimSpace(standard.ProjectCategory) != "" {
|
||||||
|
category = standard.ProjectCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weekBase := 1
|
||||||
|
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
|
||||||
|
weekBase = 18
|
||||||
|
}
|
||||||
|
if req.Week < weekBase {
|
||||||
|
if weekBase == 18 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestWeek int
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Model(&entity.ProjectFlockKandangUniformity{}).
|
||||||
|
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", req.ProjectFlockKandangId).
|
||||||
|
Select("COALESCE(MAX(week), 0)").
|
||||||
|
Scan(&latestWeek).Error; err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
|
||||||
|
}
|
||||||
|
if latestWeek == 0 && req.Week != weekBase {
|
||||||
|
if weekBase == 18 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
||||||
|
}
|
||||||
|
if latestWeek > 0 && req.Week > latestWeek+1 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,8 +532,35 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
|||||||
if req.ProjectFlockKandangId != nil {
|
if req.ProjectFlockKandangId != nil {
|
||||||
targetPFKID = *req.ProjectFlockKandangId
|
targetPFKID = *req.ProjectFlockKandangId
|
||||||
}
|
}
|
||||||
|
if targetPFKID != 0 && targetWeek > 0 {
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetPFKID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
category := strings.TrimSpace(pfk.ProjectFlock.Category)
|
||||||
|
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
|
||||||
|
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
|
||||||
|
if strings.TrimSpace(standard.ProjectCategory) != "" {
|
||||||
|
category = standard.ProjectCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weekBase := 1
|
||||||
|
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
|
||||||
|
weekBase = 18
|
||||||
|
}
|
||||||
|
if targetWeek < weekBase {
|
||||||
|
if weekBase == 18 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
||||||
|
}
|
||||||
|
}
|
||||||
if targetDate != nil {
|
if targetDate != nil {
|
||||||
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil {
|
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,7 +676,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
|||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
|
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error {
|
||||||
if projectFlockKandangID == 0 || week == 0 {
|
if projectFlockKandangID == 0 || week == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,12 +167,21 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
|
|||||||
if actorID == 0 {
|
if actorID == 0 {
|
||||||
actorID = 1
|
actorID = 1
|
||||||
}
|
}
|
||||||
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
||||||
action := entity.ApprovalActionUpdated
|
svc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
action := entity.ApprovalActionCreated
|
||||||
|
|
||||||
for id := range expenseIDs {
|
for id := range expenseIDs {
|
||||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if latestApproval == nil {
|
||||||
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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"
|
||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -751,6 +752,9 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
if receivedQty > item.SubQty {
|
if receivedQty > item.SubQty {
|
||||||
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
||||||
}
|
}
|
||||||
|
if receivedQty < item.TotalUsed {
|
||||||
|
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be lower than used amount (%.3f)", payload.PurchaseItemID, item.TotalUsed))
|
||||||
|
}
|
||||||
|
|
||||||
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
||||||
return nil, utils.BadRequest(fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
||||||
@@ -827,19 +831,37 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
receivingAction = entity.ApprovalActionUpdated
|
receivingAction = entity.ApprovalActionUpdated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
noteSuffix := "receive"
|
||||||
|
if receivingAction == entity.ApprovalActionUpdated {
|
||||||
|
noteSuffix = "edit-receive"
|
||||||
|
}
|
||||||
|
receiveNote := fmt.Sprintf("%s#%s", strings.TrimSpace(*purchase.PoNumber), noteSuffix)
|
||||||
|
|
||||||
transactionErr := s.PurchaseRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
transactionErr := s.PurchaseRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
repoTx := rPurchase.NewPurchaseRepository(tx)
|
repoTx := rPurchase.NewPurchaseRepository(tx)
|
||||||
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
||||||
|
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||||
|
|
||||||
deltas := make(map[uint]float64)
|
deltas := make(map[uint]float64)
|
||||||
affected := make(map[uint]struct{})
|
affected := make(map[uint]struct{})
|
||||||
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
|
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
|
||||||
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
|
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
|
||||||
|
totalQtyDeltas := make(map[uint]float64)
|
||||||
fifoAdds := make([]struct {
|
fifoAdds := make([]struct {
|
||||||
itemID uint
|
itemID uint
|
||||||
pwID uint
|
pwID uint
|
||||||
qty float64
|
qty float64
|
||||||
}, 0, len(prepared))
|
}, 0, len(prepared))
|
||||||
|
fifoSubs := make([]struct {
|
||||||
|
itemID uint
|
||||||
|
pwID uint
|
||||||
|
qty float64
|
||||||
|
}, 0, len(prepared))
|
||||||
|
logEntries := make([]struct {
|
||||||
|
itemID uint
|
||||||
|
pwID uint
|
||||||
|
delta float64
|
||||||
|
}, 0, len(prepared))
|
||||||
|
|
||||||
for _, prep := range prepared {
|
for _, prep := range prepared {
|
||||||
item := prep.item
|
item := prep.item
|
||||||
@@ -860,16 +882,38 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
newPWID = &pwID
|
newPWID = &pwID
|
||||||
|
|
||||||
deltaQty := prep.receivedQty - item.TotalQty
|
deltaQty := prep.receivedQty - item.TotalQty
|
||||||
switch {
|
if newPWID != nil && deltaQty != 0 {
|
||||||
case deltaQty > 0 && newPWID != nil:
|
logEntries = append(logEntries, struct {
|
||||||
fifoAdds = append(fifoAdds, struct {
|
|
||||||
itemID uint
|
itemID uint
|
||||||
pwID uint
|
pwID uint
|
||||||
qty float64
|
delta float64
|
||||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case deltaQty > 0 && newPWID != nil:
|
||||||
|
if s.FifoSvc != nil {
|
||||||
|
fifoAdds = append(fifoAdds, struct {
|
||||||
|
itemID uint
|
||||||
|
pwID uint
|
||||||
|
qty float64
|
||||||
|
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
||||||
|
} else {
|
||||||
|
deltas[*newPWID] += deltaQty
|
||||||
|
totalQtyDeltas[item.Id] += deltaQty
|
||||||
|
}
|
||||||
case deltaQty < 0 && newPWID != nil:
|
case deltaQty < 0 && newPWID != nil:
|
||||||
deltas[*newPWID] += deltaQty // negative
|
if s.FifoSvc != nil {
|
||||||
affected[*newPWID] = struct{}{}
|
fifoSubs = append(fifoSubs, struct {
|
||||||
|
itemID uint
|
||||||
|
pwID uint
|
||||||
|
qty float64
|
||||||
|
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
||||||
|
affected[*newPWID] = struct{}{}
|
||||||
|
} else {
|
||||||
|
deltas[*newPWID] += deltaQty // negative
|
||||||
|
affected[*newPWID] = struct{}{}
|
||||||
|
totalQtyDeltas[item.Id] += deltaQty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dateCopy := prep.receivedDate
|
dateCopy := prep.receivedDate
|
||||||
@@ -892,7 +936,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
|
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
|
|
||||||
if item.Price > 0 && prep.receivedQty >= 0 {
|
if prep.receivedQty >= 0 {
|
||||||
priceUpdates = append(priceUpdates, rPurchase.PurchasePricingUpdate{
|
priceUpdates = append(priceUpdates, rPurchase.PurchasePricingUpdate{
|
||||||
ItemID: item.Id,
|
ItemID: item.Id,
|
||||||
Price: item.Price,
|
Price: item.Price,
|
||||||
@@ -909,16 +953,25 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(priceUpdates) > 0 {
|
if len(priceUpdates) > 0 {
|
||||||
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
|
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(totalQtyDeltas) > 0 {
|
||||||
|
for itemID, delta := range totalQtyDeltas {
|
||||||
|
if delta == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Model(&entity.PurchaseItem{}).
|
||||||
|
Where("purchase_id = ? AND id = ?", purchase.Id, itemID).
|
||||||
|
Update("total_qty", gorm.Expr("COALESCE(total_qty,0) + ?", delta)).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update due_date based on earliest received date when receiving approved.
|
// Update due_date based on earliest received date when receiving approved.
|
||||||
if earliestReceived != nil {
|
if earliestReceived != nil {
|
||||||
due := earliestReceived.AddDate(0, 0, purchase.CreditTerm)
|
due := earliestReceived.AddDate(0, 0, purchase.CreditTerm)
|
||||||
@@ -944,6 +997,53 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, adj := range fifoSubs {
|
||||||
|
if adj.pwID == 0 || adj.qty >= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
|
||||||
|
StockableKey: fifo.StockableKeyPurchaseItems,
|
||||||
|
StockableID: adj.itemID,
|
||||||
|
ProductWarehouseID: adj.pwID,
|
||||||
|
Quantity: adj.qty,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logEntries) > 0 {
|
||||||
|
logs := make([]*entity.StockLog, 0, len(logEntries))
|
||||||
|
for _, entry := range logEntries {
|
||||||
|
if entry.pwID == 0 || entry.delta == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log := &entity.StockLog{
|
||||||
|
ProductWarehouseId: entry.pwID,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
LoggableType: string(utils.StockLogTypePurchase),
|
||||||
|
LoggableId: purchase.Id,
|
||||||
|
Notes: receiveNote,
|
||||||
|
}
|
||||||
|
if entry.delta > 0 {
|
||||||
|
log.Increase = entry.delta
|
||||||
|
} else {
|
||||||
|
log.Decrease = -entry.delta
|
||||||
|
}
|
||||||
|
logs = append(logs, log)
|
||||||
|
}
|
||||||
|
if len(logs) > 0 {
|
||||||
|
if err := stockLogRepoTx.CreateMany(c.Context(), logs, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(affected) > 0 {
|
||||||
|
if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1371,10 +1471,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
qtyCopy := effectiveQty
|
qtyCopy := effectiveQty
|
||||||
update.Quantity = &qtyCopy
|
update.Quantity = &qtyCopy
|
||||||
}
|
}
|
||||||
if syncReceiving {
|
|
||||||
qtyCopy := effectiveQty
|
|
||||||
update.TotalQty = &qtyCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
delete(requestItems, item.Id)
|
delete(requestItems, item.Id)
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ 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)),
|
||||||
|
AreaId: int64(ctx.QueryInt("area_id", 0)),
|
||||||
|
LocationId: int64(ctx.QueryInt("location_id", 0)),
|
||||||
MarketingType: ctx.Query("marketing_type", ""),
|
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", ""),
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ type HppPerKandangResponseData struct {
|
|||||||
type HppPerKandangRowDTO struct {
|
type HppPerKandangRowDTO struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
||||||
|
NameWithPeriode string `json:"name_with_periode"`
|
||||||
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
||||||
AvgWeightKg float64 `json:"avg_weight_kg"`
|
AvgWeightKg float64 `json:"avg_weight_kg"`
|
||||||
EggProductionPieces int64 `json:"egg_production_pieces"`
|
EggProductionPieces int64 `json:"egg_production_pieces"`
|
||||||
EggProductionKg float64 `json:"egg_production_kg"`
|
EggProductionKg float64 `json:"egg_production_kg"`
|
||||||
|
// EggProductionTotalWeightKg float64 `json:"egg_production_total_weight_kg"`
|
||||||
|
// EggProductionTotalPieces int64 `json:"egg_production_total_pieces"`
|
||||||
// FeedCostRp float64 `json:"feed_cost_rp"`
|
// FeedCostRp float64 `json:"feed_cost_rp"`
|
||||||
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
||||||
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
||||||
@@ -38,8 +41,8 @@ type HppPerKandangRowDTO struct {
|
|||||||
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
||||||
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
||||||
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
|
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
|
||||||
HppRp float64 `json:"hpp_rp"`
|
// HppRp float64 `json:"hpp_rp"`
|
||||||
RemainingValueRp int64 `json:"remaining_value_rp"`
|
// RemainingValueRp int64 `json:"remaining_value_rp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangRowKandangDTO struct {
|
type HppPerKandangRowKandangDTO struct {
|
||||||
|
|||||||
@@ -40,98 +40,22 @@ type RepportMarketingItemDTO struct {
|
|||||||
type Summary struct {
|
type Summary struct {
|
||||||
TotalQty int `json:"total_qty"`
|
TotalQty int `json:"total_qty"`
|
||||||
TotalWeightKg float64 `json:"total_weight_kg"`
|
TotalWeightKg float64 `json:"total_weight_kg"`
|
||||||
|
AverageWeightKg float64 `json:"average_weight_kg"`
|
||||||
|
AverageSalesPrice float64 `json:"average_sales_price"`
|
||||||
TotalSalesAmount int64 `json:"total_sales_amount"`
|
TotalSalesAmount int64 `json:"total_sales_amount"`
|
||||||
TotalHppAmount int64 `json:"total_hpp_amount"`
|
TotalHppAmount int64 `json:"total_hpp_amount"`
|
||||||
TotalHppPricePerKg float64 `json:"total_hpp_price_per_kg"`
|
TotalHppPricePerKg float64 `json:"total_hpp_price_per_kg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepportMarketingResponseDTO struct {
|
|
||||||
Items []RepportMarketingItemDTO `json:"items"`
|
|
||||||
Total *Summary `json:"total,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductRelationDTOFixed struct {
|
type ProductRelationDTOFixed struct {
|
||||||
productDTO.ProductRelationDTO
|
productDTO.ProductRelationDTO
|
||||||
ProductPrice float64 `json:"product_price"`
|
ProductPrice float64 `json:"product_price"`
|
||||||
SellingPrice *float64 `json:"selling_price,omitempty"`
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
|
func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64, agingMap map[int]int) []RepportMarketingItemDTO {
|
||||||
soDate := time.Time{}
|
|
||||||
agingDays := 0
|
|
||||||
if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
|
|
||||||
soDate = mdp.MarketingProduct.Marketing.SoDate
|
|
||||||
agingDays = int(time.Since(soDate).Hours() / 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
realizationDate := time.Time{}
|
|
||||||
if mdp.DeliveryDate != nil {
|
|
||||||
realizationDate = *mdp.DeliveryDate
|
|
||||||
}
|
|
||||||
|
|
||||||
doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
|
|
||||||
|
|
||||||
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
|
|
||||||
salesAmount := totalWeightKg * mdp.UnitPrice
|
|
||||||
|
|
||||||
var hpp float64
|
|
||||||
var hppAmount float64
|
|
||||||
if isProductEligibleForHpp(mdp, category) {
|
|
||||||
hpp = hppPricePerKg
|
|
||||||
hppAmount = totalWeightKg * hppPricePerKg
|
|
||||||
}
|
|
||||||
|
|
||||||
item := RepportMarketingItemDTO{
|
|
||||||
ID: int(mdp.Id),
|
|
||||||
SoDate: soDate,
|
|
||||||
RealizationDate: realizationDate,
|
|
||||||
AgingDays: agingDays,
|
|
||||||
DoNumber: doNumber,
|
|
||||||
MarketingType: getMarketingType(mdp),
|
|
||||||
Qty: mdp.UsageQty,
|
|
||||||
AverageWeightKg: mdp.AvgWeight,
|
|
||||||
TotalWeightKg: totalWeightKg,
|
|
||||||
SalesPricePerKg: mdp.UnitPrice,
|
|
||||||
HppPricePerKg: hpp,
|
|
||||||
SalesAmount: salesAmount,
|
|
||||||
HppAmount: hppAmount,
|
|
||||||
}
|
|
||||||
|
|
||||||
if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 {
|
|
||||||
mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse)
|
|
||||||
item.Warehouse = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
if mdp.MarketingProduct.Marketing.CustomerId != 0 {
|
|
||||||
mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer)
|
|
||||||
item.Customer = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
if mdp.MarketingProduct.Marketing.SalesPersonId != 0 {
|
|
||||||
mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson)
|
|
||||||
item.Sales = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
item.VehicleNumber = mdp.VehicleNumber
|
|
||||||
|
|
||||||
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
|
|
||||||
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
|
||||||
item.Product = newProductRelationDTOFixedPtr(&mapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) []RepportMarketingItemDTO {
|
|
||||||
items := make([]RepportMarketingItemDTO, 0, len(mdps))
|
items := make([]RepportMarketingItemDTO, 0, len(mdps))
|
||||||
for _, mdp := range mdps {
|
|
||||||
items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg, category))
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []RepportMarketingItemDTO {
|
|
||||||
items := make([]RepportMarketingItemDTO, 0, len(mdps))
|
|
||||||
for _, mdp := range mdps {
|
for _, mdp := range mdps {
|
||||||
hppPerKg := float64(0)
|
hppPerKg := float64(0)
|
||||||
category := ""
|
category := ""
|
||||||
@@ -142,101 +66,113 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
|
|||||||
category = projectFlockKandang.ProjectFlock.Category
|
category = projectFlockKandang.ProjectFlock.Category
|
||||||
}
|
}
|
||||||
|
|
||||||
item := ToRepportMarketingItemDTO(mdp, hppPerKg, category)
|
soDate := time.Time{}
|
||||||
|
agingDays := 0
|
||||||
|
if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
|
||||||
|
soDate = mdp.MarketingProduct.Marketing.SoDate
|
||||||
|
if ag, exists := agingMap[int(mdp.Id)]; exists {
|
||||||
|
agingDays = ag
|
||||||
|
} else {
|
||||||
|
agingDays = int(time.Since(soDate).Hours() / 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
realizationDate := time.Time{}
|
||||||
|
if mdp.DeliveryDate != nil {
|
||||||
|
realizationDate = *mdp.DeliveryDate
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
|
||||||
|
salesAmount := totalWeightKg * mdp.UnitPrice
|
||||||
|
|
||||||
|
var hpp float64
|
||||||
|
var hppAmount float64
|
||||||
|
|
||||||
|
var hasAyam, hasTelur, hasTrading bool
|
||||||
|
for _, flag := range mdp.MarketingProduct.ProductWarehouse.Product.Flags {
|
||||||
|
ft := utils.FlagType(flag.Name)
|
||||||
|
|
||||||
|
if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati ||
|
||||||
|
ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer {
|
||||||
|
hasAyam = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah ||
|
||||||
|
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
marketingType := "trading"
|
||||||
|
if hasTrading {
|
||||||
|
marketingType = "trading"
|
||||||
|
} else if hasTelur {
|
||||||
|
marketingType = "telur"
|
||||||
|
} else if hasAyam {
|
||||||
|
marketingType = "ayam"
|
||||||
|
}
|
||||||
|
|
||||||
|
eligibleForHpp := false
|
||||||
|
|
||||||
|
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
|
||||||
|
eligibleForHpp = hasAyam
|
||||||
|
} else {
|
||||||
|
eligibleForHpp = hasAyam || hasTelur
|
||||||
|
}
|
||||||
|
|
||||||
|
if eligibleForHpp {
|
||||||
|
hpp = hppPerKg
|
||||||
|
hppAmount = totalWeightKg * hppPerKg
|
||||||
|
}
|
||||||
|
|
||||||
|
item := RepportMarketingItemDTO{
|
||||||
|
ID: int(mdp.Id),
|
||||||
|
SoDate: soDate,
|
||||||
|
RealizationDate: realizationDate,
|
||||||
|
AgingDays: agingDays,
|
||||||
|
DoNumber: marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId),
|
||||||
|
MarketingType: marketingType,
|
||||||
|
Qty: mdp.UsageQty,
|
||||||
|
AverageWeightKg: mdp.AvgWeight,
|
||||||
|
TotalWeightKg: totalWeightKg,
|
||||||
|
SalesPricePerKg: mdp.UnitPrice,
|
||||||
|
HppPricePerKg: hpp,
|
||||||
|
SalesAmount: salesAmount,
|
||||||
|
HppAmount: hppAmount,
|
||||||
|
VehicleNumber: mdp.VehicleNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 {
|
||||||
|
mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse)
|
||||||
|
item.Warehouse = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
if mdp.MarketingProduct.Marketing.CustomerId != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer)
|
||||||
|
item.Customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
if mdp.MarketingProduct.Marketing.SalesPersonId != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson)
|
||||||
|
item.Sales = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
|
||||||
|
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
||||||
|
item.Product = newProductRelationDTOFixedPtr(&mapped)
|
||||||
|
}
|
||||||
|
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
|
||||||
hasAyam, hasTelur, hasTrading := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
|
||||||
|
|
||||||
if hasAyam {
|
|
||||||
return "ayam"
|
|
||||||
}
|
|
||||||
if hasTelur {
|
|
||||||
return "telur"
|
|
||||||
}
|
|
||||||
if hasTrading {
|
|
||||||
return "trading"
|
|
||||||
}
|
|
||||||
return "trading" // default to trading if no flags found
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool) {
|
|
||||||
if len(flags) == 0 {
|
|
||||||
return false, false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range flags {
|
|
||||||
ft := utils.FlagType(flag.Name)
|
|
||||||
|
|
||||||
if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati ||
|
|
||||||
ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer {
|
|
||||||
hasAyam = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah ||
|
|
||||||
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
|
|
||||||
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, hasTrading
|
|
||||||
}
|
|
||||||
|
|
||||||
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
|
|
||||||
hasAyam, hasTelur, _ := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
|
||||||
|
|
||||||
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
|
|
||||||
return hasAyam
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasAyam || hasTelur
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) *Summary {
|
|
||||||
if len(mdps) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalQty := 0
|
|
||||||
totalWeightKg := 0.0
|
|
||||||
totalEligibleWeightKg := 0.0
|
|
||||||
totalSalesAmount := int64(0)
|
|
||||||
totalHppAmount := int64(0)
|
|
||||||
|
|
||||||
for _, mdp := range mdps {
|
|
||||||
calculatedTotalWeight := mdp.UsageQty * mdp.AvgWeight
|
|
||||||
totalQty += int(mdp.UsageQty)
|
|
||||||
totalWeightKg += calculatedTotalWeight
|
|
||||||
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
|
|
||||||
|
|
||||||
if isProductEligibleForHpp(mdp, category) {
|
|
||||||
totalEligibleWeightKg += calculatedTotalWeight
|
|
||||||
totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalHppPricePerKg := float64(0)
|
|
||||||
if totalEligibleWeightKg > 0 {
|
|
||||||
totalHppPricePerKg = float64(totalHppAmount) / totalEligibleWeightKg
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Summary{
|
|
||||||
TotalQty: totalQty,
|
|
||||||
TotalWeightKg: totalWeightKg,
|
|
||||||
TotalSalesAmount: totalSalesAmount,
|
|
||||||
TotalHppAmount: totalHppAmount,
|
|
||||||
TotalHppPricePerKg: totalHppPricePerKg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
|
func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -244,6 +180,8 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
|
|||||||
|
|
||||||
totalQty := 0
|
totalQty := 0
|
||||||
totalWeightKg := 0.0
|
totalWeightKg := 0.0
|
||||||
|
avgSalesPrice := 0.0
|
||||||
|
avgWeightKg := 0.0
|
||||||
totalSalesAmount := int64(0)
|
totalSalesAmount := int64(0)
|
||||||
totalHppAmount := int64(0)
|
totalHppAmount := int64(0)
|
||||||
|
|
||||||
@@ -252,6 +190,7 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
|
|||||||
totalWeightKg += item.TotalWeightKg
|
totalWeightKg += item.TotalWeightKg
|
||||||
totalSalesAmount += int64(item.SalesAmount)
|
totalSalesAmount += int64(item.SalesAmount)
|
||||||
totalHppAmount += int64(item.HppAmount)
|
totalHppAmount += int64(item.HppAmount)
|
||||||
|
avgSalesPrice += item.SalesPricePerKg
|
||||||
}
|
}
|
||||||
|
|
||||||
totalHppPricePerKg := float64(0)
|
totalHppPricePerKg := float64(0)
|
||||||
@@ -259,25 +198,25 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
|
|||||||
totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg
|
totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(items) > 0 {
|
||||||
|
avgSalesPrice = avgSalesPrice / float64(len(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalQty > 0 {
|
||||||
|
avgWeightKg = totalWeightKg / float64(totalQty)
|
||||||
|
}
|
||||||
|
|
||||||
return &Summary{
|
return &Summary{
|
||||||
TotalQty: totalQty,
|
TotalQty: totalQty,
|
||||||
TotalWeightKg: totalWeightKg,
|
TotalWeightKg: totalWeightKg,
|
||||||
|
AverageWeightKg: avgWeightKg,
|
||||||
|
AverageSalesPrice: avgSalesPrice,
|
||||||
TotalSalesAmount: totalSalesAmount,
|
TotalSalesAmount: totalSalesAmount,
|
||||||
TotalHppAmount: totalHppAmount,
|
TotalHppAmount: totalHppAmount,
|
||||||
TotalHppPricePerKg: totalHppPricePerKg,
|
TotalHppPricePerKg: totalHppPricePerKg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingResponseDTO {
|
|
||||||
items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg, category)
|
|
||||||
total := ToSummary(mdps, hppPricePerKg, category)
|
|
||||||
|
|
||||||
return RepportMarketingResponseDTO{
|
|
||||||
Items: items,
|
|
||||||
Total: total,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newProductRelationDTOFixedPtr(original *productDTO.ProductRelationDTO) *ProductRelationDTOFixed {
|
func newProductRelationDTOFixedPtr(original *productDTO.ProductRelationDTO) *ProductRelationDTOFixed {
|
||||||
if original == nil {
|
if original == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ type PurchaseSupplierRowDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseSupplierSummaryDTO struct {
|
type PurchaseSupplierSummaryDTO struct {
|
||||||
TotalQty float64 `json:"total_qty"`
|
TotalQty float64 `json:"total_qty"`
|
||||||
TotalPurchaseValue float64 `json:"total_purchase_value"`
|
TotalPurchaseValue float64 `json:"total_purchase_value"`
|
||||||
TotalTransportValue float64 `json:"total_transport_value"`
|
TotalTransportValue float64 `json:"total_transport_value"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
TotalUnitPrice float64 `json:"total_unit_price"`
|
TotalUnitPrice float64 `json:"total_unit_price"`
|
||||||
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
|
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseSupplierDTO struct {
|
type PurchaseSupplierDTO struct {
|
||||||
@@ -122,11 +122,6 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
|
|||||||
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
|
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
|
||||||
summary := PurchaseSupplierSummaryDTO{}
|
summary := PurchaseSupplierSummaryDTO{}
|
||||||
|
|
||||||
var unitPriceSum float64
|
|
||||||
var unitPriceCount int
|
|
||||||
var transportUnitPriceSum float64
|
|
||||||
var transportUnitPriceCount int
|
|
||||||
|
|
||||||
for i := range items {
|
for i := range items {
|
||||||
row := ToPurchaseSupplierRowDTO(&items[i])
|
row := ToPurchaseSupplierRowDTO(&items[i])
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
@@ -136,19 +131,16 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
|
|||||||
summary.TotalTransportValue += row.TransportValue
|
summary.TotalTransportValue += row.TransportValue
|
||||||
summary.TotalAmount += row.TotalAmount
|
summary.TotalAmount += row.TotalAmount
|
||||||
|
|
||||||
unitPriceSum += row.UnitPrice
|
|
||||||
unitPriceCount++
|
|
||||||
|
|
||||||
transportUnitPriceSum += row.TransportUnitPrice
|
|
||||||
transportUnitPriceCount++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if unitPriceCount > 0 {
|
if summary.TotalQty > 0 {
|
||||||
summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount))
|
avg := summary.TotalPurchaseValue / summary.TotalQty
|
||||||
|
summary.TotalUnitPrice = math.Round(avg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if transportUnitPriceCount > 0 {
|
if summary.TotalQty > 0 {
|
||||||
summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount))
|
avg := summary.TotalTransportValue / summary.TotalQty
|
||||||
|
summary.TotalTransportUnitPrice = math.Round(avg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PurchaseSupplierDTO{
|
return PurchaseSupplierDTO{
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
chickinRepository := chickinRepo.NewChickinRepository(db)
|
chickinRepository := chickinRepo.NewChickinRepository(db)
|
||||||
recordingRepository := recordingRepo.NewRecordingRepository(db)
|
recordingRepository := recordingRepo.NewRecordingRepository(db)
|
||||||
approvalRepository := commonRepo.NewApprovalRepository(db)
|
approvalRepository := commonRepo.NewApprovalRepository(db)
|
||||||
|
hppCostRepository := commonRepo.NewHppCostRepository(db)
|
||||||
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
|
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
|
||||||
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
|
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
|
||||||
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
||||||
@@ -43,7 +44,8 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
userRepository := rUser.NewUserRepository(db)
|
userRepository := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
||||||
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
|
hppSvc := approvalService.NewHppService(hppCostRepository)
|
||||||
|
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, hppSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
|
||||||
userService := sUser.NewUserService(userRepository, validate)
|
userService := sUser.NewUserService(userRepository, validate)
|
||||||
|
|
||||||
RepportRoutes(router, userService, repportService)
|
RepportRoutes(router, userService, repportService)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
@@ -17,6 +18,8 @@ type DebtSupplierRepository interface {
|
|||||||
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
||||||
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
||||||
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
||||||
|
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
|
||||||
|
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
|
||||||
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||||
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||||
}
|
}
|
||||||
@@ -25,10 +28,30 @@ type debtSupplierRepositoryImpl struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaymentReferenceSummary struct {
|
||||||
|
Total float64
|
||||||
|
LatestPaymentDate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
||||||
return &debtSupplierRepositoryImpl{db: db}
|
return &debtSupplierRepositoryImpl{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *debtSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Table("approvals AS a").
|
||||||
|
Select("a.approvable_id, a.step_number, a.action").
|
||||||
|
Joins(`
|
||||||
|
JOIN (
|
||||||
|
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
||||||
|
FROM approvals
|
||||||
|
WHERE approvable_type = ?
|
||||||
|
GROUP BY approvable_id
|
||||||
|
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
|
||||||
|
string(utils.ApprovalWorkflowPurchase),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func resolveDebtSupplierDateColumn(filterBy string) string {
|
func resolveDebtSupplierDateColumn(filterBy string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||||
case "po_date":
|
case "po_date":
|
||||||
@@ -46,7 +69,11 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt
|
|||||||
db := r.db.WithContext(ctx).
|
db := r.db.WithContext(ctx).
|
||||||
Model(&entity.Supplier{}).
|
Model(&entity.Supplier{}).
|
||||||
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
|
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
|
||||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
|
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||||
|
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
|
||||||
|
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
||||||
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||||
|
Where("purchase_items.received_date IS NOT NULL")
|
||||||
|
|
||||||
if len(filters.SupplierIDs) > 0 {
|
if len(filters.SupplierIDs) > 0 {
|
||||||
db = db.Where("suppliers.id IN ?", filters.SupplierIDs)
|
db = db.Where("suppliers.id IN ?", filters.SupplierIDs)
|
||||||
@@ -167,7 +194,8 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context,
|
|||||||
Model(&entity.Payment{}).
|
Model(&entity.Payment{}).
|
||||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
Where("direction = ?", "OUT").
|
Where("direction = ?", "OUT").
|
||||||
Where("party_id IN ?", supplierIDs)
|
Where("party_id IN ?", supplierIDs).
|
||||||
|
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal))
|
||||||
|
|
||||||
if strings.TrimSpace(filters.StartDate) != "" {
|
if strings.TrimSpace(filters.StartDate) != "" {
|
||||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
@@ -198,7 +226,11 @@ func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplie
|
|||||||
Table("purchases").
|
Table("purchases").
|
||||||
Select("DISTINCT purchases.id").
|
Select("DISTINCT purchases.id").
|
||||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||||
Where("purchases.supplier_id IN ?", supplierIDs)
|
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
|
||||||
|
Where("purchases.supplier_id IN ?", supplierIDs).
|
||||||
|
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
||||||
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||||
|
Where("purchase_items.received_date IS NOT NULL")
|
||||||
|
|
||||||
if filters.StartDate != "" {
|
if filters.StartDate != "" {
|
||||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
@@ -238,6 +270,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
|||||||
Where("direction = ?", "OUT").
|
Where("direction = ?", "OUT").
|
||||||
Where("party_id IN ?", supplierIDs).
|
Where("party_id IN ?", supplierIDs).
|
||||||
Where("reference_number IN ?", references).
|
Where("reference_number IN ?", references).
|
||||||
|
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
||||||
Group("reference_number").
|
Group("reference_number").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -254,6 +287,75 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *debtSupplierRepositoryImpl) GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) {
|
||||||
|
if len(supplierIDs) == 0 || len(references) == 0 {
|
||||||
|
return map[string]PaymentReferenceSummary{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentRow struct {
|
||||||
|
ReferenceNumber *string `gorm:"column:reference_number"`
|
||||||
|
Total float64 `gorm:"column:total"`
|
||||||
|
LatestPaymentDate time.Time `gorm:"column:latest_payment_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]paymentRow, 0)
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.Payment{}).
|
||||||
|
Select("reference_number, SUM(nominal) AS total, MAX(payment_date) AS latest_payment_date").
|
||||||
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
|
Where("direction = ?", "OUT").
|
||||||
|
Where("party_id IN ?", supplierIDs).
|
||||||
|
Where("reference_number IN ?", references).
|
||||||
|
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
||||||
|
Group("reference_number").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]PaymentReferenceSummary, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[*row.ReferenceNumber] = PaymentReferenceSummary{
|
||||||
|
Total: row.Total,
|
||||||
|
LatestPaymentDate: row.LatestPaymentDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *debtSupplierRepositoryImpl) GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) {
|
||||||
|
if len(supplierIDs) == 0 {
|
||||||
|
return map[uint]float64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type balanceRow struct {
|
||||||
|
SupplierID uint `gorm:"column:supplier_id"`
|
||||||
|
Total float64 `gorm:"column:total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]balanceRow, 0)
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.Payment{}).
|
||||||
|
Select("party_id AS supplier_id, SUM(nominal) AS total").
|
||||||
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
|
Where("party_id IN ?", supplierIDs).
|
||||||
|
Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
|
||||||
|
Group("party_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint]float64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.SupplierID] = row.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||||
return map[uint]float64{}, nil
|
return map[uint]float64{}, nil
|
||||||
@@ -276,7 +378,11 @@ func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Con
|
|||||||
Table("purchases").
|
Table("purchases").
|
||||||
Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
|
Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
|
||||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||||
|
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
|
||||||
Where("purchases.supplier_id IN ?", supplierIDs).
|
Where("purchases.supplier_id IN ?", supplierIDs).
|
||||||
|
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
||||||
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||||
|
Where("purchase_items.received_date IS NOT NULL").
|
||||||
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
|
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
|
||||||
Group("purchases.supplier_id").
|
Group("purchases.supplier_id").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
@@ -313,6 +419,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
|
|||||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
Where("direction = ?", "OUT").
|
Where("direction = ?", "OUT").
|
||||||
Where("party_id IN ?", supplierIDs).
|
Where("party_id IN ?", supplierIDs).
|
||||||
|
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
||||||
Where("DATE(payment_date) < ?", dateFrom).
|
Where("DATE(payment_date) < ?", dateFrom).
|
||||||
Group("party_id").
|
Group("party_id").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
|
|||||||
@@ -6,48 +6,50 @@ import (
|
|||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HppPerKandangRow struct {
|
type HppPerKandangRow struct {
|
||||||
ProjectFlockKandangID uint
|
ProjectFlockKandangID uint
|
||||||
KandangID uint
|
ProjectFlockPeriod int
|
||||||
KandangName string
|
KandangID uint
|
||||||
KandangStatus string
|
KandangName string
|
||||||
LocationID uint
|
KandangStatus string
|
||||||
LocationName string
|
LocationID uint
|
||||||
PicID uint
|
LocationName string
|
||||||
PicName string
|
PicID uint
|
||||||
RecordingCount int64
|
PicName string
|
||||||
RemainingChickenBirds float64
|
RecordingCount int64
|
||||||
RemainingChickenWeight float64
|
// RemainingChickenBirds float64
|
||||||
EggProductionWeightKg float64
|
// RemainingChickenWeight float64
|
||||||
EggProductionPieces float64
|
EggProductionWeightKgRemaining float64
|
||||||
|
// EggProductionPiecesRemaining float64
|
||||||
|
// EggProductionTotalWeightKg float64
|
||||||
|
// EggProductionTotalPieces float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangCostRow struct {
|
type HppPerKandangCostRow struct {
|
||||||
KandangID uint
|
ProjectFlockKandangID uint
|
||||||
FeedCost float64
|
FeedCost float64
|
||||||
OvkCost float64
|
OvkCost float64
|
||||||
DocCost float64
|
DocCost float64
|
||||||
DocQty float64
|
DocQty float64
|
||||||
BudgetCost float64
|
BudgetCost float64
|
||||||
ExpenseCost float64
|
ExpenseCost float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangSupplierRow struct {
|
type HppPerKandangSupplierRow struct {
|
||||||
KandangID uint
|
ProjectFlockKandangID uint
|
||||||
SupplierID uint
|
SupplierID uint
|
||||||
SupplierName string
|
SupplierName string
|
||||||
SupplierAlias string
|
SupplierAlias string
|
||||||
Category string
|
Category string
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangRepository interface {
|
type HppPerKandangRepository interface {
|
||||||
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
|
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
|
||||||
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
|
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
|
||||||
GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
|
GetWeightRemainingByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type hppPerKandangRepository struct {
|
type hppPerKandangRepository struct {
|
||||||
@@ -86,6 +88,7 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
Table("project_flocks AS pf").
|
Table("project_flocks AS pf").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.id AS project_flock_kandang_id,
|
pfk.id AS project_flock_kandang_id,
|
||||||
|
pfk.period AS project_flock_period,
|
||||||
k.id AS kandang_id,
|
k.id AS kandang_id,
|
||||||
k.name AS kandang_name,
|
k.name AS kandang_name,
|
||||||
k.status AS kandang_status,
|
k.status AS kandang_status,
|
||||||
@@ -97,18 +100,27 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
|
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
|
||||||
0 AS remaining_chicken_weight,
|
0 AS remaining_chicken_weight,
|
||||||
0 AS egg_production_weight_kg,
|
0 AS egg_production_weight_kg,
|
||||||
0 AS egg_production_pieces`).
|
0 AS egg_production_pieces,
|
||||||
|
0 AS egg_production_total_weight_kg,
|
||||||
|
0 AS egg_production_total_pieces`).
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||||
|
Joins(`
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT project_flock_kandang_id, MIN(chick_in_date) AS chick_in_date
|
||||||
|
FROM project_chickins
|
||||||
|
GROUP BY project_flock_kandang_id
|
||||||
|
) AS pc ON pc.project_flock_kandang_id = pfk.id`).
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
||||||
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
|
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
|
||||||
Where("pfk.closed_at IS NULL")
|
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
|
||||||
|
Where("(pfk.closed_at IS NULL OR ? BETWEEN pc.chick_in_date AND pfk.closed_at)", start)
|
||||||
|
|
||||||
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
query = query.Group("pfk.id, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
query = query.Group("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
||||||
Order("k.id ASC")
|
Order("pfk.id ASC")
|
||||||
|
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
if err := query.Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -120,73 +132,19 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
|
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
|
||||||
var rows []HppPerKandangCostRow
|
var rows []HppPerKandangCostRow
|
||||||
|
|
||||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
|
||||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
|
||||||
|
|
||||||
latestApproval := r.db.WithContext(ctx).
|
|
||||||
Table("approvals AS a").
|
|
||||||
Select("a.approvable_id, a.action").
|
|
||||||
Joins(`
|
|
||||||
JOIN (
|
|
||||||
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
|
||||||
FROM approvals
|
|
||||||
WHERE approvable_type = ?
|
|
||||||
GROUP BY approvable_id
|
|
||||||
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
|
|
||||||
string(utils.ApprovalWorkflowRecording),
|
|
||||||
)
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
|
||||||
Table("recordings AS r").
|
|
||||||
Select(`
|
|
||||||
k.id AS kandang_id,
|
|
||||||
COALESCE(SUM(CASE
|
|
||||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
|
||||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
|
||||||
ELSE 0
|
|
||||||
END), 0) AS feed_cost,
|
|
||||||
COALESCE(SUM(CASE
|
|
||||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
|
||||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
|
||||||
ELSE 0
|
|
||||||
END), 0) AS ovk_cost`,
|
|
||||||
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
|
|
||||||
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
|
||||||
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
|
||||||
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
|
||||||
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
|
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
|
||||||
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
|
|
||||||
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
|
|
||||||
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
|
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
|
||||||
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
|
||||||
Where("r.deleted_at IS NULL").
|
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
|
|
||||||
|
|
||||||
query = query.Group("k.id").Order("k.id ASC")
|
|
||||||
|
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
docRows := make([]struct {
|
docRows := make([]struct {
|
||||||
KandangID uint
|
ProjectFlockKandangID uint
|
||||||
DocCost float64
|
DocCost float64
|
||||||
DocQty float64
|
DocQty float64
|
||||||
SupplierID *uint
|
SupplierID *uint
|
||||||
SupplierName *string
|
SupplierName *string
|
||||||
SupplierAlias *string
|
SupplierAlias *string
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
docQuery := r.db.WithContext(ctx).
|
docQuery := r.db.WithContext(ctx).
|
||||||
Table("project_chickins AS pc").
|
Table("project_chickins AS pc").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.kandang_id AS kandang_id,
|
pfk.id AS project_flock_kandang_id,
|
||||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
|
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
|
||||||
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
|
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
|
||||||
s.id AS supplier_id,
|
s.id AS supplier_id,
|
||||||
@@ -199,7 +157,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||||
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
Group("pfk.kandang_id, s.id, s.name, s.alias")
|
Group("pfk.id, s.id, s.name, s.alias")
|
||||||
|
|
||||||
if err := docQuery.Scan(&docRows).Error; err != nil {
|
if err := docQuery.Scan(&docRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -208,28 +166,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
|
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
row := rows[i]
|
row := rows[i]
|
||||||
costMap[row.KandangID] = &rows[i]
|
costMap[row.ProjectFlockKandangID] = &rows[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
docSuppliers := make([]HppPerKandangSupplierRow, 0)
|
docSuppliers := make([]HppPerKandangSupplierRow, 0)
|
||||||
docSeen := make(map[uint]map[uint]bool)
|
docSeen := make(map[uint]map[uint]bool)
|
||||||
for _, doc := range docRows {
|
for _, doc := range docRows {
|
||||||
entry, ok := costMap[doc.KandangID]
|
entry, ok := costMap[doc.ProjectFlockKandangID]
|
||||||
if !ok {
|
if !ok {
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
rows = append(rows, HppPerKandangCostRow{
|
||||||
KandangID: doc.KandangID,
|
ProjectFlockKandangID: doc.ProjectFlockKandangID,
|
||||||
})
|
})
|
||||||
entry = &rows[len(rows)-1]
|
entry = &rows[len(rows)-1]
|
||||||
costMap[doc.KandangID] = entry
|
costMap[doc.ProjectFlockKandangID] = entry
|
||||||
}
|
}
|
||||||
entry.DocCost += doc.DocCost
|
entry.DocCost += doc.DocCost
|
||||||
entry.DocQty += doc.DocQty
|
entry.DocQty += doc.DocQty
|
||||||
if doc.SupplierID != nil {
|
if doc.SupplierID != nil {
|
||||||
if docSeen[doc.KandangID] == nil {
|
if docSeen[doc.ProjectFlockKandangID] == nil {
|
||||||
docSeen[doc.KandangID] = make(map[uint]bool)
|
docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool)
|
||||||
}
|
}
|
||||||
if !docSeen[doc.KandangID][*doc.SupplierID] {
|
if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] {
|
||||||
docSeen[doc.KandangID][*doc.SupplierID] = true
|
docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true
|
||||||
supplierName := ""
|
supplierName := ""
|
||||||
if doc.SupplierName != nil {
|
if doc.SupplierName != nil {
|
||||||
supplierName = *doc.SupplierName
|
supplierName = *doc.SupplierName
|
||||||
@@ -239,140 +197,20 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
supplierAlias = *doc.SupplierAlias
|
supplierAlias = *doc.SupplierAlias
|
||||||
}
|
}
|
||||||
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
|
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
|
||||||
KandangID: doc.KandangID,
|
ProjectFlockKandangID: doc.ProjectFlockKandangID,
|
||||||
SupplierID: *doc.SupplierID,
|
SupplierID: *doc.SupplierID,
|
||||||
SupplierName: supplierName,
|
SupplierName: supplierName,
|
||||||
SupplierAlias: supplierAlias,
|
SupplierAlias: supplierAlias,
|
||||||
Category: "DOC",
|
Category: "DOC",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
budgetRows := make([]struct {
|
return rows, docSuppliers, nil
|
||||||
KandangID uint
|
|
||||||
BudgetCost float64
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
pfkUsageSub := r.db.
|
|
||||||
Table("project_chickins AS pc").
|
|
||||||
Select(`
|
|
||||||
pc.project_flock_kandang_id,
|
|
||||||
SUM(pc.usage_qty) AS kandang_usage_qty`).
|
|
||||||
Group("pc.project_flock_kandang_id")
|
|
||||||
|
|
||||||
projectUsageSub := r.db.
|
|
||||||
Table("project_chickins AS pc").
|
|
||||||
Select(`
|
|
||||||
pfk.project_flock_id,
|
|
||||||
SUM(pc.usage_qty) AS project_usage_qty`).
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
|
|
||||||
Group("pfk.project_flock_id")
|
|
||||||
|
|
||||||
budgetQuery := r.db.WithContext(ctx).
|
|
||||||
Table("project_flock_kandangs AS pfk").
|
|
||||||
Select(`
|
|
||||||
k.id AS kandang_id,
|
|
||||||
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
|
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
|
||||||
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id").
|
|
||||||
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
|
|
||||||
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
|
|
||||||
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
|
||||||
Group("k.id")
|
|
||||||
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
|
|
||||||
|
|
||||||
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, budget := range budgetRows {
|
|
||||||
entry, ok := costMap[budget.KandangID]
|
|
||||||
if !ok {
|
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
|
||||||
KandangID: budget.KandangID,
|
|
||||||
})
|
|
||||||
entry = &rows[len(rows)-1]
|
|
||||||
costMap[budget.KandangID] = entry
|
|
||||||
}
|
|
||||||
entry.BudgetCost += budget.BudgetCost
|
|
||||||
}
|
|
||||||
|
|
||||||
expenseRows := make([]struct {
|
|
||||||
KandangID uint
|
|
||||||
ExpenseCost float64
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
expenseQuery := r.db.WithContext(ctx).
|
|
||||||
Table("project_flock_kandangs AS pfk").
|
|
||||||
Select(`
|
|
||||||
k.id AS kandang_id,
|
|
||||||
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
|
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
|
||||||
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
|
|
||||||
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
|
||||||
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
|
||||||
Group("k.id")
|
|
||||||
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
|
|
||||||
|
|
||||||
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, exp := range expenseRows {
|
|
||||||
entry, ok := costMap[exp.KandangID]
|
|
||||||
if !ok {
|
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
|
||||||
KandangID: exp.KandangID,
|
|
||||||
})
|
|
||||||
entry = &rows[len(rows)-1]
|
|
||||||
costMap[exp.KandangID] = entry
|
|
||||||
}
|
|
||||||
entry.ExpenseCost += exp.ExpenseCost
|
|
||||||
}
|
|
||||||
|
|
||||||
feedSuppliers := make([]HppPerKandangSupplierRow, 0)
|
|
||||||
|
|
||||||
feedQuery := r.db.WithContext(ctx).
|
|
||||||
Table("recordings AS r").
|
|
||||||
Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
|
||||||
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
|
||||||
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
|
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
|
||||||
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
|
||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
|
|
||||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
|
||||||
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
|
||||||
Where("r.deleted_at IS NULL")
|
|
||||||
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
|
|
||||||
|
|
||||||
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range feedSuppliers {
|
|
||||||
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
|
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
|
||||||
KandangID: feedSuppliers[i].KandangID,
|
|
||||||
})
|
|
||||||
costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1]
|
|
||||||
}
|
|
||||||
feedSuppliers[i].Category = "FEED"
|
|
||||||
}
|
|
||||||
|
|
||||||
supplierRows := append(docSuppliers, feedSuppliers...)
|
|
||||||
|
|
||||||
return rows, supplierRows, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) {
|
func (r *hppPerKandangRepository) GetWeightRemainingByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) {
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
return map[uint]HppPerKandangRow{}, nil
|
return map[uint]HppPerKandangRow{}, nil
|
||||||
}
|
}
|
||||||
@@ -391,9 +229,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
|
|||||||
)
|
)
|
||||||
|
|
||||||
type eggRow struct {
|
type eggRow struct {
|
||||||
ProjectFlockKandangID uint
|
ProjectFlockKandangID uint
|
||||||
EggProductionWeightKg float64
|
EggProductionWeightKgRemaining float64
|
||||||
EggProductionPieces float64
|
// EggProductionPiecesRemaining float64
|
||||||
|
// EggProductionTotalWeightKg float64
|
||||||
|
// EggProductionTotalPieces float64
|
||||||
}
|
}
|
||||||
|
|
||||||
eggRows := make([]eggRow, 0)
|
eggRows := make([]eggRow, 0)
|
||||||
@@ -401,12 +241,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
|
|||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
Select(`
|
Select(`
|
||||||
r.project_flock_kandangs_id AS project_flock_kandang_id,
|
r.project_flock_kandangs_id AS project_flock_kandang_id,
|
||||||
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
|
COALESCE((SUM(re.weight) / NULLIF(SUM(re.total_qty), 0)) * SUM(re.total_qty - re.total_used), 0) AS egg_production_weight_kg_remaining`).
|
||||||
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
|
|
||||||
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
||||||
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
|
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
|
||||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||||
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
Where("r.record_datetime < ?", end).
|
||||||
Where("r.deleted_at IS NULL").
|
Where("r.deleted_at IS NULL").
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||||
Group("r.project_flock_kandangs_id")
|
Group("r.project_flock_kandangs_id")
|
||||||
@@ -418,9 +257,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
|
|||||||
result := make(map[uint]HppPerKandangRow, len(eggRows))
|
result := make(map[uint]HppPerKandangRow, len(eggRows))
|
||||||
for _, row := range eggRows {
|
for _, row := range eggRows {
|
||||||
result[row.ProjectFlockKandangID] = HppPerKandangRow{
|
result[row.ProjectFlockKandangID] = HppPerKandangRow{
|
||||||
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
||||||
EggProductionWeightKg: row.EggProductionWeightKg,
|
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
|
||||||
EggProductionPieces: row.EggProductionPieces,
|
// EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
|
||||||
|
// EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
|
||||||
|
// EggProductionTotalPieces: row.EggProductionTotalPieces,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +276,7 @@ func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int
|
|||||||
query = query.Where("k.location_id IN ?", locationIDs)
|
query = query.Where("k.location_id IN ?", locationIDs)
|
||||||
}
|
}
|
||||||
if len(kandangIDs) > 0 {
|
if len(kandangIDs) > 0 {
|
||||||
query = query.Where("k.id IN ?", kandangIDs)
|
query = query.Where("pfk.id IN ?", kandangIDs)
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,21 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
|
|||||||
return &purchaseSupplierRepositoryImpl{db: db}
|
return &purchaseSupplierRepositoryImpl{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *purchaseSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Table("approvals AS a").
|
||||||
|
Select("a.approvable_id, a.step_number, a.action").
|
||||||
|
Joins(`
|
||||||
|
JOIN (
|
||||||
|
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
||||||
|
FROM approvals
|
||||||
|
WHERE approvable_type = ?
|
||||||
|
GROUP BY approvable_id
|
||||||
|
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
|
||||||
|
string(utils.ApprovalWorkflowPurchase),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
|
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
|
||||||
dateColumn := "purchase_items.received_date"
|
dateColumn := "purchase_items.received_date"
|
||||||
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
|
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
|
||||||
@@ -34,10 +49,16 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
|
|||||||
dateColumn = "purchase_items.received_date"
|
dateColumn = "purchase_items.received_date"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
latestApproval := r.latestPurchaseApproval(ctx)
|
||||||
|
|
||||||
db := r.db.WithContext(ctx).
|
db := r.db.WithContext(ctx).
|
||||||
Model(&entity.Supplier{}).
|
Model(&entity.Supplier{}).
|
||||||
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
|
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
|
||||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
|
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||||
|
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", latestApproval).
|
||||||
|
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
||||||
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||||
|
Where("purchase_items.received_date IS NOT NULL")
|
||||||
|
|
||||||
if filters.SupplierId > 0 {
|
if filters.SupplierId > 0 {
|
||||||
db = db.Where("suppliers.id = ?", filters.SupplierId)
|
db = db.Where("suppliers.id = ?", filters.SupplierId)
|
||||||
@@ -152,7 +173,11 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
|
|||||||
Preload("ExpenseNonstock.Expense").
|
Preload("ExpenseNonstock.Expense").
|
||||||
Preload("ExpenseNonstock.Expense.Supplier").
|
Preload("ExpenseNonstock.Expense.Supplier").
|
||||||
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
|
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
|
||||||
Where("purchases.supplier_id IN ?", supplierIDs)
|
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
|
||||||
|
Where("purchases.supplier_id IN ?", supplierIDs).
|
||||||
|
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
||||||
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||||
|
Where("purchase_items.received_date IS NOT NULL")
|
||||||
|
|
||||||
if filters.ProductId > 0 {
|
if filters.ProductId > 0 {
|
||||||
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
|
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type repportService struct {
|
|||||||
ChickinRepo chickinRepo.ProjectChickinRepository
|
ChickinRepo chickinRepo.ProjectChickinRepository
|
||||||
RecordingRepo recordingRepo.RecordingRepository
|
RecordingRepo recordingRepo.RecordingRepository
|
||||||
ApprovalSvc approvalService.ApprovalService
|
ApprovalSvc approvalService.ApprovalService
|
||||||
|
HppSvc approvalService.HppService
|
||||||
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
||||||
DebtSupplierRepo repportRepo.DebtSupplierRepository
|
DebtSupplierRepo repportRepo.DebtSupplierRepository
|
||||||
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
||||||
@@ -85,6 +86,7 @@ func NewRepportService(
|
|||||||
chickinRepo chickinRepo.ProjectChickinRepository,
|
chickinRepo chickinRepo.ProjectChickinRepository,
|
||||||
recordingRepo recordingRepo.RecordingRepository,
|
recordingRepo recordingRepo.RecordingRepository,
|
||||||
approvalSvc approvalService.ApprovalService,
|
approvalSvc approvalService.ApprovalService,
|
||||||
|
hppSvc approvalService.HppService,
|
||||||
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
|
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
|
||||||
debtSupplierRepo repportRepo.DebtSupplierRepository,
|
debtSupplierRepo repportRepo.DebtSupplierRepository,
|
||||||
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
||||||
@@ -104,6 +106,7 @@ func NewRepportService(
|
|||||||
ChickinRepo: chickinRepo,
|
ChickinRepo: chickinRepo,
|
||||||
RecordingRepo: recordingRepo,
|
RecordingRepo: recordingRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
HppSvc: hppSvc,
|
||||||
PurchaseSupplierRepo: purchaseSupplierRepo,
|
PurchaseSupplierRepo: purchaseSupplierRepo,
|
||||||
DebtSupplierRepo: debtSupplierRepo,
|
DebtSupplierRepo: debtSupplierRepo,
|
||||||
HppPerKandangRepo: hppPerKandangRepo,
|
HppPerKandangRepo: hppPerKandangRepo,
|
||||||
@@ -165,6 +168,46 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customerGroups := make(map[uint][]entity.MarketingDeliveryProduct)
|
||||||
|
for _, dp := range deliveryProducts {
|
||||||
|
customerID := dp.MarketingProduct.Marketing.CustomerId
|
||||||
|
customerGroups[customerID] = append(customerGroups[customerID], dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
agingMap := make(map[int]int)
|
||||||
|
for customerID := range customerGroups {
|
||||||
|
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(c.Context(), &customerID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(c.Context(), customerID)
|
||||||
|
if err != nil {
|
||||||
|
initialBalance = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
runningBalance := initialBalance
|
||||||
|
for i, tx := range transactions {
|
||||||
|
if tx.TransactionType == "SALES" {
|
||||||
|
previousBalance := runningBalance
|
||||||
|
runningBalance -= tx.TotalPrice
|
||||||
|
currentBalance := runningBalance
|
||||||
|
|
||||||
|
_, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, currentBalance)
|
||||||
|
|
||||||
|
if paymentDate != nil {
|
||||||
|
agingDays := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
||||||
|
agingMap[int(tx.TransactionID)] = agingDays
|
||||||
|
} else {
|
||||||
|
agingDays := int(time.Since(tx.TransDate).Hours() / 24)
|
||||||
|
agingMap[int(tx.TransactionID)] = agingDays
|
||||||
|
}
|
||||||
|
} else if tx.TransactionType == "PAYMENT" {
|
||||||
|
runningBalance += tx.PaymentAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
projectFlockIDMap := make(map[uint]bool)
|
projectFlockIDMap := make(map[uint]bool)
|
||||||
hppMap := make(map[uint]float64)
|
hppMap := make(map[uint]float64)
|
||||||
|
|
||||||
@@ -181,7 +224,7 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap)
|
items := dto.ToMarketingReportItems(deliveryProducts, hppMap, agingMap)
|
||||||
return items, total, nil
|
return items, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,12 +465,10 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine customer IDs to process
|
|
||||||
var customerIDs []uint
|
var customerIDs []uint
|
||||||
var totalCustomers int64
|
var totalCustomers int64
|
||||||
|
|
||||||
if len(params.CustomerIDs) > 0 {
|
if len(params.CustomerIDs) > 0 {
|
||||||
// Specific customer IDs mode (no pagination)
|
|
||||||
customerIDs = params.CustomerIDs
|
customerIDs = params.CustomerIDs
|
||||||
totalCustomers = int64(len(customerIDs))
|
totalCustomers = int64(len(customerIDs))
|
||||||
|
|
||||||
@@ -435,7 +476,6 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
return []dto.CustomerPaymentReportItem{}, 0, nil
|
return []dto.CustomerPaymentReportItem{}, 0, nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple customers mode with pagination
|
|
||||||
page := params.Page
|
page := params.Page
|
||||||
limit := params.Limit
|
limit := params.Limit
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
@@ -464,9 +504,13 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
result = append(result, item)
|
|
||||||
|
if len(item.Rows) > 0 {
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalCustomers = int64(len(result))
|
||||||
return result, totalCustomers, nil
|
return result, totalCustomers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,14 +547,8 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
row.Status = status
|
row.Status = status
|
||||||
|
|
||||||
if status == "LUNAS" {
|
if status == "LUNAS" {
|
||||||
if previousBalance >= tx.TotalPrice {
|
if paymentDate != nil {
|
||||||
days := 0
|
|
||||||
row.AgingDay = &days
|
|
||||||
} else if paymentDate != nil {
|
|
||||||
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
||||||
if days < 0 {
|
|
||||||
days = 0
|
|
||||||
}
|
|
||||||
row.AgingDay = &days
|
row.AgingDay = &days
|
||||||
} else {
|
} else {
|
||||||
days := 0
|
days := 0
|
||||||
@@ -518,9 +556,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
days := int(time.Since(tx.TransDate).Hours() / 24)
|
days := int(time.Since(tx.TransDate).Hours() / 24)
|
||||||
if days < 0 {
|
|
||||||
days = 0
|
|
||||||
}
|
|
||||||
row.AgingDay = &days
|
row.AgingDay = &days
|
||||||
}
|
}
|
||||||
} else if tx.TransactionType == "PAYMENT" {
|
} else if tx.TransactionType == "PAYMENT" {
|
||||||
@@ -579,13 +614,60 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
||||||
currentSales := transactions[currentIndex]
|
currentSales := transactions[currentIndex]
|
||||||
|
|
||||||
// Status Logic:
|
|
||||||
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
|
|
||||||
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
|
|
||||||
// 3. DIBAYAR SEBAGIAN: has payment but not enough
|
|
||||||
// 4. BELUM LUNAS: no payment at all
|
|
||||||
|
|
||||||
if previousBalance >= currentSales.TotalPrice {
|
if previousBalance >= currentSales.TotalPrice {
|
||||||
|
type paymentAllocation struct {
|
||||||
|
date time.Time
|
||||||
|
amount float64
|
||||||
|
consumed float64
|
||||||
|
}
|
||||||
|
allocations := []paymentAllocation{}
|
||||||
|
runningBalance := 0.0
|
||||||
|
|
||||||
|
for i := 0; i < currentIndex; i++ {
|
||||||
|
if transactions[i].TransactionType == "PAYMENT" {
|
||||||
|
allocations = append(allocations, paymentAllocation{
|
||||||
|
date: transactions[i].TransDate,
|
||||||
|
amount: transactions[i].PaymentAmount,
|
||||||
|
consumed: 0,
|
||||||
|
})
|
||||||
|
runningBalance += transactions[i].PaymentAmount
|
||||||
|
} else if transactions[i].TransactionType == "SALES" {
|
||||||
|
salesAmount := transactions[i].TotalPrice
|
||||||
|
remainingToConsume := salesAmount
|
||||||
|
|
||||||
|
for j := range allocations {
|
||||||
|
if remainingToConsume <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
available := allocations[j].amount - allocations[j].consumed
|
||||||
|
if available > 0 {
|
||||||
|
consume := available
|
||||||
|
if consume > remainingToConsume {
|
||||||
|
consume = remainingToConsume
|
||||||
|
}
|
||||||
|
allocations[j].consumed += consume
|
||||||
|
remainingToConsume -= consume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runningBalance -= salesAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
amountNeeded := currentSales.TotalPrice
|
||||||
|
for _, alloc := range allocations {
|
||||||
|
available := alloc.amount - alloc.consumed
|
||||||
|
if available > 0 {
|
||||||
|
if amountNeeded <= available {
|
||||||
|
return "LUNAS", &alloc.date
|
||||||
|
} else {
|
||||||
|
amountNeeded -= available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allocations) > 0 {
|
||||||
|
return "LUNAS", &allocations[0].date
|
||||||
|
}
|
||||||
return "LUNAS", nil
|
return "LUNAS", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +716,6 @@ func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionRe
|
|||||||
if record.Day != nil {
|
if record.Day != nil {
|
||||||
result.Woa = float64(*record.Day)
|
result.Woa = float64(*record.Day)
|
||||||
}
|
}
|
||||||
// avgWeight := calculateAverageBodyWeight(record.BodyWeights)
|
|
||||||
avgWeight := 1.0
|
avgWeight := 1.0
|
||||||
if avgWeight > 0 {
|
if avgWeight > 0 {
|
||||||
result.Bw = avgWeight
|
result.Bw = avgWeight
|
||||||
@@ -782,7 +863,7 @@ func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKa
|
|||||||
var rows []entity.ProjectFlockKandangUniformity
|
var rows []entity.ProjectFlockKandangUniformity
|
||||||
if err := s.DB.WithContext(ctx).
|
if err := s.DB.WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockKandangUniformity{}).
|
Model(&entity.ProjectFlockKandangUniformity{}).
|
||||||
Select("week, uniformity, uniform_date, id").
|
Select("week, uniformity, uniform_date, id, chart_data").
|
||||||
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
Where("week IN ?", weeks).
|
Where("week IN ?", weeks).
|
||||||
Order("uniform_date DESC").
|
Order("uniform_date DESC").
|
||||||
@@ -1073,6 +1154,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||||
@@ -1087,6 +1173,16 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
DeltaBalance float64
|
DeltaBalance float64
|
||||||
CountTotals bool
|
CountTotals bool
|
||||||
}
|
}
|
||||||
|
type debtSupplierAllocation struct {
|
||||||
|
RowIndex int
|
||||||
|
SortTime time.Time
|
||||||
|
Amount float64
|
||||||
|
Purchase entity.Purchase
|
||||||
|
}
|
||||||
|
type paymentAllocation struct {
|
||||||
|
Date time.Time
|
||||||
|
Amount float64
|
||||||
|
}
|
||||||
|
|
||||||
for _, supplierID := range supplierIDs {
|
for _, supplierID := range supplierIDs {
|
||||||
supplier, exists := supplierMap[supplierID]
|
supplier, exists := supplierMap[supplierID]
|
||||||
@@ -1094,15 +1190,17 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
|
||||||
items := purchasesBySupplier[supplierID]
|
items := purchasesBySupplier[supplierID]
|
||||||
paymentItems := paymentsBySupplier[supplierID]
|
paymentItems := paymentsBySupplier[supplierID]
|
||||||
total := dto.DebtSupplierTotalDTO{}
|
total := dto.DebtSupplierTotalDTO{}
|
||||||
|
|
||||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||||
|
purchaseAllocations := make([]debtSupplierAllocation, 0, len(items))
|
||||||
for _, purchase := range items {
|
for _, purchase := range items {
|
||||||
row := buildDebtSupplierRow(purchase, now, location)
|
row := buildDebtSupplierRow(purchase, now, location)
|
||||||
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||||
|
rowIndex := len(combinedRows)
|
||||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||||
Row: row,
|
Row: row,
|
||||||
SortTime: sortTime,
|
SortTime: sortTime,
|
||||||
@@ -1110,6 +1208,24 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
DeltaBalance: -row.TotalPrice,
|
DeltaBalance: -row.TotalPrice,
|
||||||
CountTotals: true,
|
CountTotals: true,
|
||||||
})
|
})
|
||||||
|
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
||||||
|
RowIndex: rowIndex,
|
||||||
|
SortTime: sortTime,
|
||||||
|
Amount: row.TotalPrice,
|
||||||
|
Purchase: purchase,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentAllocations := make([]paymentAllocation, 0, len(paymentItems)+1)
|
||||||
|
initialAllocation := initialBalanceTotals[supplierID] + initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||||
|
paymentCarry := 0.0
|
||||||
|
if initialAllocation > 0 && len(purchaseAllocations) > 0 {
|
||||||
|
paymentAllocations = append(paymentAllocations, paymentAllocation{
|
||||||
|
Date: purchaseAllocations[0].SortTime,
|
||||||
|
Amount: initialAllocation,
|
||||||
|
})
|
||||||
|
} else if initialAllocation < 0 {
|
||||||
|
paymentCarry = -initialAllocation
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, payment := range paymentItems {
|
for _, payment := range paymentItems {
|
||||||
@@ -1122,6 +1238,53 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
DeltaBalance: payment.Nominal,
|
DeltaBalance: payment.Nominal,
|
||||||
CountTotals: false,
|
CountTotals: false,
|
||||||
})
|
})
|
||||||
|
paymentAllocations = append(paymentAllocations, paymentAllocation{
|
||||||
|
Date: sortTime,
|
||||||
|
Amount: payment.Nominal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(purchaseAllocations) > 0 && len(paymentAllocations) > 0 {
|
||||||
|
sort.SliceStable(purchaseAllocations, func(i, j int) bool {
|
||||||
|
return purchaseAllocations[i].SortTime.Before(purchaseAllocations[j].SortTime)
|
||||||
|
})
|
||||||
|
sort.SliceStable(paymentAllocations, func(i, j int) bool {
|
||||||
|
return paymentAllocations[i].Date.Before(paymentAllocations[j].Date)
|
||||||
|
})
|
||||||
|
remaining := make([]float64, len(purchaseAllocations))
|
||||||
|
for i := range purchaseAllocations {
|
||||||
|
remaining[i] = purchaseAllocations[i].Amount
|
||||||
|
}
|
||||||
|
purchaseIndex := 0
|
||||||
|
for _, pay := range paymentAllocations {
|
||||||
|
amount := pay.Amount
|
||||||
|
if amount <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if paymentCarry > 0 {
|
||||||
|
used := math.Min(amount, paymentCarry)
|
||||||
|
paymentCarry -= used
|
||||||
|
amount -= used
|
||||||
|
}
|
||||||
|
for amount > 0 && purchaseIndex < len(remaining) {
|
||||||
|
if remaining[purchaseIndex] <= 0 {
|
||||||
|
purchaseIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
used := math.Min(amount, remaining[purchaseIndex])
|
||||||
|
remaining[purchaseIndex] -= used
|
||||||
|
amount -= used
|
||||||
|
if remaining[purchaseIndex] <= 0.000001 {
|
||||||
|
allocation := purchaseAllocations[purchaseIndex]
|
||||||
|
combinedRows[allocation.RowIndex].Row.Status = "Lunas"
|
||||||
|
combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location)
|
||||||
|
purchaseIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if purchaseIndex >= len(remaining) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.SliceStable(combinedRows, func(i, j int) bool {
|
sort.SliceStable(combinedRows, func(i, j int) bool {
|
||||||
@@ -1318,6 +1481,55 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc
|
|||||||
return purchase.CreatedAt.In(loc)
|
return purchase.CreatedAt.In(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectDebtSupplierReferences(purchases []entity.Purchase) []string {
|
||||||
|
if len(purchases) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(purchases))
|
||||||
|
result := make([]string, 0, len(purchases))
|
||||||
|
for _, purchase := range purchases {
|
||||||
|
ref := resolveDebtSupplierReference(purchase)
|
||||||
|
if ref == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[ref]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[ref] = struct{}{}
|
||||||
|
result = append(result, ref)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDebtSupplierReference(purchase entity.Purchase) string {
|
||||||
|
if purchase.PoNumber != nil {
|
||||||
|
if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref := strings.TrimSpace(purchase.PrNumber); ref != "" {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
|
||||||
|
if totalPrice <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return paymentTotal >= totalPrice-0.000001
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
|
||||||
|
prDate := purchase.CreatedAt.In(loc)
|
||||||
|
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
|
||||||
|
stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
|
||||||
|
if stopDate.Before(startDate) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(stopDate.Sub(startDate).Hours() / 24)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1364,27 +1576,29 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
eggMap, err := s.HppPerKandangRepo.GetEggProductionByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
|
eggMap, err := s.HppPerKandangRepo.GetWeightRemainingByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for pfkID, egg := range eggMap {
|
for pfkID, egg := range eggMap {
|
||||||
if rowIdx, ok := pfkIndex[pfkID]; ok {
|
if rowIdx, ok := pfkIndex[pfkID]; ok {
|
||||||
repoRows[rowIdx].EggProductionWeightKg = egg.EggProductionWeightKg
|
repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
|
||||||
repoRows[rowIdx].EggProductionPieces = egg.EggProductionPieces
|
// repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
|
||||||
|
// repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
|
||||||
|
// repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
||||||
for _, row := range costRows {
|
for _, row := range costRows {
|
||||||
costMap[row.KandangID] = HppCostAggregate{
|
costMap[row.ProjectFlockKandangID] = HppCostAggregate{
|
||||||
FeedCost: row.FeedCost,
|
// FeedCost: row.FeedCost,
|
||||||
OvkCost: row.OvkCost,
|
// OvkCost: row.OvkCost,
|
||||||
DocCost: row.DocCost,
|
DocCost: row.DocCost,
|
||||||
DocQty: row.DocQty,
|
DocQty: row.DocQty,
|
||||||
BudgetCost: row.BudgetCost,
|
// BudgetCost: row.BudgetCost,
|
||||||
ExpenseCost: row.ExpenseCost,
|
// ExpenseCost: row.ExpenseCost,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1407,15 +1621,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
category = "DOC"
|
category = "DOC"
|
||||||
}
|
}
|
||||||
|
|
||||||
if seen[sup.KandangID] == nil {
|
if seen[sup.ProjectFlockKandangID] == nil {
|
||||||
seen[sup.KandangID] = make(map[uint]bool)
|
seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
|
||||||
}
|
}
|
||||||
if seen[sup.KandangID][sup.SupplierID] {
|
if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[sup.KandangID][sup.SupplierID] = true
|
seen[sup.ProjectFlockKandangID][sup.SupplierID] = true
|
||||||
|
|
||||||
targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{
|
targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{
|
||||||
ID: int64(sup.SupplierID),
|
ID: int64(sup.SupplierID),
|
||||||
Name: sup.SupplierName,
|
Name: sup.SupplierName,
|
||||||
Alias: sup.SupplierAlias,
|
Alias: sup.SupplierAlias,
|
||||||
@@ -1442,12 +1656,9 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
||||||
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
||||||
var totalBirds int64
|
var totalBirds int64
|
||||||
var totalWeight float64
|
|
||||||
var totalEggPieces int64
|
var totalEggPieces int64
|
||||||
var totalEggKg float64
|
var totalEggKg float64
|
||||||
var totalRemainingValueRp int64
|
|
||||||
var totalEggValueRp int64
|
var totalEggValueRp int64
|
||||||
var totalHppSum float64
|
|
||||||
var totalHppCount int
|
var totalHppCount int
|
||||||
var totalDocPriceSum float64
|
var totalDocPriceSum float64
|
||||||
var totalDocPriceCount int
|
var totalDocPriceCount int
|
||||||
@@ -1461,26 +1672,40 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
birdsFloat := row.RemainingChickenBirds
|
var eggPiecesFloatRemaining float64
|
||||||
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
|
||||||
birdsFloat = 0
|
var eggTotalPiecesFloat float64
|
||||||
|
var eggWeightFloat float64
|
||||||
|
eggHpp := 0.0
|
||||||
|
if s.HppSvc != nil {
|
||||||
|
hppCost, err := s.HppSvc.CalculateHppCost(row.ProjectFlockKandangID, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if hppCost != nil {
|
||||||
|
// eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg
|
||||||
|
eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir
|
||||||
|
eggHpp = hppCost.Estimation.HargaKg
|
||||||
|
eggTotalPiecesFloat = hppCost.Estimation.Butir
|
||||||
|
eggWeightFloat = hppCost.Estimation.Kg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
weightFloat := row.RemainingChickenWeight
|
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
||||||
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
eggPiecesFloatRemaining = 0
|
||||||
weightFloat = 0
|
|
||||||
}
|
}
|
||||||
eggPiecesFloat := row.EggProductionPieces
|
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
|
||||||
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
|
eggTotalPiecesFloat = 0
|
||||||
eggPiecesFloat = 0
|
}
|
||||||
|
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
|
||||||
|
eggRemainingWeightFloatRemaining = 0
|
||||||
}
|
}
|
||||||
eggWeightFloat := row.EggProductionWeightKg
|
|
||||||
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
|
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
|
||||||
eggWeightFloat = 0
|
eggWeightFloat = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
avgWeight := 0.0
|
avgWeight := 0.0
|
||||||
if eggPiecesFloat > 0 {
|
if eggTotalPiecesFloat > 0 {
|
||||||
avgWeight = eggWeightFloat / eggPiecesFloat
|
avgWeight = eggWeightFloat / eggTotalPiecesFloat
|
||||||
}
|
}
|
||||||
if params.WeightMin != nil && avgWeight < *params.WeightMin {
|
if params.WeightMin != nil && avgWeight < *params.WeightMin {
|
||||||
continue
|
continue
|
||||||
@@ -1496,28 +1721,19 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
weightMax := weightMin + 0.09
|
weightMax := weightMin + 0.09
|
||||||
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
||||||
|
|
||||||
rowBirds := int64(math.Round(birdsFloat))
|
costEntry := costMap[row.ProjectFlockKandangID]
|
||||||
costEntry := costMap[row.KandangID]
|
|
||||||
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
|
||||||
hppRp := 0.0
|
|
||||||
if weightFloat > 0 {
|
|
||||||
hppRp = totalCost / weightFloat
|
|
||||||
}
|
|
||||||
eggHpp := 0.0
|
|
||||||
if eggWeightFloat > 0 {
|
|
||||||
eggHpp = totalCost / eggWeightFloat
|
|
||||||
}
|
|
||||||
|
|
||||||
rowEggPieces := int64(math.Round(eggPiecesFloat))
|
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
||||||
rowEggValue := int64(eggHpp * eggWeightFloat)
|
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
|
||||||
rowRemainingValue := int64(hppRp * weightFloat)
|
|
||||||
avgDocPrice := int64(0)
|
avgDocPrice := int64(0)
|
||||||
if costEntry.DocQty > 0 {
|
if costEntry.DocQty > 0 {
|
||||||
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
|
||||||
|
|
||||||
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
|
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
|
||||||
ID: int(row.KandangID),
|
ID: int(row.ProjectFlockKandangID),
|
||||||
Kandang: dto.HppPerKandangRowKandangDTO{
|
Kandang: dto.HppPerKandangRowKandangDTO{
|
||||||
ID: int64(row.KandangID),
|
ID: int64(row.KandangID),
|
||||||
Name: row.KandangName,
|
Name: row.KandangName,
|
||||||
@@ -1535,32 +1751,22 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
WeightMin: weightMin,
|
WeightMin: weightMin,
|
||||||
WeightMax: weightMax,
|
WeightMax: weightMax,
|
||||||
},
|
},
|
||||||
AvgWeightKg: avgWeight,
|
AvgWeightKg: avgWeight,
|
||||||
// FeedCostRp: costEntry.FeedCost,
|
NameWithPeriode: nameWithPeriod,
|
||||||
// OvkCostRp: costEntry.OvkCost,
|
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
||||||
DocSuppliers: docSupplierMap[row.KandangID],
|
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
||||||
FeedSuppliers: feedSupplierMap[row.KandangID],
|
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
||||||
EggProductionPieces: rowEggPieces,
|
EggProductionKg: eggRemainingWeightFloatRemaining,
|
||||||
EggProductionKg: eggWeightFloat,
|
|
||||||
AverageDocPriceRp: avgDocPrice,
|
AverageDocPriceRp: avgDocPrice,
|
||||||
// HppRp: hppRp,
|
EggHppRpPerKg: eggHpp,
|
||||||
EggHppRpPerKg: eggHpp,
|
EggValueRp: rowEggValue,
|
||||||
RemainingValueRp: rowRemainingValue,
|
|
||||||
EggValueRp: rowEggValue,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
totalBirds += rowBirds
|
|
||||||
totalWeight += weightFloat
|
|
||||||
totalEggPieces += rowEggPieces
|
totalEggPieces += rowEggPieces
|
||||||
totalEggKg += eggWeightFloat
|
totalEggKg += eggRemainingWeightFloatRemaining
|
||||||
totalRemainingValueRp += rowRemainingValue
|
|
||||||
totalEggValueRp += rowEggValue
|
totalEggValueRp += rowEggValue
|
||||||
totalAvgWeightSum += avgWeight
|
totalAvgWeightSum += avgWeight
|
||||||
totalAvgWeightCount++
|
totalAvgWeightCount++
|
||||||
if weightFloat > 0 {
|
|
||||||
totalHppSum += hppRp
|
|
||||||
totalHppCount++
|
|
||||||
}
|
|
||||||
if avgDocPrice > 0 {
|
if avgDocPrice > 0 {
|
||||||
totalDocPriceSum += float64(avgDocPrice)
|
totalDocPriceSum += float64(avgDocPrice)
|
||||||
totalDocPriceCount++
|
totalDocPriceCount++
|
||||||
@@ -1587,23 +1793,20 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
rangeSummary := rangeAgg.Summary
|
rangeSummary := rangeAgg.Summary
|
||||||
rangeAgg.RemainingBirds += rowBirds
|
|
||||||
rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
|
|
||||||
rangeAgg.AvgWeightSum += avgWeight
|
rangeAgg.AvgWeightSum += avgWeight
|
||||||
rangeAgg.AvgWeightCount++
|
rangeAgg.AvgWeightCount++
|
||||||
for _, supplier := range feedSupplierMap[row.KandangID] {
|
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
|
||||||
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
|
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
|
||||||
rangeAgg.FeedSuppliers[supplier.ID] = supplier
|
rangeAgg.FeedSuppliers[supplier.ID] = supplier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, supplier := range docSupplierMap[row.KandangID] {
|
for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
|
||||||
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
|
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
|
||||||
rangeAgg.DocSuppliers[supplier.ID] = supplier
|
rangeAgg.DocSuppliers[supplier.ID] = supplier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rangeSummary.EggProductionPieces += rowEggPieces
|
rangeSummary.EggProductionPieces += rowEggPieces
|
||||||
rangeSummary.EggProductionKg += eggWeightFloat
|
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
||||||
rangeSummary.RemainingValueRp += rowRemainingValue
|
|
||||||
rangeSummary.EggValueRp += rowEggValue
|
rangeSummary.EggValueRp += rowEggValue
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
rangeAgg.EggHppSum += eggHpp
|
rangeAgg.EggHppSum += eggHpp
|
||||||
@@ -1750,6 +1953,9 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
|
||||||
|
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
|
||||||
|
}
|
||||||
|
|
||||||
params := &validation.HppPerKandangQuery{
|
params := &validation.HppPerKandangQuery{
|
||||||
Page: page,
|
Page: page,
|
||||||
|
|||||||
@@ -17,14 +17,16 @@ type ExpenseQuery struct {
|
|||||||
|
|
||||||
type MarketingQuery struct {
|
type MarketingQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=100"`
|
Search string `query:"search" validate:"omitempty,max=100"`
|
||||||
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
||||||
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"`
|
||||||
|
AreaId int64 `query:"area_id" validate:"omitempty"`
|
||||||
|
LocationId int64 `query:"location_id" validate:"omitempty"`
|
||||||
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
|
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"`
|
||||||
SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"`
|
SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"`
|
||||||
@@ -56,7 +58,7 @@ type DebtSupplierQuery struct {
|
|||||||
|
|
||||||
type HppPerKandangQuery struct {
|
type HppPerKandangQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
||||||
Period string `query:"period" validate:"required"`
|
Period string `query:"period" validate:"required"`
|
||||||
ShowUnrecorded bool `query:"show_unrecorded"`
|
ShowUnrecorded bool `query:"show_unrecorded"`
|
||||||
AreaIDs []int64 `query:"-"`
|
AreaIDs []int64 `query:"-"`
|
||||||
@@ -68,7 +70,7 @@ type HppPerKandangQuery struct {
|
|||||||
|
|
||||||
type ProductionResultQuery struct {
|
type ProductionResultQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
|
||||||
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
|
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ const (
|
|||||||
StockLogTypeTransfer StockLogType = "TRANSFER"
|
StockLogTypeTransfer StockLogType = "TRANSFER"
|
||||||
StockLogTypeMarketing StockLogType = "MARKETING"
|
StockLogTypeMarketing StockLogType = "MARKETING"
|
||||||
StockLogTypeChikin StockLogType = "CHICKIN"
|
StockLogTypeChikin StockLogType = "CHICKIN"
|
||||||
|
StockLogTypePurchase StockLogType = "PURCHASE"
|
||||||
|
StockLogTypeRecording StockLogType = "RECORDING"
|
||||||
)
|
)
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ package fifo
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Usable Keys
|
// Usable Keys
|
||||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION"
|
||||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||||
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
|
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||||
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
|
||||||
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
||||||
|
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
||||||
|
|
||||||
// Stockable Keys
|
// Stockable Keys
|
||||||
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
|
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
|
||||||
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
||||||
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
||||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||||
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
||||||
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
usagePtr := new(float64)
|
usagePtr := new(float64)
|
||||||
*usagePtr = item.Qty
|
*usagePtr = item.Qty
|
||||||
pending := item.PendingQty
|
|
||||||
if pending == nil {
|
|
||||||
pending = new(float64)
|
|
||||||
}
|
|
||||||
result = append(result, entity.RecordingStock{
|
result = append(result, entity.RecordingStock{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
ProductWarehouseId: item.ProductWarehouseId,
|
ProductWarehouseId: item.ProductWarehouseId,
|
||||||
UsageQty: usagePtr,
|
UsageQty: usagePtr,
|
||||||
PendingQty: pending,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user