mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09c951c16a | |||
| b615570036 | |||
| c793c3cf9a | |||
| b3e0410f5a | |||
| a882d5a687 | |||
| c0848b6d2d | |||
| b240478ed5 | |||
| 3052497fc0 | |||
| 71c62c5e02 | |||
| 768961d7d6 | |||
| 8cd9627a51 | |||
| 378d633ea4 | |||
| fb193fc61f | |||
| af7aabdec8 | |||
| bac36b4f00 | |||
| 687d02313b | |||
| 7d3602d829 | |||
| 533e9aca6f | |||
| dcfb5e10b4 | |||
| fbeccf4cdc | |||
| eda50930e7 | |||
| 0bc5480a1d | |||
| ef482dd1b9 | |||
| 302f0ed877 | |||
| 31c48ee1da | |||
| 8ad11af9c9 | |||
| 688d3fa757 | |||
| 08be60c229 | |||
| 50b19dc1c3 | |||
| e770526c1a | |||
| 77af262662 | |||
| 21ff1c8ab7 | |||
| 2ca84ecffe | |||
| 4d334e8d5c | |||
| 62522a751f | |||
| 62ccc2e5d6 | |||
| 13fc246f21 | |||
| 89293a843e | |||
| 8efe9b668b | |||
| 89480deeb0 | |||
| 4146342120 | |||
| b375fb964e | |||
| fe002c9602 | |||
| f1032b44d1 | |||
| 7f2401311b | |||
| 8792161c02 | |||
| 37c26d5877 | |||
| c316a6d7a9 | |||
| 3a89e18b16 | |||
| c6dc94a4e1 | |||
| aeb5433346 | |||
| e004354420 | |||
| 804ff45dbd | |||
| 2a884a8d09 | |||
| 7daa509cd0 | |||
| 9fc2d0556e | |||
| c2a89910fb | |||
| 1847e5590a | |||
| 57094f664c | |||
| f8b6e12d16 | |||
| e12c34db13 | |||
| 3012d260ec | |||
| f6e872c0aa | |||
| 8a639f127c | |||
| 2acaa10b60 | |||
| bd9d41e161 | |||
| 06d8d0b795 | |||
| 1d5e7b6e1a | |||
| da190f1b05 | |||
| c8905eb715 | |||
| 7f1d796b65 | |||
| 6c7ff3f415 | |||
| 03fbf7f4b7 | |||
| 7545e9b37d | |||
| 69f38bf16a | |||
| 5730053e04 | |||
| b087a703ef | |||
| 00edcb6add | |||
| de6580d11c | |||
| dcd6008946 | |||
| 711f58abae | |||
| ffd3c905fe | |||
| 6e4a8617da | |||
| 8a57d5d675 | |||
| 5de81f6315 | |||
| e50dd096a4 | |||
| 7551d11888 | |||
| 7444cfac31 | |||
| 1b5b5bc847 | |||
| 5d7b613ffc | |||
| 33e89d65ab | |||
| 0f4cc6e379 | |||
| 590df26a1f | |||
| ce7ce778fd | |||
| eaa208f733 | |||
| b088eebac5 | |||
| 3c10866208 | |||
| f7a392be52 | |||
| 4bd8319e3b | |||
| bba2dec8c6 | |||
| f7b70d4b14 | |||
| 9f28294dc3 | |||
| 6a166ceb86 | |||
| f0b4fe916c | |||
| f37bf4d22d | |||
| ac5edb36e7 | |||
| 8fab5d7d91 | |||
| 5ddfb2c745 | |||
| 5cfa4d4a59 | |||
| 80b2cafd2f | |||
| b47f26d448 | |||
| 2f22182605 | |||
| e2d352721c | |||
| 068fe4329e | |||
| 15be8dcbea | |||
| 041e8763ac | |||
| 644e9911e4 | |||
| bb04cb53d9 | |||
| 048e607290 | |||
| 18441eb19f | |||
| 526e14f26e | |||
| 539081ce99 | |||
| d568b87e01 | |||
| 9515848d8f | |||
| c15ff8a211 | |||
| d1d94357cf | |||
| 67f5165bfb | |||
| 1217f34dcd | |||
| ae41422776 | |||
| 3978951d8f | |||
| 3422fceec7 | |||
| 09f1b29359 | |||
| 167d18fe87 | |||
| 473f4504ea | |||
| dc7dc0ba47 | |||
| a54129866e | |||
| d40243be4b | |||
| 525ff650f2 | |||
| c1e9b5a975 | |||
| 272367d8ef | |||
| d3c7d65bf5 | |||
| 944fd860a3 | |||
| af79db8726 | |||
| b42ca5e6fb | |||
| 3b2c6f16c3 | |||
| 359e982e76 | |||
| bc0bf7fe16 | |||
| 70a7b1b888 | |||
| 17d55bd2c0 | |||
| 9cc86df1ed | |||
| b7914e8294 | |||
| d33119661a | |||
| 8a57d439dc | |||
| 3d76854273 | |||
| b7a3882f20 | |||
| 29933a5df9 | |||
| f8aee4be7b | |||
| 4ee5bf3628 | |||
| 0f06dff761 | |||
| 0629c5ccf6 | |||
| 43eb1df118 | |||
| 338312edd1 | |||
| f7522636e2 | |||
| b11f03dfda | |||
| 76e65704d7 | |||
| 857a3c284b | |||
| 5606b9c4a3 | |||
| 7af78d04dd | |||
| 2650e919e7 | |||
| 7b2d3ae025 | |||
| 64e8de2344 | |||
| 2be9ae36c1 | |||
| 6c08fe23ca | |||
| 8a64300ddd | |||
| 9164550263 | |||
| a2d2c4269a | |||
| 90f363bfdb | |||
| a7a784970d | |||
| 18b0663dc6 | |||
| 375e057e7c | |||
| 9336289573 | |||
| 76d5b6b69a | |||
| 0a84e427c1 | |||
| cad91957b3 | |||
| fca2d63c6e | |||
| f5a016b74b | |||
| 82a7bada05 | |||
| c6626cb6f5 | |||
| ebfa88e721 | |||
| 705138795c | |||
| 538372a43a | |||
| 7a26ca5fe5 | |||
| a08466a28e | |||
| 1bdaf63763 | |||
| d8fb427734 | |||
| c9ebd88e9d | |||
| 0c6d42070a | |||
| 8725d79f8f | |||
| 39909d1c2e | |||
| 556540e97f | |||
| e421307965 | |||
| 1b5437bc01 | |||
| 7d6573fabd | |||
| ce083bccdc | |||
| dc4729c3b9 | |||
| bec6a93152 | |||
| 42853aaac0 | |||
| 610555c3cf | |||
| c60c40af03 | |||
| 2d098cb6b1 | |||
| 30231fabe9 | |||
| e738a97e4c | |||
| 81f4a5e33e | |||
| 1e9fdd2b0d | |||
| b6a60d5009 |
+4
-1
@@ -9,11 +9,13 @@ main
|
|||||||
bin/
|
bin/
|
||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
|
.air.toml
|
||||||
Makefile
|
Makefile
|
||||||
docker-compose.local.yml
|
docker-compose.local.yml
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
|
Dockerfile
|
||||||
Dockerfile.local
|
Dockerfile.local
|
||||||
|
.gitlab-ci.yml
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
vendor
|
vendor
|
||||||
@@ -27,3 +29,4 @@ coverage/
|
|||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+29
-11
@@ -1,20 +1,38 @@
|
|||||||
FROM golang:1.23-alpine
|
# =========================
|
||||||
|
# Builder stage
|
||||||
|
# =========================
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
# Install dependensi dasar
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
RUN apk add --no-cache git curl bash build-base
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Air (pakai repo baru air-verse)
|
|
||||||
RUN go install github.com/air-verse/air@v1.52.3
|
|
||||||
|
|
||||||
WORKDIR /lti-api
|
|
||||||
|
|
||||||
# Cache dependencies
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Build API binary
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
|
||||||
|
|
||||||
|
# Build SEED binary (pastikan cmd/seed ada)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Runtime stage
|
||||||
|
# =========================
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
|
||||||
|
&& adduser -D -H -u 10001 appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/lti-api /app/lti-api
|
||||||
|
COPY --from=builder /app/lti-seed /app/lti-seed
|
||||||
|
|
||||||
|
USER appuser
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
CMD ["air", "-c", ".air.toml"]
|
CMD ["/app/lti-api"]
|
||||||
|
|||||||
@@ -110,4 +110,4 @@ IT Development PT Mitra Berlian Unggas Group
|
|||||||
|
|
||||||
## 📃 License
|
## 📃 License
|
||||||
|
|
||||||
This project is private. All rights reserved.
|
> This project is private. All rights reserved.
|
||||||
|
|||||||
@@ -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:
|
|
||||||
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgconn v1.14.1
|
github.com/jackc/pgconn v1.14.1
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
@@ -60,7 +61,6 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
|||||||
@@ -262,14 +262,10 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
|||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
|
||||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
|
||||||
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
||||||
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
|||||||
@@ -228,7 +228,13 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case delta > 0:
|
case delta > 0:
|
||||||
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
|
|
||||||
|
var excludedStockables []fifo.StockableKey
|
||||||
|
if cfg.ExcludedStockables != nil {
|
||||||
|
excludedStockables = cfg.ExcludedStockables
|
||||||
|
}
|
||||||
|
|
||||||
|
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -410,8 +416,9 @@ func (s *fifoService) allocateFromStock(
|
|||||||
usableKey fifo.UsableKey,
|
usableKey fifo.UsableKey,
|
||||||
usableID uint,
|
usableID uint,
|
||||||
requestQty float64,
|
requestQty float64,
|
||||||
|
excludedStockables []fifo.StockableKey,
|
||||||
) (*allocationOutcome, error) {
|
) (*allocationOutcome, error) {
|
||||||
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -492,14 +499,24 @@ func (s *fifoService) allocateFromStock(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
|
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint, excludedStockables []fifo.StockableKey) ([]stockLot, error) {
|
||||||
configs := fifo.Stockables()
|
configs := fifo.Stockables()
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create exclusion set for faster lookup
|
||||||
|
excludedSet := make(map[fifo.StockableKey]bool)
|
||||||
|
for _, key := range excludedStockables {
|
||||||
|
excludedSet[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
var lots []stockLot
|
var lots []stockLot
|
||||||
for key, cfg := range configs {
|
for key, cfg := range configs {
|
||||||
|
// Skip excluded stockables
|
||||||
|
if excludedSet[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||||
|
|
||||||
@@ -616,7 +633,13 @@ func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.D
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
|
// Get excluded stockables from candidate usable config
|
||||||
|
var excludedStockables []fifo.StockableKey
|
||||||
|
if candidate.Config.ExcludedStockables != nil {
|
||||||
|
excludedStockables = candidate.Config.ExcludedStockables
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ var (
|
|||||||
SSOAuthorizeURL string
|
SSOAuthorizeURL string
|
||||||
SSOTokenURL string
|
SSOTokenURL string
|
||||||
SSOGetMeURL string
|
SSOGetMeURL string
|
||||||
|
SSOPortalURL string
|
||||||
SSOClients map[string]SSOClientConfig
|
SSOClients map[string]SSOClientConfig
|
||||||
SSOAccessCookieName string
|
SSOAccessCookieName string
|
||||||
SSORefreshCookieName string
|
SSORefreshCookieName string
|
||||||
@@ -131,6 +132,7 @@ func init() {
|
|||||||
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
|
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
|
||||||
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
|
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
|
||||||
SSOGetMeURL = viper.GetString("SSO_GETME_URL")
|
SSOGetMeURL = viper.GetString("SSO_GETME_URL")
|
||||||
|
SSOPortalURL = strings.TrimSpace(viper.GetString("SSO_PORTAL_URL"))
|
||||||
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
|
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
|
||||||
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
|
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
|
||||||
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func FiberConfig() fiber.Config {
|
|||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
ServerHeader: "Fiber",
|
ServerHeader: "Fiber",
|
||||||
AppName: "Fiber API",
|
AppName: "Fiber API",
|
||||||
|
BodyLimit: 8 * 1024 * 1024,
|
||||||
ErrorHandler: utils.ErrorHandler,
|
ErrorHandler: utils.ErrorHandler,
|
||||||
JSONEncoder: sonic.Marshal,
|
JSONEncoder: sonic.Marshal,
|
||||||
JSONDecoder: sonic.Unmarshal,
|
JSONDecoder: sonic.Unmarshal,
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
DROP SEQUENCE IF EXISTS expenses_ref_seq;
|
||||||
DROP TABLE IF EXISTS expenses;
|
DROP TABLE IF EXISTS expenses;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
-- Drop function and sequence for sales order numbers
|
-- Drop function and sequence for sales order numbers
|
||||||
DROP FUNCTION IF EXISTS generate_so_number();
|
|
||||||
DROP SEQUENCE IF EXISTS so_number_seq;
|
DROP SEQUENCE IF EXISTS so_number_seq;
|
||||||
|
DROP FUNCTION IF EXISTS generate_so_number();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
DROP TABLE IF EXISTS daily_checklist_tasks;
|
-- Drop tables in correct order (child tables before parent tables)
|
||||||
|
DROP TABLE IF EXISTS daily_checklist_activity_task_assignments; -- Child table with FK to daily_checklist_activity_tasks
|
||||||
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
|
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
|
||||||
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
|
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
|
||||||
|
DROP TABLE IF EXISTS daily_checklist_tasks;
|
||||||
DROP TABLE IF EXISTS daily_checklist_phases;
|
DROP TABLE IF EXISTS daily_checklist_phases;
|
||||||
DROP TABLE IF EXISTS daily_checklists;
|
DROP TABLE IF EXISTS daily_checklists;
|
||||||
DROP TABLE IF EXISTS checklists;
|
DROP TABLE IF EXISTS checklists;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_recordings_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT fk_recordings_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT fk_recordings_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandangs_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_recordings_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT fk_recordings_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT fk_recordings_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandangs_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Revert back to NO ACTION (RESTRICT behavior)
|
||||||
|
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||||
|
FOREIGN KEY (expense_id) REFERENCES expenses(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
|
-- Revert expense_realizations FK
|
||||||
|
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Drop existing FK constraints
|
||||||
|
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
|
||||||
|
|
||||||
|
-- Recreate with ON DELETE CASCADE
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||||
|
FOREIGN KEY (expense_id) REFERENCES expenses(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Drop and recreate expense_realizations FK
|
||||||
|
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Revert back to NO ACTION (for rollback safety)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE marketing_products DROP CONSTRAINT IF EXISTS fk_marketing_products_marketing_id;
|
||||||
|
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||||
|
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_marketing_product_id;
|
||||||
|
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||||
|
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Ensure marketing_products FK is CASCADE (it should already be, but let's make sure)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Drop existing FK if exists
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_marketing_products_marketing_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE marketing_products DROP CONSTRAINT fk_marketing_products_marketing_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Recreate with ON DELETE CASCADE
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||||
|
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Ensure marketing_delivery_products FK is CASCADE
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Drop existing FK if exists
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_marketing_delivery_products_marketing_product_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP CONSTRAINT fk_marketing_delivery_products_marketing_product_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Recreate with ON DELETE CASCADE
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||||
|
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END $$;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
-- Drop foreign key and column
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS product_warehouse_id;
|
||||||
|
|
||||||
|
-- Drop index
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
-- Add product_warehouse_id to laying_transfers for FIFO support
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN product_warehouse_id BIGINT;
|
||||||
|
|
||||||
|
-- Add foreign key
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add index
|
||||||
|
CREATE INDEX idx_laying_transfers_product_warehouse_id
|
||||||
|
ON laying_transfers(product_warehouse_id);
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- Rollback: Remove STOCKABLE fields from laying_transfers
|
||||||
|
|
||||||
|
-- Drop index
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
|
||||||
|
|
||||||
|
-- Drop foreign key constraint
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id;
|
||||||
|
|
||||||
|
-- Drop columns
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS dest_product_warehouse_id,
|
||||||
|
DROP COLUMN IF EXISTS total_qty,
|
||||||
|
DROP COLUMN IF EXISTS total_used;
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
-- Add STOCKABLE fields to laying_transfers for destination warehouse
|
||||||
|
-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable)
|
||||||
|
|
||||||
|
-- Add columns for STOCKABLE role (destination warehouse)
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN dest_product_warehouse_id BIGINT,
|
||||||
|
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Add foreign key constraint
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
|
||||||
|
FOREIGN KEY (dest_product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add index for performance
|
||||||
|
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
|
||||||
|
ON laying_transfers(dest_product_warehouse_id);
|
||||||
|
|
||||||
|
-- Add comment for documentation
|
||||||
|
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for STOCKABLE role';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Remove chart_data, uniform_date, and related indexes
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_uniform_date;
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP COLUMN IF EXISTS chart_data,
|
||||||
|
DROP COLUMN IF EXISTS uniform_date;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- Add uniform_date (if missing), chart_data, and unique constraint for uniformity records
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD COLUMN IF NOT EXISTS uniform_date TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS chart_data JSONB;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'deleted_at'
|
||||||
|
) THEN
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
|
||||||
|
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
ELSE
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
|
||||||
|
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_uniform_date
|
||||||
|
ON project_flock_kandang_uniformity (uniform_date);
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
-- Remove expense_nonstock_id from stock_transfer_details
|
||||||
|
ALTER TABLE stock_transfer_details DROP CONSTRAINT IF EXISTS fk_stock_transfer_details_expense_nonstock;
|
||||||
|
ALTER TABLE stock_transfer_details DROP COLUMN IF EXISTS expense_nonstock_id;
|
||||||
|
DROP INDEX IF EXISTS idx_stock_transfer_details_expense_nonstock_id;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add expense_nonstock_id to stock_transfer_details
|
||||||
|
-- This allows tracking expedition/transport costs for stock transfers (same as purchase)
|
||||||
|
|
||||||
|
ALTER TABLE stock_transfer_details
|
||||||
|
ADD COLUMN expense_nonstock_id BIGINT,
|
||||||
|
ADD CONSTRAINT fk_stock_transfer_details_expense_nonstock
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Create index for better query performance
|
||||||
|
CREATE INDEX idx_stock_transfer_details_expense_nonstock_id ON stock_transfer_details(expense_nonstock_id);
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE payments
|
||||||
|
DROP COLUMN IF EXISTS party_account_number;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE payments
|
||||||
|
ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS projects;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS projects;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Revert master data foreign keys to CASCADE delete (except FCR)
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
|
||||||
|
REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Update master data foreign keys to RESTRICT delete (except FCR)
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
|
||||||
|
REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
-- Rollback: Revert FIFO fields back to laying_transfers from detail tables
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 1: Remove FIFO columns from detail tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Add back old qty column first
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Now drop FIFO columns
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
DROP COLUMN IF EXISTS usage_qty,
|
||||||
|
DROP COLUMN IF EXISTS pending_usage_qty;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
DROP COLUMN IF EXISTS total_qty,
|
||||||
|
DROP COLUMN IF EXISTS total_used;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 2: Add back FIFO columns to laying_transfers table
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Add columns back for USABLE role (source warehouse)
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN product_warehouse_id BIGINT,
|
||||||
|
ADD COLUMN pending_usage_qty NUMERIC(15, 3),
|
||||||
|
ADD COLUMN usage_qty NUMERIC(15, 3);
|
||||||
|
|
||||||
|
-- Add columns back for STOCKABLE role (destination warehouse)
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN dest_product_warehouse_id BIGINT,
|
||||||
|
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 3: Recreate foreign key constraints
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
-- Add source product warehouse FK
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Add destination product warehouse FK
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
|
||||||
|
FOREIGN KEY (dest_product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 4: Recreate indexes for performance
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfers_product_warehouse_id
|
||||||
|
ON laying_transfers(product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
|
||||||
|
ON laying_transfers(dest_product_warehouse_id);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 5: Recreate comments for documentation
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for FIFO STOCKABLE role';
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
-- Move FIFO fields from laying_transfers to detail tables (sources & targets)
|
||||||
|
-- This enables proper FIFO integration for transfer laying with multiple sources and targets
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 1: Remove FIFO-related columns from laying_transfers table
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Drop foreign key constraints first
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Drop source product warehouse FK
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_laying_transfers_product_warehouse_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT fk_laying_transfers_product_warehouse_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Drop destination product warehouse FK
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_laying_transfers_dest_product_warehouse_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT fk_laying_transfers_dest_product_warehouse_id;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Drop indexes
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
|
||||||
|
|
||||||
|
-- Remove columns from laying_transfers
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS product_warehouse_id,
|
||||||
|
DROP COLUMN IF EXISTS dest_product_warehouse_id,
|
||||||
|
DROP COLUMN IF EXISTS pending_usage_qty,
|
||||||
|
DROP COLUMN IF EXISTS usage_qty,
|
||||||
|
DROP COLUMN IF EXISTS total_qty,
|
||||||
|
DROP COLUMN IF EXISTS total_used;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 2: Add FIFO columns to laying_transfer_sources (USABLE role)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD COLUMN usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN pending_usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
-- Add comments for documentation
|
||||||
|
COMMENT ON COLUMN laying_transfer_sources.usage_qty IS 'Quantity consumed from this source - for FIFO USABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfer_sources.pending_usage_qty IS 'Quantity pending to consume from this source - for FIFO USABLE role';
|
||||||
|
|
||||||
|
-- Drop old qty column as it's replaced by usage_qty
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
DROP COLUMN IF EXISTS qty;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PART 3: Add FIFO columns to laying_transfer_targets (STOCKABLE role)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
-- Add comments for documentation
|
||||||
|
COMMENT ON COLUMN laying_transfer_targets.total_qty IS 'Total lot quantity introduced to this target warehouse - for FIFO STOCKABLE role';
|
||||||
|
COMMENT ON COLUMN laying_transfer_targets.total_used IS 'Quantity already consumed from this lot at target warehouse - for FIFO STOCKABLE role';
|
||||||
|
|
||||||
|
-- Drop old qty column as it's replaced by total_qty
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
DROP COLUMN IF EXISTS qty;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hen_day'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hen_house'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'egg_mass'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3),
|
||||||
|
ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,3);
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
|
||||||
|
(hand_day IS NULL OR hand_day >= 0) AND
|
||||||
|
(hand_house IS NULL OR hand_house >= 0) AND
|
||||||
|
(feed_intake IS NULL OR feed_intake >= 0) AND
|
||||||
|
(egg_mesh IS NULL OR egg_mesh >= 0) AND
|
||||||
|
(egg_weight IS NULL OR egg_weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP COLUMN IF EXISTS daily_gain,
|
||||||
|
DROP COLUMN IF EXISTS avg_daily_gain;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hand_day'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hand_house'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'egg_mesh'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
|
||||||
|
(hen_day IS NULL OR hen_day >= 0) AND
|
||||||
|
(hen_house IS NULL OR hen_house >= 0) AND
|
||||||
|
(feed_intake IS NULL OR feed_intake >= 0) AND
|
||||||
|
(egg_mass IS NULL OR egg_mass >= 0) AND
|
||||||
|
(egg_weight IS NULL OR egg_weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Rollback: remove price from supplier relations
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
DROP COLUMN IF EXISTS price;
|
||||||
|
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
DROP COLUMN IF EXISTS price;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Migration: add price to supplier relations
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP COLUMN IF EXISTS total_used,
|
||||||
|
DROP COLUMN IF EXISTS total_qty;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
UPDATE recording_eggs
|
||||||
|
SET total_qty = qty
|
||||||
|
WHERE total_qty = 0;
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
-- Rollback: add price back to nonstock_suppliers
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
-- Migration: remove price from nonstock_suppliers
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
DROP COLUMN IF EXISTS price;
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
-- Rollback: Remove requested_qty column from laying_transfer_sources table
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
DROP COLUMN IF EXISTS requested_qty;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
-- Add requested_qty column to laying_transfer_sources table
|
||||||
|
-- This field stores the quantity requested by user during create/update
|
||||||
|
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
-- Add comment for documentation
|
||||||
|
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
|
||||||
@@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Tax: tax,
|
Tax: tax,
|
||||||
ExpiryPeriod: seed.Expiry,
|
ExpiryPeriod: seed.Expiry,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
|
IsVisible: seed.IsVisible,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&product).Error; err != nil {
|
if err := tx.Create(&product).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dashboard struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -12,8 +12,6 @@ type LayingTransfer struct {
|
|||||||
FromProjectFlockId uint `gorm:"not null"`
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
ToProjectFlockId uint `gorm:"not null"`
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
TransferDate time.Time `gorm:"type:date;not null"`
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
Notes string `gorm:"type:text"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ type LayingTransferSource struct {
|
|||||||
LayingTransferId uint `gorm:"index;not null"`
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
SourceProjectFlockKandangId uint `gorm:"not null"`
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint `gorm:""`
|
ProductWarehouseId *uint `gorm:""`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
|
||||||
|
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ type LayingTransferTarget struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
LayingTransferId uint `gorm:"index;not null"`
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
TargetProjectFlockKandangId uint `gorm:"not null"`
|
TargetProjectFlockKandangId uint `gorm:"not null"`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
|
||||||
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
|
||||||
ProductWarehouseId *uint `gorm:""`
|
ProductWarehouseId *uint `gorm:""`
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Payment struct {
|
|||||||
TransactionType string `gorm:"type:varchar(50)"`
|
TransactionType string `gorm:"type:varchar(50)"`
|
||||||
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
||||||
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
||||||
|
PartyAccountNumber *string `gorm:"type:varchar(50)"`
|
||||||
PaymentDate time.Time `gorm:"not null"`
|
PaymentDate time.Time `gorm:"not null"`
|
||||||
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
||||||
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Phases struct {
|
|||||||
Category string `gorm:"type:category_code;not null"`
|
Category string `gorm:"type:category_code;not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
ActivityCount int `gorm:"-" json:"-"`
|
||||||
|
|
||||||
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
|
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
IsVisible bool `gorm:"column:is_visible;default:true"`
|
IsVisible bool ``
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "time"
|
|||||||
type ProductSupplier struct {
|
type ProductSupplier struct {
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"not null"`
|
||||||
SupplierId uint `gorm:"not null"`
|
SupplierId uint `gorm:"not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type ProjectFlockKandangUniformity struct {
|
type ProjectFlockKandangUniformity struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
@@ -13,6 +16,7 @@ type ProjectFlockKandangUniformity struct {
|
|||||||
ProjectFlockKandangId uint `gorm:"not null"`
|
ProjectFlockKandangId uint `gorm:"not null"`
|
||||||
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
ChartData json.RawMessage `gorm:"type:jsonb"`
|
||||||
UniformDate *time.Time `gorm:"type:timestamptz"`
|
UniformDate *time.Time `gorm:"type:timestamptz"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ type ProjectFlockKandang struct {
|
|||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestProjectFlockApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
LatestChickinApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ type Recording struct {
|
|||||||
CumIntake *int `gorm:"column:cum_intake"`
|
CumIntake *int `gorm:"column:cum_intake"`
|
||||||
FcrValue *float64 `gorm:"column:fcr_value"`
|
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||||
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
||||||
HandDay *float64 `gorm:"column:hand_day"`
|
HenDay *float64 `gorm:"column:hen_day"`
|
||||||
HandHouse *float64 `gorm:"column:hand_house"`
|
HenHouse *float64 `gorm:"column:hen_house"`
|
||||||
FeedIntake *float64 `gorm:"column:feed_intake"`
|
FeedIntake *float64 `gorm:"column:feed_intake"`
|
||||||
EggMesh *float64 `gorm:"column:egg_mesh"`
|
EggMass *float64 `gorm:"column:egg_mass"`
|
||||||
EggWeight *float64 `gorm:"column:egg_weight"`
|
EggWeight *float64 `gorm:"column:egg_weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
@@ -34,11 +34,11 @@ type Recording struct {
|
|||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
|
||||||
StandardHandDay *float64 `gorm:"-"`
|
StandardHenDay *float64 `gorm:"-"`
|
||||||
StandardHandHouse *float64 `gorm:"-"`
|
StandardHenHouse *float64 `gorm:"-"`
|
||||||
StandardFeedIntake *float64 `gorm:"-"`
|
StandardFeedIntake *float64 `gorm:"-"`
|
||||||
StandardMaxDepletion *float64 `gorm:"-"`
|
StandardMaxDepletion *float64 `gorm:"-"`
|
||||||
StandardEggMesh *float64 `gorm:"-"`
|
StandardEggMass *float64 `gorm:"-"`
|
||||||
StandardEggWeight *float64 `gorm:"-"`
|
StandardEggWeight *float64 `gorm:"-"`
|
||||||
StandardFcr *float64 `gorm:"-"`
|
StandardFcr *float64 `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ type RecordingEgg struct {
|
|||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
Qty int `gorm:"column:qty;not null"`
|
Qty int `gorm:"column:qty;not null"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
TotalUsed float64 `gorm:"column:total_used"`
|
||||||
Weight *float64 `gorm:"column:weight"`
|
Weight *float64 `gorm:"column:weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,19 +8,13 @@ type StockTransferDetail struct {
|
|||||||
StockTransferId uint64
|
StockTransferId uint64
|
||||||
ProductId uint64
|
ProductId uint64
|
||||||
|
|
||||||
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
|
|
||||||
// Tracking stock yang DIAMBIL dari source warehouse
|
|
||||||
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
|
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
|
||||||
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
|
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
|
||||||
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
|
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
|
||||||
|
|
||||||
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) ===
|
|
||||||
// Tracking stock yang DITAMBAHKAN ke destination warehouse
|
|
||||||
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
||||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
||||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
||||||
|
ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"`
|
||||||
// === METADATA ===
|
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
@@ -30,5 +24,6 @@ type StockTransferDetail struct {
|
|||||||
Product *Product `gorm:"foreignKey:ProductId"`
|
Product *Product `gorm:"foreignKey:ProductId"`
|
||||||
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
|
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
|
||||||
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
|
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
|
const (
|
||||||
|
P_DashboardGetAll = "lti.dashboard.list"
|
||||||
|
)
|
||||||
|
|
||||||
// project-flock
|
// project-flock
|
||||||
const (
|
const (
|
||||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
||||||
@@ -24,8 +28,9 @@ const (
|
|||||||
P_ExpenseUpdateOne = "lti.expense.update"
|
P_ExpenseUpdateOne = "lti.expense.update"
|
||||||
P_ExpenseGetOne = "lti.expense.detail"
|
P_ExpenseGetOne = "lti.expense.detail"
|
||||||
P_ExpenseDeleteOne = "lti.expense.delete"
|
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_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
||||||
|
P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president"
|
||||||
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
||||||
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
||||||
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
||||||
@@ -44,7 +49,10 @@ const (
|
|||||||
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
||||||
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
||||||
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
||||||
|
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
|
||||||
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
||||||
|
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
|
||||||
|
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -134,17 +142,17 @@ const (
|
|||||||
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
||||||
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
||||||
|
|
||||||
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
|
P_ProductCategoriesGetAll = "lti.master.product_categories.list"
|
||||||
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
|
P_ProductCategoriesGetOne = "lti.master.product_categories.detail"
|
||||||
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
|
P_ProductCategoriesCreateOne = "lti.master.product_categories.create"
|
||||||
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
|
P_ProductCategoriesUpdateOne = "lti.master.product_categories.update"
|
||||||
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
|
P_ProductCategoriesDeleteOne = "lti.master.product_categories.delete"
|
||||||
|
|
||||||
P_ProductsGetAll = "lti.master.Products.list"
|
P_ProductsGetAll = "lti.master.products.list"
|
||||||
P_ProductsGetOne = "lti.master.Products.detail"
|
P_ProductsGetOne = "lti.master.products.detail"
|
||||||
P_ProductsCreateOne = "lti.master.Products.create"
|
P_ProductsCreateOne = "lti.master.products.create"
|
||||||
P_ProductsUpdateOne = "lti.master.Products.update"
|
P_ProductsUpdateOne = "lti.master.products.update"
|
||||||
P_ProductsDeleteOne = "lti.master.Products.delete"
|
P_ProductsDeleteOne = "lti.master.products.delete"
|
||||||
|
|
||||||
P_SuppliersGetAll = "lti.master.suppliers.list"
|
P_SuppliersGetAll = "lti.master.suppliers.list"
|
||||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
||||||
@@ -207,15 +215,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
P_PurchaseGetAll = "lti.Purchase.list"
|
P_PurchaseGetAll = "lti.purchase.list"
|
||||||
P_PurchaseGetOne = "lti.Purchase.detail"
|
P_PurchaseGetOne = "lti.purchase.detail"
|
||||||
P_PurchaseCreateOne = "lti.Purchase.create"
|
P_PurchaseCreateOne = "lti.purchase.create"
|
||||||
P_PurchaseUpdateOne = "lti.Purchase.update"
|
P_PurchaseUpdateOne = "lti.purchase.update"
|
||||||
P_PurchaseDeleteOne = "lti.Purchase.delete"
|
P_PurchaseDeleteOne = "lti.purchase.delete"
|
||||||
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
|
P_PurchaseItemDeleteOne = "lti.purchase.delete.item"
|
||||||
P_PurchaseReceive = "lti.Purchase.receive"
|
P_PurchaseReceive = "lti.purchase.receive"
|
||||||
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
|
P_PurchaseApprovalStaff = "lti.purchase.approve.staff"
|
||||||
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
|
P_PurchaseApprovalManager = "lti.purchase.approve.manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -232,3 +240,15 @@ const (
|
|||||||
P_UserGetAll = "lti.users.list"
|
P_UserGetAll = "lti.users.list"
|
||||||
P_UserGetOne = "lti.users.detail"
|
P_UserGetOne = "lti.users.detail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// daily-checklist
|
||||||
|
const (
|
||||||
|
P_DailyChecklistDashboardList = "lti.daily_checklist.dashboard.list"
|
||||||
|
P_DailyChecklistCreateOne = "lti.daily_checklist.create"
|
||||||
|
P_DailyChecklistGetAll = "lti.daily_checklist.list"
|
||||||
|
P_DailyChecklistGetOne = "lti.daily_checklist.detail"
|
||||||
|
P_DailyChecklistReports = "lti.daily_checklist.reports"
|
||||||
|
P_DailyChecklistEmployee = "lti.daily_checklist.master_data.employee"
|
||||||
|
P_DailyChecklistActivity = "lti.daily_checklist.master_data.activity"
|
||||||
|
P_DailyChecklistActivityConfig = "lti.daily_checklist.master_data.configuration"
|
||||||
|
)
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ import (
|
|||||||
type ClosingController struct {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
ClosingService service.ClosingService
|
||||||
SapronakService service.SapronakService
|
SapronakService service.SapronakService
|
||||||
|
ClosingKeuanganService service.ClosingKeuanganService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
|
||||||
return &ClosingController{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
ClosingService: closingService,
|
||||||
SapronakService: sapronakService,
|
SapronakService: sapronakService,
|
||||||
|
ClosingKeuanganService: closingKeuanganService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetOverheadByProjectFlockKandang(c *fiber.Ctx) error {
|
||||||
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(projectParam)
|
||||||
|
if err != nil || projectFlockID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(pfkID)
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get overhead by project flock kandang successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||||
param := c.Params("projectFlockId")
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
@@ -86,7 +118,17 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
|
var kandangID *uint
|
||||||
|
if raw := c.Query("kandang_id"); raw != "" {
|
||||||
|
kandangInt, convErr := strconv.Atoi(raw)
|
||||||
|
if convErr != nil || kandangInt <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
kandangUint := uint(kandangInt)
|
||||||
|
kandangID = &kandangUint
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetClosingSummary(c, uint(id), kandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -108,12 +150,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -123,19 +160,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get closing penjualan successfully",
|
Message: "Get closing penjualan successfully",
|
||||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) error {
|
||||||
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(projectParam)
|
||||||
|
if err != nil || projectFlockID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(pfkID)
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), &kandangID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing penjualan by project flock kandang successfully",
|
||||||
|
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
projectFlockID, err := strconv.Atoi(projectParam)
|
||||||
if err != nil {
|
if err != nil || projectFlockID <= 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
var projectFlockKandangID *uint
|
||||||
|
if kandangParam != "" {
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
kandangID := uint(pfkID)
|
||||||
|
projectFlockKandangID = &kandangID
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -162,6 +240,14 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
}
|
}
|
||||||
|
if raw := c.Query("kandang_id"); raw != "" {
|
||||||
|
kandangInt, convErr := strconv.Atoi(raw)
|
||||||
|
if convErr != nil || kandangInt <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
kandangUint := uint(kandangInt)
|
||||||
|
query.KandangID = &kandangUint
|
||||||
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
@@ -247,14 +333,14 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
projectFlockID, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
|
result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -268,6 +354,34 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetClosingKeuanganByKandang(c *fiber.Ctx) error {
|
||||||
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(projectParam)
|
||||||
|
if err != nil || projectFlockID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing keuangan by kandang successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
@@ -338,7 +452,18 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
|
var kandangID *uint
|
||||||
|
if raw := c.Query("kandang_id"); raw != "" {
|
||||||
|
kandangInt, convErr := strconv.Atoi(raw)
|
||||||
|
if convErr != nil || kandangInt <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
kandangUint := uint(kandangInt)
|
||||||
|
kandangID = &kandangUint
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id), kandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,39 +59,65 @@ type ClosingSummaryDTO struct {
|
|||||||
StatusClosing string `json:"closing_status"`
|
StatusClosing string `json:"closing_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClosingSummaryKandangDTO struct {
|
||||||
|
FlockID uint `json:"flock_id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
LocationName string `json:"location_name"`
|
||||||
|
Population int `json:"population"`
|
||||||
|
PopulationFormatted string `json:"population_formatted"`
|
||||||
|
ProjectType string `json:"project_type"`
|
||||||
|
ClosingDate string `json:"closing_date"`
|
||||||
|
KandangName string `json:"kandang_name"`
|
||||||
|
ChickInDate string `json:"chick_in_date"`
|
||||||
|
PicName string `json:"pic_name"`
|
||||||
|
ApprovalDate string `json:"approval_date"`
|
||||||
|
ProjectStatus string `json:"project_status"`
|
||||||
|
}
|
||||||
|
|
||||||
type ClosingPurchaseDTO struct {
|
type ClosingPurchaseDTO struct {
|
||||||
InitialPopulation int `json:"initial_population"`
|
InitialPopulation int `json:"initial_population"`
|
||||||
ClaimCulling int `json:"claim_culling"`
|
ClaimCulling int `json:"claim_culling"`
|
||||||
FinalPopulation int `json:"final_population"`
|
FinalPopulation int `json:"final_population"`
|
||||||
FeedIn float64 `json:"feed_in"`
|
FeedIn float64 `json:"feed_in"`
|
||||||
FeedUsed float64 `json:"feed_used"`
|
FeedUsed float64 `json:"feed_used"`
|
||||||
FeedUsedPerHead float64 `json:"feed_used_per_head"`
|
// FeedUsedPerHead float64 `json:"feed_used_per_head"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingSalesDTO struct {
|
type ClosingSalesDTO struct {
|
||||||
SalesPopulation int `json:"sales_population"`
|
SalesPopulation int `json:"sales_population"`
|
||||||
SalesWeight float64 `json:"sales_weight"`
|
SalesWeight float64 `json:"sales_weight"`
|
||||||
AverageWeight float64 `json:"average_weight"`
|
AverageWeight float64 `json:"avg_weight"`
|
||||||
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
|
AverageSellingPrice float64 `json:"avg_selling_price"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingEggSalesDTO struct {
|
type ClosingEggSalesDTO struct {
|
||||||
EggPieces int `json:"egg_pieces"`
|
EggPieces int `json:"egg_pieces"`
|
||||||
EggMassKg float64 `json:"egg_mass_kg"`
|
EggMassKg float64 `json:"egg_mass"`
|
||||||
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
|
AverageEggWeightKg float64 `json:"avg_egg_weight"`
|
||||||
AverageSellingPrice float64 `json:"egg_average_selling_price"`
|
AverageSellingPrice float64 `json:"avg_selling_price"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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:"mortality_std"`
|
MortalityStd float64 `json:"mor_std"`
|
||||||
MortalityAct float64 `json:"mortality_act"`
|
MortalityAct float64 `json:"mor_act"`
|
||||||
DeffMortality float64 `json:"deff_mortality"`
|
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:"deff_fcr"`
|
DeffFcr float64 `json:"fcr_diff"`
|
||||||
Awg float64 `json:"awg"`
|
AwgAct float64 `json:"awg_act"`
|
||||||
|
AwgStd float64 `json:"awg_std"`
|
||||||
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
|
FeedIntakeStd float64 `json:"feed_intake_std"`
|
||||||
|
HenDayAct *float64 `json:"hen_day_act,omitempty"`
|
||||||
|
HendayStd float64 `json:"hen_day_std"`
|
||||||
|
EggMass *float64 `json:"egg_mass,omitempty"`
|
||||||
|
EggMassStd float64 `json:"egg_mass_std"`
|
||||||
|
EggWeight *float64 `json:"egg_weight,omitempty"`
|
||||||
|
EggWeightStd float64 `json:"egg_weight_std"`
|
||||||
|
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
||||||
|
HenHouseStd float64 `json:"hen_housed_std"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingSalesGroupDTO struct {
|
type ClosingSalesGroupDTO struct {
|
||||||
@@ -164,7 +190,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
|||||||
var total float64
|
var total float64
|
||||||
for _, h := range history {
|
for _, h := range history {
|
||||||
for _, chickin := range h.Chickins {
|
for _, chickin := range h.Chickins {
|
||||||
total += chickin.UsageQty + chickin.PendingUsageQty
|
total += chickin.UsageQty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
|
|||||||
@@ -1,134 +1,103 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
// === CLOSING KEUANGAN CODES ===
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
// Closing HPP Codes
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
type ClosingHPPCode string
|
||||||
)
|
|
||||||
|
|
||||||
// === CONSTANTS ===
|
|
||||||
const (
|
const (
|
||||||
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
HPPCodePakan ClosingHPPCode = "PAKAN"
|
||||||
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
HPPCodeOVK ClosingHPPCode = "OVK"
|
||||||
HPPLabelOverhead = "Pengeluaran Overhead"
|
HPPCodeDOC ClosingHPPCode = "DOC"
|
||||||
HPPLabelEkspedisi = "Beban Ekspedisi"
|
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
|
||||||
HPPSummaryLabel = "HPP"
|
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
|
||||||
|
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
|
||||||
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
|
||||||
PLSalesTypeEgg = "Penjualan Telur"
|
|
||||||
|
|
||||||
PLItemTypeSapronak = "Pembelian Sapronak"
|
|
||||||
PLItemTypeOverhead = "Pengeluaran Overhead"
|
|
||||||
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
|
||||||
|
|
||||||
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
|
||||||
PLSummaryLabelSubTotal = "SUB TOTAL"
|
|
||||||
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
|
||||||
|
|
||||||
PurchaseLabelPrefix = "Pembelian "
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// === CONTEXT STRUCTS ===
|
// Closing Profit Loss Codes
|
||||||
|
type ClosingProfitLossCode string
|
||||||
|
|
||||||
type CalculationContext struct {
|
const (
|
||||||
TotalPopulation float64
|
PLCodeSales ClosingProfitLossCode = "SALES"
|
||||||
TotalWeightProduced float64
|
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
|
||||||
TotalEggWeightKg float64
|
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
|
||||||
TotalDepletion float64
|
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
|
||||||
TotalWeightSold float64
|
)
|
||||||
ActualPopulation float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingKeuanganInput struct {
|
// === NEW CLOSING KEUANGAN DTO ===
|
||||||
ProjectFlockCategory string
|
|
||||||
PurchaseItems []entities.PurchaseItem
|
|
||||||
Budgets []entities.ProjectBudget
|
|
||||||
Realizations []entities.ExpenseRealization
|
|
||||||
DeliveryProducts []entities.MarketingDeliveryProduct
|
|
||||||
Chickins []entities.ProjectChickin
|
|
||||||
TotalWeightProduced float64
|
|
||||||
TotalEggWeightKg float64
|
|
||||||
TotalDepletion float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// === BASE METRICS ===
|
|
||||||
|
|
||||||
|
// FinancialMetrics represents financial metrics with per unit and total amounts
|
||||||
type FinancialMetrics struct {
|
type FinancialMetrics struct {
|
||||||
RpPerBird float64 `json:"rp_per_bird"`
|
RpPerBird float64 `json:"rp_per_bird"`
|
||||||
RpPerKg float64 `json:"rp_per_kg"`
|
RpPerKg float64 `json:"rp_per_kg"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Comparison struct {
|
// HPPItem represents an item in HPP section
|
||||||
|
type HPPItem struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Category string `json:"category"` // "purchase" or "overhead"
|
||||||
|
Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI"
|
||||||
|
Label string `json:"label"`
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
Realization FinancialMetrics `json:"realization"`
|
Realization FinancialMetrics `json:"realization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === HPP PURCHASES PACKAGE ===
|
// HPPSummary represents summary for HPP section
|
||||||
|
type HPPSummary struct {
|
||||||
type HppItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Comparison
|
|
||||||
}
|
|
||||||
|
|
||||||
type HppGroup struct {
|
|
||||||
GroupName string `json:"group_name"`
|
|
||||||
Data []HppItem `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SummaryHpp struct {
|
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Comparison `json:"-"`
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
|
Realization FinancialMetrics `json:"realization"`
|
||||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPurchasesSection struct {
|
// HPPSection represents HPP data section
|
||||||
Hpp []HppGroup `json:"hpp"`
|
type HPPSection struct {
|
||||||
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
Items []HPPItem `json:"items"`
|
||||||
|
Summary HPPSummary `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === PROFIT LOSS PACKAGE ===
|
// ProfitLossItem represents an item in Profit & Loss section
|
||||||
|
type ProfitLossItem struct {
|
||||||
type PLItem struct {
|
Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI"
|
||||||
Type string `json:"type"`
|
|
||||||
FinancialMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
type PLSummaryItem struct {
|
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
FinancialMetrics
|
Type string `json:"type"` // "income", "purchase", "overhead"
|
||||||
|
RpPerBird float64 `json:"rp_per_bird"`
|
||||||
|
RpPerKg float64 `json:"rp_per_kg"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PLSummaryGroup struct {
|
// ProfitLossSummary represents summary for Profit & Loss section
|
||||||
GrossProfit PLSummaryItem `json:"gross_profit"`
|
type ProfitLossSummary struct {
|
||||||
SubTotal PLSummaryItem `json:"sub_total"`
|
GrossProfit FinancialMetrics `json:"gross_profit"`
|
||||||
NetProfit PLSummaryItem `json:"net_profit"`
|
SubTotal FinancialMetrics `json:"sub_total"`
|
||||||
}
|
NetProfit FinancialMetrics `json:"net_profit"`
|
||||||
|
|
||||||
type ProfitLossData struct {
|
|
||||||
Penjualan []PLItem `json:"penjualan"`
|
|
||||||
Pembelian []PLItem `json:"pembelian"`
|
|
||||||
Overhead PLItem `json:"overhead"`
|
|
||||||
Ekspedisi PLItem `json:"ekspedisi"`
|
|
||||||
Summary PLSummaryGroup `json:"summary"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossSection represents Profit & Loss data section
|
||||||
type ProfitLossSection struct {
|
type ProfitLossSection struct {
|
||||||
Data ProfitLossData `json:"data"`
|
Items []ProfitLossItem `json:"items"`
|
||||||
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === RESPONSE DTO (ROOT) ===
|
// ClosingKeuanganData represents the main data structure
|
||||||
|
type ClosingKeuanganData struct {
|
||||||
type ReportResponse struct {
|
HPP HPPSection `json:"hpp"`
|
||||||
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
|
||||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClosingKeuanganResponse represents the full API response
|
||||||
|
type ClosingKeuanganResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data ClosingKeuanganData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
// === MAPPER FUNCTIONS ===
|
// === MAPPER FUNCTIONS ===
|
||||||
|
|
||||||
|
// ToFinancialMetrics creates FinancialMetrics from values
|
||||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
||||||
return FinancialMetrics{
|
return FinancialMetrics{
|
||||||
RpPerBird: rpPerBird,
|
RpPerBird: rpPerBird,
|
||||||
@@ -137,453 +106,80 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
// ToHPPItem creates HPP item
|
||||||
return Comparison{
|
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
|
||||||
|
return HPPItem{
|
||||||
|
ID: id,
|
||||||
|
Category: category,
|
||||||
|
Code: code,
|
||||||
|
Label: label,
|
||||||
Budgeting: budgeting,
|
Budgeting: budgeting,
|
||||||
Realization: realization,
|
Realization: realization,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === HPP PENGELUARAN (from Purchase Items) ===
|
// ToHPPSummary creates HPP summary
|
||||||
|
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
|
||||||
func getFlagLabel(flagType utils.FlagType) string {
|
return HPPSummary{
|
||||||
return PurchaseLabelPrefix + string(flagType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
|
||||||
flags := []utils.FlagType{
|
|
||||||
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
|
|
||||||
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
|
|
||||||
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
|
|
||||||
}
|
|
||||||
|
|
||||||
items := []HppItem{}
|
|
||||||
seenFlags := make(map[utils.FlagType]bool)
|
|
||||||
|
|
||||||
for _, item := range purchaseItems {
|
|
||||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range item.Product.Flags {
|
|
||||||
flagType := utils.FlagType(flag.Name)
|
|
||||||
|
|
||||||
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
|
|
||||||
amount := sumPurchasesByFlag(purchaseItems, flagType)
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
items = append(items, HppItem{
|
|
||||||
Type: getFlagLabel(flagType),
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
|
||||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
seenFlags[flagType] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
|
|
||||||
|
|
||||||
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
|
|
||||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return HppItem{
|
|
||||||
Type: HPPLabelOverhead,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
|
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
|
||||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return HppItem{
|
|
||||||
Type: HPPLabelEkspedisi,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
|
||||||
items := []HppItem{}
|
|
||||||
|
|
||||||
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
|
||||||
|
|
||||||
if budgetAmount > 0 || realizationAmount > 0 {
|
|
||||||
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
|
||||||
|
|
||||||
return HppGroup{
|
|
||||||
GroupName: HPPGroupBahanBaku,
|
|
||||||
Data: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP SUMMARY ===
|
|
||||||
|
|
||||||
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
|
|
||||||
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
|
||||||
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
|
||||||
totalBudget := purchaseTotal + budgetTotal
|
|
||||||
|
|
||||||
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
|
|
||||||
|
|
||||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
summary := SummaryHpp{
|
|
||||||
Label: label,
|
Label: label,
|
||||||
Comparison: ToComparison(
|
Budgeting: budgeting,
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
Realization: realization,
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
EggBudgeting: eggBudgeting,
|
||||||
),
|
EggRealization: eggRealization,
|
||||||
}
|
|
||||||
|
|
||||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
|
||||||
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
|
|
||||||
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
|
|
||||||
|
|
||||||
summary.EggBudgeting = &FinancialMetrics{
|
|
||||||
RpPerBird: 0,
|
|
||||||
RpPerKg: budgetEggRpPerKg,
|
|
||||||
Amount: totalBudget,
|
|
||||||
}
|
|
||||||
summary.EggRealization = &FinancialMetrics{
|
|
||||||
RpPerBird: 0,
|
|
||||||
RpPerKg: realizationEggRpPerKg,
|
|
||||||
Amount: totalRealization,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
|
|
||||||
hppGroups := []HppGroup{
|
|
||||||
{
|
|
||||||
GroupName: HPPGroupPengeluaran,
|
|
||||||
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
|
|
||||||
},
|
|
||||||
ToHppBahanBakuGroup(budgets, realizations, ctx),
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
|
|
||||||
|
|
||||||
return HppPurchasesSection{
|
|
||||||
Hpp: hppGroups,
|
|
||||||
SummaryHpp: summaryHpp,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === PROFIT & LOSS ===
|
// ToHPPSection creates HPP section
|
||||||
|
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
|
||||||
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
|
return HPPSection{
|
||||||
return PLItem{
|
Items: items,
|
||||||
Type: itemType,
|
|
||||||
FinancialMetrics: metrics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
|
|
||||||
return PLSummaryItem{
|
|
||||||
Label: label,
|
|
||||||
FinancialMetrics: metrics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
|
|
||||||
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
|
|
||||||
for _, item := range items {
|
|
||||||
totalAmount += item.Amount
|
|
||||||
totalPerBird += item.RpPerBird
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
|
|
||||||
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
|
|
||||||
items := []PLItem{}
|
|
||||||
|
|
||||||
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
|
|
||||||
|
|
||||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
|
||||||
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
|
|
||||||
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
|
|
||||||
} else {
|
|
||||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
purchaseAmount := sumPurchaseTotal(purchases)
|
|
||||||
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
|
|
||||||
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
|
|
||||||
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
|
|
||||||
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
|
|
||||||
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
|
|
||||||
|
|
||||||
grossProfit := totalPenjualan - totalPembelian
|
|
||||||
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
|
|
||||||
|
|
||||||
totalOtherExpenses := totalOverhead + totalEkspedisi
|
|
||||||
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
|
|
||||||
|
|
||||||
netProfit := grossProfit - totalOtherExpenses
|
|
||||||
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
|
|
||||||
|
|
||||||
return PLSummaryGroup{
|
|
||||||
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
|
|
||||||
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
|
|
||||||
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
|
|
||||||
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
|
||||||
|
|
||||||
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
|
|
||||||
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
|
|
||||||
|
|
||||||
return ProfitLossData{
|
|
||||||
Penjualan: penjualanItems,
|
|
||||||
Pembelian: pembelianItems,
|
|
||||||
Overhead: totalOverhead,
|
|
||||||
Ekspedisi: totalEkspedisi,
|
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
|
// ToProfitLossItem creates Profit & Loss item
|
||||||
return ProfitLossSection{
|
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
|
||||||
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
return ProfitLossItem{
|
||||||
|
Code: code,
|
||||||
|
Label: label,
|
||||||
|
Type: itemType,
|
||||||
|
RpPerBird: rpPerBird,
|
||||||
|
RpPerKg: rpPerKg,
|
||||||
|
Amount: amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggregatePLItems(items []PLItem, label string) PLItem {
|
// ToProfitLossSummary creates Profit & Loss summary
|
||||||
totalAmount, totalPerBird := sumPLItems(items)
|
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
|
||||||
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
return ProfitLossSummary{
|
||||||
|
GrossProfit: grossProfit,
|
||||||
|
SubTotal: subTotal,
|
||||||
|
NetProfit: netProfit,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
// ToProfitLossSection creates Profit & Loss section
|
||||||
return ReportResponse{
|
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
|
||||||
HppPurchases: hppPurchases,
|
return ProfitLossSection{
|
||||||
|
Items: items,
|
||||||
|
Summary: summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToClosingKeuanganData creates complete closing keuangan data
|
||||||
|
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
|
||||||
|
return ClosingKeuanganData{
|
||||||
|
HPP: hpp,
|
||||||
ProfitLoss: profitLoss,
|
ProfitLoss: profitLoss,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
// ToSuccessClosingKeuanganResponse creates success response
|
||||||
var totalPopulation float64
|
func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse {
|
||||||
var totalWeightSold float64
|
return ClosingKeuanganResponse{
|
||||||
|
Code: 200,
|
||||||
for _, chickin := range input.Chickins {
|
Status: "success",
|
||||||
totalPopulation += chickin.UsageQty
|
Message: "Get closing keuangan successfully",
|
||||||
}
|
Data: data,
|
||||||
|
|
||||||
for _, delivery := range input.DeliveryProducts {
|
|
||||||
totalWeightSold += delivery.TotalWeight
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := CalculationContext{
|
|
||||||
TotalPopulation: totalPopulation,
|
|
||||||
TotalWeightProduced: input.TotalWeightProduced,
|
|
||||||
TotalEggWeightKg: input.TotalEggWeightKg,
|
|
||||||
TotalDepletion: input.TotalDepletion,
|
|
||||||
TotalWeightSold: totalWeightSold,
|
|
||||||
ActualPopulation: totalPopulation - input.TotalDepletion,
|
|
||||||
}
|
|
||||||
|
|
||||||
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
|
|
||||||
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
|
|
||||||
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
|
|
||||||
overheadItems := ToOverheadItems(input.Realizations, ctx)
|
|
||||||
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
|
|
||||||
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
|
||||||
|
|
||||||
return ToReportResponse(hppSection, plSection)
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HELPER FUNCTIONS ===
|
|
||||||
|
|
||||||
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
|
|
||||||
if totalPopulation > 0 {
|
|
||||||
rpPerBird = amount / totalPopulation
|
|
||||||
}
|
|
||||||
if totalWeightSold > 0 {
|
|
||||||
rpPerKg = amount / totalWeightSold
|
|
||||||
}
|
|
||||||
return rpPerBird, rpPerKg
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
|
|
||||||
for _, flag := range flags {
|
|
||||||
if strings.ToUpper(flag.Name) == string(flagType) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
|
|
||||||
return func(item *entities.PurchaseItem) bool {
|
|
||||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasProductFlag(item.Product.Flags, flagType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
|
||||||
return func(realization *entities.ExpenseRealization) bool {
|
|
||||||
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
|
||||||
hasFlag := filterRealizationByNonstockFlag(flagType)
|
|
||||||
return func(realization *entities.ExpenseRealization) bool {
|
|
||||||
return !hasFlag(realization)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
|
|
||||||
amount := 0.0
|
|
||||||
for i := range items {
|
|
||||||
if filter(&items[i]) {
|
|
||||||
amount += extractor(&items[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
|
|
||||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
|
|
||||||
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
|
|
||||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
|
|
||||||
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
|
|
||||||
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
|
|
||||||
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isChickenProductFlag(flagType utils.FlagType) bool {
|
|
||||||
switch flagType {
|
|
||||||
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
|
|
||||||
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEggProductFlag(flagType utils.FlagType) bool {
|
|
||||||
switch flagType {
|
|
||||||
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
|
|
||||||
utils.FlagTelurPutih, utils.FlagTelurRetak:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSalesTypeFromProductFlags(product *entities.Product) string {
|
|
||||||
if product == nil || len(product.Flags) == 0 {
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range product.Flags {
|
|
||||||
flagType := utils.FlagType(strings.ToUpper(flag.Name))
|
|
||||||
|
|
||||||
if isEggProductFlag(flagType) {
|
|
||||||
return PLSalesTypeEgg
|
|
||||||
}
|
|
||||||
if isChickenProductFlag(flagType) {
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
|
|
||||||
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
|
|
||||||
categorized := make(map[string][]entities.MarketingDeliveryProduct)
|
|
||||||
|
|
||||||
for _, delivery := range deliveries {
|
|
||||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
|
||||||
salesType := getSalesTypeFromProductFlags(&product)
|
|
||||||
|
|
||||||
categorized[salesType] = append(categorized[salesType], delivery)
|
|
||||||
}
|
|
||||||
|
|
||||||
return categorized
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
|
|
||||||
amount := 0.0
|
|
||||||
for _, delivery := range deliveries {
|
|
||||||
amount += delivery.TotalPrice
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,16 +55,21 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|||||||
kandang = &mapped
|
kandang = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var realizationDate time.Time
|
||||||
|
if e.DeliveryDate != nil {
|
||||||
|
realizationDate = *e.DeliveryDate
|
||||||
|
}
|
||||||
|
|
||||||
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: *e.DeliveryDate,
|
RealizationDate: realizationDate,
|
||||||
Age: age,
|
Age: age,
|
||||||
DoNumber: doNumber,
|
DoNumber: doNumber,
|
||||||
Product: product,
|
Product: product,
|
||||||
Customer: customer,
|
Customer: customer,
|
||||||
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
Qty: e.UsageQty,
|
||||||
Weight: e.TotalWeight,
|
Weight: e.TotalWeight,
|
||||||
AvgWeight: e.AvgWeight,
|
AvgWeight: e.AvgWeight,
|
||||||
Price: e.UnitPrice,
|
Price: e.UnitPrice,
|
||||||
@@ -82,7 +87,7 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
|
|
||||||
return PenjualanRealisasiResponseDTO{
|
return PenjualanRealisasiResponseDTO{
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
|
|||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
|
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
|
||||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||||
latestDateByNonstockID := make(map[uint]string)
|
latestDateByNonstockID := make(map[uint]string)
|
||||||
|
|
||||||
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
|||||||
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
|
||||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
budgetQty := budgets[i].Qty
|
||||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
budgetPrice := budgets[i].Price
|
||||||
|
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||||
|
|
||||||
|
// Budget division: per kandang view only
|
||||||
|
if isPerKandang && totalKandangCount > 0 {
|
||||||
|
budgetQty = budgetQty / float64(totalKandangCount)
|
||||||
|
budgetTotal = budgetTotal / float64(totalKandangCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range realizations {
|
for i := range realizations {
|
||||||
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
|||||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
}
|
}
|
||||||
|
|
||||||
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
qty := realizations[i].Qty
|
||||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||||
|
|
||||||
|
// Farm-level expense division
|
||||||
|
if realizations[i].ExpenseNonstock.Expense != nil &&
|
||||||
|
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
|
||||||
|
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
|
||||||
|
|
||||||
|
if len(projectFlockIDs) > 0 {
|
||||||
|
totalKandangInAllProjects := 0
|
||||||
|
for _, pfID := range projectFlockIDs {
|
||||||
|
if count, exists := projectFlockKandangCountMap[pfID]; exists {
|
||||||
|
totalKandangInAllProjects += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalKandangInAllProjects > 0 {
|
||||||
|
if isPerKandang {
|
||||||
|
qty = qty / float64(totalKandangInAllProjects)
|
||||||
|
totalAmount = totalAmount / float64(totalKandangInAllProjects)
|
||||||
|
} else {
|
||||||
|
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
|
||||||
|
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
|
||||||
|
perKandangQty := qty / float64(totalKandangInAllProjects)
|
||||||
|
|
||||||
|
qty = perKandangQty * float64(totalKandangCount)
|
||||||
|
totalAmount = perKandangAmount * float64(totalKandangCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].ActualQuantity += qty
|
||||||
|
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
|
||||||
|
|
||||||
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||||
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||||
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Helper Functions ===
|
func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
|
||||||
|
if projectFlockJSON == "" {
|
||||||
|
return []uint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlocks []uint
|
||||||
|
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
|
||||||
|
return []uint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectFlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
func countProjectFlocksInJSON(projectFlockJSON string) int {
|
||||||
|
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
|
||||||
|
if len(projectFlocks) == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return len(projectFlocks)
|
||||||
|
}
|
||||||
|
|
||||||
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||||
if nonstock != nil && nonstock.Id != 0 {
|
if nonstock != nil && nonstock.Id != 0 {
|
||||||
|
|||||||
@@ -134,7 +134,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
report = &SapronakReportDTO{}
|
report = &SapronakReportDTO{}
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := strings.ToUpper(strings.TrimSpace(flag))
|
normalizeFlag := func(raw string) string {
|
||||||
|
normalized := strings.ToUpper(strings.TrimSpace(raw))
|
||||||
|
if normalized == "PULLET" {
|
||||||
|
return "DOC"
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
filter := normalizeFlag(flag)
|
||||||
|
|
||||||
byFlag := map[string]**SapronakCategoryDTO{}
|
byFlag := map[string]**SapronakCategoryDTO{}
|
||||||
if filter == "" || filter == "DOC" {
|
if filter == "" || filter == "DOC" {
|
||||||
@@ -149,10 +156,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
byFlag["PAKAN"] = &result.Pakan
|
byFlag["PAKAN"] = &result.Pakan
|
||||||
}
|
}
|
||||||
if filter == "" || filter == "PULLET" {
|
|
||||||
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
|
||||||
byFlag["PULLET"] = &result.Pullet
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate := func(t *time.Time) string {
|
formatDate := func(t *time.Time) string {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
@@ -162,7 +165,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range report.Groups {
|
for _, group := range report.Groups {
|
||||||
flagKey := strings.ToUpper(group.Flag)
|
flagKey := normalizeFlag(group.Flag)
|
||||||
ptr := byFlag[flagKey]
|
ptr := byFlag[flagKey]
|
||||||
if ptr == nil || *ptr == nil {
|
if ptr == nil || *ptr == nil {
|
||||||
continue
|
continue
|
||||||
@@ -182,7 +185,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
for idx, item := range group.Items {
|
for idx, item := range group.Items {
|
||||||
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
productKey := strings.ToUpper(flagKey + "|" + item.ProductName)
|
||||||
baseRow := SapronakCategoryRowDTO{
|
baseRow := SapronakCategoryRowDTO{
|
||||||
ID: idx + 1,
|
ID: idx + 1,
|
||||||
Date: formatDate(item.Tanggal),
|
Date: formatDate(item.Tanggal),
|
||||||
@@ -246,7 +249,5 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
buildTotals(result.Doc, "TOTAL DOC")
|
buildTotals(result.Doc, "TOTAL DOC")
|
||||||
buildTotals(result.Ovk, "TOTAL OVK")
|
buildTotals(result.Ovk, "TOTAL OVK")
|
||||||
buildTotals(result.Pakan, "TOTAL PAKAN")
|
buildTotals(result.Pakan, "TOTAL PAKAN")
|
||||||
buildTotals(result.Pullet, "TOTAL PULLET")
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
@@ -24,6 +25,7 @@ type ClosingModule struct{}
|
|||||||
|
|
||||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
closingRepo := rClosing.NewClosingRepository(db)
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
|
closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
@@ -33,13 +35,16 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||||
chickinRepo := rChickin.NewChickinRepository(db)
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||||
|
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||||
|
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
||||||
|
closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo)
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ type ClosingRepository interface {
|
|||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
||||||
|
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
|
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
|
||||||
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
||||||
@@ -31,7 +33,6 @@ type ClosingRepository interface {
|
|||||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
|
|
||||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +166,23 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
|
|||||||
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
|
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProjectChickin{}).
|
||||||
|
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Select("COALESCE(SUM(usage_qty), 0)").
|
||||||
|
Scan(&total).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
@@ -299,6 +317,7 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo
|
|||||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||||
Where("e.category = ?", "BOP").
|
Where("e.category = ?", "BOP").
|
||||||
|
Where("e.realization_date IS NOT NULL").
|
||||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
||||||
|
|
||||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
||||||
@@ -454,8 +473,8 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
COALESCE(fw.name, '') AS source_warehouse,
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
'' AS destination_warehouse,
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
COALESCE(tw.name, '') AS destination,
|
'' AS destination,
|
||||||
std.usage_qty AS quantity,
|
std.usage_qty AS quantity,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
'Transfer to other unit' AS notes
|
'Transfer to other unit' AS notes
|
||||||
@@ -503,8 +522,8 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
w.name AS source_warehouse,
|
w.name AS source_warehouse,
|
||||||
'' AS destination_warehouse,
|
'RETAIL CUSTOMER' AS destination_warehouse,
|
||||||
'RETAIL CUSTOMER' AS destination,
|
'' AS destination,
|
||||||
mp.qty AS quantity,
|
mp.qty AS quantity,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
m.notes AS notes
|
m.notes AS notes
|
||||||
@@ -515,6 +534,13 @@ JOIN products prod ON prod.id = pw.product_id
|
|||||||
JOIN uoms u ON u.id = prod.uom_id
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
WHERE pw.project_flock_kandang_id IN ?
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_id = pw.product_id
|
||||||
|
AND f.flagable_type = 'products'
|
||||||
|
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
|
||||||
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -870,146 +896,58 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
|
incomingQuery := r.withCtx(ctx).
|
||||||
|
Table("stock_transfer_details AS std").
|
||||||
|
Select(`
|
||||||
|
std.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
st.transfer_date::timestamp AS date,
|
||||||
|
COALESCE(st.movement_number, '') AS reference,
|
||||||
|
COALESCE(std.total_qty, 0) AS qty_in,
|
||||||
|
0 AS qty_out,
|
||||||
|
COALESCE(p.product_price, 0) AS price
|
||||||
|
`).
|
||||||
|
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll)
|
||||||
|
incoming, err := scanAndGroupDetails(incomingQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
|
||||||
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
|
||||||
return ref
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("TRF-%d", row.ID)
|
|
||||||
})
|
|
||||||
return in, out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActualUsageCostRow struct {
|
outgoingQuery := r.withCtx(ctx).
|
||||||
ProductID uint `gorm:"column:product_id"`
|
Table("stock_allocations AS sa").
|
||||||
ProductName string `gorm:"column:product_name"`
|
|
||||||
FlagName string `gorm:"column:flag_name"`
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
AveragePrice float64 `gorm:"column:average_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return []ActualUsageCostRow{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
db := r.DB().WithContext(ctx)
|
|
||||||
|
|
||||||
// Get all project flock kandang IDs for this project flock
|
|
||||||
var pfkIDs []uint
|
|
||||||
err := db.Table("project_flock_kandangs").
|
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
|
||||||
Pluck("id", &pfkIDs).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pfkIDs) == 0 {
|
|
||||||
return []ActualUsageCostRow{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []ActualUsageCostRow
|
|
||||||
|
|
||||||
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll)
|
|
||||||
purchaseStockableKey := "PURCHASE_ITEMS"
|
|
||||||
transferStockableKey := "STOCK_TRANSFER_DETAILS"
|
|
||||||
|
|
||||||
recordingQuery := db.
|
|
||||||
Table("recordings AS r").
|
|
||||||
Select(`
|
Select(`
|
||||||
pw.product_id AS product_id,
|
std.product_id AS product_id,
|
||||||
p.name AS product_name,
|
p.name AS product_name,
|
||||||
COALESCE(f.name, tf.name) AS flag_name,
|
f.name AS flag,
|
||||||
COALESCE(SUM(
|
st.transfer_date::timestamp AS date,
|
||||||
CASE
|
COALESCE(st.movement_number, '') AS reference,
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
0 AS qty_in,
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||||
ELSE 0
|
COALESCE(p.product_price, 0) AS price
|
||||||
END
|
|
||||||
), 0) AS total_qty,
|
|
||||||
COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0) AS total_price,
|
|
||||||
COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0) AS qty_divisor,
|
|
||||||
COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0) / NULLIF(COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0), 0) AS average_price`,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey).
|
|
||||||
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
|
||||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
|
||||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
|
||||||
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?",
|
|
||||||
"recording_stocks", entity.StockAllocationStatusActive).
|
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
|
||||||
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
|
|
||||||
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
|
|
||||||
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
|
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("r.project_flock_kandangs_id IN ?", pfkIDs).
|
|
||||||
Where("r.deleted_at IS NULL").
|
|
||||||
Group("pw.product_id, p.name, COALESCE(f.name, tf.name)")
|
|
||||||
|
|
||||||
if err := recordingQuery.Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Part 2: Get usage from project_chickins (DOC, Pullet)
|
|
||||||
chickinQuery := db.
|
|
||||||
Table("project_chickins AS pc").
|
|
||||||
Select(`
|
|
||||||
pw.product_id AS product_id,
|
|
||||||
p.name AS product_name,
|
|
||||||
f.name AS flag_name,
|
|
||||||
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
|
|
||||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
|
|
||||||
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
|
|
||||||
`).
|
`).
|
||||||
Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id").
|
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
|
||||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
|
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
Where("pc.project_flock_kandang_id IN ?", pfkIDs).
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
Where("pc.usage_qty > 0").
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Group("pw.product_id, p.name, f.name")
|
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
var chickinRows []ActualUsageCostRow
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
if err := chickinQuery.Scan(&chickinRows).Error; err != nil {
|
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
||||||
return nil, err
|
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge results
|
return incoming, outgoing, nil
|
||||||
rows = append(rows, chickinRows...)
|
|
||||||
|
|
||||||
return rows, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClosingKeuanganRepository handles database operations for closing keuangan
|
||||||
|
type ClosingKeuanganRepository interface {
|
||||||
|
repository.BaseRepository[interface{}]
|
||||||
|
|
||||||
|
// All Product Usage
|
||||||
|
GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error)
|
||||||
|
|
||||||
|
// Depletion per kandang
|
||||||
|
GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
|
|
||||||
|
// Weight produced from uniformity per kandang
|
||||||
|
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
|
|
||||||
|
// DB returns the underlying GORM DB instance
|
||||||
|
DB() *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingKeuanganRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[interface{}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository {
|
||||||
|
return &ClosingKeuanganRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result Rows
|
||||||
|
|
||||||
|
type ProductUsageRow struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
FlagNames string `gorm:"column:flag_names"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
TotalPengeluaran float64 `gorm:"column:total_pengeluaran"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang
|
||||||
|
// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments
|
||||||
|
// flagFilters: optional filter to get only specific flags (e.g., ["PAKAN", "OVK"]), empty means get all
|
||||||
|
func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return []ProductUsageRow{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubQueryResult struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AggregatedResult struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
PriceCount int `gorm:"-"` // For calculating average price
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlagResult struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
FlagNames string `gorm:"column:flag_names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allResults []SubQueryResult
|
||||||
|
|
||||||
|
// Subquery 1: Recordings
|
||||||
|
var recordingsResults []SubQueryResult
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("recordings r").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(CASE "+
|
||||||
|
"WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(sa.qty, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'STOCK_TRANSFER_IN' THEN COALESCE(std.usage_qty, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'TRANSFERTOLAYING_IN' THEN COALESCE(ltt.total_used, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN COALESCE(adjs.total_used, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'PROJECT_FLOCK_POPULATION' THEN COALESCE(pfp.total_used_qty, 0) "+
|
||||||
|
"ELSE 0 END), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(CASE WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(pi.price, 0) END), 0) as price").
|
||||||
|
Joins("JOIN recording_stocks rs ON rs.recording_id = r.id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'").
|
||||||
|
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'").
|
||||||
|
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'").
|
||||||
|
Joins("LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'").
|
||||||
|
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = 'PROJECT_FLOCK_POPULATION'").
|
||||||
|
Where("r.project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&recordingsResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get recordings product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Recordings query: %d results for projectFlockKandangID=%d\n", len(recordingsResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, recordingsResults...)
|
||||||
|
|
||||||
|
// Subquery 2: Chickins
|
||||||
|
var chickinsResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("project_chickins pc").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(pc.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("pc.usage_qty > 0").
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&chickinsResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get chickins product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Chickins query: %d results for projectFlockKandangID=%d\n", len(chickinsResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, chickinsResults...)
|
||||||
|
|
||||||
|
// Subquery 3: Marketing Delivery
|
||||||
|
var marketingResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("marketing_delivery_products mdp").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(mdp.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&marketingResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get marketing product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Marketing query: %d results for projectFlockKandangID=%d\n", len(marketingResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, marketingResults...)
|
||||||
|
|
||||||
|
// Subquery 4: Laying Transfer Sources
|
||||||
|
var layingTransferResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("laying_transfer_sources lts").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(lts.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&layingTransferResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get laying transfer product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Laying Transfer query: %d results for projectFlockKandangID=%d\n", len(layingTransferResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, layingTransferResults...)
|
||||||
|
|
||||||
|
// Subquery 5: Stock Transfer Details
|
||||||
|
var stockTransferResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("stock_transfer_details std").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(std.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&stockTransferResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get stock transfer product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Stock Transfer query: %d results for projectFlockKandangID=%d\n", len(stockTransferResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, stockTransferResults...)
|
||||||
|
|
||||||
|
// Subquery 6: Adjustment Stocks
|
||||||
|
var adjustmentResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("adjustment_stocks ads").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(ads.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("ads.usage_qty > 0").
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&adjustmentResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get adjustment product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Adjustment query: %d results for projectFlockKandangID=%d\n", len(adjustmentResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, adjustmentResults...)
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] Total raw results before aggregation: %d items\n", len(allResults))
|
||||||
|
|
||||||
|
// Aggregate results by product_id
|
||||||
|
aggregatedMap := make(map[uint]*AggregatedResult)
|
||||||
|
for _, result := range allResults {
|
||||||
|
key := result.ProductID
|
||||||
|
if existing, exists := aggregatedMap[key]; exists {
|
||||||
|
existing.TotalQty += result.TotalQty
|
||||||
|
existing.Price += result.Price
|
||||||
|
existing.PriceCount++
|
||||||
|
} else {
|
||||||
|
aggregatedMap[key] = &AggregatedResult{
|
||||||
|
ProductID: result.ProductID,
|
||||||
|
ProductName: result.ProductName,
|
||||||
|
TotalQty: result.TotalQty,
|
||||||
|
Price: result.Price,
|
||||||
|
PriceCount: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] Aggregated to %d unique products\n", len(aggregatedMap))
|
||||||
|
|
||||||
|
// Get flags for all products
|
||||||
|
productIDs := make([]uint, 0, len(aggregatedMap))
|
||||||
|
for id := range aggregatedMap {
|
||||||
|
productIDs = append(productIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagResults []FlagResult
|
||||||
|
if len(productIDs) > 0 {
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("products p").
|
||||||
|
Select("p.id as product_id, STRING_AGG(DISTINCT f.name, ', ') as flag_names").
|
||||||
|
Joins("LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id").
|
||||||
|
Where("p.id IN ?", productIDs).
|
||||||
|
Group("p.id").
|
||||||
|
Scan(&flagResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product flags: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Fetched flags for %d products\n", len(flagResults))
|
||||||
|
|
||||||
|
// Build flag map
|
||||||
|
flagMap := make(map[uint]string)
|
||||||
|
for _, flag := range flagResults {
|
||||||
|
flagMap[flag.ProductID] = flag.FlagNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine results and calculate average price
|
||||||
|
results := make([]ProductUsageRow, 0, len(aggregatedMap))
|
||||||
|
for _, agg := range aggregatedMap {
|
||||||
|
avgPrice := float64(0)
|
||||||
|
if agg.PriceCount > 0 {
|
||||||
|
avgPrice = agg.Price / float64(agg.PriceCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
flagNames := flagMap[agg.ProductID]
|
||||||
|
|
||||||
|
// Apply flag filters if provided
|
||||||
|
if len(flagFilters) > 0 {
|
||||||
|
// Check if any of the flagFilters exist in flagNames
|
||||||
|
matched := false
|
||||||
|
for _, filter := range flagFilters {
|
||||||
|
if containsIgnoreCase(flagNames, filter) {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
continue // Skip this product if no flag matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, ProductUsageRow{
|
||||||
|
ProductID: agg.ProductID,
|
||||||
|
ProductName: agg.ProductName,
|
||||||
|
FlagNames: flagNames,
|
||||||
|
TotalQty: agg.TotalQty,
|
||||||
|
Price: avgPrice,
|
||||||
|
TotalPengeluaran: agg.TotalQty * avgPrice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] After filtering with flagFilters=%v: %d results\n", flagFilters, len(results))
|
||||||
|
for i, r := range results {
|
||||||
|
fmt.Printf("[REPO] Result[%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f\n",
|
||||||
|
i, r.ProductID, r.ProductName, r.FlagNames, r.TotalQty, r.Price, r.TotalPengeluaran)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by product name
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].ProductName < results[j].ProductName
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] Final sorted results: %d items\n", len(results))
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalDepletionByProjectFlockKandangID gets total depletion for a specific kandang
|
||||||
|
func (r *ClosingKeuanganRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
|
var result float64
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_depletions").
|
||||||
|
Select("COALESCE(SUM(recording_depletions.qty), 0)").
|
||||||
|
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
|
||||||
|
Where("project_flock_kandangs.id = ?", projectFlockKandangID).
|
||||||
|
Scan(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalWeightProducedFromUniformityByProjectFlockKandangID calculates total weight produced from uniformity data for a specific kandang
|
||||||
|
// Formula: (mean_up / 1.10) * chick_qty_of_weight / 1000
|
||||||
|
func (r *ClosingKeuanganRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniformity struct {
|
||||||
|
MeanUp float64
|
||||||
|
ChickQtyOfWeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("project_flock_kandang_uniformity").
|
||||||
|
Select("mean_up, chick_qty_of_weight").
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Order("id DESC").
|
||||||
|
Limit(1).
|
||||||
|
Scan(&uniformity).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate weight: (mean_up / 1.10) * chick_qty_of_weight / 1000
|
||||||
|
totalWeight := (uniformity.MeanUp / 1.10) * uniformity.ChickQtyOfWeight / 1000
|
||||||
|
|
||||||
|
return totalWeight, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsIgnoreCase checks if a string contains a substring (case-insensitive)
|
||||||
|
func containsIgnoreCase(str, substr string) bool {
|
||||||
|
return strings.Contains(strings.ToUpper(str), strings.ToUpper(substr))
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
|
||||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closings")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
@@ -23,8 +23,10 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
|
|
||||||
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
||||||
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
|
||||||
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
||||||
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
||||||
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
||||||
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
||||||
@@ -32,4 +34,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
||||||
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
||||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,6 +18,7 @@ import (
|
|||||||
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
@@ -32,12 +35,11 @@ import (
|
|||||||
type ClosingService interface {
|
type ClosingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
||||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
||||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
|
||||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ type closingService struct {
|
|||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ClosingRepository
|
Repository repository.ClosingRepository
|
||||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
MarketingRepo marketingRepository.MarketingRepository
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
@@ -54,14 +57,17 @@ type closingService struct {
|
|||||||
ChickinRepo chickinRepository.ProjectChickinRepository
|
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||||
PurchaseRepo purchaseRepository.PurchaseRepository
|
PurchaseRepo purchaseRepository.PurchaseRepository
|
||||||
RecordingRepo recordingRepository.RecordingRepository
|
RecordingRepo recordingRepository.RecordingRepository
|
||||||
|
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
|
||||||
|
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
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, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
|
||||||
return &closingService{
|
return &closingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
ProjectFlockRepo: projectFlockRepo,
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
@@ -70,6 +76,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
|||||||
ChickinRepo: chickinRepo,
|
ChickinRepo: chickinRepo,
|
||||||
PurchaseRepo: purchaseRepo,
|
PurchaseRepo: purchaseRepo,
|
||||||
RecordingRepo: recordingRepo,
|
RecordingRepo: recordingRepo,
|
||||||
|
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||||
|
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +102,7 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
|
|||||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withClosingRelations(db)
|
db = s.withClosingRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
|
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -129,38 +137,28 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
|
|||||||
return projectFlock, nil
|
return projectFlock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
|
||||||
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
|
||||||
return db.
|
|
||||||
Preload("MarketingProduct").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
|
||||||
Preload("MarketingProduct.Marketing").
|
|
||||||
Preload("MarketingProduct.Marketing.Customer").
|
|
||||||
Order("marketing_delivery_products.delivery_date DESC")
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(realisasi) == 0 {
|
if len(realisasi) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
|
return []entity.MarketingDeliveryProduct{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return realisasi, nil
|
return realisasi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if kandangID != nil {
|
||||||
|
return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID)
|
||||||
|
}
|
||||||
|
|
||||||
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||||
@@ -181,6 +179,124 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
|
|||||||
return &summary, nil
|
return &summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) {
|
||||||
|
if projectFlockID == 0 || kandangID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
var kandang entity.ProjectFlockKandang
|
||||||
|
if err := db.
|
||||||
|
Preload("Kandang").
|
||||||
|
Preload("Kandang.Location").
|
||||||
|
Preload("Kandang.Pic").
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Where("kandang_id = ?", kandangID).
|
||||||
|
First(&kandang).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
var project entity.ProjectFlock
|
||||||
|
if err := db.
|
||||||
|
Select("id", "category").
|
||||||
|
First(&project, projectFlockID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
var population float64
|
||||||
|
if err := db.
|
||||||
|
Table("project_flock_populations pfp").
|
||||||
|
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||||
|
Where("pc.project_flock_kandang_id = ?", kandang.Id).
|
||||||
|
Select("COALESCE(SUM(pfp.total_qty), 0)").
|
||||||
|
Scan(&population).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
|
||||||
|
}
|
||||||
|
|
||||||
|
var chickInDate time.Time
|
||||||
|
if err := db.
|
||||||
|
Table("project_chickins").
|
||||||
|
Where("project_flock_kandang_id = ?", kandang.Id).
|
||||||
|
Select("MIN(chick_in_date)").
|
||||||
|
Scan(&chickInDate).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusProject := "Belum Selesai"
|
||||||
|
var approvalDate string
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
minStep uint16
|
||||||
|
latestActionAt time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
if minStep == 0 || rec.StepNumber < minStep {
|
||||||
|
minStep = rec.StepNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) {
|
||||||
|
latestActionAt = rec.ActionAt
|
||||||
|
statusProject = rec.StepName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusProject == "" && minStep > 0 {
|
||||||
|
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok {
|
||||||
|
statusProject = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !latestActionAt.IsZero() {
|
||||||
|
approvalDate = latestActionAt.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closingDate := ""
|
||||||
|
if kandang.ClosedAt != nil {
|
||||||
|
closingDate = kandang.ClosedAt.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
chickInDateStr := ""
|
||||||
|
if !chickInDate.IsZero() {
|
||||||
|
chickInDateStr = chickInDate.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
populationInt := int(population)
|
||||||
|
|
||||||
|
return &dto.ClosingSummaryKandangDTO{
|
||||||
|
FlockID: projectFlockID,
|
||||||
|
Period: kandang.Period,
|
||||||
|
LocationName: kandang.Kandang.Location.Name,
|
||||||
|
Population: populationInt,
|
||||||
|
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||||
|
ProjectType: project.Category,
|
||||||
|
ClosingDate: closingDate,
|
||||||
|
KandangName: kandang.Kandang.Name,
|
||||||
|
ChickInDate: chickInDateStr,
|
||||||
|
PicName: kandang.Kandang.Pic.Name,
|
||||||
|
ApprovalDate: approvalDate,
|
||||||
|
ProjectStatus: statusProject,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
@@ -220,7 +336,9 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var projectFlockKandangIDs []uint
|
var projectFlockKandangIDs []uint
|
||||||
if params.Type == validation.SapronakTypeOutgoing {
|
if params.KandangID != nil && *params.KandangID > 0 {
|
||||||
|
projectFlockKandangIDs = []uint{*params.KandangID}
|
||||||
|
} else if params.Type == validation.SapronakTypeOutgoing {
|
||||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||||
@@ -303,10 +421,10 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
|
|||||||
|
|
||||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
var ids []uint
|
var ids []uint
|
||||||
err := s.Repository.DB().WithContext(ctx).
|
query := s.Repository.DB().WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockKandang{}).
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Where("project_flock_id = ?", projectFlockID)
|
||||||
Pluck("id", &ids).Error
|
err := query.Order("id ASC").Pluck("id", &ids).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -369,126 +487,94 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
|||||||
return statusProject, statusClosing, nil
|
return statusProject, statusClosing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
|
||||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
totalKandangCount := len(projectFlockKandangs)
|
||||||
|
|
||||||
|
// Build kandang count map for farm expense division
|
||||||
|
projectFlockKandangCountMap := make(map[uint]int)
|
||||||
|
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
|
||||||
|
|
||||||
|
involvedProjectFlocks := make(map[uint]bool)
|
||||||
|
for _, realization := range realizations {
|
||||||
|
if realization.ExpenseNonstock != nil &&
|
||||||
|
realization.ExpenseNonstock.Expense != nil &&
|
||||||
|
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
|
||||||
|
var projectFlockIDs []uint
|
||||||
|
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
|
||||||
|
for _, pfID := range projectFlockIDs {
|
||||||
|
if pfID != projectFlockID {
|
||||||
|
involvedProjectFlocks[pfID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pfID := range involvedProjectFlocks {
|
||||||
|
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
|
||||||
|
projectFlockKandangCountMap[pfID] = len(pfKandangs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalChickinQty float64
|
var totalChickinQty float64
|
||||||
|
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 {
|
for _, chickin := range chickins {
|
||||||
totalChickinQty += chickin.UsageQty
|
totalChickinQty += chickin.UsageQty
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
totalActualPopulation := totalChickinQty - totalDepletion
|
totalActualPopulation := totalChickinQty - totalDepletion
|
||||||
|
|
||||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
|
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
|
||||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
|
||||||
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
|
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return err == nil, err
|
|
||||||
}},
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get actual usage cost instead of purchase items
|
|
||||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert actual usage rows to pseudo purchase items
|
|
||||||
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
|
||||||
|
|
||||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("MarketingProduct").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product")
|
|
||||||
})
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
|
||||||
}
|
|
||||||
|
|
||||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
input := dto.ClosingKeuanganInput{
|
|
||||||
ProjectFlockCategory: projectFlock.Category,
|
|
||||||
PurchaseItems: purchaseItems,
|
|
||||||
Budgets: budgets,
|
|
||||||
Realizations: realizations,
|
|
||||||
DeliveryProducts: deliveryProducts,
|
|
||||||
Chickins: chickins,
|
|
||||||
TotalWeightProduced: totalWeightProduced,
|
|
||||||
TotalEggWeightKg: totalEggWeightKg,
|
|
||||||
TotalDepletion: totalDepletion,
|
|
||||||
}
|
|
||||||
|
|
||||||
report := dto.ToClosingKeuanganReport(input)
|
|
||||||
|
|
||||||
return &report, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
@@ -521,12 +607,28 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) {
|
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
}
|
}
|
||||||
|
|
||||||
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
var projectFlockKandangIDs []uint
|
||||||
|
if kandangID != nil && *kandangID > 0 {
|
||||||
|
projectFlockKandangIDs = []uint{*kandangID}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||||
}
|
}
|
||||||
@@ -535,19 +637,29 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
}
|
}
|
||||||
|
|
||||||
var population float64
|
population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
||||||
for _, history := range project.KandangHistory {
|
if err != nil {
|
||||||
for _, chickin := range history.Chickins {
|
s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err)
|
||||||
population += chickin.UsageQty + chickin.PendingUsageQty
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
|
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
|
||||||
|
|
||||||
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
|
s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data")
|
||||||
|
}
|
||||||
|
var fcrActFromRecording *float64
|
||||||
|
if targetAverages.FcrCount > 0 {
|
||||||
|
fcrAvg := targetAverages.FcrAvg
|
||||||
|
fcrActFromRecording = &fcrAvg
|
||||||
}
|
}
|
||||||
|
|
||||||
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
||||||
@@ -556,6 +668,40 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
averageFeedIntake := targetAverages.FeedIntakeAvg
|
||||||
|
|
||||||
|
feedIntakeStd := 0.0
|
||||||
|
var mortalityStdFromGrowth *float64
|
||||||
|
if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil {
|
||||||
|
growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
|
||||||
|
if growthErr != nil {
|
||||||
|
if !errors.Is(growthErr, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data")
|
||||||
|
}
|
||||||
|
} else if growthDetail != nil {
|
||||||
|
if growthDetail.FeedIntake != nil {
|
||||||
|
feedIntakeStd = *growthDetail.FeedIntake
|
||||||
|
}
|
||||||
|
if growthDetail.MaxDepletion != nil {
|
||||||
|
mortalityStdFromGrowth = growthDetail.MaxDepletion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var productionStandardDetail *entity.ProductionStandardDetail
|
||||||
|
if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil {
|
||||||
|
productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
productionStandardDetail = nil
|
||||||
|
} else {
|
||||||
|
s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
|
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
|
||||||
@@ -578,10 +724,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
|
||||||
}
|
}
|
||||||
|
|
||||||
feedUsedPerHead := 0.0
|
// feedUsedPerHead := 0.0
|
||||||
if population > 0 {
|
// if population > 0 {
|
||||||
feedUsedPerHead = feedUsed / population
|
// feedUsedPerHead = feedUsed / population
|
||||||
}
|
// }
|
||||||
|
|
||||||
purchase := dto.ClosingPurchaseDTO{
|
purchase := dto.ClosingPurchaseDTO{
|
||||||
InitialPopulation: int(population),
|
InitialPopulation: int(population),
|
||||||
@@ -589,7 +735,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
FinalPopulation: int(finalPopulation),
|
FinalPopulation: int(finalPopulation),
|
||||||
FeedIn: feedIn,
|
FeedIn: feedIn,
|
||||||
FeedUsed: feedUsed,
|
FeedUsed: feedUsed,
|
||||||
FeedUsedPerHead: feedUsedPerHead,
|
// FeedUsedPerHead: feedUsedPerHead,
|
||||||
}
|
}
|
||||||
|
|
||||||
chickenFlagNames := []string{string(utils.FlagPullet)}
|
chickenFlagNames := []string{string(utils.FlagPullet)}
|
||||||
@@ -622,6 +768,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
|
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
|
||||||
|
if fcrActFromRecording != nil {
|
||||||
|
chickenPerformance.FcrAct = *fcrActFromRecording
|
||||||
|
}
|
||||||
|
|
||||||
var eggSales *dto.ClosingEggSalesDTO
|
var eggSales *dto.ClosingEggSalesDTO
|
||||||
var eggPerformance *dto.ClosingPerformanceDTO
|
var eggPerformance *dto.ClosingPerformanceDTO
|
||||||
@@ -669,6 +818,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
|
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
|
||||||
|
if fcrActFromRecording != nil {
|
||||||
|
eggPerf.FcrAct = *fcrActFromRecording
|
||||||
|
}
|
||||||
eggPerformance = &eggPerf
|
eggPerformance = &eggPerf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,15 +837,63 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
DeffMortality: chickenPerformance.DeffMortality,
|
DeffMortality: chickenPerformance.DeffMortality,
|
||||||
}
|
}
|
||||||
if eggPerformance != nil {
|
if eggPerformance != nil {
|
||||||
performance.FcrStd = eggPerformance.FcrStd
|
// performance.FcrStd = eggPerformance.FcrStd
|
||||||
performance.FcrAct = eggPerformance.FcrAct
|
performance.FcrAct = eggPerformance.FcrAct
|
||||||
performance.DeffFcr = eggPerformance.DeffFcr
|
// performance.DeffFcr = eggPerformance.DeffFcr
|
||||||
performance.Awg = eggPerformance.Awg
|
performance.AwgAct = eggPerformance.AwgAct
|
||||||
} else {
|
} else {
|
||||||
performance.FcrStd = chickenPerformance.FcrStd
|
// performance.FcrStd = chickenPerformance.FcrStd
|
||||||
performance.FcrAct = chickenPerformance.FcrAct
|
performance.FcrAct = chickenPerformance.FcrAct
|
||||||
performance.DeffFcr = chickenPerformance.DeffFcr
|
// performance.DeffFcr = chickenPerformance.DeffFcr
|
||||||
performance.Awg = chickenPerformance.Awg
|
performance.AwgAct = chickenPerformance.AwgAct
|
||||||
|
}
|
||||||
|
performance.FeedIntake = averageFeedIntake
|
||||||
|
performance.FeedIntakeStd = feedIntakeStd
|
||||||
|
if targetAverages.CumDepletionRateCount > 0 {
|
||||||
|
performance.MortalityAct = targetAverages.CumDepletionRateAvg
|
||||||
|
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
|
||||||
|
}
|
||||||
|
if mortalityStdFromGrowth != nil {
|
||||||
|
performance.MortalityStd = *mortalityStdFromGrowth
|
||||||
|
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
|
||||||
|
}
|
||||||
|
if !isGrowing {
|
||||||
|
if targetAverages.HenDayCount > 0 {
|
||||||
|
henDayAct := targetAverages.HenDayAvg
|
||||||
|
performance.HenDayAct = &henDayAct
|
||||||
|
}
|
||||||
|
if targetAverages.HenHouseCount > 0 {
|
||||||
|
henHouseAct := targetAverages.HenHouseAvg
|
||||||
|
performance.HenHouseAct = &henHouseAct
|
||||||
|
}
|
||||||
|
if targetAverages.EggWeightCount > 0 {
|
||||||
|
eggWeight := targetAverages.EggWeightAvg
|
||||||
|
performance.EggWeight = &eggWeight
|
||||||
|
}
|
||||||
|
if targetAverages.EggMassCount > 0 {
|
||||||
|
eggMass := targetAverages.EggMassAvg
|
||||||
|
performance.EggMass = &eggMass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
||||||
|
if productionStandardDetail != nil {
|
||||||
|
if productionStandardDetail.StandardFCR != nil {
|
||||||
|
performance.FcrStd = *productionStandardDetail.StandardFCR
|
||||||
|
}
|
||||||
|
if !isGrowing {
|
||||||
|
if productionStandardDetail.TargetHenDayProduction != nil {
|
||||||
|
performance.HendayStd = *productionStandardDetail.TargetHenDayProduction
|
||||||
|
}
|
||||||
|
if productionStandardDetail.TargetHenHouseProduction != nil {
|
||||||
|
performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction
|
||||||
|
}
|
||||||
|
if productionStandardDetail.TargetEggWeight != nil {
|
||||||
|
performance.EggWeightStd = *productionStandardDetail.TargetEggWeight
|
||||||
|
}
|
||||||
|
if productionStandardDetail.TargetEggMass != nil {
|
||||||
|
performance.EggMassStd = *productionStandardDetail.TargetEggMass
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := dto.ClosingProductionReportDTO{
|
result := dto.ClosingProductionReportDTO{
|
||||||
@@ -739,6 +939,46 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
|
|||||||
return totalAgeWeeks / totalQty, nil
|
return totalAgeWeeks / totalQty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
firstKandangID := projectFlockKandangIDs[0]
|
||||||
|
|
||||||
|
var chickin entity.ProjectChickin
|
||||||
|
if err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Where("project_flock_kandang_id = ?", firstKandangID).
|
||||||
|
Order("chick_in_date ASC").
|
||||||
|
First(&chickin).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if recording == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if recording.RecordDatetime.Before(chickin.ChickInDate) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate)
|
||||||
|
weekFloat := elapsed.Hours() / (24 * 7)
|
||||||
|
week := int(math.Ceil(weekFloat))
|
||||||
|
if week <= 0 {
|
||||||
|
week = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return week, nil
|
||||||
|
}
|
||||||
|
|
||||||
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
|
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
|
||||||
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
|
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
|
||||||
|
|
||||||
@@ -769,7 +1009,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
|
|||||||
FcrStd: fcrStd,
|
FcrStd: fcrStd,
|
||||||
FcrAct: fcrAct,
|
FcrAct: fcrAct,
|
||||||
DeffFcr: deffFcr,
|
DeffFcr: deffFcr,
|
||||||
Awg: awg,
|
AwgAct: awg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,52 +1031,3 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
|
|||||||
return closest.Mortality, closest.FcrNumber
|
return closest.Mortality, closest.FcrNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem {
|
|
||||||
if len(actualUsageRows) == 0 {
|
|
||||||
return []entity.PurchaseItem{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all product IDs
|
|
||||||
productIDs := make([]uint, len(actualUsageRows))
|
|
||||||
for i, row := range actualUsageRows {
|
|
||||||
productIDs[i] = row.ProductID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch products with flags from repository
|
|
||||||
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
|
|
||||||
products = []entity.Product{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create product map
|
|
||||||
productMap := make(map[uint]*entity.Product)
|
|
||||||
for i := range products {
|
|
||||||
productMap[products[i].Id] = &products[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to pseudo purchase items
|
|
||||||
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
|
|
||||||
for _, row := range actualUsageRows {
|
|
||||||
product := productMap[row.ProductID]
|
|
||||||
|
|
||||||
// Skip if product not found
|
|
||||||
if product == nil {
|
|
||||||
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
purchaseItem := entity.PurchaseItem{
|
|
||||||
Id: 0, // Pseudo item, no ID
|
|
||||||
ProductId: row.ProductID,
|
|
||||||
TotalQty: row.TotalQty,
|
|
||||||
TotalPrice: row.TotalPrice,
|
|
||||||
Price: row.AveragePrice,
|
|
||||||
Product: product,
|
|
||||||
}
|
|
||||||
|
|
||||||
purchaseItems = append(purchaseItems, purchaseItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
return purchaseItems
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,640 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
|
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClosingKeuanganService handles closing keuangan business logic
|
||||||
|
type ClosingKeuanganService interface {
|
||||||
|
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error)
|
||||||
|
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type closingKeuanganService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
ClosingKeuanganRepo repository.ClosingKeuanganRepository
|
||||||
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
|
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||||
|
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||||
|
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||||
|
RecordingRepo recordingRepository.RecordingRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingKeuanganService(
|
||||||
|
closingKeuanganRepo repository.ClosingKeuanganRepository,
|
||||||
|
projectFlockRepo projectflockRepository.ProjectflockRepository,
|
||||||
|
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||||
|
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
|
||||||
|
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
|
||||||
|
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
|
||||||
|
chickinRepo chickinRepository.ProjectChickinRepository,
|
||||||
|
recordingRepo recordingRepository.RecordingRepository,
|
||||||
|
) ClosingKeuanganService {
|
||||||
|
return &closingKeuanganService{
|
||||||
|
Log: utils.Log,
|
||||||
|
ClosingKeuanganRepo: closingKeuanganRepo,
|
||||||
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
|
ProjectBudgetRepo: projectBudgetRepo,
|
||||||
|
ChickinRepo: chickinRepo,
|
||||||
|
RecordingRepo: recordingRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preload Nonstock.Flags manually
|
||||||
|
var budgetIDs []uint
|
||||||
|
for _, b := range budgets {
|
||||||
|
budgetIDs = append(budgetIDs, b.Id)
|
||||||
|
}
|
||||||
|
if len(budgetIDs) > 0 {
|
||||||
|
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
|
||||||
|
Preload("Nonstock.Flags").
|
||||||
|
Where("id IN ?", budgetIDs).
|
||||||
|
Find(&budgets).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all kandang for this project flock
|
||||||
|
kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and fetch project flock kandang
|
||||||
|
kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
|
||||||
|
}
|
||||||
|
if kandang.ProjectFlockId != projectFlockID {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preload Nonstock.Flags manually
|
||||||
|
var budgetIDs []uint
|
||||||
|
for _, b := range budgets {
|
||||||
|
budgetIDs = append(budgetIDs, b.Id)
|
||||||
|
}
|
||||||
|
if len(budgetIDs) > 0 {
|
||||||
|
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
|
||||||
|
Preload("Nonstock.Flags").
|
||||||
|
Where("id IN ?", budgetIDs).
|
||||||
|
Find(&budgets).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangs := []entity.ProjectFlockKandang{*kandang}
|
||||||
|
|
||||||
|
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) {
|
||||||
|
// Define flag filters using constants
|
||||||
|
pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)}
|
||||||
|
ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}
|
||||||
|
ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}
|
||||||
|
allFilters := append(pakanFilters, ovkFilters...)
|
||||||
|
allFilters = append(allFilters, ayamFilters...)
|
||||||
|
|
||||||
|
var allProductUsageRows []repository.ProductUsageRow
|
||||||
|
|
||||||
|
// Get ALL product usage
|
||||||
|
for _, kandang := range kandangs {
|
||||||
|
rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters)
|
||||||
|
if err == nil {
|
||||||
|
allProductUsageRows = append(allProductUsageRows, rows...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classify into categories based on flag priority
|
||||||
|
var pakanProductUsageRows []repository.ProductUsageRow
|
||||||
|
var ovkProductUsageRows []repository.ProductUsageRow
|
||||||
|
var ayamProductUsageRows []repository.ProductUsageRow
|
||||||
|
|
||||||
|
for _, row := range allProductUsageRows {
|
||||||
|
// Parse flag names from comma-separated string
|
||||||
|
flagNames := strings.Split(row.FlagNames, ",")
|
||||||
|
|
||||||
|
hasPakanFlag := false
|
||||||
|
hasOvkFlag := false
|
||||||
|
hasAyamFlag := false
|
||||||
|
|
||||||
|
for _, flag := range flagNames {
|
||||||
|
flag = strings.TrimSpace(flag)
|
||||||
|
if containsItem(pakanFilters, flag) {
|
||||||
|
hasPakanFlag = true
|
||||||
|
}
|
||||||
|
if containsItem(ovkFilters, flag) {
|
||||||
|
hasOvkFlag = true
|
||||||
|
}
|
||||||
|
if containsItem(ayamFilters, flag) {
|
||||||
|
hasAyamFlag = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority: PAKAN > OVK > AYAM
|
||||||
|
if hasPakanFlag {
|
||||||
|
pakanProductUsageRows = append(pakanProductUsageRows, row)
|
||||||
|
} else if hasOvkFlag {
|
||||||
|
ovkProductUsageRows = append(ovkProductUsageRows, row)
|
||||||
|
} else if hasAyamFlag {
|
||||||
|
ayamProductUsageRows = append(ayamProductUsageRows, row)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate total price for each category
|
||||||
|
var totalPakanPrice, totalOvkPrice, totalAyamPrice float64
|
||||||
|
for _, row := range pakanProductUsageRows {
|
||||||
|
totalPakanPrice += row.TotalPengeluaran
|
||||||
|
}
|
||||||
|
for _, row := range ovkProductUsageRows {
|
||||||
|
totalOvkPrice += row.TotalPengeluaran
|
||||||
|
}
|
||||||
|
for _, row := range ayamProductUsageRows {
|
||||||
|
totalAyamPrice += row.TotalPengeluaran
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this is per-kandang or per-project-flock scope
|
||||||
|
isPerKandang := len(kandangs) == 1
|
||||||
|
var projectFlockKandangID *uint
|
||||||
|
if isPerKandang {
|
||||||
|
kandangID := kandangs[0].Id
|
||||||
|
projectFlockKandangID = &kandangID
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Fetch realizations
|
||||||
|
var realizations []entity.ExpenseRealization
|
||||||
|
if isPerKandang && projectFlockKandangID != nil {
|
||||||
|
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID)
|
||||||
|
} else {
|
||||||
|
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = db.Preload("MarketingProduct").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product")
|
||||||
|
return db
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by kandang if scope is per-kandang (manual filtering after fetch)
|
||||||
|
if isPerKandang && projectFlockKandangID != nil {
|
||||||
|
filteredProducts := make([]entity.MarketingDeliveryProduct, 0)
|
||||||
|
for _, dp := range deliveryProducts {
|
||||||
|
pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId
|
||||||
|
if pfKandangID != nil && *pfKandangID == *projectFlockKandangID {
|
||||||
|
filteredProducts = append(filteredProducts, dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deliveryProducts = filteredProducts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch chickins
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
if isPerKandang && projectFlockKandangID != nil {
|
||||||
|
chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
||||||
|
} else {
|
||||||
|
chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total depletion
|
||||||
|
var totalDepletion float64
|
||||||
|
if isPerKandang && projectFlockKandangID != nil {
|
||||||
|
totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
||||||
|
} else {
|
||||||
|
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
totalDepletion = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id)
|
||||||
|
if err != nil {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get actual weight from uniformity data
|
||||||
|
var totalWeightFromUniformity float64
|
||||||
|
if isPerKandang && projectFlockKandangID != nil {
|
||||||
|
totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
||||||
|
} else {
|
||||||
|
totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
} else if totalWeightFromUniformity > 0 {
|
||||||
|
totalWeightProduced = totalWeightFromUniformity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch egg data only for Laying category
|
||||||
|
var totalEggWeightKg float64
|
||||||
|
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
// TODO: Replace with actual method to get egg weight from RecordingRepo
|
||||||
|
// totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id)
|
||||||
|
// For now, set to 0 as placeholder
|
||||||
|
totalEggWeightKg = 0
|
||||||
|
} else {
|
||||||
|
totalEggWeightKg = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build new DTO structure
|
||||||
|
|
||||||
|
// Calculate totals
|
||||||
|
var totalPopulation float64
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
totalPopulation += chickin.UsageQty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual population (total population - depletion)
|
||||||
|
actualPopulation := totalPopulation - totalDepletion
|
||||||
|
|
||||||
|
// Calculate budget totals by category
|
||||||
|
calculateBudgetByFlag := func(flags []string) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, budget := range budgets {
|
||||||
|
if budget.Nonstock != nil {
|
||||||
|
for _, nonstockFlag := range budget.Nonstock.Flags {
|
||||||
|
flagName := strings.ToUpper(nonstockFlag.Name)
|
||||||
|
for _, targetFlag := range flags {
|
||||||
|
if flagName == strings.ToUpper(targetFlag) {
|
||||||
|
total += budget.Price * budget.Qty
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Budget per category
|
||||||
|
budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"})
|
||||||
|
budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"})
|
||||||
|
budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"})
|
||||||
|
budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"})
|
||||||
|
|
||||||
|
// Operational budget = total budget - pakan - ovk - ayam - ekspedisi
|
||||||
|
totalBudgetAmount := 0.0
|
||||||
|
for _, budget := range budgets {
|
||||||
|
totalBudgetAmount += budget.Price * budget.Qty
|
||||||
|
}
|
||||||
|
budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate realization totals
|
||||||
|
var totalRealizationAmount float64
|
||||||
|
var totalEkspedisiRealization float64
|
||||||
|
for _, realization := range realizations {
|
||||||
|
amount := realization.Price * realization.Qty
|
||||||
|
totalRealizationAmount += amount
|
||||||
|
|
||||||
|
// Check if this is ekspedisi (need to check nonstock flags)
|
||||||
|
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil {
|
||||||
|
for _, flag := range realization.ExpenseNonstock.Nonstock.Flags {
|
||||||
|
if flag.Name == "EKSPEDISI" {
|
||||||
|
totalEkspedisiRealization += amount
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization
|
||||||
|
|
||||||
|
// Filter delivery products based on category
|
||||||
|
var filteredDeliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
for _, delivery := range deliveryProducts {
|
||||||
|
// Get product from delivery
|
||||||
|
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
product := delivery.MarketingProduct.ProductWarehouse.Product
|
||||||
|
isEggProduct := false
|
||||||
|
isChickenProduct := false
|
||||||
|
|
||||||
|
// Check product flags
|
||||||
|
for _, flag := range product.Flags {
|
||||||
|
flagName := strings.ToUpper(flag.Name)
|
||||||
|
|
||||||
|
// Egg product flags
|
||||||
|
if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" ||
|
||||||
|
flagName == "TELURPUTIH" || flagName == "TELURRETAK" {
|
||||||
|
isEggProduct = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chicken product flags
|
||||||
|
if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" {
|
||||||
|
isChickenProduct = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter based on project flock category
|
||||||
|
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
// Laying: only egg products
|
||||||
|
if isEggProduct {
|
||||||
|
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Growing/Contract Growing: only chicken products
|
||||||
|
if isChickenProduct || (!isEggProduct && !isChickenProduct) {
|
||||||
|
// Include if chicken product or if no specific flags (default to chicken)
|
||||||
|
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate total weight sold and sales amount from filtered products
|
||||||
|
var totalWeightSold float64
|
||||||
|
var totalSalesAmount float64
|
||||||
|
for _, delivery := range filteredDeliveryProducts {
|
||||||
|
totalWeightSold += delivery.TotalWeight
|
||||||
|
totalSalesAmount += delivery.TotalPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate metrics - always use kg ayam for rp_per_kg
|
||||||
|
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||||
|
if actualPopulation > 0 {
|
||||||
|
rpPerBird = amount / actualPopulation // Use actual population
|
||||||
|
}
|
||||||
|
if totalWeightProduced > 0 {
|
||||||
|
rpPerKg = amount / totalWeightProduced
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate metrics for profit loss (use total population and total weight produced)
|
||||||
|
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||||
|
if totalPopulation > 0 {
|
||||||
|
rpPerBird = amount / totalPopulation
|
||||||
|
}
|
||||||
|
if totalWeightProduced > 0 {
|
||||||
|
rpPerKg = amount / totalWeightProduced
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HPP Items using constants
|
||||||
|
hppItems := []dto.HPPItem{}
|
||||||
|
|
||||||
|
// PAKAN item
|
||||||
|
pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan)
|
||||||
|
pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice)
|
||||||
|
hppItems = append(hppItems, dto.ToHPPItem(
|
||||||
|
1,
|
||||||
|
"purchase",
|
||||||
|
string(dto.HPPCodePakan),
|
||||||
|
"Pembelian Pakan",
|
||||||
|
dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan),
|
||||||
|
dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice),
|
||||||
|
))
|
||||||
|
|
||||||
|
// OVK item
|
||||||
|
ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk)
|
||||||
|
ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice)
|
||||||
|
hppItems = append(hppItems, dto.ToHPPItem(
|
||||||
|
2,
|
||||||
|
"purchase",
|
||||||
|
string(dto.HPPCodeOVK),
|
||||||
|
"Pembelian OVK",
|
||||||
|
dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk),
|
||||||
|
dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice),
|
||||||
|
))
|
||||||
|
|
||||||
|
// DOC/DEPRESIASI item
|
||||||
|
docCode := string(dto.HPPCodeDOC)
|
||||||
|
docLabel := "Pembelian DOC"
|
||||||
|
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
docCode = string(dto.HPPCodeDepresiasi)
|
||||||
|
docLabel = "Depresiasi"
|
||||||
|
}
|
||||||
|
docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam)
|
||||||
|
docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice)
|
||||||
|
hppItems = append(hppItems, dto.ToHPPItem(
|
||||||
|
3,
|
||||||
|
"purchase",
|
||||||
|
docCode,
|
||||||
|
docLabel,
|
||||||
|
dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam),
|
||||||
|
dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice),
|
||||||
|
))
|
||||||
|
|
||||||
|
// OVERHEAD item
|
||||||
|
overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational)
|
||||||
|
overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization)
|
||||||
|
hppItems = append(hppItems, dto.ToHPPItem(
|
||||||
|
4,
|
||||||
|
"overhead",
|
||||||
|
string(dto.HPPCodeOverhead),
|
||||||
|
"Pengeluaran Overhead",
|
||||||
|
dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational),
|
||||||
|
dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization),
|
||||||
|
))
|
||||||
|
|
||||||
|
// EKSPEDISI item
|
||||||
|
ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi)
|
||||||
|
ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization)
|
||||||
|
hppItems = append(hppItems, dto.ToHPPItem(
|
||||||
|
5,
|
||||||
|
"overhead",
|
||||||
|
string(dto.HPPCodeEkspedisi),
|
||||||
|
"Beban Ekspedisi",
|
||||||
|
dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi),
|
||||||
|
dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization),
|
||||||
|
))
|
||||||
|
|
||||||
|
// HPP Summary
|
||||||
|
totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi
|
||||||
|
totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization
|
||||||
|
|
||||||
|
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
|
||||||
|
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
|
||||||
|
|
||||||
|
var eggBudgeting, eggRealization *dto.FinancialMetrics
|
||||||
|
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 {
|
||||||
|
eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg
|
||||||
|
eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg
|
||||||
|
eggBudgeting = &dto.FinancialMetrics{
|
||||||
|
RpPerBird: 0,
|
||||||
|
RpPerKg: eggBudgetRpPerKg,
|
||||||
|
Amount: totalBudgetHpp,
|
||||||
|
}
|
||||||
|
eggRealization = &dto.FinancialMetrics{
|
||||||
|
RpPerBird: 0,
|
||||||
|
RpPerKg: eggRealizationRpPerKg,
|
||||||
|
Amount: totalRealizationHpp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hppSummary := dto.ToHPPSummary(
|
||||||
|
"HPP",
|
||||||
|
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
|
||||||
|
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
|
||||||
|
eggBudgeting,
|
||||||
|
eggRealization,
|
||||||
|
)
|
||||||
|
|
||||||
|
hppSection := dto.ToHPPSection(hppItems, hppSummary)
|
||||||
|
|
||||||
|
// Build Profit Loss Items using constants
|
||||||
|
plItems := []dto.ProfitLossItem{}
|
||||||
|
|
||||||
|
// SALES item
|
||||||
|
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
|
||||||
|
salesLabel := "Penjualan Ayam"
|
||||||
|
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
salesLabel = "Penjualan Telur"
|
||||||
|
}
|
||||||
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
|
string(dto.PLCodeSales),
|
||||||
|
salesLabel,
|
||||||
|
"income",
|
||||||
|
salesRpPerBird,
|
||||||
|
salesRpPerKg,
|
||||||
|
totalSalesAmount,
|
||||||
|
))
|
||||||
|
|
||||||
|
// SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK
|
||||||
|
totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice
|
||||||
|
sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird
|
||||||
|
sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg
|
||||||
|
sapronakLabel := "Pengeluaran Sapronak"
|
||||||
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
|
string(dto.PLCodeSapronak),
|
||||||
|
sapronakLabel,
|
||||||
|
"purchase",
|
||||||
|
sapronakRpPerBird,
|
||||||
|
sapronakRpPerKg,
|
||||||
|
totalSapronakAmount,
|
||||||
|
))
|
||||||
|
|
||||||
|
// OVERHEAD item
|
||||||
|
overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization)
|
||||||
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
|
string(dto.PLCodeOverhead),
|
||||||
|
"Overhead",
|
||||||
|
"overhead",
|
||||||
|
overheadRpPerBird,
|
||||||
|
overheadRpPerKg,
|
||||||
|
totalOperationalRealization,
|
||||||
|
))
|
||||||
|
|
||||||
|
// EKSPEDISI item
|
||||||
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
|
string(dto.PLCodeEkspedisi),
|
||||||
|
"Ekspedisi",
|
||||||
|
"overhead",
|
||||||
|
ekspedisiRealizationRpPerBird,
|
||||||
|
ekspedisiRealizationRpPerKg,
|
||||||
|
totalEkspedisiRealization,
|
||||||
|
))
|
||||||
|
|
||||||
|
// Profit Loss Summary
|
||||||
|
// Gross Profit = Sales - (DOC + PAKAN + OVK) only
|
||||||
|
// Gross Profit should NOT include overhead and ekspedisi
|
||||||
|
costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice
|
||||||
|
costOfGoodsSoldRpPerBird := sapronakRpPerBird
|
||||||
|
|
||||||
|
grossProfit := totalSalesAmount - costOfGoodsSold
|
||||||
|
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
|
||||||
|
|
||||||
|
// Operating Expenses (Overhead + Ekspedisi)
|
||||||
|
totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization
|
||||||
|
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird
|
||||||
|
|
||||||
|
// Net Profit = Gross Profit - Operating Expenses
|
||||||
|
netProfit := grossProfit - totalOperatingExpenses
|
||||||
|
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
|
||||||
|
|
||||||
|
plSummary := dto.ToProfitLossSummary(
|
||||||
|
dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit),
|
||||||
|
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses),
|
||||||
|
dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit),
|
||||||
|
)
|
||||||
|
|
||||||
|
profitLossSection := dto.ToProfitLossSection(plItems, plSummary)
|
||||||
|
|
||||||
|
// Build complete response
|
||||||
|
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsItem checks if a string exists in a slice
|
||||||
|
func containsItem(slice []string, item string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if strings.EqualFold(s, item) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -112,7 +111,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
@@ -126,8 +125,6 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
KandangName: pfk.Kandang.Name,
|
KandangName: pfk.Kandang.Name,
|
||||||
Period: pfk.Period,
|
Period: pfk.Period,
|
||||||
Status: status,
|
Status: status,
|
||||||
StartDate: nil,
|
|
||||||
EndDate: nil,
|
|
||||||
TotalIncomingValue: totalIncoming,
|
TotalIncomingValue: totalIncoming,
|
||||||
TotalUsageValue: totalUsage,
|
TotalUsageValue: totalUsage,
|
||||||
Items: items,
|
Items: items,
|
||||||
@@ -318,7 +315,7 @@ func buildSapronakDetails(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||||
// For sapronak closing report we intentionally ignore date range
|
// For sapronak closing report we intentionally ignore date range
|
||||||
// and aggregate all historical transactions for the kandang/project.
|
// and aggregate all historical transactions for the kandang/project.
|
||||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||||
@@ -359,7 +356,11 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
if filterFlag == "" {
|
if filterFlag == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return strings.ToUpper(f) == filterFlag
|
candidate := strings.ToUpper(f)
|
||||||
|
if filterFlag == "DOC" || filterFlag == "PULLET" {
|
||||||
|
return candidate == "DOC" || candidate == "PULLET"
|
||||||
|
}
|
||||||
|
return candidate == filterFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
// For project flocks with category GROWING, pullet usage from chickin
|
// For project flocks with category GROWING, pullet usage from chickin
|
||||||
@@ -415,6 +416,22 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
return groupMap[flag]
|
return groupMap[flag]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if flag == "" && len(details) > 0 {
|
||||||
|
flag = details[0].Flag
|
||||||
|
}
|
||||||
|
if name == "" && len(details) > 0 {
|
||||||
|
name = details[0].ProductName
|
||||||
|
}
|
||||||
|
return flag, name
|
||||||
|
}
|
||||||
|
|
||||||
for _, row := range incoming {
|
for _, row := range incoming {
|
||||||
if !matchesFlag(row.Flag) {
|
if !matchesFlag(row.Flag) {
|
||||||
continue
|
continue
|
||||||
@@ -550,19 +567,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range incomingDetails {
|
for productID, details := range incomingDetails {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
d.Flag = flag
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
d.ProductName = name
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -571,19 +587,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range adjIncoming {
|
for productID, details := range adjIncoming {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
d.Flag = flag
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
d.ProductName = name
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -592,19 +607,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range usageDetails {
|
for productID, details := range usageDetails {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
d.Flag = flag
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
d.ProductName = name
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
@@ -612,19 +626,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range adjOutgoing {
|
for productID, details := range adjOutgoing {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
d.Flag = flag
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
d.ProductName = name
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
@@ -632,19 +645,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range transIncoming {
|
for productID, details := range transIncoming {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
d.Flag = flag
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
d.ProductName = name
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -653,19 +665,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range transOutgoing {
|
for productID, details := range transOutgoing {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
|
if d.Flag == "" {
|
||||||
d.Flag = flag
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
d.ProductName = name
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ type ClosingSapronakQuery struct {
|
|||||||
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Status: status,
|
Status: status,
|
||||||
Category: item.Category,
|
Category: item.Category,
|
||||||
|
RejectReason: item.RejectReason,
|
||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Kandang: kandang,
|
Kandang: kandang,
|
||||||
CreatedUser: nil,
|
CreatedUser: nil,
|
||||||
@@ -150,6 +151,10 @@ func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error {
|
|||||||
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
|
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
|
||||||
EmployeeID: summary.EmployeeID,
|
EmployeeID: summary.EmployeeID,
|
||||||
EmployeeName: summary.EmployeeName,
|
EmployeeName: summary.EmployeeName,
|
||||||
|
Kandang: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: summary.KandangID,
|
||||||
|
Name: summary.KandangName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,12 +308,22 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
documentDTOs := make([]dto.DailyChecklistDocumentDTO, len(detail.DocumentURLs))
|
||||||
|
for i, doc := range detail.DocumentURLs {
|
||||||
|
documentDTOs[i] = dto.DailyChecklistDocumentDTO{
|
||||||
|
Id: doc.ID,
|
||||||
|
Name: doc.Name,
|
||||||
|
Size: doc.Size,
|
||||||
|
URL: doc.URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get dailyChecklist successfully",
|
Message: "Get dailyChecklist successfully",
|
||||||
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress),
|
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +357,12 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||||
|
}
|
||||||
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type DailyChecklistListDTO struct {
|
|||||||
TotalPhase int `json:"total_phase"`
|
TotalPhase int `json:"total_phase"`
|
||||||
TotalActivity int `json:"total_activity"`
|
TotalActivity int `json:"total_activity"`
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
|
RejectReason *string `json:"reject_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistDetailDTO struct {
|
type DailyChecklistDetailDTO struct {
|
||||||
@@ -40,6 +41,14 @@ type DailyChecklistDetailDTO struct {
|
|||||||
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
|
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
|
||||||
TotalActivity int `json:"total_activity"`
|
TotalActivity int `json:"total_activity"`
|
||||||
Progress float64 `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
|
DocumentURLs []DailyChecklistDocumentDTO `json:"document_urls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistDocumentDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size float64 `json:"size"`
|
||||||
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistSummaryDTO struct {
|
type DailyChecklistSummaryDTO struct {
|
||||||
@@ -57,6 +66,7 @@ type DailyChecklistSummaryDTO struct {
|
|||||||
type DailyChecklistPerformanceOverviewDTO struct {
|
type DailyChecklistPerformanceOverviewDTO struct {
|
||||||
EmployeeID uint `json:"employee_id"`
|
EmployeeID uint `json:"employee_id"`
|
||||||
EmployeeName string `json:"employee_name"`
|
EmployeeName string `json:"employee_name"`
|
||||||
|
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
|
||||||
TotalActivity int `json:"total_activity"`
|
TotalActivity int `json:"total_activity"`
|
||||||
ActivityDone int `json:"activity_done"`
|
ActivityDone int `json:"activity_done"`
|
||||||
ActivityLeft int `json:"activity_left"`
|
ActivityLeft int `json:"activity_left"`
|
||||||
@@ -165,10 +175,11 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
|
|||||||
TotalPhase: 0,
|
TotalPhase: 0,
|
||||||
TotalActivity: 0,
|
TotalActivity: 0,
|
||||||
Progress: 0,
|
Progress: 0,
|
||||||
|
RejectReason: e.RejectReason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO {
|
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO {
|
||||||
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
|
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
|
||||||
for _, phase := range phases {
|
for _, phase := range phases {
|
||||||
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
|
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
|
||||||
@@ -228,5 +239,6 @@ func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.
|
|||||||
AssignedEmployees: assignedDTOs,
|
AssignedEmployees: assignedDTOs,
|
||||||
TotalActivity: totalActivities,
|
TotalActivity: totalActivities,
|
||||||
Progress: progress,
|
Progress: progress,
|
||||||
|
DocumentURLs: documentURLs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package dailyChecklists
|
package dailyChecklists
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
|
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
|
||||||
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
|
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
|
||||||
@@ -19,8 +24,13 @@ func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
|||||||
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
|
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
|
||||||
phasesRepo := rPhases.NewPhasesRepository(db)
|
phasesRepo := rPhases.NewPhasesRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||||
|
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to create document service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate)
|
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate, documentSvc)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
DailyChecklistRoutes(router, userService, dailyChecklistService)
|
DailyChecklistRoutes(router, userService, dailyChecklistService)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dailyChecklists
|
package dailyChecklists
|
||||||
|
|
||||||
import (
|
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"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
||||||
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,51 +13,51 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
|||||||
ctrl := controller.NewDailyChecklistController(s)
|
ctrl := controller.NewDailyChecklistController(s)
|
||||||
|
|
||||||
route := v1.Group("/daily-checklists")
|
route := v1.Group("/daily-checklists")
|
||||||
// route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_DailyChecklistGetAll), ctrl.GetAll)
|
||||||
route.Get("/report", ctrl.GetReport)
|
route.Get("/report", m.RequirePermissions(m.P_DailyChecklistReports), ctrl.GetReport)
|
||||||
|
|
||||||
route.Get("/summary", ctrl.GetSummary)
|
route.Get("/summary", m.RequirePermissions(m.P_DailyChecklistDashboardList), ctrl.GetSummary)
|
||||||
|
|
||||||
route.Get("/report", ctrl.GetReport)
|
// route.Get("/report", ctrl.GetReport)
|
||||||
|
|
||||||
// create daily checklist
|
// upsert daily checklist
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateOne)
|
||||||
|
|
||||||
// get detail data daily checklist by id
|
// get detail data daily checklist by id
|
||||||
route.Get("/relation/:idDailyChecklist", ctrl.GetOne)
|
route.Get("/relation/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistGetOne), ctrl.GetOne)
|
||||||
|
|
||||||
// get phases by daily checklist id
|
// get phases by daily checklist id
|
||||||
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist)
|
route.Get("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetPhaseByIdChecklist)
|
||||||
|
|
||||||
// create task
|
// create task
|
||||||
/*
|
/*
|
||||||
ketika add phase
|
ketika add phase
|
||||||
*/
|
*/
|
||||||
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
|
route.Post("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateDailyChecklistPhase)
|
||||||
|
|
||||||
// create assigment
|
// create assigment
|
||||||
/*
|
/*
|
||||||
ketika add ABK
|
ketika add ABK
|
||||||
*/
|
*/
|
||||||
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
|
route.Post("/assignment/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateAssignment)
|
||||||
|
|
||||||
// remove assignment
|
// remove assignment
|
||||||
/*
|
/*
|
||||||
ketika remove ABK
|
ketika remove ABK
|
||||||
*/
|
*/
|
||||||
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
|
route.Delete("/:idDailyChecklist/assignments/:idEmployee", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.RemoveAssignment)
|
||||||
|
|
||||||
//get all tasks
|
//get all tasks
|
||||||
route.Get("/tasks", ctrl.GetAllTasks)
|
route.Get("/tasks", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetAllTasks)
|
||||||
|
|
||||||
// update assignment
|
// update assignment
|
||||||
/*
|
/*
|
||||||
ketika check dan uncheck tugas oleh ABK
|
ketika check dan uncheck tugas oleh ABK
|
||||||
*/
|
*/
|
||||||
route.Post("/assignment", ctrl.UpdateAssignment)
|
route.Post("/assignment", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateAssignment)
|
||||||
|
|
||||||
route.Patch("/:idDailyChecklist", ctrl.UpdateOne)
|
route.Patch("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateOne)
|
||||||
route.Delete("/:idDailyChecklist", ctrl.DeleteOne)
|
route.Delete("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.DeleteOne)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
|
||||||
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
|
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
@@ -43,6 +45,14 @@ type dailyChecklistService struct {
|
|||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.DailyChecklistRepository
|
Repository repository.DailyChecklistRepository
|
||||||
PhaseRepo phaseRepo.PhasesRepository
|
PhaseRepo phaseRepo.PhasesRepository
|
||||||
|
DocumentSvc commonSvc.DocumentService
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistDocument struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Size float64
|
||||||
|
URL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistDetail struct {
|
type DailyChecklistDetail struct {
|
||||||
@@ -52,6 +62,7 @@ type DailyChecklistDetail struct {
|
|||||||
AssignedEmployees []entity.Employee
|
AssignedEmployees []entity.Employee
|
||||||
TotalActivities int
|
TotalActivities int
|
||||||
Progress float64
|
Progress float64
|
||||||
|
DocumentURLs []DailyChecklistDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistListItem struct {
|
type DailyChecklistListItem struct {
|
||||||
@@ -60,6 +71,7 @@ type DailyChecklistListItem struct {
|
|||||||
Date time.Time
|
Date time.Time
|
||||||
Category string
|
Category string
|
||||||
Status *string
|
Status *string
|
||||||
|
RejectReason *string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
Kandang entity.Kandang
|
Kandang entity.Kandang
|
||||||
@@ -108,12 +120,13 @@ type DailyChecklistReportCategory struct {
|
|||||||
Baik int
|
Baik int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
|
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService {
|
||||||
return &dailyChecklistService{
|
return &dailyChecklistService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
PhaseRepo: phaseRepo,
|
PhaseRepo: phaseRepo,
|
||||||
|
DocumentSvc: documentSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +171,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
|
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
like := "%" + params.Search + "%"
|
like := "%" + params.Search + "%"
|
||||||
db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like)
|
db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like)
|
||||||
}
|
}
|
||||||
|
|
||||||
countDB := db.Session(&gorm.Session{})
|
countDB := db.Session(&gorm.Session{})
|
||||||
@@ -174,6 +187,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
Date time.Time
|
Date time.Time
|
||||||
Category string
|
Category string
|
||||||
Status *string
|
Status *string
|
||||||
|
RejectReason *string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
KandangID uint
|
KandangID uint
|
||||||
@@ -192,6 +206,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
dc.date,
|
dc.date,
|
||||||
dc.category,
|
dc.category,
|
||||||
dc.status,
|
dc.status,
|
||||||
|
dc.reject_reason,
|
||||||
dc.created_at,
|
dc.created_at,
|
||||||
dc.updated_at,
|
dc.updated_at,
|
||||||
dc.kandang_id,
|
dc.kandang_id,
|
||||||
@@ -265,6 +280,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
Date: row.Date,
|
Date: row.Date,
|
||||||
Category: row.Category,
|
Category: row.Category,
|
||||||
Status: row.Status,
|
Status: row.Status,
|
||||||
|
RejectReason: row.RejectReason,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: row.CreatedAt,
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: row.UpdatedAt,
|
||||||
Kandang: kandangMap[row.KandangID],
|
Kandang: kandangMap[row.KandangID],
|
||||||
@@ -345,6 +361,29 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
|
|||||||
progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100)
|
progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
documentURLs := make([]DailyChecklistDocument, 0)
|
||||||
|
if s.DocumentSvc != nil {
|
||||||
|
documents, err := s.DocumentSvc.ListByTarget(c.Context(), string(utils.DocumentTypeDailyChecklist), uint64(id))
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list documents for daily checklist %d: %+v", id, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, doc := range documents {
|
||||||
|
url, err := s.DocumentSvc.PresignURL(c.Context(), doc, 0)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to presign document %d for daily checklist %d: %+v", doc.Id, id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
documentURLs = append(documentURLs, DailyChecklistDocument{
|
||||||
|
ID: doc.Id,
|
||||||
|
Name: doc.Name,
|
||||||
|
Size: doc.Size,
|
||||||
|
URL: url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &DailyChecklistDetail{
|
return &DailyChecklistDetail{
|
||||||
Checklist: *checklist,
|
Checklist: *checklist,
|
||||||
Phases: phases,
|
Phases: phases,
|
||||||
@@ -352,6 +391,7 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
|
|||||||
AssignedEmployees: assignedEmployees,
|
AssignedEmployees: assignedEmployees,
|
||||||
TotalActivities: totalActivities,
|
TotalActivities: totalActivities,
|
||||||
Progress: progress,
|
Progress: progress,
|
||||||
|
DocumentURLs: documentURLs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +417,7 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
|
err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}},
|
Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}},
|
||||||
DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}),
|
DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}),
|
||||||
}).Create(createBody).Error
|
}).Create(createBody).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
|
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
|
||||||
@@ -392,6 +432,22 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deletedIDs := make([]uint, 0)
|
||||||
|
if req.DeletedDocumentIDs != nil {
|
||||||
|
parts := strings.Split(*req.DeletedDocumentIDs, ",")
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsedID, err := strconv.ParseUint(part, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid deleted_document_ids")
|
||||||
|
}
|
||||||
|
deletedIDs = append(deletedIDs, uint(parsedID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateBody := map[string]any{
|
updateBody := map[string]any{
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
}
|
}
|
||||||
@@ -400,6 +456,40 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
updateBody["reject_reason"] = *req.RejectReason
|
updateBody["reject_reason"] = *req.RejectReason
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deletedIDs) > 0 && s.DocumentSvc != nil {
|
||||||
|
if err := s.DocumentSvc.DeleteDocuments(c.Context(), deletedIDs, true); err != nil {
|
||||||
|
s.Log.Errorf("Failed to delete daily checklist documents: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete daily checklist documents")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Documents) > 0 {
|
||||||
|
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||||
|
for idx, file := range req.Documents {
|
||||||
|
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
||||||
|
File: file,
|
||||||
|
Type: string(utils.DocumentTypeDailyChecklist),
|
||||||
|
Index: &idx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||||
|
DocumentableType: string(utils.DocumentTypeDailyChecklist),
|
||||||
|
DocumentableID: uint64(id),
|
||||||
|
CreatedBy: &actorID,
|
||||||
|
Files: documentFiles,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to upload daily checklist documents: %+v", err)
|
||||||
|
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload daily checklist documents")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
|
||||||
@@ -869,7 +959,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
|
|||||||
Joins("JOIN areas a ON a.id = loc.area_id").
|
Joins("JOIN areas a ON a.id = loc.area_id").
|
||||||
Joins("JOIN phases p ON p.id = dcat.phase_id").
|
Joins("JOIN phases p ON p.id = dcat.phase_id").
|
||||||
Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month).
|
Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month).
|
||||||
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year)
|
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year).
|
||||||
|
Where("dc.status = ?", "APPROVED")
|
||||||
|
|
||||||
if params.AreaID != nil {
|
if params.AreaID != nil {
|
||||||
db = db.Where("a.id = ?", *params.AreaID)
|
db = db.Where("a.id = ?", *params.AreaID)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Date string `json:"date" validate:"required"`
|
Date string `json:"date" validate:"required"`
|
||||||
KandangId uint `json:"kandang_id" validate:"required"`
|
KandangId uint `json:"kandang_id" validate:"required"`
|
||||||
@@ -8,8 +12,10 @@ type Create struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Status string `json:"status" validate:"required"`
|
Status string `form:"status" json:"status" validate:"required"`
|
||||||
RejectReason *string `json:"reject_reason"`
|
RejectReason *string `form:"reject_reason" json:"reject_reason"`
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
|
DeletedDocumentIDs *string `form:"deleted_document_ids" json:"deleted_document_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardController struct {
|
||||||
|
DashboardService service.DashboardService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboardController(dashboardService service.DashboardService) *DashboardController {
|
||||||
|
return &DashboardController{
|
||||||
|
DashboardService: dashboardService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DashboardController) GetAll(c *fiber.Ctx) error {
|
||||||
|
parseStringListParam := func(param string) ([]string, error) {
|
||||||
|
if param == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(param, ",")
|
||||||
|
result := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, strconv.ErrSyntax
|
||||||
|
}
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parseUintListParam := func(param string) ([]uint, error) {
|
||||||
|
if param == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(param, ",")
|
||||||
|
ids := make([]uint, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, strconv.ErrSyntax
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids = append(ids, uint(parsed))
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lokasiIds, err := parseUintListParam(c.Query("location_ids", ""))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_ids")
|
||||||
|
}
|
||||||
|
|
||||||
|
flockIds, err := parseUintListParam(c.Query("flock_ids", ""))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid flock_ids")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangIds, err := parseUintListParam(c.Query("kandang_ids", ""))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_ids")
|
||||||
|
}
|
||||||
|
|
||||||
|
include, err := parseStringListParam(strings.ToLower(c.Query("include", "")))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid include")
|
||||||
|
}
|
||||||
|
|
||||||
|
analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview)))
|
||||||
|
metric := strings.ToLower(strings.TrimSpace(c.Query("metric", "")))
|
||||||
|
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: strings.TrimSpace(c.Query("search", "")),
|
||||||
|
PerformanceOverviewFilter: validation.PerformanceOverviewFilter{
|
||||||
|
StartDate: c.Query("start_date", ""),
|
||||||
|
EndDate: c.Query("end_date", ""),
|
||||||
|
AnalysisMode: analysisMode,
|
||||||
|
ComparisonType: strings.ToUpper(strings.TrimSpace(c.Query("comparison_type", ""))),
|
||||||
|
Metric: metric,
|
||||||
|
LokasiIds: lokasiIds,
|
||||||
|
FlockIds: flockIds,
|
||||||
|
KandangIds: kandangIds,
|
||||||
|
Include: include,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.AnalysisMode == validation.AnalysisModeComparison && query.ComparisonType == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "comparison_type is required for comparison mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
startDate, endDate, endExclusive, err := parsePeriodDates(query.StartDate, query.EndDate, location)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.PeriodStart = startDate
|
||||||
|
query.PeriodEnd = endDate
|
||||||
|
query.PeriodEndExclusive = endExclusive
|
||||||
|
|
||||||
|
result, totalResults, err := u.DashboardService.GetAll(c.Context(), query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFilter := query.StartDate != "" ||
|
||||||
|
query.EndDate != "" ||
|
||||||
|
len(query.LokasiIds) > 0 ||
|
||||||
|
len(query.FlockIds) > 0 ||
|
||||||
|
len(query.KandangIds) > 0 ||
|
||||||
|
len(query.Include) > 0 ||
|
||||||
|
query.ComparisonType != "" ||
|
||||||
|
query.Metric != "" ||
|
||||||
|
query.AnalysisMode != validation.AnalysisModeOverview
|
||||||
|
|
||||||
|
var filters interface{}
|
||||||
|
if hasFilter {
|
||||||
|
filters = dto.DashboardFiltersDTO{
|
||||||
|
StartDate: query.StartDate,
|
||||||
|
EndDate: query.EndDate,
|
||||||
|
AnalysisMode: query.AnalysisMode,
|
||||||
|
ComparisonType: query.ComparisonType,
|
||||||
|
Metric: query.Metric,
|
||||||
|
LokasiIds: defaultUintSlice(query.LokasiIds),
|
||||||
|
FlockIds: defaultUintSlice(query.FlockIds),
|
||||||
|
KandangIds: defaultUintSlice(query.KandangIds),
|
||||||
|
Include: query.Include,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithMeta{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get dashboard successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
Filters: filters,
|
||||||
|
},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultUintSlice(values []uint) []uint {
|
||||||
|
if values == nil {
|
||||||
|
return []uint{}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
if startDateRaw != "" {
|
||||||
|
parsed, err := time.ParseInLocation("2006-01-02", startDateRaw, location)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "start_date must follow format YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
startDate = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDateRaw != "" {
|
||||||
|
parsed, err := time.ParseInLocation("2006-01-02", endDateRaw, location)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must follow format YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
endDate = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDate.Before(startDate) {
|
||||||
|
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
|
||||||
|
}
|
||||||
|
|
||||||
|
endExclusive := endDate.AddDate(0, 0, 1)
|
||||||
|
return startDate, endDate, endExclusive, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type DashboardListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardDetailDTO struct {
|
||||||
|
DashboardListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardFiltersDTO struct {
|
||||||
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
|
AnalysisMode string `json:"analysis_mode"`
|
||||||
|
ComparisonType string `json:"comparison_type,omitempty"`
|
||||||
|
Metric string `json:"metric,omitempty"`
|
||||||
|
LokasiIds []uint `json:"location_ids"`
|
||||||
|
FlockIds []uint `json:"flock_ids"`
|
||||||
|
KandangIds []uint `json:"kandang_ids"`
|
||||||
|
Include []string `json:"include,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardStatisticsDTO struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
PercentLastMonth float64 `json:"percent_last_month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardPerformanceOverviewDTO struct {
|
||||||
|
StatisticsData []DashboardStatisticsDTO `json:"statistics_data"`
|
||||||
|
Charts map[string]DashboardChartDTO `json:"charts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardChartSeriesDTO struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Unit string `json:"unit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardChartDTO struct {
|
||||||
|
Series []DashboardChartSeriesDTO `json:"series"`
|
||||||
|
Dataset []map[string]interface{} `json:"dataset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToDashboardListDTO(e entity.Dashboard) DashboardListDTO {
|
||||||
|
var createdUser *userDTO.UserRelationDTO
|
||||||
|
if e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return DashboardListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToDashboardListDTOs(e []entity.Dashboard) []DashboardListDTO {
|
||||||
|
result := make([]DashboardListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToDashboardListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
|
||||||
|
sDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
|
||||||
|
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardModule struct{}
|
||||||
|
|
||||||
|
func (DashboardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
dashboardRepo := rDashboard.NewDashboardRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
DashboardRoutes(router, userService, dashboardService)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardRepository interface {
|
||||||
|
repository.BaseRepository[entity.Dashboard]
|
||||||
|
GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error)
|
||||||
|
SumDepletions(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
SumInitialPopulation(ctx context.Context, endDate time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
SumSapronakCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
SumBopCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
SumEkspedisiCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error)
|
||||||
|
SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
|
||||||
|
GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error)
|
||||||
|
GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error)
|
||||||
|
GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error)
|
||||||
|
GetStandardFcrWeekly(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyFcrMetric, error)
|
||||||
|
GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error)
|
||||||
|
GetComparisonWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType, metric string) ([]ComparisonWeeklyMetric, error)
|
||||||
|
GetComparisonWeeklyUniformityMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonUniformityMetric, error)
|
||||||
|
GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error)
|
||||||
|
GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error)
|
||||||
|
GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Dashboard]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboardRepository(db *gorm.DB) DashboardRepository {
|
||||||
|
return &DashboardRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Dashboard](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SellingPriceAggregate struct {
|
||||||
|
TotalPrice float64
|
||||||
|
TotalWeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedUsageByUom struct {
|
||||||
|
TotalQty float64
|
||||||
|
UomName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingWeeklyMetric struct {
|
||||||
|
Week int
|
||||||
|
HenDay float64
|
||||||
|
EggWeight float64
|
||||||
|
FeedIntake float64
|
||||||
|
FcrValue float64
|
||||||
|
CumDepletionRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniformityWeeklyMetric struct {
|
||||||
|
Week int
|
||||||
|
Uniformity float64
|
||||||
|
AverageWeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StandardWeeklyMetric struct {
|
||||||
|
Week int
|
||||||
|
StdLaying float64
|
||||||
|
StdEggWeight float64
|
||||||
|
StdFeedIntake float64
|
||||||
|
StdUniformity float64
|
||||||
|
StdDepletion float64
|
||||||
|
StdBodyWeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StandardWeeklyFcrMetric struct {
|
||||||
|
Week int
|
||||||
|
StdFcr float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComparisonSeries struct {
|
||||||
|
Id uint
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComparisonWeeklyMetric struct {
|
||||||
|
Week int
|
||||||
|
SeriesId uint
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComparisonUniformityMetric struct {
|
||||||
|
Week int
|
||||||
|
SeriesId uint
|
||||||
|
Uniformity float64
|
||||||
|
AverageWeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type EggQualityWeeklyMetric struct {
|
||||||
|
Week int
|
||||||
|
NormalQty float64
|
||||||
|
AbnormalQty float64
|
||||||
|
TotalQty float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeeklyEggWeightMetric struct {
|
||||||
|
Week int
|
||||||
|
EggWeightGrams float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeeklyFeedUsageMetric struct {
|
||||||
|
Week int
|
||||||
|
TotalQty float64
|
||||||
|
UomName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDashboardFilters(db *gorm.DB, filters *validation.DashboardFilter) *gorm.DB {
|
||||||
|
if filters == nil {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
if len(filters.FlockIds) > 0 {
|
||||||
|
db = db.Where("pfk.project_flock_id IN ?", filters.FlockIds)
|
||||||
|
}
|
||||||
|
if len(filters.KandangIds) > 0 {
|
||||||
|
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||||
|
}
|
||||||
|
if len(filters.LokasiIds) > 0 {
|
||||||
|
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error) {
|
||||||
|
var rows []RecordingWeeklyMetric
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select(`((r.day - 1) / 7 + 1) AS week,
|
||||||
|
COALESCE(AVG(r.hen_day), 0) AS hen_day,
|
||||||
|
COALESCE(AVG(r.egg_weight), 0) AS egg_weight,
|
||||||
|
COALESCE(AVG(r.feed_intake), 0) AS feed_intake,
|
||||||
|
COALESCE(AVG(r.fcr_value), 0) AS fcr_value,
|
||||||
|
COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`).
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("r.day IS NOT NULL AND r.day > 0")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error) {
|
||||||
|
var rows []UniformityWeeklyMetric
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("project_flock_kandang_uniformity AS u").
|
||||||
|
Select(`u.week AS week,
|
||||||
|
COALESCE(AVG(u.uniformity), 0) AS uniformity,
|
||||||
|
COALESCE(AVG((u.chart_data->'statistics'->>'average_weight')::numeric), 0) AS average_weight`).
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("u.uniform_date IS NOT NULL").
|
||||||
|
Where("u.uniform_date >= ? AND u.uniform_date < ?", start, end)
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group("u.week").Order("u.week ASC").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error) {
|
||||||
|
if len(weeks) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
standardIDs := r.standardIDSubquery(filters)
|
||||||
|
if standardIDs == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []StandardWeeklyMetric
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("standard_growth_details AS sgd").
|
||||||
|
Select(`sgd.week AS week,
|
||||||
|
COALESCE(AVG(psd.target_hen_day_production), 0) AS std_laying,
|
||||||
|
COALESCE(AVG(psd.target_egg_weight), 0) AS std_egg_weight,
|
||||||
|
COALESCE(AVG(sgd.feed_intake), 0) AS std_feed_intake,
|
||||||
|
COALESCE(AVG(sgd.min_uniformity), 0) AS std_uniformity,
|
||||||
|
COALESCE(AVG(sgd.max_depletion), 0) AS std_depletion,
|
||||||
|
COALESCE(AVG(sgd.target_mean_bw), 0) AS std_body_weight`).
|
||||||
|
Joins("LEFT JOIN production_standard_details AS psd ON psd.production_standard_id = sgd.production_standard_id AND psd.week = sgd.week").
|
||||||
|
Where("sgd.week IN ?", weeks).
|
||||||
|
Where("sgd.production_standard_id IN (?)", standardIDs)
|
||||||
|
|
||||||
|
if err := db.Group("sgd.week").Order("sgd.week ASC").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetStandardFcrWeekly(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyFcrMetric, error) {
|
||||||
|
if len(weeks) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterClause := ""
|
||||||
|
filterArgs := make([]interface{}, 0)
|
||||||
|
if filters != nil {
|
||||||
|
if len(filters.FlockIds) > 0 {
|
||||||
|
filterClause += " AND pf.id IN ?"
|
||||||
|
filterArgs = append(filterArgs, filters.FlockIds)
|
||||||
|
}
|
||||||
|
if len(filters.KandangIds) > 0 {
|
||||||
|
filterClause += " AND k.id IN ?"
|
||||||
|
filterArgs = append(filterArgs, filters.KandangIds)
|
||||||
|
}
|
||||||
|
if len(filters.LokasiIds) > 0 {
|
||||||
|
filterClause += " AND k.location_id IN ?"
|
||||||
|
filterArgs = append(filterArgs, filters.LokasiIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
WITH src AS (
|
||||||
|
SELECT DISTINCT pf.production_standard_id, pf.fcr_id
|
||||||
|
FROM project_flocks pf
|
||||||
|
JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id
|
||||||
|
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||||
|
WHERE pf.production_standard_id > 0 AND pf.fcr_id > 0
|
||||||
|
%s
|
||||||
|
),
|
||||||
|
actual AS (
|
||||||
|
SELECT u.week AS week,
|
||||||
|
pf.fcr_id AS fcr_id,
|
||||||
|
AVG((u.chart_data->'statistics'->>'average_weight')::numeric) AS avg_weight
|
||||||
|
FROM project_flock_kandang_uniformity u
|
||||||
|
JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id
|
||||||
|
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||||
|
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||||
|
WHERE u.week IN ? AND u.uniform_date IS NOT NULL AND pf.fcr_id > 0
|
||||||
|
%s
|
||||||
|
GROUP BY u.week, pf.fcr_id
|
||||||
|
),
|
||||||
|
target AS (
|
||||||
|
SELECT sgd.week AS week,
|
||||||
|
src.fcr_id AS fcr_id,
|
||||||
|
AVG(sgd.target_mean_bw) AS target_mean_bw
|
||||||
|
FROM standard_growth_details sgd
|
||||||
|
JOIN src ON src.production_standard_id = sgd.production_standard_id
|
||||||
|
WHERE sgd.week IN ?
|
||||||
|
GROUP BY sgd.week, src.fcr_id
|
||||||
|
),
|
||||||
|
weights AS (
|
||||||
|
SELECT COALESCE(a.week, t.week) AS week,
|
||||||
|
COALESCE(a.fcr_id, t.fcr_id) AS fcr_id,
|
||||||
|
COALESCE(
|
||||||
|
CASE WHEN a.avg_weight > 10 THEN a.avg_weight / 1000 ELSE a.avg_weight END,
|
||||||
|
CASE WHEN t.target_mean_bw > 10 THEN t.target_mean_bw / 1000 ELSE t.target_mean_bw END
|
||||||
|
) AS weight
|
||||||
|
FROM actual a
|
||||||
|
FULL OUTER JOIN target t ON t.week = a.week AND t.fcr_id = a.fcr_id
|
||||||
|
)
|
||||||
|
SELECT w.week AS week,
|
||||||
|
COALESCE(AVG(
|
||||||
|
COALESCE(
|
||||||
|
(SELECT fs.fcr_number
|
||||||
|
FROM fcr_standards fs
|
||||||
|
WHERE fs.fcr_id = w.fcr_id
|
||||||
|
AND fs.weight >= w.weight
|
||||||
|
ORDER BY fs.weight ASC
|
||||||
|
LIMIT 1),
|
||||||
|
(SELECT fs.fcr_number
|
||||||
|
FROM fcr_standards fs
|
||||||
|
WHERE fs.fcr_id = w.fcr_id
|
||||||
|
ORDER BY fs.weight DESC
|
||||||
|
LIMIT 1)
|
||||||
|
)
|
||||||
|
), 0) AS std_fcr
|
||||||
|
FROM weights w
|
||||||
|
GROUP BY w.week
|
||||||
|
ORDER BY w.week ASC
|
||||||
|
`, filterClause, filterClause)
|
||||||
|
|
||||||
|
args := make([]interface{}, 0, len(filterArgs)*2+2)
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
args = append(args, weeks)
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
args = append(args, weeks)
|
||||||
|
|
||||||
|
var rows []StandardWeeklyFcrMetric
|
||||||
|
if err := r.DB().WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_eggs AS re").
|
||||||
|
Select("COALESCE(SUM(re.qty * re.weight), 0)").
|
||||||
|
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Scan(&total).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
grams, err := r.SumEggProductionWeightGrams(ctx, start, end, filters)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return grams / 1000, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error) {
|
||||||
|
var rows []FeedUsageByUom
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_stocks AS rs").
|
||||||
|
Select("COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
||||||
|
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN uoms ON uoms.id = p.uom_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ? AND UPPER(f.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group("LOWER(uoms.name)").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumDepletions(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_depletions AS rd").
|
||||||
|
Select("COALESCE(SUM(rd.qty), 0)").
|
||||||
|
Joins("JOIN recordings AS r ON r.id = rd.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Scan(&total).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumInitialPopulation(ctx context.Context, endDate time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
endOfDate := endDate.AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("project_chickins AS pc").
|
||||||
|
Select("COALESCE(SUM(pc.usage_qty), 0)").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("pc.chick_in_date < ?", endOfDate).
|
||||||
|
Where("pc.deleted_at IS NULL")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Scan(&total).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error) {
|
||||||
|
var result SellingPriceAggregate
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("marketing_delivery_products AS mdp").
|
||||||
|
Select("COALESCE(SUM(mdp.total_price), 0) AS total_price, COALESCE(SUM(mdp.total_weight), 0) AS total_weight").
|
||||||
|
Joins("JOIN marketing_products AS mp ON mp.id = mdp.marketing_product_id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = mp.product_warehouse_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pw.project_flock_kandang_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("mdp.delivery_date IS NOT NULL").
|
||||||
|
Where("mdp.delivery_date >= ? AND mdp.delivery_date < ?", start, end)
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Scan(&result).Error; err != nil {
|
||||||
|
return SellingPriceAggregate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumSapronakCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("purchase_items AS pi").
|
||||||
|
Select("COALESCE(SUM(pi.total_price), 0) AS total").
|
||||||
|
Joins("JOIN products AS p ON p.id = pi.product_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("LEFT JOIN product_warehouses AS pw ON pw.id = pi.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = COALESCE(pi.project_flock_kandang_id, pw.project_flock_kandang_id)").
|
||||||
|
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("f.name IN ?", []utils.FlagType{utils.FlagDOC, utils.FlagPakan, utils.FlagOVK}).
|
||||||
|
Where("pi.received_date IS NOT NULL").
|
||||||
|
Where("pi.received_date >= ? AND pi.received_date < ?", start, end)
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Scan(&total).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumBopCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
return r.sumExpenseRealization(ctx, start, end, filters, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Where("e.category = ?", utils.ExpenseCategoryBOP).
|
||||||
|
Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id").
|
||||||
|
Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi).
|
||||||
|
Where("f.id IS NULL")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) SumEkspedisiCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
|
||||||
|
return r.sumExpenseRealization(ctx, start, end, filters, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Joins("JOIN nonstocks AS n ON n.id = en.nonstock_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||||
|
Where("f.name = ?", utils.FlagEkspedisi)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) sumExpenseRealization(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, modifier func(*gorm.DB) *gorm.DB) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("expense_realizations AS er").
|
||||||
|
Select("COALESCE(SUM(er.qty * er.price), 0) AS total").
|
||||||
|
Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id").
|
||||||
|
Joins("JOIN expenses AS e ON e.id = en.expense_id").
|
||||||
|
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = en.project_flock_kandang_id").
|
||||||
|
Joins("LEFT JOIN kandangs AS k ON k.id = COALESCE(en.kandang_id, pfk.kandang_id)").
|
||||||
|
Where("e.realization_date >= ? AND e.realization_date < ?", start, end)
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
db = modifier(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Scan(&total).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) standardIDSubquery(filters *validation.DashboardFilter) *gorm.DB {
|
||||||
|
db := r.DB().
|
||||||
|
Table("project_flocks AS pf").
|
||||||
|
Select("DISTINCT pf.production_standard_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("pf.production_standard_id > 0")
|
||||||
|
|
||||||
|
if filters != nil {
|
||||||
|
if len(filters.FlockIds) > 0 {
|
||||||
|
db = db.Where("pf.id IN ?", filters.FlockIds)
|
||||||
|
}
|
||||||
|
if len(filters.KandangIds) > 0 {
|
||||||
|
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||||
|
}
|
||||||
|
if len(filters.LokasiIds) > 0 {
|
||||||
|
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) standardSourceSubquery(filters *validation.DashboardFilter) *gorm.DB {
|
||||||
|
db := r.DB().
|
||||||
|
Table("project_flocks AS pf").
|
||||||
|
Select("DISTINCT pf.production_standard_id, pf.fcr_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("pf.production_standard_id > 0").
|
||||||
|
Where("pf.fcr_id > 0")
|
||||||
|
|
||||||
|
if filters != nil {
|
||||||
|
if len(filters.FlockIds) > 0 {
|
||||||
|
db = db.Where("pf.id IN ?", filters.FlockIds)
|
||||||
|
}
|
||||||
|
if len(filters.KandangIds) > 0 {
|
||||||
|
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||||
|
}
|
||||||
|
if len(filters.LokasiIds) > 0 {
|
||||||
|
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) {
|
||||||
|
seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []ComparisonSeries
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select(fmt.Sprintf("%s AS id, %s AS label", seriesExpr, labelExpr)).
|
||||||
|
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 project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||||
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group(groupExpr).Order(orderExpr).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType, metric string) ([]ComparisonWeeklyMetric, error) {
|
||||||
|
seriesExpr, _, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metricExpr, err := comparisonMetricColumn(metric)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []ComparisonWeeklyMetric
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select(fmt.Sprintf(`((r.day - 1) / 7 + 1) AS week,
|
||||||
|
%s AS series_id,
|
||||||
|
COALESCE(AVG(%s), 0) AS value`, seriesExpr, metricExpr)).
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||||
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("r.day IS NOT NULL AND r.day > 0")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
groupBy := fmt.Sprintf("week, %s", groupExpr)
|
||||||
|
orderBy := fmt.Sprintf("week ASC, %s", orderExpr)
|
||||||
|
if err := db.Group(groupBy).Order(orderBy).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetComparisonWeeklyUniformityMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonUniformityMetric, error) {
|
||||||
|
seriesExpr, _, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []ComparisonUniformityMetric
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("project_flock_kandang_uniformity AS u").
|
||||||
|
Select(fmt.Sprintf(`u.week AS week,
|
||||||
|
%s AS series_id,
|
||||||
|
COALESCE(AVG(u.uniformity), 0) AS uniformity,
|
||||||
|
COALESCE(AVG((u.chart_data->'statistics'->>'average_weight')::numeric), 0) AS average_weight`, seriesExpr)).
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||||
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
|
Where("u.uniform_date IS NOT NULL").
|
||||||
|
Where("u.uniform_date >= ? AND u.uniform_date < ?", start, end)
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
groupBy := fmt.Sprintf("u.week, %s", groupExpr)
|
||||||
|
orderBy := fmt.Sprintf("u.week ASC, %s", orderExpr)
|
||||||
|
if err := db.Group(groupBy).Order(orderBy).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error) {
|
||||||
|
var rows []EggQualityWeeklyMetric
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_eggs AS re").
|
||||||
|
Select(`
|
||||||
|
((r.day - 1) / 7 + 1) AS week,
|
||||||
|
COALESCE(SUM(CASE WHEN f.name = ? THEN re.qty ELSE 0 END), 0) AS normal_qty,
|
||||||
|
COALESCE(SUM(CASE WHEN f.name IN (?, ?, ?) THEN re.qty ELSE 0 END), 0) AS abnormal_qty,
|
||||||
|
COALESCE(SUM(re.qty), 0) AS total_qty`,
|
||||||
|
utils.FlagTelurUtuh,
|
||||||
|
utils.FlagTelurPutih,
|
||||||
|
utils.FlagTelurRetak,
|
||||||
|
utils.FlagTelurPecah,
|
||||||
|
).
|
||||||
|
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = re.product_warehouse_id").
|
||||||
|
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where("f.name IN ?", []utils.FlagType{utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah}).
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("r.day IS NOT NULL AND r.day > 0")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error) {
|
||||||
|
var rows []WeeklyEggWeightMetric
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_eggs AS re").
|
||||||
|
Select(`
|
||||||
|
((r.day - 1) / 7 + 1) AS week,
|
||||||
|
COALESCE(SUM(re.qty * re.weight), 0) AS egg_weight_grams`).
|
||||||
|
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("r.day IS NOT NULL AND r.day > 0")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DashboardRepositoryImpl) GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error) {
|
||||||
|
var rows []WeeklyFeedUsageMetric
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_stocks AS rs").
|
||||||
|
Select(`
|
||||||
|
((r.day - 1) / 7 + 1) AS week,
|
||||||
|
COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty,
|
||||||
|
LOWER(uoms.name) AS uom_name`).
|
||||||
|
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN uoms ON uoms.id = p.uom_id").
|
||||||
|
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ? AND UPPER(f.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("r.day IS NOT NULL AND r.day > 0")
|
||||||
|
|
||||||
|
db = applyDashboardFilters(db, filters)
|
||||||
|
|
||||||
|
if err := db.Group("week, LOWER(uoms.name)").Order("week ASC").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparisonSeriesColumns(comparisonType string) (string, string, string, string, error) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(comparisonType)) {
|
||||||
|
case validation.ComparisonTypeFarm:
|
||||||
|
return "loc.id", "loc.name", "loc.id, loc.name", "loc.name", nil
|
||||||
|
case validation.ComparisonTypeFlock:
|
||||||
|
return "pf.id", "pf.flock_name", "pf.id, pf.flock_name", "pf.flock_name", nil
|
||||||
|
case validation.ComparisonTypeKandang:
|
||||||
|
return "k.id", "k.name", "k.id, k.name", "k.name", nil
|
||||||
|
default:
|
||||||
|
return "", "", "", "", fmt.Errorf("invalid comparison_type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparisonMetricColumn(metric string) (string, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(metric)) {
|
||||||
|
case validation.MetricFcr:
|
||||||
|
return "r.fcr_value", nil
|
||||||
|
case validation.MetricMortality:
|
||||||
|
return "r.cum_depletion_rate", nil
|
||||||
|
case validation.MetricLaying:
|
||||||
|
return "r.hen_day", nil
|
||||||
|
case validation.MetricEggWeight:
|
||||||
|
return "r.egg_weight", nil
|
||||||
|
case validation.MetricFeedIntake:
|
||||||
|
return "r.feed_intake", nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid metric")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/controllers"
|
||||||
|
dashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DashboardRoutes(v1 fiber.Router, u user.UserService, s dashboard.DashboardService) {
|
||||||
|
ctrl := controller.NewDashboardController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/dashboards")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
route.Get("/",m.RequirePermissions(m.P_DashboardGetAll) ,ctrl.GetAll)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnalysisModeOverview = "OVERVIEW"
|
||||||
|
AnalysisModeComparison = "COMPARISON"
|
||||||
|
|
||||||
|
ComparisonTypeFarm = "FARM"
|
||||||
|
ComparisonTypeFlock = "FLOCK"
|
||||||
|
ComparisonTypeKandang = "KANDANG"
|
||||||
|
|
||||||
|
MetricFcr = "fcr"
|
||||||
|
MetricMortality = "mortality"
|
||||||
|
MetricLaying = "laying"
|
||||||
|
MetricEggWeight = "egg_weight"
|
||||||
|
MetricFeedIntake = "feed_intake"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
PerformanceOverviewFilter
|
||||||
|
PeriodStart time.Time `json:"-" query:"-"`
|
||||||
|
PeriodEnd time.Time `json:"-" query:"-"`
|
||||||
|
PeriodEndExclusive time.Time `json:"-" query:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerformanceOverviewFilter struct {
|
||||||
|
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
AnalysisMode string `query:"analysis_mode" validate:"omitempty,oneof=OVERVIEW COMPARISON"`
|
||||||
|
ComparisonType string `query:"comparison_type" validate:"omitempty,oneof=FARM FLOCK KANDANG"`
|
||||||
|
Metric string `query:"metric" validate:"omitempty,oneof=fcr mortality laying egg_weight feed_intake"`
|
||||||
|
LokasiIds []uint `query:"location_ids" validate:"omitempty,dive,gt=0"`
|
||||||
|
FlockIds []uint `query:"flock_ids" validate:"omitempty,dive,gt=0"`
|
||||||
|
KandangIds []uint `query:"kandang_ids" validate:"omitempty,dive,gt=0"`
|
||||||
|
Include []string `query:"include" validate:"omitempty,dive,oneof=statistics charts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardFilter struct {
|
||||||
|
LokasiIds []uint
|
||||||
|
FlockIds []uint
|
||||||
|
KandangIds []uint
|
||||||
|
}
|
||||||
@@ -229,10 +229,12 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
path := c.Path()
|
path := c.Path()
|
||||||
approvalType := ""
|
approvalType := ""
|
||||||
if strings.Contains(path, "/approvals/manager") {
|
if strings.Contains(path, "/approvals/head-area") {
|
||||||
approvalType = "manager"
|
approvalType = "head-area"
|
||||||
} else if strings.Contains(path, "/approvals/finance") {
|
} else if strings.Contains(path, "/approvals/finance") {
|
||||||
approvalType = "finance"
|
approvalType = "finance"
|
||||||
|
} else if strings.Contains(path, "/approvals/unit-vice-president") {
|
||||||
|
approvalType = "unit-vice-president"
|
||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -32,6 +33,7 @@ type ExpenseBaseDTO struct {
|
|||||||
|
|
||||||
type ExpenseListDTO struct {
|
type ExpenseListDTO struct {
|
||||||
ExpenseBaseDTO
|
ExpenseBaseDTO
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -140,8 +142,11 @@ func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
|
|||||||
latestApproval = &mapped
|
latestApproval = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grandTotal := calculateGrandTotal(&e)
|
||||||
|
|
||||||
return ExpenseListDTO{
|
return ExpenseListDTO{
|
||||||
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
||||||
|
GrandTotal: grandTotal,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
@@ -344,3 +349,25 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
|
|||||||
|
|
||||||
return kandangs
|
return kandangs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateGrandTotal(expense *entity.Expense) float64 {
|
||||||
|
|
||||||
|
useRealization := expense.LatestApproval != nil && expense.LatestApproval.StepNumber >= uint16(utils.ExpenseStepRealisasi)
|
||||||
|
|
||||||
|
if useRealization {
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, ns := range expense.Nonstocks {
|
||||||
|
if ns.Realization != nil {
|
||||||
|
total += ns.Realization.Qty * ns.Realization.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, ns := range expense.Nonstocks {
|
||||||
|
total += ns.Qty * ns.Price
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -15,6 +16,7 @@ type ExpenseRealizationRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||||
|
GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error)
|
||||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +57,40 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
|
|||||||
return realizations, err
|
return realizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) {
|
||||||
|
var realizations []entity.ExpenseRealization
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Preload("ExpenseNonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Flags").
|
||||||
|
Preload("ExpenseNonstock.Expense").
|
||||||
|
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||||
|
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||||
|
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
||||||
|
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
|
||||||
|
Where("expenses.realization_date IS NOT NULL")
|
||||||
|
|
||||||
|
if projectFlockKandangID != nil {
|
||||||
|
db = db.Where(`(
|
||||||
|
expense_nonstocks.project_flock_kandang_id = ? OR
|
||||||
|
(expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND
|
||||||
|
expense_nonstocks.project_flock_kandang_id IS NULL) OR
|
||||||
|
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
|
||||||
|
)`, *projectFlockKandangID, *projectFlockKandangID, fmt.Sprintf("[%d]", projectFlockID))
|
||||||
|
} else {
|
||||||
|
db = db.Where(`(
|
||||||
|
project_flock_kandangs.project_flock_id = ? OR
|
||||||
|
kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR
|
||||||
|
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
|
||||||
|
)`, projectFlockID, projectFlockID, fmt.Sprintf("[%d]", projectFlockID))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Find(&realizations).Error
|
||||||
|
return realizations, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
||||||
var realizations []entity.ExpenseRealization
|
var realizations []entity.ExpenseRealization
|
||||||
var total int64
|
var total int64
|
||||||
@@ -75,7 +111,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
|
|||||||
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
||||||
|
|
||||||
if filters.Search != "" {
|
if filters.Search != "" {
|
||||||
db = db.Where("expenses.category LIKE ? OR expenses.reference_number LIKE ? OR expenses.po_number LIKE ? OR expenses.notes LIKE ? OR suppliers.name LIKE ?",
|
db = db.Where("expenses.category ILIKE ? OR expenses.reference_number ILIKE ? OR expenses.po_number ILIKE ? OR expenses.notes ILIKE ? OR suppliers.name ILIKE ?",
|
||||||
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
|
||||||
route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
|
||||||
route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
|
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/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.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
||||||
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
||||||
route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
|
route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens
|
|||||||
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("category LIKE ?", "%"+params.Search+"%")
|
return db.Where("category ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -211,6 +211,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
if !hasKandang && req.Category == string(utils.ExpenseCategoryBOP) {
|
if !hasKandang && req.Category == string(utils.ExpenseCategoryBOP) {
|
||||||
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
||||||
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
||||||
}
|
}
|
||||||
@@ -395,6 +396,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["supplier_id"] = *req.SupplierID
|
updateBody["supplier_id"] = *req.SupplierID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Notes != nil {
|
||||||
|
updateBody["notes"] = *req.Notes
|
||||||
|
}
|
||||||
|
|
||||||
if req.LocationID != nil {
|
if req.LocationID != nil {
|
||||||
locationID := uint(*req.LocationID)
|
locationID := uint(*req.LocationID)
|
||||||
updateBody["location_id"] = locationID
|
updateBody["location_id"] = locationID
|
||||||
@@ -567,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
}
|
}
|
||||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
|
||||||
|
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionUpdated
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
|
|
||||||
|
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
|
||||||
|
|
||||||
|
if previousStep < utils.ExpenseStepPengajuan {
|
||||||
|
previousStep = utils.ExpenseStepPengajuan
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowExpense,
|
utils.ApprovalWorkflowExpense,
|
||||||
id,
|
id,
|
||||||
utils.ExpenseStepPengajuan,
|
previousStep,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
@@ -1048,21 +1061,30 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stepNumber approvalutils.ApprovalStep
|
var stepNumber approvalutils.ApprovalStep
|
||||||
if approvalType == "manager" {
|
if approvalType == "head-area" {
|
||||||
|
|
||||||
stepNumber = utils.ExpenseStepManager
|
stepNumber = utils.ExpenseStepHeadArea
|
||||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
|
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
|
||||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
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" {
|
} else if approvalType == "finance" {
|
||||||
|
|
||||||
stepNumber = utils.ExpenseStepFinance
|
stepNumber = utils.ExpenseStepFinance
|
||||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) {
|
if latestApproval.StepNumber != uint16(utils.ExpenseStepUnitVicePresident) {
|
||||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
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 {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Update struct {
|
|||||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
||||||
|
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,17 +104,22 @@ func partyFromInitial(e entity.Payment) Party {
|
|||||||
Id: e.PartyId,
|
Id: e.PartyId,
|
||||||
Type: e.PartyType,
|
Type: e.PartyType,
|
||||||
}
|
}
|
||||||
|
if e.PartyAccountNumber != nil {
|
||||||
|
party.AccountNumber = *e.PartyAccountNumber
|
||||||
|
}
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
switch utils.PaymentParty(e.PartyType) {
|
||||||
case utils.PaymentPartyCustomer:
|
case utils.PaymentPartyCustomer:
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
if e.Customer != nil && e.Customer.Id != 0 {
|
||||||
party.Name = e.Customer.Name
|
party.Name = e.Customer.Name
|
||||||
|
if party.AccountNumber == "" {
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
party.AccountNumber = e.Customer.AccountNumber
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case utils.PaymentPartySupplier:
|
case utils.PaymentPartySupplier:
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
party.Name = e.Supplier.Name
|
party.Name = e.Supplier.Name
|
||||||
if e.Supplier.AccountNumber != nil {
|
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
party.AccountNumber = *e.Supplier.AccountNumber
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
TransactionType: string(utils.TransactionTypeSaldoAwal),
|
TransactionType: string(utils.TransactionTypeSaldoAwal),
|
||||||
PartyType: party,
|
PartyType: party,
|
||||||
PartyId: req.PartyId,
|
PartyId: req.PartyId,
|
||||||
|
PartyAccountNumber: nil,
|
||||||
PaymentDate: time.Now(),
|
PaymentDate: time.Now(),
|
||||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
PaymentMethod: string(utils.PaymentMethodSaldo),
|
||||||
BankId: req.BankId,
|
BankId: req.BankId,
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
TransactionType: string(utils.TransactionTypeInjection),
|
TransactionType: string(utils.TransactionTypeInjection),
|
||||||
PartyType: string(utils.PaymentPartyCustomer),
|
PartyType: string(utils.PaymentPartyCustomer),
|
||||||
PartyId: 0,
|
PartyId: 0,
|
||||||
|
PartyAccountNumber: nil,
|
||||||
PaymentDate: adjustmentDate,
|
PaymentDate: adjustmentDate,
|
||||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
PaymentMethod: string(utils.PaymentMethodSaldo),
|
||||||
BankId: req.BankId,
|
BankId: req.BankId,
|
||||||
|
|||||||
@@ -127,17 +127,22 @@ func partyFromPayment(e entity.Payment) Party {
|
|||||||
Id: e.PartyId,
|
Id: e.PartyId,
|
||||||
Type: e.PartyType,
|
Type: e.PartyType,
|
||||||
}
|
}
|
||||||
|
if e.PartyAccountNumber != nil {
|
||||||
|
party.AccountNumber = *e.PartyAccountNumber
|
||||||
|
}
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
switch utils.PaymentParty(e.PartyType) {
|
||||||
case utils.PaymentPartyCustomer:
|
case utils.PaymentPartyCustomer:
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
if e.Customer != nil && e.Customer.Id != 0 {
|
||||||
party.Name = e.Customer.Name
|
party.Name = e.Customer.Name
|
||||||
|
if party.AccountNumber == "" {
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
party.AccountNumber = e.Customer.AccountNumber
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case utils.PaymentPartySupplier:
|
case utils.PaymentPartySupplier:
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
party.Name = e.Supplier.Name
|
party.Name = e.Supplier.Name
|
||||||
if e.Supplier.AccountNumber != nil {
|
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
party.AccountNumber = *e.Supplier.AccountNumber
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService
|
|||||||
route := v1.Group("/payments")
|
route := v1.Group("/payments")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
|
route.Post("/", m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
|
||||||
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user