mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FEAT/BE/report_customer_payment
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
# .air.toml
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
||||
bin = "tmp/main"
|
||||
full_bin = "APP_ENV=dev ./tmp/main"
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
exclude_dir = ["vendor", "tmp"]
|
||||
|
||||
[log]
|
||||
time = true
|
||||
+3
-1
@@ -13,7 +13,8 @@ bin/
|
||||
Makefile
|
||||
docker-compose.local.yml
|
||||
docker-compose.yaml
|
||||
Dockerfile.local
|
||||
Dockerfile
|
||||
.gitlab-ci.yml
|
||||
# Go build cache
|
||||
.gocache/
|
||||
vendor
|
||||
@@ -27,3 +28,4 @@ coverage/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
+152
-69
@@ -1,90 +1,173 @@
|
||||
stages:
|
||||
- build
|
||||
- migrate
|
||||
- deploy
|
||||
- seed
|
||||
|
||||
default:
|
||||
tags:
|
||||
- self-hosted-stg
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
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"
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
before_script:
|
||||
- echo "🧰 Installing dependencies..."
|
||||
- apk update && apk add --no-cache openssh git curl bash
|
||||
IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
|
||||
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
|
||||
|
||||
# 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
|
||||
DEPLOY_DIR: "/opt/deploy/stg-lti-api"
|
||||
COMPOSE_FILE: "docker-compose.yaml"
|
||||
|
||||
# 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
|
||||
# =========================
|
||||
# BUILD (AUTO)
|
||||
# =========================
|
||||
build_staging:
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
|
||||
script:
|
||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
- >
|
||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||
echo "✅ Build image: $IMAGE_NAME"
|
||||
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
||||
|
||||
echo "✅ Push image: $IMAGE_NAME"
|
||||
docker push "$IMAGE_NAME"
|
||||
|
||||
echo "✅ Tag latest: $IMAGE_LATEST"
|
||||
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||
docker push "$IMAGE_LATEST"
|
||||
|
||||
|
||||
# =========================
|
||||
# MIGRATE (AUTO)
|
||||
# =========================
|
||||
migrate_staging:
|
||||
stage: migrate
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
needs:
|
||||
- job: build_staging
|
||||
artifacts: false
|
||||
script: |
|
||||
set -e
|
||||
echo "✅ Running migrations (staging) ..."
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||
|
||||
# ✅ load env dari server
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
# ✅ validasi
|
||||
test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1)
|
||||
test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1)
|
||||
test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1)
|
||||
test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1)
|
||||
test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1)
|
||||
|
||||
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}"
|
||||
echo "✅ DATABASE_URL=$DATABASE_URL"
|
||||
|
||||
# ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!)
|
||||
echo "✅ Ensuring postgres & redis running ..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true
|
||||
|
||||
# ✅ Ambil network key dari compose
|
||||
COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')"
|
||||
echo "✅ Compose network key: $COMPOSE_NETWORK_KEY"
|
||||
|
||||
# ✅ Cari network name yang dipakai docker
|
||||
NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)"
|
||||
test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1)
|
||||
|
||||
echo "✅ Docker network detected: $NETWORK_NAME"
|
||||
|
||||
# ✅ Migrations dari repo (CI workspace)
|
||||
echo "✅ Checking migrations from repo..."
|
||||
ls -lah "$CI_PROJECT_DIR/internal/database/migrations"
|
||||
|
||||
echo "✅ Running migrations via migrate/migrate container"
|
||||
set +e
|
||||
out=$(docker run --rm \
|
||||
--network "$NETWORK_NAME" \
|
||||
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
|
||||
migrate/migrate:v4.15.2 \
|
||||
-path=/migrations -database "$DATABASE_URL" up 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
|
||||
cd /home/devops/docker/deployment/development/lti-api
|
||||
echo "$out"
|
||||
|
||||
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
||||
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
||||
# ✅ Handle no change dengan benar (tidak false-success)
|
||||
if echo "$out" | grep -qi "no change"; then
|
||||
echo "✅ No change (already up to date)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
if [ $code -ne 0 ]; then
|
||||
echo "❌ Migration failed with exit code $code"
|
||||
exit $code
|
||||
fi
|
||||
|
||||
# Fetch/reset pakai SSH
|
||||
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
||||
git reset --hard origin/development
|
||||
echo "✅ Migration applied successfully"
|
||||
|
||||
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}";
|
||||
# =========================
|
||||
# DEPLOY (AUTO)
|
||||
# =========================
|
||||
deploy_staging:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
needs:
|
||||
- job: migrate_staging
|
||||
artifacts: false
|
||||
- job: build_staging
|
||||
artifacts: false
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
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;
|
||||
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)
|
||||
|
||||
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;
|
||||
docker compose -f "$COMPOSE_FILE" pull
|
||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
docker image prune -f
|
||||
|
||||
echo "📡 Sending notification to Discord...";
|
||||
curl -sS -H "Content-Type: application/json" \
|
||||
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||
|
||||
only:
|
||||
- development
|
||||
# =========================
|
||||
# SEED (MANUAL)
|
||||
# =========================
|
||||
seed_staging:
|
||||
stage: seed
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
needs:
|
||||
- job: deploy_staging
|
||||
artifacts: false
|
||||
when: manual
|
||||
allow_failure: false
|
||||
script: |
|
||||
set -e
|
||||
cd "$DEPLOY_DIR"
|
||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
|
||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||
|
||||
environment:
|
||||
name: development
|
||||
docker compose -f "$COMPOSE_FILE" pull seed || true
|
||||
docker compose -f "$COMPOSE_FILE" run --rm seed
|
||||
+29
-11
@@ -1,20 +1,38 @@
|
||||
FROM golang:1.23-alpine
|
||||
# =========================
|
||||
# Builder stage
|
||||
# =========================
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
# Install dependensi dasar
|
||||
RUN apk add --no-cache git curl bash build-base
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
WORKDIR /app
|
||||
|
||||
# Install Air (pakai repo baru air-verse)
|
||||
RUN go install github.com/air-verse/air@v1.52.3
|
||||
|
||||
WORKDIR /lti-api
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build API binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
|
||||
|
||||
# Build SEED binary (pastikan cmd/seed ada)
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
|
||||
|
||||
# =========================
|
||||
# Runtime stage
|
||||
# =========================
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
|
||||
&& adduser -D -H -u 10001 appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/lti-api /app/lti-api
|
||||
COPY --from=builder /app/lti-seed /app/lti-seed
|
||||
|
||||
USER appuser
|
||||
EXPOSE 8081
|
||||
|
||||
CMD ["air", "-c", ".air.toml"]
|
||||
CMD ["/app/lti-api"]
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
services:
|
||||
postgresdb:
|
||||
image: postgres:alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "${DB_PORT_HOST:-5542}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
- ./internal/database/init:/docker-entrypoint-initdb.d
|
||||
networks: [go-network]
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT_HOST:-6381}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks: [go-network]
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.local
|
||||
image: cosmtrek/air:v1.52.3
|
||||
working_dir: /lti-api
|
||||
volumes:
|
||||
- .:/lti-api
|
||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||
command: air -c .air.toml
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_HOST: postgresdb
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${DB_USER:-postgres}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
DB_NAME: ${DB_NAME:-db_lti_erp}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||
ports:
|
||||
- "${APP_PORT:-8081}:8081"
|
||||
depends_on:
|
||||
postgresdb:
|
||||
condition: service_healthy
|
||||
networks: [go-network]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
go-mod-cache:
|
||||
go-build-cache:
|
||||
|
||||
networks:
|
||||
go-network:
|
||||
name: lti-api_go-network
|
||||
driver: bridge
|
||||
@@ -1,98 +0,0 @@
|
||||
services:
|
||||
dev-api-lti:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: dev-api-lti
|
||||
working_dir: /lti-api
|
||||
command: ["/bin/sh", "scripts/entrypoint.sh"]
|
||||
ports:
|
||||
- "8081:8081"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# override agar koneksi ke container internal
|
||||
DB_HOST: dev-postgres-lti
|
||||
DB_PORT: 5432
|
||||
REDIS_URL: redis://dev-redis-lti:6379/0
|
||||
volumes:
|
||||
- .:/lti-api
|
||||
- ./.air.toml:/lti-api/.air.toml:ro
|
||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||
depends_on:
|
||||
- dev-postgres-lti
|
||||
- dev-redis-lti
|
||||
networks:
|
||||
- lti-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "2.0"
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: "1.0"
|
||||
memory: 512M
|
||||
|
||||
dev-postgres-lti:
|
||||
image: postgres:15-alpine
|
||||
container_name: dev-postgres-lti
|
||||
restart: always
|
||||
env_file:
|
||||
- credential/.env.db
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- dev-postgres-lti-data:/var/lib/postgresql/data
|
||||
- ./credential:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- lti-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: "0.5"
|
||||
memory: 512M
|
||||
|
||||
dev-redis-lti:
|
||||
image: redis:7-alpine
|
||||
container_name: dev-redis-lti
|
||||
restart: always
|
||||
ports:
|
||||
- "6380:6379"
|
||||
networks:
|
||||
- lti-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "0.5"
|
||||
memory: 512M
|
||||
reservations:
|
||||
cpus: "0.2"
|
||||
memory: 256M
|
||||
|
||||
networks:
|
||||
lti-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
dev-postgres-lti-data:
|
||||
@@ -28,8 +28,9 @@ const (
|
||||
P_ExpenseUpdateOne = "lti.expense.update"
|
||||
P_ExpenseGetOne = "lti.expense.detail"
|
||||
P_ExpenseDeleteOne = "lti.expense.delete"
|
||||
P_ExpenseApprovalManager = "lti.expense.approve.manager"
|
||||
P_ExpenseApprovalHeadArea = "lti.expense.approve.head_area"
|
||||
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
||||
P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president"
|
||||
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
||||
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
||||
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
||||
|
||||
@@ -78,6 +78,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetOverheadByProjectFlockKandang(c *fiber.Ctx) error {
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectFlockID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
pfkID, err := strconv.Atoi(kandangParam)
|
||||
if err != nil || pfkID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
kandangID := uint(pfkID)
|
||||
|
||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get overhead by project flock kandang successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
@@ -108,12 +138,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
}
|
||||
|
||||
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,19 +148,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing penjualan successfully",
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) error {
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectFlockID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
pfkID, err := strconv.Atoi(kandangParam)
|
||||
if err != nil || pfkID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
kandangID := uint(pfkID)
|
||||
|
||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), &kandangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing penjualan by project flock kandang successfully",
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
projectFlockID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectFlockID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
||||
var projectFlockKandangID *uint
|
||||
if kandangParam != "" {
|
||||
pfkID, err := strconv.Atoi(kandangParam)
|
||||
if err != nil || pfkID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
kandangID := uint(pfkID)
|
||||
projectFlockKandangID = &kandangID
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@ type HppGroup struct {
|
||||
|
||||
type SummaryHpp struct {
|
||||
Label string `json:"label"`
|
||||
Comparison `json:"-"`
|
||||
Budgeting FinancialMetrics `json:"budgeting"`
|
||||
Realization FinancialMetrics `json:"realization"`
|
||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||
}
|
||||
@@ -247,10 +248,8 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
|
||||
|
||||
summary := SummaryHpp{
|
||||
Label: label,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||
),
|
||||
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||
}
|
||||
|
||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
||||
|
||||
@@ -87,7 +87,7 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||
return result
|
||||
}
|
||||
|
||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
|
||||
return PenjualanRealisasiResponseDTO{
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
|
||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||
latestDateByNonstockID := make(map[uint]string)
|
||||
|
||||
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||
|
||||
budgetQty := budgets[i].Qty
|
||||
budgetPrice := budgets[i].Price
|
||||
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||
|
||||
// Budget division: per kandang view only
|
||||
if isPerKandang && totalKandangCount > 0 {
|
||||
budgetQty = budgetQty / float64(totalKandangCount)
|
||||
budgetTotal = budgetTotal / float64(totalKandangCount)
|
||||
}
|
||||
|
||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
|
||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
|
||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
|
||||
}
|
||||
|
||||
for i := range realizations {
|
||||
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||
}
|
||||
|
||||
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||
qty := realizations[i].Qty
|
||||
totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||
|
||||
// Farm-level expense division
|
||||
if realizations[i].ExpenseNonstock.Expense != nil &&
|
||||
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
|
||||
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
|
||||
|
||||
if len(projectFlockIDs) > 0 {
|
||||
totalKandangInAllProjects := 0
|
||||
for _, pfID := range projectFlockIDs {
|
||||
if count, exists := projectFlockKandangCountMap[pfID]; exists {
|
||||
totalKandangInAllProjects += count
|
||||
}
|
||||
}
|
||||
|
||||
if totalKandangInAllProjects > 0 {
|
||||
if isPerKandang {
|
||||
qty = qty / float64(totalKandangInAllProjects)
|
||||
totalAmount = totalAmount / float64(totalKandangInAllProjects)
|
||||
} else {
|
||||
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
|
||||
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
|
||||
perKandangQty := qty / float64(totalKandangInAllProjects)
|
||||
|
||||
qty = perKandangQty * float64(totalKandangCount)
|
||||
totalAmount = perKandangAmount * float64(totalKandangCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
overheadsByNonstockID[nonstockID].ActualQuantity += qty
|
||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
|
||||
|
||||
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
}
|
||||
}
|
||||
|
||||
// === Helper Functions ===
|
||||
func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
|
||||
if projectFlockJSON == "" {
|
||||
return []uint{}
|
||||
}
|
||||
|
||||
var projectFlocks []uint
|
||||
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
|
||||
return []uint{}
|
||||
}
|
||||
|
||||
return projectFlocks
|
||||
}
|
||||
|
||||
func countProjectFlocksInJSON(projectFlockJSON string) int {
|
||||
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
|
||||
if len(projectFlocks) == 0 {
|
||||
return 1
|
||||
}
|
||||
return len(projectFlocks)
|
||||
}
|
||||
|
||||
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||
if nonstock != nil && nonstock.Id != 0 {
|
||||
|
||||
@@ -37,7 +37,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
|
||||
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
||||
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
|
||||
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
||||
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
||||
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
||||
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
||||
@@ -32,4 +34,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
||||
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
@@ -32,9 +33,9 @@ import (
|
||||
type ClosingService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
|
||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
||||
@@ -46,6 +47,7 @@ type closingService struct {
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||
MarketingRepo marketingRepository.MarketingRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
@@ -56,12 +58,13 @@ type closingService struct {
|
||||
RecordingRepo recordingRepository.RecordingRepository
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockRepo: projectFlockRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
@@ -129,24 +132,9 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
|
||||
return projectFlock, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
|
||||
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
})
|
||||
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,16 +142,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
|
||||
return []entity.MarketingDeliveryProduct{}, nil
|
||||
}
|
||||
|
||||
filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi))
|
||||
for _, item := range realisasi {
|
||||
|
||||
if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 ||
|
||||
item.UnitPrice != 0 || item.TotalPrice != 0 {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
return realisasi, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||
@@ -379,35 +358,90 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
return statusProject, statusClosing, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalKandangCount := len(projectFlockKandangs)
|
||||
|
||||
// Build kandang count map for farm expense division
|
||||
projectFlockKandangCountMap := make(map[uint]int)
|
||||
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
|
||||
|
||||
involvedProjectFlocks := make(map[uint]bool)
|
||||
for _, realization := range realizations {
|
||||
if realization.ExpenseNonstock != nil &&
|
||||
realization.ExpenseNonstock.Expense != nil &&
|
||||
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
|
||||
var projectFlockIDs []uint
|
||||
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
|
||||
for _, pfID := range projectFlockIDs {
|
||||
if pfID != projectFlockID {
|
||||
involvedProjectFlocks[pfID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for pfID := range involvedProjectFlocks {
|
||||
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
|
||||
projectFlockKandangCountMap[pfID] = len(pfKandangs)
|
||||
}
|
||||
}
|
||||
|
||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var totalChickinQty float64
|
||||
var totalDepletion float64
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
for _, chickin := range chickins {
|
||||
if chickin.ProjectFlockKandangId == *projectFlockKandangID {
|
||||
totalChickinQty += chickin.UsageQty
|
||||
}
|
||||
}
|
||||
|
||||
var depletionResult float64
|
||||
err = s.RecordingRepo.DB().WithContext(c.Context()).
|
||||
Table("recording_depletions").
|
||||
Select("COALESCE(SUM(recording_depletions.qty), 0)").
|
||||
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
|
||||
Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID).
|
||||
Scan(&depletionResult).Error
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err)
|
||||
} else {
|
||||
totalDepletion = depletionResult
|
||||
}
|
||||
} else {
|
||||
for _, chickin := range chickins {
|
||||
totalChickinQty += chickin.UsageQty
|
||||
}
|
||||
|
||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
totalActualPopulation := totalChickinQty - totalDepletion
|
||||
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dailyChecklists
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
||||
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -13,7 +13,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
||||
ctrl := controller.NewDailyChecklistController(s)
|
||||
|
||||
route := v1.Group("/daily-checklists")
|
||||
// route.Use(m.Auth(u))
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/report", ctrl.GetReport)
|
||||
@@ -22,7 +22,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
||||
|
||||
route.Get("/report", ctrl.GetReport)
|
||||
|
||||
// create daily checklist
|
||||
// upsert daily checklist
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
|
||||
// get detail data daily checklist by id
|
||||
|
||||
@@ -98,12 +98,14 @@ func (s dashboardService) buildPerformanceStatistics(ctx context.Context, params
|
||||
endDate := params.PeriodEnd
|
||||
endExclusive := params.PeriodEndExclusive
|
||||
|
||||
hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, filter, startDate, endExclusive, endDate, location)
|
||||
globalStartDate, globalEndDate, globalEndExclusive := currentPeriodDates(location)
|
||||
|
||||
hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, globalStartDate, globalEndExclusive, globalEndDate, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, filter, endDate, location)
|
||||
sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, globalEndDate, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -271,15 +273,15 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
|
||||
weekFeed := weeklyFeedMap[week]
|
||||
|
||||
actFcr := 0.0
|
||||
if weekFeed > 0 {
|
||||
actFcr = weekEgg / weekFeed
|
||||
if weekEgg > 0 {
|
||||
actFcr = weekFeed / weekEgg
|
||||
}
|
||||
|
||||
cumEgg += weekEgg
|
||||
cumFeed += weekFeed
|
||||
actFcrCum := 0.0
|
||||
if cumFeed > 0 {
|
||||
actFcrCum = cumEgg / cumFeed
|
||||
if cumEgg > 0 {
|
||||
actFcrCum = cumFeed / cumEgg
|
||||
}
|
||||
|
||||
bodyWeightDataset = append(bodyWeightDataset, map[string]interface{}{
|
||||
@@ -357,10 +359,10 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
|
||||
},
|
||||
"fcr": {
|
||||
Series: []dto.DashboardChartSeriesDTO{
|
||||
{Id: "act_fcr", Label: "Act. FCR", Unit: "%"},
|
||||
{Id: "std_fcr", Label: "STD. FCR", Unit: "%"},
|
||||
{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "%"},
|
||||
{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "%"},
|
||||
{Id: "act_fcr", Label: "Act. FCR", Unit: "kg/kg"},
|
||||
{Id: "std_fcr", Label: "STD. FCR", Unit: "kg/kg"},
|
||||
{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "kg/kg"},
|
||||
{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "kg/kg"},
|
||||
},
|
||||
Dataset: fcrDataset,
|
||||
},
|
||||
@@ -843,12 +845,12 @@ func percentDelta(current, last float64) float64 {
|
||||
return (current - last) / last
|
||||
}
|
||||
|
||||
func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
|
||||
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, filter)
|
||||
func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
|
||||
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
totalCost, err := s.sumHppCost(ctx, filter, startDate, endExclusive)
|
||||
totalCost, err := s.sumHppCost(ctx, nil, startDate, endExclusive)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -859,11 +861,11 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *valida
|
||||
}
|
||||
|
||||
lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
|
||||
lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, filter)
|
||||
lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
lastCost, err := s.sumHppCost(ctx, filter, lastMonthStart, lastMonthEndExclusive)
|
||||
lastCost, err := s.sumHppCost(ctx, nil, lastMonthStart, lastMonthEndExclusive)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -876,16 +878,16 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *valida
|
||||
return hppCurrent, hppLast, nil
|
||||
}
|
||||
|
||||
func (s dashboardService) calculateSellingPrice(ctx context.Context, filter *validation.DashboardFilter, endDate time.Time, location *time.Location) (float64, float64, error) {
|
||||
func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) {
|
||||
startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
|
||||
currentEndExclusive := endDate.AddDate(0, 0, 1)
|
||||
|
||||
currentAvg, err := s.avgSellingPrice(ctx, filter, startPrevMonth, currentEndExclusive)
|
||||
currentAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, currentEndExclusive)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
lastAvg, err := s.avgSellingPrice(ctx, filter, startPrevMonth, endPrevMonthExclusive)
|
||||
lastAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, endPrevMonthExclusive)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -935,11 +937,11 @@ func (s dashboardService) fcrValue(ctx context.Context, filter *validation.Dashb
|
||||
}
|
||||
feedUsageGrams := feedUsageToGrams(feedRows)
|
||||
|
||||
if feedUsageGrams <= 0 {
|
||||
if eggWeightGrams <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return eggWeightGrams / feedUsageGrams, nil
|
||||
return feedUsageGrams / eggWeightGrams, nil
|
||||
}
|
||||
|
||||
func (s dashboardService) mortalityValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
|
||||
@@ -1027,3 +1029,11 @@ func monthRange(t time.Time, location *time.Location) (time.Time, time.Time) {
|
||||
endExclusive := start.AddDate(0, 1, 0)
|
||||
return start, endExclusive
|
||||
}
|
||||
|
||||
func currentPeriodDates(location *time.Location) (time.Time, time.Time, time.Time) {
|
||||
now := time.Now().In(location)
|
||||
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
|
||||
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
|
||||
endExclusive := endDate.AddDate(0, 0, 1)
|
||||
return startDate, endDate, endExclusive
|
||||
}
|
||||
|
||||
@@ -229,10 +229,12 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error {
|
||||
|
||||
path := c.Path()
|
||||
approvalType := ""
|
||||
if strings.Contains(path, "/approvals/manager") {
|
||||
approvalType = "manager"
|
||||
if strings.Contains(path, "/approvals/head-area") {
|
||||
approvalType = "head-area"
|
||||
} else if strings.Contains(path, "/approvals/finance") {
|
||||
approvalType = "finance"
|
||||
} else if strings.Contains(path, "/approvals/unit-vice-president") {
|
||||
approvalType = "unit-vice-president"
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -15,6 +16,7 @@ type ExpenseRealizationRepository interface {
|
||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||
GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
||||
}
|
||||
|
||||
@@ -55,6 +57,40 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
|
||||
return realizations, err
|
||||
}
|
||||
|
||||
func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) {
|
||||
var realizations []entity.ExpenseRealization
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Preload("ExpenseNonstock").
|
||||
Preload("ExpenseNonstock.Nonstock").
|
||||
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||
Preload("ExpenseNonstock.Nonstock.Flags").
|
||||
Preload("ExpenseNonstock.Expense").
|
||||
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
||||
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
|
||||
Where("expenses.realization_date IS NOT NULL")
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
db = db.Where(`(
|
||||
expense_nonstocks.project_flock_kandang_id = ? OR
|
||||
(expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND
|
||||
expense_nonstocks.project_flock_kandang_id IS NULL) OR
|
||||
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
|
||||
)`, *projectFlockKandangID, *projectFlockKandangID, fmt.Sprintf("[%d]", projectFlockID))
|
||||
} else {
|
||||
db = db.Where(`(
|
||||
project_flock_kandangs.project_flock_id = ? OR
|
||||
kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR
|
||||
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
|
||||
)`, projectFlockID, projectFlockID, fmt.Sprintf("[%d]", projectFlockID))
|
||||
}
|
||||
|
||||
err := db.Find(&realizations).Error
|
||||
return realizations, err
|
||||
}
|
||||
|
||||
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
||||
var realizations []entity.ExpenseRealization
|
||||
var total int64
|
||||
|
||||
@@ -27,8 +27,11 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
||||
route.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
|
||||
route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
|
||||
route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
|
||||
route.Post("/approvals/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval)
|
||||
|
||||
route.Post("/approvals/head-area", m.RequirePermissions(m.P_ExpenseApprovalHeadArea), ctrl.Approval)
|
||||
route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
|
||||
route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval)
|
||||
|
||||
route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
||||
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
||||
route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
|
||||
|
||||
@@ -1049,21 +1049,30 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
||||
}
|
||||
|
||||
var stepNumber approvalutils.ApprovalStep
|
||||
if approvalType == "manager" {
|
||||
if approvalType == "head-area" {
|
||||
|
||||
stepNumber = utils.ExpenseStepManager
|
||||
stepNumber = utils.ExpenseStepHeadArea
|
||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
|
||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Cannot process at Manager step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
|
||||
fmt.Sprintf("Cannot process at Head Area step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
|
||||
}
|
||||
} else if approvalType == "unit-vice-president" {
|
||||
|
||||
stepNumber = utils.ExpenseStepUnitVicePresident
|
||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepHeadArea) {
|
||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Cannot process at Unit Vice President step. Latest approval is at %s step. Expected previous step: Head Area", currentStepName))
|
||||
}
|
||||
|
||||
} else if approvalType == "finance" {
|
||||
|
||||
stepNumber = utils.ExpenseStepFinance
|
||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) {
|
||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepUnitVicePresident) {
|
||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Manager", currentStepName))
|
||||
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Unit Vice President", currentStepName))
|
||||
}
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
|
||||
|
||||
@@ -84,7 +84,6 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
||||
expenseIDs := make(map[uint64]struct{})
|
||||
expenseNonstockIDs := make([]uint64, 0)
|
||||
|
||||
|
||||
for _, item := range items {
|
||||
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
|
||||
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
|
||||
@@ -106,13 +105,11 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||
for expenseID := range expenseIDs {
|
||||
var count int64
|
||||
@@ -122,7 +119,6 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
if count == 0 {
|
||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||
return err
|
||||
@@ -220,7 +216,6 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
||||
for _, gi := range items {
|
||||
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
|
||||
|
||||
|
||||
price := gi.shippingCostTotal
|
||||
if gi.payload.TransportPerItem != nil {
|
||||
price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
|
||||
@@ -251,14 +246,16 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
action := entity.ApprovalActionApproved
|
||||
actorID := uint(transfer.CreatedBy)
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
@@ -328,7 +325,6 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
|
||||
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("Details").
|
||||
@@ -348,7 +344,6 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
|
||||
for i := range transfer.Details {
|
||||
detailMap[transfer.Details[i].Id] = &transfer.Details[i]
|
||||
|
||||
|
||||
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
|
||||
if deliveryItem.StockTransferDelivery != nil {
|
||||
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
|
||||
@@ -395,17 +390,14 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
shippingCostTotal := shippingCostMap[detail.Id]
|
||||
|
||||
|
||||
totalPrice := shippingCostTotal
|
||||
if payload.TransportPerItem != nil {
|
||||
|
||||
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
|
||||
}
|
||||
|
||||
|
||||
warehouseID := uint(payload.WarehouseID)
|
||||
if warehouseID == 0 && transfer.ToWarehouse != nil {
|
||||
warehouseID = uint(transfer.ToWarehouse.Id)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type MarketingDeliveryProductRepository interface {
|
||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
|
||||
@@ -53,6 +54,43 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
|
||||
return deliveryProducts, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||
Distinct("marketing_delivery_products.*")
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
|
||||
}
|
||||
|
||||
db = db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
|
||||
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
@@ -99,13 +137,14 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
|
||||
}).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
|
||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" {
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||
}
|
||||
|
||||
if filters.ProductId > 0 || filters.Search != "" {
|
||||
if filters.ProductId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||
}
|
||||
|
||||
@@ -139,6 +178,29 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||
}
|
||||
|
||||
if filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||
Group("marketing_delivery_products.id")
|
||||
|
||||
switch filters.MarketingType {
|
||||
case "ayam":
|
||||
db = db.Where("flags.name IN (?)", []string{
|
||||
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
|
||||
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
|
||||
})
|
||||
case "telur":
|
||||
db = db.Where("flags.name IN (?)", []string{
|
||||
string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah),
|
||||
string(utils.FlagTelurPutih), string(utils.FlagTelurRetak),
|
||||
})
|
||||
case "trading":
|
||||
db = db.Where("flags.name IN (?)", []string{
|
||||
string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia),
|
||||
string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
|
||||
if filters.FilterBy == "so_date" {
|
||||
if filters.StartDate != "" {
|
||||
|
||||
@@ -20,7 +20,7 @@ type Update struct {
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
||||
|
||||
@@ -14,7 +14,7 @@ type Update struct {
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||
}
|
||||
|
||||
@@ -618,7 +618,10 @@ func (b *expenseBridge) createExpenseViaService(
|
||||
actorID = 1
|
||||
}
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
|
||||
@@ -82,6 +82,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
||||
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
||||
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
||||
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
||||
MarketingType: ctx.Query("marketing_type", ""),
|
||||
FilterBy: ctx.Query("filter_by", ""),
|
||||
StartDate: ctx.Query("start_date", ""),
|
||||
EndDate: ctx.Query("end_date", ""),
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
type DebtSupplierRowDTO struct {
|
||||
PrNumber string `json:"pr_number"`
|
||||
PoNumber string `json:"po_number"`
|
||||
PrDate string `json:"pr_date"`
|
||||
PoDate string `json:"po_date"`
|
||||
ReceivedDate string `json:"received_date"`
|
||||
Aging int `json:"aging"`
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
||||
@@ -21,6 +21,7 @@ type DebtSupplierRowDTO struct {
|
||||
DebtPrice float64 `json:"debt_price"`
|
||||
Status string `json:"status"`
|
||||
TravelNumber string `json:"travel_number"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
|
||||
type DebtSupplierTotalDTO struct {
|
||||
@@ -32,6 +33,7 @@ type DebtSupplierTotalDTO struct {
|
||||
|
||||
type DebtSupplierDTO struct {
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||
InitialBalance float64 `json:"initial_balance"`
|
||||
Rows []DebtSupplierRowDTO `json:"rows"`
|
||||
Total DebtSupplierTotalDTO `json:"total"`
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -22,7 +26,7 @@ type RepportMarketingItemDTO struct {
|
||||
DoNumber string `json:"do_number"`
|
||||
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
|
||||
VehicleNumber string `json:"vehicle_number"`
|
||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||
Product *ProductRelationDTOFixed `json:"product,omitempty"`
|
||||
MarketingType string `json:"marketing_type"`
|
||||
Qty float64 `json:"qty"`
|
||||
AverageWeightKg float64 `json:"average_weight_kg"`
|
||||
@@ -46,6 +50,12 @@ type RepportMarketingResponseDTO struct {
|
||||
Total *Summary `json:"total,omitempty"`
|
||||
}
|
||||
|
||||
type ProductRelationDTOFixed struct {
|
||||
productDTO.ProductRelationDTO
|
||||
ProductPrice float64 `json:"product_price"`
|
||||
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||
}
|
||||
|
||||
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
|
||||
soDate := time.Time{}
|
||||
agingDays := 0
|
||||
@@ -106,7 +116,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
|
||||
|
||||
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
|
||||
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
||||
item.Product = &mapped
|
||||
item.Product = newProductRelationDTOFixedPtr(&mapped)
|
||||
}
|
||||
|
||||
return item
|
||||
@@ -139,7 +149,7 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
|
||||
}
|
||||
|
||||
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
||||
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||
hasAyam, hasTelur, hasTrading := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||
|
||||
if hasAyam {
|
||||
return "ayam"
|
||||
@@ -147,12 +157,15 @@ func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
||||
if hasTelur {
|
||||
return "telur"
|
||||
}
|
||||
if hasTrading {
|
||||
return "trading"
|
||||
}
|
||||
return "trading" // default to trading if no flags found
|
||||
}
|
||||
|
||||
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
|
||||
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool) {
|
||||
if len(flags) == 0 {
|
||||
return false, false
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
@@ -167,13 +180,18 @@ func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
|
||||
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
|
||||
return hasAyam, hasTelur, hasTrading
|
||||
}
|
||||
|
||||
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
|
||||
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||
hasAyam, hasTelur, _ := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||
|
||||
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
|
||||
return hasAyam
|
||||
@@ -259,3 +277,39 @@ func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPr
|
||||
Total: total,
|
||||
}
|
||||
}
|
||||
|
||||
func newProductRelationDTOFixedPtr(original *productDTO.ProductRelationDTO) *ProductRelationDTOFixed {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
fixed := ProductRelationDTOFixed{
|
||||
ProductRelationDTO: *original,
|
||||
ProductPrice: original.ProductPrice,
|
||||
SellingPrice: original.SellingPrice,
|
||||
}
|
||||
return &fixed
|
||||
}
|
||||
|
||||
func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
|
||||
type Alias struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProductPrice float64 `json:"product_price"`
|
||||
SellingPrice *float64 `json:"selling_price"`
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||
Flags *[]string `json:"flags,omitempty"`
|
||||
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
||||
}
|
||||
|
||||
return json.Marshal(&Alias{
|
||||
Id: p.ProductRelationDTO.Id,
|
||||
Name: p.ProductRelationDTO.Name,
|
||||
ProductPrice: p.ProductPrice,
|
||||
SellingPrice: p.SellingPrice,
|
||||
Uom: p.ProductRelationDTO.Uom,
|
||||
Flags: p.ProductRelationDTO.Flags,
|
||||
ProductCategory: p.ProductRelationDTO.ProductCategory,
|
||||
Suppliers: p.ProductRelationDTO.Suppliers,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import (
|
||||
type DebtSupplierRepository interface {
|
||||
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
|
||||
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
||||
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
||||
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
||||
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||
}
|
||||
|
||||
type debtSupplierRepositoryImpl struct {
|
||||
@@ -28,10 +31,10 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
||||
|
||||
func resolveDebtSupplierDateColumn(filterBy string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||
case "receive_date":
|
||||
return "purchases.receive_date"
|
||||
case "po_date":
|
||||
return "purchases.po_date"
|
||||
case "pr_date":
|
||||
return "purchases.created_at"
|
||||
case "do_date", "received_date", "":
|
||||
return "purchase_items.received_date"
|
||||
default:
|
||||
@@ -157,6 +160,39 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context
|
||||
return purchases, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) {
|
||||
if len(supplierIDs) == 0 {
|
||||
return []entity.Payment{}, nil
|
||||
}
|
||||
|
||||
db := r.db.WithContext(ctx).
|
||||
Model(&entity.Payment{}).
|
||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||
Where("direction = ?", "OUT").
|
||||
Where("party_id IN ?", supplierIDs)
|
||||
|
||||
if strings.TrimSpace(filters.StartDate) != "" {
|
||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||
db = db.Where("DATE(payment_date) >= ?", dateFrom)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(filters.EndDate) != "" {
|
||||
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
|
||||
db = db.Where("DATE(payment_date) <= ?", dateTo)
|
||||
}
|
||||
}
|
||||
|
||||
var payments []entity.Payment
|
||||
if err := db.
|
||||
Order("payment_date ASC, id ASC").
|
||||
Find(&payments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payments, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) {
|
||||
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
||||
|
||||
@@ -219,3 +255,76 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
dateFrom, err := utils.ParseDateString(filters.StartDate)
|
||||
if err != nil {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
||||
|
||||
type purchaseTotalRow struct {
|
||||
SupplierID uint `gorm:"column:supplier_id"`
|
||||
Total float64 `gorm:"column:total"`
|
||||
}
|
||||
|
||||
rows := make([]purchaseTotalRow, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("purchases").
|
||||
Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
|
||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||
Where("purchases.supplier_id IN ?", supplierIDs).
|
||||
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
|
||||
Group("purchases.supplier_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.SupplierID] = row.Total
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
dateFrom, err := utils.ParseDateString(filters.StartDate)
|
||||
if err != nil {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
type paymentTotalRow struct {
|
||||
SupplierID uint `gorm:"column:supplier_id"`
|
||||
Total float64 `gorm:"column:total"`
|
||||
}
|
||||
|
||||
rows := make([]paymentTotalRow, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.Payment{}).
|
||||
Select("party_id AS supplier_id, SUM(nominal) AS total").
|
||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||
Where("direction = ?", "OUT").
|
||||
Where("party_id IN ?", supplierIDs).
|
||||
Where("DATE(payment_date) < ?", dateFrom).
|
||||
Group("party_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.SupplierID] = row.Total
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -643,8 +643,8 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
|
||||
}
|
||||
|
||||
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
|
||||
if params.FilterBy == "" {
|
||||
params.FilterBy = "do_date"
|
||||
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
|
||||
params.FilterBy = "received_date"
|
||||
}
|
||||
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
@@ -676,6 +676,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(c.Context(), supplierIDs, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
||||
references := make([]string, 0)
|
||||
seenRefs := make(map[string]struct{})
|
||||
@@ -698,6 +703,21 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
||||
for _, payment := range payments {
|
||||
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
|
||||
}
|
||||
|
||||
initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||
@@ -711,29 +731,81 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
continue
|
||||
}
|
||||
|
||||
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||
|
||||
items := purchasesBySupplier[supplierID]
|
||||
rows := make([]dto.DebtSupplierRowDTO, 0, len(items))
|
||||
paymentItems := paymentsBySupplier[supplierID]
|
||||
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
|
||||
total := dto.DebtSupplierTotalDTO{}
|
||||
|
||||
type debtSupplierRowItem struct {
|
||||
Row dto.DebtSupplierRowDTO
|
||||
SortTime time.Time
|
||||
Order int
|
||||
DeltaBalance float64
|
||||
CountTotals bool
|
||||
}
|
||||
|
||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||
for _, purchase := range items {
|
||||
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
|
||||
rows = append(rows, row)
|
||||
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||
Row: row,
|
||||
SortTime: sortTime,
|
||||
Order: 0,
|
||||
DeltaBalance: -row.TotalPrice,
|
||||
CountTotals: true,
|
||||
})
|
||||
}
|
||||
|
||||
for _, payment := range paymentItems {
|
||||
row := buildDebtSupplierPaymentRow(payment, location)
|
||||
sortTime := payment.PaymentDate.In(location)
|
||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||
Row: row,
|
||||
SortTime: sortTime,
|
||||
Order: 1,
|
||||
DeltaBalance: payment.Nominal,
|
||||
CountTotals: false,
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(combinedRows, func(i, j int) bool {
|
||||
if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) {
|
||||
return combinedRows[i].Order < combinedRows[j].Order
|
||||
}
|
||||
return combinedRows[i].SortTime.Before(combinedRows[j].SortTime)
|
||||
})
|
||||
|
||||
balance := initialBalance
|
||||
for i := range combinedRows {
|
||||
balance += combinedRows[i].DeltaBalance
|
||||
combinedRows[i].Row.Balance = balance
|
||||
|
||||
if combinedRows[i].CountTotals {
|
||||
row := combinedRows[i].Row
|
||||
if row.Aging > total.Aging {
|
||||
total.Aging = row.Aging
|
||||
}
|
||||
total.TotalPrice += row.TotalPrice
|
||||
total.PaymentPrice += row.PaymentPrice
|
||||
total.DebtPrice += row.DebtPrice
|
||||
} else {
|
||||
combinedRows[i].Row.DebtPrice = balance
|
||||
}
|
||||
}
|
||||
|
||||
sortDesc := strings.EqualFold(params.SortOrder, "desc")
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
if sortDesc {
|
||||
return rows[i].PrDate > rows[j].PrDate
|
||||
for i := len(combinedRows) - 1; i >= 0; i-- {
|
||||
rows = append(rows, combinedRows[i].Row)
|
||||
}
|
||||
} else {
|
||||
for i := range combinedRows {
|
||||
rows = append(rows, combinedRows[i].Row)
|
||||
}
|
||||
}
|
||||
return rows[i].PrDate < rows[j].PrDate
|
||||
})
|
||||
|
||||
var supplierDTORef *supplierDTO.SupplierRelationDTO
|
||||
if supplier.Id != 0 {
|
||||
@@ -743,6 +815,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
|
||||
result = append(result, dto.DebtSupplierDTO{
|
||||
Supplier: supplierDTORef,
|
||||
InitialBalance: initialBalance,
|
||||
Rows: rows,
|
||||
Total: total,
|
||||
})
|
||||
@@ -770,6 +843,7 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
|
||||
totalPrice := 0.0
|
||||
travelNumber := "-"
|
||||
receivedDate := ""
|
||||
var area *areaDTO.AreaRelationDTO
|
||||
var warehouse *warehouseDTO.WarehouseRelationDTO
|
||||
|
||||
@@ -788,8 +862,19 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
}
|
||||
}
|
||||
|
||||
earliestReceived := time.Time{}
|
||||
for _, item := range purchase.Items {
|
||||
totalPrice += item.TotalPrice
|
||||
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||
continue
|
||||
}
|
||||
received := item.ReceivedDate.In(loc)
|
||||
if earliestReceived.IsZero() || received.Before(earliestReceived) {
|
||||
earliestReceived = received
|
||||
}
|
||||
}
|
||||
if !earliestReceived.IsZero() {
|
||||
receivedDate = earliestReceived.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,8 +906,8 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
return dto.DebtSupplierRowDTO{
|
||||
PrNumber: prNumber,
|
||||
PoNumber: poNumber,
|
||||
PrDate: prDate.Format("2006-01-02"),
|
||||
PoDate: poDate,
|
||||
ReceivedDate: receivedDate,
|
||||
Aging: aging,
|
||||
Area: area,
|
||||
Warehouse: warehouse,
|
||||
@@ -836,6 +921,62 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
}
|
||||
}
|
||||
|
||||
func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO {
|
||||
referenceNumber := ""
|
||||
if payment.ReferenceNumber != nil {
|
||||
referenceNumber = *payment.ReferenceNumber
|
||||
}
|
||||
|
||||
prNumber := payment.PaymentCode
|
||||
if strings.TrimSpace(prNumber) == "" {
|
||||
prNumber = referenceNumber
|
||||
}
|
||||
|
||||
return dto.DebtSupplierRowDTO{
|
||||
PrNumber: prNumber,
|
||||
PoNumber: referenceNumber,
|
||||
PoDate: "-",
|
||||
ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"),
|
||||
Aging: 0,
|
||||
Area: nil,
|
||||
Warehouse: nil,
|
||||
DueDate: "-",
|
||||
DueStatus: "-",
|
||||
TotalPrice: 0,
|
||||
PaymentPrice: payment.Nominal,
|
||||
DebtPrice: 0,
|
||||
Status: "Pembayaran",
|
||||
TravelNumber: "-",
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
|
||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||
case "po_date":
|
||||
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
||||
return purchase.PoDate.In(loc)
|
||||
}
|
||||
case "pr_date":
|
||||
return purchase.CreatedAt.In(loc)
|
||||
default:
|
||||
earliest := time.Time{}
|
||||
for _, item := range purchase.Items {
|
||||
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||
continue
|
||||
}
|
||||
received := item.ReceivedDate.In(loc)
|
||||
if earliest.IsZero() || received.Before(earliest) {
|
||||
earliest = received
|
||||
}
|
||||
}
|
||||
if !earliest.IsZero() {
|
||||
return earliest
|
||||
}
|
||||
}
|
||||
|
||||
return purchase.CreatedAt.In(loc)
|
||||
}
|
||||
|
||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,6 +23,7 @@ type MarketingQuery struct {
|
||||
ProductId int64 `query:"product_id" validate:"omitempty"`
|
||||
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
||||
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
||||
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
|
||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
@@ -49,7 +50,7 @@ type DebtSupplierQuery struct {
|
||||
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=do_date po_date pr_date"`
|
||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"`
|
||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
|
||||
@@ -356,15 +356,17 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
const (
|
||||
ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES")
|
||||
ExpenseStepPengajuan approvalutils.ApprovalStep = 1
|
||||
ExpenseStepManager approvalutils.ApprovalStep = 2
|
||||
ExpenseStepFinance approvalutils.ApprovalStep = 3
|
||||
ExpenseStepRealisasi approvalutils.ApprovalStep = 4
|
||||
ExpenseStepSelesai approvalutils.ApprovalStep = 5
|
||||
ExpenseStepHeadArea approvalutils.ApprovalStep = 2
|
||||
ExpenseStepUnitVicePresident approvalutils.ApprovalStep = 3
|
||||
ExpenseStepFinance approvalutils.ApprovalStep = 4
|
||||
ExpenseStepRealisasi approvalutils.ApprovalStep = 5
|
||||
ExpenseStepSelesai approvalutils.ApprovalStep = 6
|
||||
)
|
||||
|
||||
var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
ExpenseStepPengajuan: "Pengajuan",
|
||||
ExpenseStepManager: "Approval Manager",
|
||||
ExpenseStepHeadArea: "Approval Head Area",
|
||||
ExpenseStepUnitVicePresident: "Approval Business Unit Vice President",
|
||||
ExpenseStepFinance: "Approval Finance",
|
||||
ExpenseStepRealisasi: "Realisasi",
|
||||
ExpenseStepSelesai: "Selesai",
|
||||
|
||||
Reference in New Issue
Block a user