mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' into fix/hpp-calculate
This commit is contained in:
+17
-87
@@ -1,90 +1,20 @@
|
|||||||
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_COMMIT_BRANCH == "development"'
|
||||||
DEPLOY_APP: "LTI-MBUGROUP"
|
|
||||||
# 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,133 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -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()) == "" {
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,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 +190,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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,23 +12,31 @@ 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"`
|
DoNumber string `json:"do_number"`
|
||||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
Weight float64 `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
AvgWeight float64 `json:"avg_weight"`
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
Price float64 `json:"price"`
|
SalesPrice float64 `json:"sales_price"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
TotalSalesPrice float64 `json:"total_sales_price"`
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
ActualPrice float64 `json:"actual_price"`
|
||||||
PaymentStatus string `json:"payment_status"`
|
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 ===
|
||||||
@@ -63,19 +71,41 @@ 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: age,
|
||||||
DoNumber: doNumber,
|
DoNumber: doNumber,
|
||||||
Product: product,
|
Product: product,
|
||||||
Customer: customer,
|
Customer: customer,
|
||||||
Qty: e.UsageQty,
|
Qty: e.UsageQty,
|
||||||
Weight: e.TotalWeight,
|
Weight: e.TotalWeight,
|
||||||
AvgWeight: e.AvgWeight,
|
AvgWeight: e.AvgWeight,
|
||||||
Price: e.UnitPrice,
|
SalesPrice: e.MarketingProduct.UnitPrice,
|
||||||
TotalPrice: e.TotalPrice,
|
TotalSalesPrice: e.MarketingProduct.TotalPrice,
|
||||||
Kandang: kandang,
|
ActualPrice: e.UnitPrice,
|
||||||
PaymentStatus: "Paid",
|
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,25 +117,13 @@ 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 {
|
|
||||||
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 {
|
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
|
||||||
|
|||||||
@@ -930,19 +930,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
|
||||||
|
|||||||
@@ -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").
|
||||||
|
|||||||
@@ -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,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").
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -225,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)
|
||||||
@@ -238,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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -171,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
|
||||||
@@ -399,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 {
|
||||||
@@ -485,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).
|
||||||
|
|||||||
@@ -1157,7 +1157,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,7 +1169,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,7 +1328,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
|
eggMass = (totalEggWeightGrams / remainingChick) / 1000
|
||||||
updates["egg_mass"] = eggMass
|
updates["egg_mass"] = eggMass
|
||||||
recording.EggMass = &eggMass
|
recording.EggMass = &eggMass
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -70,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -156,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())
|
||||||
@@ -174,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,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) {
|
||||||
@@ -582,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 {
|
||||||
@@ -773,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -844,6 +844,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
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))
|
||||||
|
|
||||||
for _, prep := range prepared {
|
for _, prep := range prepared {
|
||||||
item := prep.item
|
item := prep.item
|
||||||
@@ -877,9 +882,18 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
totalQtyDeltas[item.Id] += 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 {
|
||||||
totalQtyDeltas[item.Id] += deltaQty
|
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
|
||||||
@@ -919,10 +933,6 @@ 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
|
||||||
@@ -967,6 +977,26 @@ 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(affected) > 0 {
|
||||||
|
if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -165,6 +165,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 +221,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 +462,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 +473,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 {
|
||||||
@@ -574,15 +611,7 @@ 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 {
|
||||||
// Cari payment yang digunakan untuk melunasi sales ini dengan FIFO
|
|
||||||
// Track payment allocations that are consumed by previous sales
|
|
||||||
type paymentAllocation struct {
|
type paymentAllocation struct {
|
||||||
date time.Time
|
date time.Time
|
||||||
amount float64
|
amount float64
|
||||||
@@ -591,7 +620,6 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
|||||||
allocations := []paymentAllocation{}
|
allocations := []paymentAllocation{}
|
||||||
runningBalance := 0.0
|
runningBalance := 0.0
|
||||||
|
|
||||||
// Process all transactions before current sales to build allocation map
|
|
||||||
for i := 0; i < currentIndex; i++ {
|
for i := 0; i < currentIndex; i++ {
|
||||||
if transactions[i].TransactionType == "PAYMENT" {
|
if transactions[i].TransactionType == "PAYMENT" {
|
||||||
allocations = append(allocations, paymentAllocation{
|
allocations = append(allocations, paymentAllocation{
|
||||||
@@ -604,7 +632,6 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
|||||||
salesAmount := transactions[i].TotalPrice
|
salesAmount := transactions[i].TotalPrice
|
||||||
remainingToConsume := salesAmount
|
remainingToConsume := salesAmount
|
||||||
|
|
||||||
// Consume from oldest allocations first (FIFO)
|
|
||||||
for j := range allocations {
|
for j := range allocations {
|
||||||
if remainingToConsume <= 0 {
|
if remainingToConsume <= 0 {
|
||||||
break
|
break
|
||||||
@@ -623,22 +650,18 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now find which allocation covers the current sales
|
|
||||||
amountNeeded := currentSales.TotalPrice
|
amountNeeded := currentSales.TotalPrice
|
||||||
for _, alloc := range allocations {
|
for _, alloc := range allocations {
|
||||||
available := alloc.amount - alloc.consumed
|
available := alloc.amount - alloc.consumed
|
||||||
if available > 0 {
|
if available > 0 {
|
||||||
if amountNeeded <= available {
|
if amountNeeded <= available {
|
||||||
// This allocation fully covers the sales
|
|
||||||
return "LUNAS", &alloc.date
|
return "LUNAS", &alloc.date
|
||||||
} else {
|
} else {
|
||||||
// This allocation partially covers, continue to next
|
|
||||||
amountNeeded -= available
|
amountNeeded -= available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, use the oldest allocation
|
|
||||||
if len(allocations) > 0 {
|
if len(allocations) > 0 {
|
||||||
return "LUNAS", &allocations[0].date
|
return "LUNAS", &allocations[0].date
|
||||||
}
|
}
|
||||||
@@ -690,7 +713,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
|
||||||
@@ -1570,12 +1592,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
|
||||||
@@ -1589,14 +1608,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// birdsFloat := row.RemainingChickenBirds
|
|
||||||
// if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
|
||||||
// birdsFloat = 0
|
|
||||||
// }
|
|
||||||
// weightFloat := row.RemainingChickenWeight
|
|
||||||
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
|
||||||
// weightFloat = 0
|
|
||||||
// }
|
|
||||||
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
|
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
|
||||||
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
||||||
eggPiecesFloatRemaining = 0
|
eggPiecesFloatRemaining = 0
|
||||||
@@ -1632,13 +1643,8 @@ 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.ProjectFlockKandangID]
|
||||||
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
||||||
// hppRp := 0.0
|
|
||||||
// if weightFloat > 0 {
|
|
||||||
// hppRp = totalCost / weightFloat
|
|
||||||
// }
|
|
||||||
eggHpp := 0.0
|
eggHpp := 0.0
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
eggHpp = (totalCost / eggWeightFloat) / 1000
|
eggHpp = (totalCost / eggWeightFloat) / 1000
|
||||||
@@ -1646,7 +1652,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
|
|
||||||
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
||||||
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
|
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))
|
||||||
@@ -1673,35 +1678,22 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
WeightMin: weightMin,
|
WeightMin: weightMin,
|
||||||
WeightMax: weightMax,
|
WeightMax: weightMax,
|
||||||
},
|
},
|
||||||
AvgWeightKg: avgWeight,
|
AvgWeightKg: avgWeight,
|
||||||
NameWithPeriode: nameWithPeriod,
|
NameWithPeriode: nameWithPeriod,
|
||||||
// FeedCostRp: costEntry.FeedCost,
|
|
||||||
// OvkCostRp: costEntry.OvkCost,
|
|
||||||
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
||||||
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
||||||
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
||||||
EggProductionKg: eggRemainingWeightFloatRemaining,
|
EggProductionKg: eggRemainingWeightFloatRemaining,
|
||||||
// EggProductionTotalWeightKg: eggWeightFloat,
|
AverageDocPriceRp: avgDocPrice,
|
||||||
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
|
EggHppRpPerKg: eggHpp,
|
||||||
AverageDocPriceRp: avgDocPrice,
|
EggValueRp: rowEggValue,
|
||||||
// HppRp: hppRp,
|
|
||||||
EggHppRpPerKg: eggHpp,
|
|
||||||
// RemainingValueRp: rowRemainingValue,
|
|
||||||
EggValueRp: rowEggValue,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// totalBirds += rowBirds
|
|
||||||
// totalWeight += weightFloat
|
|
||||||
totalEggPieces += rowEggPieces
|
totalEggPieces += rowEggPieces
|
||||||
totalEggKg += eggRemainingWeightFloatRemaining
|
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++
|
||||||
@@ -1728,8 +1720,6 @@ 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.ProjectFlockKandangID] {
|
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
|
||||||
@@ -1744,7 +1734,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
}
|
}
|
||||||
rangeSummary.EggProductionPieces += rowEggPieces
|
rangeSummary.EggProductionPieces += rowEggPieces
|
||||||
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
||||||
// rangeSummary.RemainingValueRp += rowRemainingValue
|
|
||||||
rangeSummary.EggValueRp += rowEggValue
|
rangeSummary.EggValueRp += rowEggValue
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
rangeAgg.EggHppSum += eggHpp
|
rangeAgg.EggHppSum += eggHpp
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type MarketingQuery struct {
|
|||||||
AreaId int64 `query:"area_id" validate:"omitempty"`
|
AreaId int64 `query:"area_id" validate:"omitempty"`
|
||||||
LocationId int64 `query:"location_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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user