mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FEAT/BE/report_customer_payment
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
# .air.toml
|
||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
||||||
|
bin = "tmp/main"
|
||||||
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
exclude_dir = ["vendor", "tmp"]
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = true
|
||||||
+2
-1
@@ -9,11 +9,12 @@ 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
|
||||||
|
Dockerfile.local
|
||||||
.gitlab-ci.yml
|
.gitlab-ci.yml
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
|
|||||||
+81
-164
@@ -1,173 +1,90 @@
|
|||||||
stages:
|
stages:
|
||||||
- build
|
|
||||||
- migrate
|
|
||||||
- deploy
|
- deploy
|
||||||
- seed
|
|
||||||
|
|
||||||
default:
|
deploy-dev:
|
||||||
tags:
|
|
||||||
- self-hosted-stg
|
|
||||||
|
|
||||||
workflow:
|
|
||||||
rules:
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
|
||||||
when: always
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
variables:
|
|
||||||
DOCKER_BUILDKIT: "1"
|
|
||||||
|
|
||||||
IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
|
|
||||||
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
|
||||||
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
|
|
||||||
|
|
||||||
DEPLOY_DIR: "/opt/deploy/stg-lti-api"
|
|
||||||
COMPOSE_FILE: "docker-compose.yaml"
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# BUILD (AUTO)
|
|
||||||
# =========================
|
|
||||||
build_staging:
|
|
||||||
stage: build
|
|
||||||
rules:
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
docker info
|
|
||||||
|
|
||||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
|
||||||
|
|
||||||
echo "✅ Build image: $IMAGE_NAME"
|
|
||||||
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
|
||||||
|
|
||||||
echo "✅ Push image: $IMAGE_NAME"
|
|
||||||
docker push "$IMAGE_NAME"
|
|
||||||
|
|
||||||
echo "✅ Tag latest: $IMAGE_LATEST"
|
|
||||||
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
|
||||||
docker push "$IMAGE_LATEST"
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# MIGRATE (AUTO)
|
|
||||||
# =========================
|
|
||||||
migrate_staging:
|
|
||||||
stage: migrate
|
|
||||||
rules:
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
|
||||||
needs:
|
|
||||||
- job: build_staging
|
|
||||||
artifacts: false
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
echo "✅ Running migrations (staging) ..."
|
|
||||||
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
|
||||||
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
|
||||||
|
|
||||||
# ✅ load env dari server
|
|
||||||
set -a
|
|
||||||
. ./.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
# ✅ validasi
|
|
||||||
test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1)
|
|
||||||
test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1)
|
|
||||||
test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1)
|
|
||||||
test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1)
|
|
||||||
test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1)
|
|
||||||
|
|
||||||
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}"
|
|
||||||
echo "✅ DATABASE_URL=$DATABASE_URL"
|
|
||||||
|
|
||||||
# ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!)
|
|
||||||
echo "✅ Ensuring postgres & redis running ..."
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true
|
|
||||||
|
|
||||||
# ✅ Ambil network key dari compose
|
|
||||||
COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')"
|
|
||||||
echo "✅ Compose network key: $COMPOSE_NETWORK_KEY"
|
|
||||||
|
|
||||||
# ✅ Cari network name yang dipakai docker
|
|
||||||
NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)"
|
|
||||||
test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1)
|
|
||||||
|
|
||||||
echo "✅ Docker network detected: $NETWORK_NAME"
|
|
||||||
|
|
||||||
# ✅ Migrations dari repo (CI workspace)
|
|
||||||
echo "✅ Checking migrations from repo..."
|
|
||||||
ls -lah "$CI_PROJECT_DIR/internal/database/migrations"
|
|
||||||
|
|
||||||
echo "✅ Running migrations via migrate/migrate container"
|
|
||||||
set +e
|
|
||||||
out=$(docker run --rm \
|
|
||||||
--network "$NETWORK_NAME" \
|
|
||||||
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
|
|
||||||
migrate/migrate:v4.15.2 \
|
|
||||||
-path=/migrations -database "$DATABASE_URL" up 2>&1)
|
|
||||||
code=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "$out"
|
|
||||||
|
|
||||||
# ✅ Handle no change dengan benar (tidak false-success)
|
|
||||||
if echo "$out" | grep -qi "no change"; then
|
|
||||||
echo "✅ No change (already up to date)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $code -ne 0 ]; then
|
|
||||||
echo "❌ Migration failed with exit code $code"
|
|
||||||
exit $code
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Migration applied successfully"
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# DEPLOY (AUTO)
|
|
||||||
# =========================
|
|
||||||
deploy_staging:
|
|
||||||
stage: deploy
|
stage: deploy
|
||||||
rules:
|
image: alpine:3.20
|
||||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
variables:
|
||||||
needs:
|
DEPLOY_APP: "LTI-MBUGROUP"
|
||||||
- job: migrate_staging
|
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||||
artifacts: false
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
- job: build_staging
|
GIT_DEPTH: "1"
|
||||||
artifacts: false
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
docker info
|
|
||||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
|
||||||
|
|
||||||
cd "$DEPLOY_DIR"
|
before_script:
|
||||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
- echo "🧰 Installing dependencies..."
|
||||||
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
- apk update && apk add --no-cache openssh git curl bash
|
||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" pull
|
# Setup SSH di runner
|
||||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
- mkdir -p ~/.ssh
|
||||||
docker image prune -f
|
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||||
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
|
- eval "$(ssh-agent -s)"
|
||||||
|
- ssh-add ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||||
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
|
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# =========================
|
script:
|
||||||
# SEED (MANUAL)
|
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||||
# =========================
|
|
||||||
seed_staging:
|
|
||||||
stage: seed
|
|
||||||
rules:
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
|
||||||
needs:
|
|
||||||
- job: deploy_staging
|
|
||||||
artifacts: false
|
|
||||||
when: manual
|
|
||||||
allow_failure: false
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
|
|
||||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
|
||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" pull seed || true
|
- >
|
||||||
docker compose -f "$COMPOSE_FILE" run --rm seed
|
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd /home/devops/docker/deployment/development/lti-api
|
||||||
|
|
||||||
|
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
||||||
|
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
||||||
|
|
||||||
|
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Fetch/reset pakai SSH
|
||||||
|
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
||||||
|
git reset --hard origin/development
|
||||||
|
|
||||||
|
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||||
|
"; then
|
||||||
|
STATUS='success';
|
||||||
|
else
|
||||||
|
STATUS='failed';
|
||||||
|
fi;
|
||||||
|
|
||||||
|
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
||||||
|
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
COLOR=3066993;
|
||||||
|
TITLE="✅ Deployment API Succeeded";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
||||||
|
else
|
||||||
|
COLOR=15158332;
|
||||||
|
TITLE="❌ Deployment API Failed Gaes";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
||||||
|
fi;
|
||||||
|
|
||||||
|
echo "{
|
||||||
|
\"username\": \"CI Bot\",
|
||||||
|
\"embeds\": [{
|
||||||
|
\"title\": \"$TITLE\",
|
||||||
|
\"description\": \"$DESC\",
|
||||||
|
\"color\": $COLOR,
|
||||||
|
\"fields\": [
|
||||||
|
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
||||||
|
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
||||||
|
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
||||||
|
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}" > payload.json;
|
||||||
|
|
||||||
|
echo "📡 Sending notification to Discord...";
|
||||||
|
curl -sS -H "Content-Type: application/json" \
|
||||||
|
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||||
|
|
||||||
|
only:
|
||||||
|
- development
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -5,6 +5,7 @@ import "time"
|
|||||||
type NonstockSupplier struct {
|
type NonstockSupplier struct {
|
||||||
NonstockId uint `gorm:"not null"`
|
NonstockId 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"`
|
||||||
|
|
||||||
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
Nonstock Nonstock `gorm:"foreignKey:NonstockId;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"`
|
||||||
|
|||||||
@@ -116,7 +116,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
|
||||||
}
|
}
|
||||||
@@ -228,6 +238,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")
|
||||||
@@ -404,7 +422,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,omitempty"`
|
||||||
|
EggMass *float64 `json:"egg_mass,omitempty"`
|
||||||
|
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
|
||||||
|
EggWeight *float64 `json:"egg_weight,omitempty"`
|
||||||
|
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
|
||||||
|
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
||||||
|
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -33,11 +34,13 @@ 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, projectFlockKandangRepo, 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)
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -18,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)
|
||||||
@@ -166,6 +167,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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,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"
|
||||||
@@ -34,9 +36,9 @@ 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, projectFlockKandangID *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)
|
||||||
|
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
||||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
|
|
||||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
||||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||||
@@ -56,9 +58,11 @@ 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, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
|
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,
|
||||||
@@ -73,6 +77,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
|||||||
ChickinRepo: chickinRepo,
|
ChickinRepo: chickinRepo,
|
||||||
PurchaseRepo: purchaseRepo,
|
PurchaseRepo: purchaseRepo,
|
||||||
RecordingRepo: recordingRepo,
|
RecordingRepo: recordingRepo,
|
||||||
|
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||||
|
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,11 +151,15 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF
|
|||||||
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")
|
||||||
@@ -170,6 +180,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")
|
||||||
@@ -210,7 +338,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
|
|
||||||
var projectFlockKandangIDs []uint
|
var projectFlockKandangIDs []uint
|
||||||
if params.Type == validation.SapronakTypeOutgoing {
|
if params.Type == validation.SapronakTypeOutgoing {
|
||||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
|
||||||
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)
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
@@ -290,12 +418,15 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *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
|
if kandangID != nil {
|
||||||
|
query = query.Where("kandang_id = ?", *kandangID)
|
||||||
|
}
|
||||||
|
err := query.Order("id ASC").Pluck("id", &ids).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -554,12 +685,22 @@ 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)
|
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
@@ -568,19 +709,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
|
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)
|
||||||
@@ -589,6 +740,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)
|
||||||
@@ -611,10 +796,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),
|
||||||
@@ -622,7 +807,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)}
|
||||||
@@ -655,6 +840,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
|
||||||
@@ -702,6 +890,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,15 +909,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{
|
||||||
@@ -772,6 +1011,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)
|
||||||
|
|
||||||
@@ -802,7 +1081,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
|
|||||||
FcrStd: fcrStd,
|
FcrStd: fcrStd,
|
||||||
FcrAct: fcrAct,
|
FcrAct: fcrAct,
|
||||||
DeffFcr: deffFcr,
|
DeffFcr: deffFcr,
|
||||||
Awg: awg,
|
AwgAct: awg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingSapronakQuery struct {
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
|
||||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
@@ -23,7 +22,7 @@ type NonstockListDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Flags []string `json:"flags"`
|
Flags []string `json:"flags"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
Suppliers []NonstockSupplierDTO `json:"suppliers"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -33,6 +32,14 @@ type NonstockDetailDTO struct {
|
|||||||
NonstockListDTO
|
NonstockListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NonstockSupplierDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO {
|
func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO {
|
||||||
@@ -99,21 +106,27 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
|
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []NonstockSupplierDTO {
|
||||||
if len(relations) == 0 {
|
if len(relations) == 0 {
|
||||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
return make([]NonstockSupplierDTO, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
result := make([]NonstockSupplierDTO, 0, len(relations))
|
||||||
for _, relation := range relations {
|
for _, relation := range relations {
|
||||||
if relation.Supplier.Id == 0 {
|
if relation.Supplier.Id == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
result = append(result, NonstockSupplierDTO{
|
||||||
|
Id: relation.Supplier.Id,
|
||||||
|
Name: relation.Supplier.Name,
|
||||||
|
Alias: relation.Supplier.Alias,
|
||||||
|
Category: relation.Supplier.Category,
|
||||||
|
Price: relation.Price,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
return make([]NonstockSupplierDTO, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
type NonstockRepository interface {
|
type NonstockRepository interface {
|
||||||
repository.BaseRepository[entity.Nonstock]
|
repository.BaseRepository[entity.Nonstock]
|
||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error
|
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error
|
||||||
UomExists(ctx context.Context, uomID uint) (bool, error)
|
UomExists(ctx context.Context, uomID uint) (bool, error)
|
||||||
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
||||||
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
|
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
|
||||||
@@ -40,13 +40,13 @@ func (r *NonstockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, e
|
|||||||
return repository.Exists[entity.Nonstock](ctx, r.DB(), id)
|
return repository.Exists[entity.Nonstock](ctx, r.DB(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
|
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error {
|
||||||
db := tx
|
db := tx
|
||||||
if db == nil {
|
if db == nil {
|
||||||
db = r.DB()
|
db = r.DB()
|
||||||
}
|
}
|
||||||
|
|
||||||
if supplierIDs == nil {
|
if suppliers == nil {
|
||||||
return db.WithContext(ctx).
|
return db.WithContext(ctx).
|
||||||
Where("nonstock_id = ?", nonstockID).
|
Where("nonstock_id = ?", nonstockID).
|
||||||
Delete(&entity.NonstockSupplier{}).
|
Delete(&entity.NonstockSupplier{}).
|
||||||
@@ -61,18 +61,31 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingMap := make(map[uint]struct{}, len(existing))
|
existingMap := make(map[uint]entity.NonstockSupplier, len(existing))
|
||||||
for _, rel := range existing {
|
for _, rel := range existing {
|
||||||
existingMap[rel.SupplierId] = struct{}{}
|
existingMap[rel.SupplierId] = rel
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingMap := make(map[uint]struct{}, len(supplierIDs))
|
incomingMap := make(map[uint]struct{}, len(suppliers))
|
||||||
for _, id := range supplierIDs {
|
for _, rel := range suppliers {
|
||||||
incomingMap[id] = struct{}{}
|
incomingMap[rel.SupplierId] = struct{}{}
|
||||||
if _, exists := existingMap[id]; exists {
|
if existingRel, exists := existingMap[rel.SupplierId]; exists {
|
||||||
|
if existingRel.Price != rel.Price {
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entity.NonstockSupplier{}).
|
||||||
|
Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierId).
|
||||||
|
Update("price", rel.Price).
|
||||||
|
Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id}
|
record := entity.NonstockSupplier{
|
||||||
|
NonstockId: nonstockID,
|
||||||
|
SupplierId: rel.SupplierId,
|
||||||
|
Price: rel.Price,
|
||||||
|
}
|
||||||
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
|
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,25 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
|
var (
|
||||||
if len(supplierIDs) > 0 {
|
supplierLinks []entity.NonstockSupplier
|
||||||
|
supplierIDs []uint
|
||||||
|
)
|
||||||
|
if len(req.Suppliers) > 0 {
|
||||||
|
seen := make(map[uint]struct{}, len(req.Suppliers))
|
||||||
|
supplierLinks = make([]entity.NonstockSupplier, 0, len(req.Suppliers))
|
||||||
|
supplierIDs = make([]uint, 0, len(req.Suppliers))
|
||||||
|
for _, supplier := range req.Suppliers {
|
||||||
|
if _, exists := seen[supplier.SupplierID]; exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
|
||||||
|
}
|
||||||
|
seen[supplier.SupplierID] = struct{}{}
|
||||||
|
supplierIDs = append(supplierIDs, supplier.SupplierID)
|
||||||
|
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
|
||||||
|
SupplierId: supplier.SupplierID,
|
||||||
|
Price: supplier.Price,
|
||||||
|
})
|
||||||
|
}
|
||||||
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||||
if supplierErr != nil {
|
if supplierErr != nil {
|
||||||
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
||||||
@@ -155,7 +172,7 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
|
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,15 +210,27 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
|||||||
updateBody["uom_id"] = *req.UomID
|
updateBody["uom_id"] = *req.UomID
|
||||||
}
|
}
|
||||||
|
|
||||||
var supplierIDs []uint
|
var supplierLinks []entity.NonstockSupplier
|
||||||
var supplierUpdate bool
|
var supplierUpdate bool
|
||||||
if req.SupplierIDs != nil {
|
if req.Suppliers != nil {
|
||||||
supplierUpdate = true
|
supplierUpdate = true
|
||||||
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
|
if len(*req.Suppliers) > 0 {
|
||||||
if len(supplierIDs) > 0 {
|
seen := make(map[uint]struct{}, len(*req.Suppliers))
|
||||||
var supplierList []entity.Supplier
|
supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.Suppliers))
|
||||||
var supplierErr error
|
supplierIDs := make([]uint, 0, len(*req.Suppliers))
|
||||||
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
for _, supplier := range *req.Suppliers {
|
||||||
|
if _, exists := seen[supplier.SupplierID]; exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
|
||||||
|
}
|
||||||
|
seen[supplier.SupplierID] = struct{}{}
|
||||||
|
supplierIDs = append(supplierIDs, supplier.SupplierID)
|
||||||
|
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
|
||||||
|
SupplierId: supplier.SupplierID,
|
||||||
|
Price: supplier.Price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||||
if supplierErr != nil {
|
if supplierErr != nil {
|
||||||
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
|
||||||
@@ -253,11 +282,7 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
if supplierUpdate {
|
if supplierUpdate {
|
||||||
var ids []uint
|
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
|
||||||
if len(supplierIDs) > 0 {
|
|
||||||
ids = supplierIDs
|
|
||||||
}
|
|
||||||
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
type SupplierPrice struct {
|
||||||
|
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||||
|
Price float64 `json:"price" validate:"required,gte=0"`
|
||||||
|
}
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3,max=50"`
|
Name string `json:"name" validate:"required_strict,min=3,max=50"`
|
||||||
UomID uint `json:"uom_id" validate:"required,gt=0"`
|
UomID uint `json:"uom_id" validate:"required,gt=0"`
|
||||||
SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"`
|
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||||
Flags []string `json:"flags" validate:"dive,max=50"`
|
Flags []string `json:"flags" validate:"dive,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
|
||||||
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
|
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||||
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
|
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
@@ -22,6 +24,8 @@ type ProductionStandardService interface {
|
|||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
EnsureWeekStart(ctx context.Context, standardID uint, category string) error
|
||||||
|
EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type productionStandardService struct {
|
type productionStandardService struct {
|
||||||
@@ -299,3 +303,80 @@ func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s productionStandardService) EnsureWeekStart(ctx context.Context, standardID uint, category string) error {
|
||||||
|
if standardID == 0 || strings.TrimSpace(category) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToUpper(category) {
|
||||||
|
case string(utils.ProjectFlockCategoryLaying):
|
||||||
|
details, err := s.ProductionStandardDetailRepo.GetByProductionStandardID(ctx, standardID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
startWeek := 0
|
||||||
|
if len(details) > 0 {
|
||||||
|
startWeek = details[0].Week
|
||||||
|
}
|
||||||
|
if startWeek != 18 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock")
|
||||||
|
}
|
||||||
|
case string(utils.ProjectFlockCategoryGrowing):
|
||||||
|
details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(ctx, standardID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
startWeek := 0
|
||||||
|
if len(details) > 0 {
|
||||||
|
startWeek = details[0].Week
|
||||||
|
}
|
||||||
|
if startWeek != 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productionStandardService) EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error {
|
||||||
|
if standardID == 0 || day <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
upperCategory := strings.ToUpper(category)
|
||||||
|
weekBase := 1
|
||||||
|
if upperCategory == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
weekBase = 18
|
||||||
|
}
|
||||||
|
week := ((day - 1) / 7) + weekBase
|
||||||
|
if week <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if upperCategory == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
detail, err := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if detail == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
growthDetail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if growthDetail == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
|
||||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
@@ -20,7 +19,7 @@ type ProductRelationDTO struct {
|
|||||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
Flags *[]string `json:"flags,omitempty"`
|
Flags *[]string `json:"flags,omitempty"`
|
||||||
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
Suppliers []ProductSupplierDTO `json:"suppliers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductListDTO struct {
|
type ProductListDTO struct {
|
||||||
@@ -35,7 +34,7 @@ type ProductListDTO struct {
|
|||||||
Flags []string `json:"flags"`
|
Flags []string `json:"flags"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
Suppliers []ProductSupplierDTO `json:"suppliers"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -45,6 +44,14 @@ type ProductDetailDTO struct {
|
|||||||
ProductListDTO
|
ProductListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductSupplierDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
|
func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
|
||||||
@@ -134,21 +141,27 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
|
func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO {
|
||||||
if len(relations) == 0 {
|
if len(relations) == 0 {
|
||||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
return make([]ProductSupplierDTO, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
result := make([]ProductSupplierDTO, 0, len(relations))
|
||||||
for _, relation := range relations {
|
for _, relation := range relations {
|
||||||
if relation.Supplier.Id == 0 {
|
if relation.Supplier.Id == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
result = append(result, ProductSupplierDTO{
|
||||||
|
Id: relation.Supplier.Id,
|
||||||
|
Name: relation.Supplier.Name,
|
||||||
|
Alias: relation.Supplier.Alias,
|
||||||
|
Category: relation.Supplier.Category,
|
||||||
|
Price: relation.Price,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
return make([]ProductSupplierDTO, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type ProductRepository interface {
|
|||||||
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
|
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
|
||||||
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
||||||
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
|
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
|
||||||
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
|
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error
|
||||||
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
|
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
|
||||||
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
|
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
|
||||||
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error)
|
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error)
|
||||||
@@ -102,13 +102,13 @@ func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productI
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIds []uint) error {
|
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error {
|
||||||
db := tx
|
db := tx
|
||||||
if db == nil {
|
if db == nil {
|
||||||
db = r.DB()
|
db = r.DB()
|
||||||
}
|
}
|
||||||
|
|
||||||
if supplierIds == nil {
|
if suppliers == nil {
|
||||||
return db.WithContext(ctx).
|
return db.WithContext(ctx).
|
||||||
Where("product_id = ?", productID).
|
Where("product_id = ?", productID).
|
||||||
Delete(&entity.ProductSupplier{}).
|
Delete(&entity.ProductSupplier{}).
|
||||||
@@ -123,18 +123,31 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingMap := make(map[uint]struct{}, len(existing))
|
existingMap := make(map[uint]entity.ProductSupplier, len(existing))
|
||||||
for _, rel := range existing {
|
for _, rel := range existing {
|
||||||
existingMap[rel.SupplierId] = struct{}{}
|
existingMap[rel.SupplierId] = rel
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingMap := make(map[uint]struct{}, len(supplierIds))
|
incomingMap := make(map[uint]struct{}, len(suppliers))
|
||||||
for _, id := range supplierIds {
|
for _, rel := range suppliers {
|
||||||
incomingMap[id] = struct{}{}
|
incomingMap[rel.SupplierId] = struct{}{}
|
||||||
if _, exists := existingMap[id]; exists {
|
if existingRel, exists := existingMap[rel.SupplierId]; exists {
|
||||||
|
if existingRel.Price != rel.Price {
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entity.ProductSupplier{}).
|
||||||
|
Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierId).
|
||||||
|
Update("price", rel.Price).
|
||||||
|
Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
record := entity.ProductSupplier{ProductId: productID, SupplierId: id}
|
record := entity.ProductSupplier{
|
||||||
|
ProductId: productID,
|
||||||
|
SupplierId: rel.SupplierId,
|
||||||
|
Price: rel.Price,
|
||||||
|
}
|
||||||
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
|
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,9 +138,25 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
|
var (
|
||||||
var err error
|
supplierLinks []entity.ProductSupplier
|
||||||
if len(supplierIDs) > 0 {
|
supplierIDs []uint
|
||||||
|
)
|
||||||
|
if len(req.Suppliers) > 0 {
|
||||||
|
seen := make(map[uint]struct{}, len(req.Suppliers))
|
||||||
|
supplierLinks = make([]entity.ProductSupplier, 0, len(req.Suppliers))
|
||||||
|
supplierIDs = make([]uint, 0, len(req.Suppliers))
|
||||||
|
for _, supplier := range req.Suppliers {
|
||||||
|
if _, exists := seen[supplier.SupplierID]; exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
|
||||||
|
}
|
||||||
|
seen[supplier.SupplierID] = struct{}{}
|
||||||
|
supplierIDs = append(supplierIDs, supplier.SupplierID)
|
||||||
|
supplierLinks = append(supplierLinks, entity.ProductSupplier{
|
||||||
|
SupplierId: supplier.SupplierID,
|
||||||
|
Price: supplier.Price,
|
||||||
|
})
|
||||||
|
}
|
||||||
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to validate suppliers: %+v", err)
|
s.Log.Errorf("Failed to validate suppliers: %+v", err)
|
||||||
@@ -180,7 +196,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
CreatedBy: 1,
|
CreatedBy: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
repoTx := s.Repository.WithTx(tx)
|
repoTx := s.Repository.WithTx(tx)
|
||||||
|
|
||||||
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
|
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
|
||||||
@@ -191,7 +207,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
|
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -276,15 +292,27 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|
||||||
var suppliers []entity.Supplier
|
var supplierLinks []entity.ProductSupplier
|
||||||
var supplierIDs []uint
|
|
||||||
var supplierUpdate bool
|
var supplierUpdate bool
|
||||||
if req.SupplierIDs != nil {
|
if req.Suppliers != nil {
|
||||||
supplierUpdate = true
|
supplierUpdate = true
|
||||||
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
|
if len(*req.Suppliers) > 0 {
|
||||||
if len(supplierIDs) > 0 {
|
seen := make(map[uint]struct{}, len(*req.Suppliers))
|
||||||
var err error
|
supplierLinks = make([]entity.ProductSupplier, 0, len(*req.Suppliers))
|
||||||
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
supplierIDs := make([]uint, 0, len(*req.Suppliers))
|
||||||
|
for _, supplier := range *req.Suppliers {
|
||||||
|
if _, exists := seen[supplier.SupplierID]; exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
|
||||||
|
}
|
||||||
|
seen[supplier.SupplierID] = struct{}{}
|
||||||
|
supplierIDs = append(supplierIDs, supplier.SupplierID)
|
||||||
|
supplierLinks = append(supplierLinks, entity.ProductSupplier{
|
||||||
|
SupplierId: supplier.SupplierID,
|
||||||
|
Price: supplier.Price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to validate suppliers: %+v", err)
|
s.Log.Errorf("Failed to validate suppliers: %+v", err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
|
||||||
@@ -336,11 +364,7 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if supplierUpdate {
|
if supplierUpdate {
|
||||||
var ids []uint
|
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
|
||||||
if len(supplierIDs) > 0 {
|
|
||||||
ids = supplierIDs
|
|
||||||
}
|
|
||||||
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
type SupplierPrice struct {
|
||||||
|
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||||
|
Price float64 `json:"price" validate:"required,gte=0"`
|
||||||
|
}
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3,max=50"`
|
Name string `json:"name" validate:"required_strict,min=3,max=50"`
|
||||||
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
|
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
|
||||||
@@ -10,7 +15,7 @@ type Create struct {
|
|||||||
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
|
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
|
||||||
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
|
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
|
||||||
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
|
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
|
||||||
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||||
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
|
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +29,7 @@ type Update struct {
|
|||||||
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
|
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
|
||||||
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
|
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
|
||||||
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
|
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
|
||||||
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||||
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
|
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type SupplierNonstockDTO struct {
|
type SupplierNonstockDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
Flags []string `json:"flags"`
|
Flags []string `json:"flags"`
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,7 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst
|
|||||||
result = append(result, SupplierNonstockDTO{
|
result = append(result, SupplierNonstockDTO{
|
||||||
Id: Nonstock.Id,
|
Id: Nonstock.Id,
|
||||||
Name: Nonstock.Name,
|
Name: Nonstock.Name,
|
||||||
|
Price: relation.Price,
|
||||||
Uom: uomRef,
|
Uom: uomRef,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type SupplierProductDTO struct {
|
type SupplierProductDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
|
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
|
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
SupplierPrice float64 `json:"supplier_price"`
|
||||||
Flags []string `json:"flags"`
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
@@ -42,12 +43,13 @@ func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProduct
|
|||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, SupplierProductDTO{
|
result = append(result, SupplierProductDTO{
|
||||||
Id: product.Id,
|
Id: product.Id,
|
||||||
Name: product.Name,
|
Name: product.Name,
|
||||||
ProductPrice: product.ProductPrice,
|
ProductPrice: product.ProductPrice,
|
||||||
SellingPrice: product.SellingPrice,
|
SellingPrice: product.SellingPrice,
|
||||||
Uom: uomRef,
|
SupplierPrice: relation.Price,
|
||||||
Flags: flags,
|
Uom: uomRef,
|
||||||
|
Flags: flags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -14,44 +14,83 @@ import (
|
|||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type RecordingProjectFlockDTO struct {
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
|
FlockName string `json:"flock_name"`
|
||||||
|
ProjectFlockCategory string `json:"project_flock_category"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
||||||
|
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
||||||
|
TotalChickQty float64 `json:"total_chick_qty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingProductionStandardDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Week int `json:"week"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HenDayStd float64 `json:"hen_day_std"`
|
||||||
|
HenHouseStd float64 `json:"hen_house_std"`
|
||||||
|
FeedIntakeStd float64 `json:"feed_intake_std"`
|
||||||
|
MaxDepletionStd float64 `json:"max_depletion_std"`
|
||||||
|
EggMassStd float64 `json:"egg_mass_std"`
|
||||||
|
EggWeightStd float64 `json:"egg_weight_std"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingFcrDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FcrStd float64 `json:"fcr_std"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingAreaDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingLocationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingWarehouseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Area *RecordingAreaDTO `json:"area,omitempty"`
|
||||||
|
Location *RecordingLocationDTO `json:"location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type RecordingRelationDTO struct {
|
type RecordingRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlock RecordingProjectFlockDTO `json:"project_flock"`
|
||||||
RecordDatetime time.Time `json:"record_datetime"`
|
RecordDatetime time.Time `json:"record_datetime"`
|
||||||
Day int `json:"day"`
|
Day int `json:"day"`
|
||||||
ProjectFlockCategory string `json:"project_flock_category"`
|
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
||||||
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
||||||
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
CumIntake int `json:"cum_intake"`
|
||||||
CumIntake int `json:"cum_intake"`
|
FcrValue float64 `json:"fcr_value"`
|
||||||
FcrValue float64 `json:"fcr_value"`
|
HenDay float64 `json:"hen_day"`
|
||||||
TotalChickQty float64 `json:"total_chick_qty"`
|
HenHouse float64 `json:"hen_house"`
|
||||||
HenDay float64 `json:"hen_day"`
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
HenHouse float64 `json:"hen_house"`
|
EggMass float64 `json:"egg_mass"`
|
||||||
FeedIntake float64 `json:"feed_intake"`
|
EggWeight float64 `json:"egg_weight"`
|
||||||
EggMass float64 `json:"egg_mass"`
|
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||||
EggWeight float64 `json:"egg_weight"`
|
|
||||||
StandardHenDay *float64 `json:"hen_day_std,omitempty"`
|
|
||||||
StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
|
|
||||||
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
|
|
||||||
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
|
|
||||||
StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
|
|
||||||
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
|
|
||||||
StandardFcr *float64 `json:"fcr_std,omitempty"`
|
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingListDTO struct {
|
type RecordingListDTO struct {
|
||||||
RecordingRelationDTO
|
RecordingRelationDTO
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingDetailDTO struct {
|
type RecordingDetailDTO struct {
|
||||||
RecordingListDTO
|
RecordingListDTO
|
||||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
||||||
Stocks []RecordingStockDTO `json:"stocks"`
|
ProductCategory string `json:"product_category"`
|
||||||
Eggs []RecordingEggDTO `json:"eggs"`
|
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||||
|
Stocks []RecordingStockDTO `json:"stocks"`
|
||||||
|
Eggs []RecordingEggDTO `json:"eggs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingDepletionDTO struct {
|
type RecordingDepletionDTO struct {
|
||||||
@@ -63,7 +102,7 @@ type RecordingDepletionDTO struct {
|
|||||||
type RecordingStockDTO struct {
|
type RecordingStockDTO struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
UsageAmount float64 `json:"usage_amount"`
|
UsageAmount float64 `json:"usage_amount"`
|
||||||
PendingQty *float64 `json:"pending_qty,omitempty"`
|
PendingQty float64 `json:"pending_qty"`
|
||||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,117 +114,10 @@ type RecordingEggDTO struct {
|
|||||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingProductWarehouseDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
ProductId uint `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
WarehouseId uint `json:"warehouse_id"`
|
|
||||||
WarehouseName string `json:"warehouse_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|
||||||
var (
|
|
||||||
projectFlockCategory string
|
|
||||||
day int
|
|
||||||
totalDepletionQty float64
|
|
||||||
cumDepletionRate float64
|
|
||||||
cumIntake int
|
|
||||||
fcrValue float64
|
|
||||||
totalChickQty float64
|
|
||||||
henDay float64
|
|
||||||
henHouse float64
|
|
||||||
feedIntake float64
|
|
||||||
eggMass float64
|
|
||||||
eggWeight float64
|
|
||||||
)
|
|
||||||
|
|
||||||
if e.Day != nil {
|
|
||||||
day = *e.Day
|
|
||||||
}
|
|
||||||
if e.TotalDepletionQty != nil {
|
|
||||||
totalDepletionQty = *e.TotalDepletionQty
|
|
||||||
}
|
|
||||||
if e.CumDepletionRate != nil {
|
|
||||||
cumDepletionRate = *e.CumDepletionRate
|
|
||||||
}
|
|
||||||
if e.CumIntake != nil {
|
|
||||||
cumIntake = *e.CumIntake
|
|
||||||
}
|
|
||||||
if e.FcrValue != nil {
|
|
||||||
fcrValue = *e.FcrValue
|
|
||||||
}
|
|
||||||
if e.TotalChickQty != nil {
|
|
||||||
totalChickQty = *e.TotalChickQty
|
|
||||||
}
|
|
||||||
if e.HenDay != nil {
|
|
||||||
henDay = *e.HenDay
|
|
||||||
}
|
|
||||||
if e.HenHouse != nil {
|
|
||||||
henHouse = *e.HenHouse
|
|
||||||
}
|
|
||||||
if e.FeedIntake != nil {
|
|
||||||
feedIntake = *e.FeedIntake
|
|
||||||
}
|
|
||||||
if e.EggMass != nil {
|
|
||||||
eggMass = *e.EggMass
|
|
||||||
}
|
|
||||||
if e.EggWeight != nil {
|
|
||||||
eggWeight = *e.EggWeight
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
|
||||||
category := e.ProjectFlockKandang.ProjectFlock.Category
|
|
||||||
projectFlockCategory = category
|
|
||||||
}
|
|
||||||
|
|
||||||
latestApproval := defaultRecordingLatestApproval(e)
|
|
||||||
if e.LatestApproval != nil {
|
|
||||||
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
|
||||||
latestApproval = snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecordingRelationDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
|
||||||
RecordDatetime: e.RecordDatetime,
|
|
||||||
Day: day,
|
|
||||||
ProjectFlockCategory: projectFlockCategory,
|
|
||||||
TotalDepletionQty: totalDepletionQty,
|
|
||||||
CumDepletionRate: cumDepletionRate,
|
|
||||||
CumIntake: cumIntake,
|
|
||||||
FcrValue: fcrValue,
|
|
||||||
TotalChickQty: totalChickQty,
|
|
||||||
HenDay: henDay,
|
|
||||||
HenHouse: henHouse,
|
|
||||||
FeedIntake: feedIntake,
|
|
||||||
EggMass: eggMass,
|
|
||||||
EggWeight: eggWeight,
|
|
||||||
StandardHenDay: e.StandardHenDay,
|
|
||||||
StandardHenHouse: e.StandardHenHouse,
|
|
||||||
StandardFeedIntake: e.StandardFeedIntake,
|
|
||||||
StandardMaxDepletion: e.StandardMaxDepletion,
|
|
||||||
StandardEggMass: e.StandardEggMass,
|
|
||||||
StandardEggWeight: e.StandardEggWeight,
|
|
||||||
StandardFcr: e.StandardFcr,
|
|
||||||
Approval: latestApproval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
|
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
return toRecordingListDTO(e)
|
||||||
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
|
||||||
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
|
|
||||||
createdUser = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecordingListDTO{
|
|
||||||
RecordingRelationDTO: ToRecordingRelationDTO(e),
|
|
||||||
CreatedAt: e.CreatedAt,
|
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
CreatedUser: createdUser,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
||||||
@@ -197,20 +129,15 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||||
listDTO := ToRecordingListDTO(e)
|
listDTO := toRecordingListDTO(e)
|
||||||
|
|
||||||
var eggs []RecordingEggDTO
|
|
||||||
if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) {
|
|
||||||
eggs = ToRecordingEggDTOs(e.Eggs)
|
|
||||||
} else if len(e.Eggs) > 0 {
|
|
||||||
eggs = ToRecordingEggDTOs(e.Eggs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecordingDetailDTO{
|
return RecordingDetailDTO{
|
||||||
RecordingListDTO: listDTO,
|
RecordingListDTO: listDTO,
|
||||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
Warehouse: recordingWarehouseDTO(e),
|
||||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
ProductCategory: recordingProductCategory(e),
|
||||||
Eggs: eggs,
|
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||||
|
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||||
|
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,11 +160,15 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
|||||||
if s.UsageQty != nil {
|
if s.UsageQty != nil {
|
||||||
usageAmount = *s.UsageQty
|
usageAmount = *s.UsageQty
|
||||||
}
|
}
|
||||||
|
var pendingQty float64
|
||||||
|
if s.PendingQty != nil {
|
||||||
|
pendingQty = *s.PendingQty
|
||||||
|
}
|
||||||
|
|
||||||
result[i] = RecordingStockDTO{
|
result[i] = RecordingStockDTO{
|
||||||
ProductWarehouseId: s.ProductWarehouseId,
|
ProductWarehouseId: s.ProductWarehouseId,
|
||||||
UsageAmount: usageAmount,
|
UsageAmount: usageAmount,
|
||||||
PendingQty: s.PendingQty,
|
PendingQty: pendingQty,
|
||||||
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
|
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,6 +189,184 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toRecordingListDTO(e entity.Recording) RecordingListDTO {
|
||||||
|
relation := toRecordingRelationDTO(e)
|
||||||
|
|
||||||
|
var createdUser *userDTO.UserRelationDTO
|
||||||
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RecordingListDTO{
|
||||||
|
RecordingRelationDTO: relation,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||||
|
latestApproval := defaultRecordingLatestApproval(e)
|
||||||
|
if e.LatestApproval != nil {
|
||||||
|
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
latestApproval = snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
return RecordingRelationDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
ProjectFlock: toRecordingProjectFlockDTO(e),
|
||||||
|
RecordDatetime: e.RecordDatetime,
|
||||||
|
Day: intValue(e.Day),
|
||||||
|
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||||
|
CumDepletionRate: floatValue(e.CumDepletionRate),
|
||||||
|
CumIntake: intValue(e.CumIntake),
|
||||||
|
FcrValue: floatValue(e.FcrValue),
|
||||||
|
HenDay: floatValue(e.HenDay),
|
||||||
|
HenHouse: floatValue(e.HenHouse),
|
||||||
|
FeedIntake: floatValue(e.FeedIntake),
|
||||||
|
EggMass: floatValue(e.EggMass),
|
||||||
|
EggWeight: floatValue(e.EggWeight),
|
||||||
|
Approval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO {
|
||||||
|
result := RecordingProjectFlockDTO{
|
||||||
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk := e.ProjectFlockKandang
|
||||||
|
if pfk == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfk.ProjectFlock.Id != 0 {
|
||||||
|
result.FlockName = pfk.ProjectFlock.FlockName
|
||||||
|
if pfk.ProjectFlock.Category != "" {
|
||||||
|
result.ProjectFlockCategory = strings.ToUpper(pfk.ProjectFlock.Category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Period = pfk.Period
|
||||||
|
|
||||||
|
if pfk.ProjectFlock.ProductionStandard.Id != 0 {
|
||||||
|
result.ProductionStandart = &RecordingProductionStandardDTO{
|
||||||
|
Id: pfk.ProjectFlock.ProductionStandard.Id,
|
||||||
|
Week: recordingWeekValue(e),
|
||||||
|
Name: pfk.ProjectFlock.ProductionStandard.Name,
|
||||||
|
HenDayStd: floatValue(e.StandardHenDay),
|
||||||
|
HenHouseStd: floatValue(e.StandardHenHouse),
|
||||||
|
FeedIntakeStd: floatValue(e.StandardFeedIntake),
|
||||||
|
MaxDepletionStd: floatValue(e.StandardMaxDepletion),
|
||||||
|
EggMassStd: floatValue(e.StandardEggMass),
|
||||||
|
EggWeightStd: floatValue(e.StandardEggWeight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil {
|
||||||
|
result.Fcr = &RecordingFcrDTO{
|
||||||
|
Id: pfk.ProjectFlock.Fcr.Id,
|
||||||
|
Name: pfk.ProjectFlock.Fcr.Name,
|
||||||
|
FcrStd: floatValue(e.StandardFcr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TotalChickQty = floatValue(e.TotalChickQty)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordingWeekValue(e entity.Recording) int {
|
||||||
|
day := intValue(e.Day)
|
||||||
|
if day <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
weekBase := 1
|
||||||
|
if isLayingRecording(e) {
|
||||||
|
weekBase = 18
|
||||||
|
}
|
||||||
|
return ((day - 1) / 7) + weekBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLayingRecording(e entity.Recording) bool {
|
||||||
|
if e.ProjectFlockKandang == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying))
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordingProductCategory(e entity.Recording) string {
|
||||||
|
if e.ProjectFlockKandang == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
project := e.ProjectFlockKandang.ProjectFlock
|
||||||
|
if project.Id == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if project.ProductionStandard.Id != 0 && project.ProductionStandard.ProjectCategory != "" {
|
||||||
|
return strings.ToUpper(project.ProductionStandard.ProjectCategory)
|
||||||
|
}
|
||||||
|
if project.Category != "" {
|
||||||
|
return strings.ToUpper(project.Category)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
|
||||||
|
pw := primaryProductWarehouse(e)
|
||||||
|
if pw == nil || pw.Warehouse.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mapWarehouseDTO(&pw.Warehouse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
|
||||||
|
if len(e.Stocks) > 0 {
|
||||||
|
pw := e.Stocks[0].ProductWarehouse
|
||||||
|
if pw.Id != 0 {
|
||||||
|
return &pw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(e.Depletions) > 0 {
|
||||||
|
pw := e.Depletions[0].ProductWarehouse
|
||||||
|
if pw.Id != 0 {
|
||||||
|
return &pw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(e.Eggs) > 0 {
|
||||||
|
pw := e.Eggs[0].ProductWarehouse
|
||||||
|
if pw.Id != 0 {
|
||||||
|
return &pw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapWarehouseDTO(wh *entity.Warehouse) *RecordingWarehouseDTO {
|
||||||
|
if wh == nil || wh.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dto := &RecordingWarehouseDTO{
|
||||||
|
Id: wh.Id,
|
||||||
|
Name: wh.Name,
|
||||||
|
}
|
||||||
|
if wh.Area.Id != 0 {
|
||||||
|
dto.Area = &RecordingAreaDTO{
|
||||||
|
Id: wh.Area.Id,
|
||||||
|
Name: wh.Area.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wh.Location != nil && wh.Location.Id != 0 {
|
||||||
|
dto.Location = &RecordingLocationDTO{
|
||||||
|
Id: wh.Location.Id,
|
||||||
|
Name: wh.Location.Name,
|
||||||
|
Address: wh.Location.Address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
|
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
|
||||||
if pw == nil {
|
if pw == nil {
|
||||||
return productWarehouseDTO.ProductWarehouseDTO{}
|
return productWarehouseDTO.ProductWarehouseDTO{}
|
||||||
@@ -271,6 +380,20 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro
|
|||||||
return *mapped
|
return *mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func floatValue(value *float64) float64 {
|
||||||
|
if value == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
|
func intValue(value *int) int {
|
||||||
|
if value == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
||||||
result := approvalDTO.ApprovalRelationDTO{}
|
result := approvalDTO.ApprovalRelationDTO{}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
|
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
@@ -29,6 +31,16 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
|
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||||
|
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
|
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||||
|
|
||||||
|
productionStandardService := sProductionStandard.NewProductionStandardService(
|
||||||
|
productionStandardRepo,
|
||||||
|
productionStandardDetailRepo,
|
||||||
|
standardGrowthDetailRepo,
|
||||||
|
validate,
|
||||||
|
)
|
||||||
|
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
@@ -63,6 +75,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
approvalRepo,
|
approvalRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
fifoService,
|
fifoService,
|
||||||
|
productionStandardService,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|||||||
@@ -48,12 +48,30 @@ type RecordingRepository interface {
|
|||||||
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
||||||
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
||||||
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
|
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
|
||||||
|
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingRepositoryImpl struct {
|
type RecordingRepositoryImpl struct {
|
||||||
*repository.BaseRepositoryImpl[entity.Recording]
|
*repository.BaseRepositoryImpl[entity.Recording]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecordingTargetAverages struct {
|
||||||
|
HenDayAvg float64
|
||||||
|
HenDayCount int64
|
||||||
|
HenHouseAvg float64
|
||||||
|
HenHouseCount int64
|
||||||
|
EggWeightAvg float64
|
||||||
|
EggWeightCount int64
|
||||||
|
EggMassAvg float64
|
||||||
|
EggMassCount int64
|
||||||
|
FeedIntakeAvg float64
|
||||||
|
FeedIntakeCount int64
|
||||||
|
FcrAvg float64
|
||||||
|
FcrCount int64
|
||||||
|
CumDepletionRateAvg float64
|
||||||
|
CumDepletionRateCount int64
|
||||||
|
}
|
||||||
|
|
||||||
func NewRecordingRepository(db *gorm.DB) RecordingRepository {
|
func NewRecordingRepository(db *gorm.DB) RecordingRepository {
|
||||||
return &RecordingRepositoryImpl{
|
return &RecordingRepositoryImpl{
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
|
||||||
@@ -64,19 +82,28 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
return db.
|
return db.
|
||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
Preload("ProjectFlockKandang").
|
Preload("ProjectFlockKandang").
|
||||||
|
Preload("ProjectFlockKandang.Kandang").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock").
|
Preload("ProjectFlockKandang.ProjectFlock").
|
||||||
|
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
||||||
|
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||||
Preload("Depletions").
|
Preload("Depletions").
|
||||||
Preload("Depletions.ProductWarehouse").
|
Preload("Depletions.ProductWarehouse").
|
||||||
Preload("Depletions.ProductWarehouse.Product").
|
Preload("Depletions.ProductWarehouse.Product").
|
||||||
Preload("Depletions.ProductWarehouse.Warehouse").
|
Preload("Depletions.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Depletions.ProductWarehouse.Warehouse.Area").
|
||||||
|
Preload("Depletions.ProductWarehouse.Warehouse.Location").
|
||||||
Preload("Stocks").
|
Preload("Stocks").
|
||||||
Preload("Stocks.ProductWarehouse").
|
Preload("Stocks.ProductWarehouse").
|
||||||
Preload("Stocks.ProductWarehouse.Product").
|
Preload("Stocks.ProductWarehouse.Product").
|
||||||
Preload("Stocks.ProductWarehouse.Warehouse").
|
Preload("Stocks.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Stocks.ProductWarehouse.Warehouse.Area").
|
||||||
|
Preload("Stocks.ProductWarehouse.Warehouse.Location").
|
||||||
Preload("Eggs").
|
Preload("Eggs").
|
||||||
Preload("Eggs.ProductWarehouse").
|
Preload("Eggs.ProductWarehouse").
|
||||||
Preload("Eggs.ProductWarehouse.Product").
|
Preload("Eggs.ProductWarehouse.Product").
|
||||||
Preload("Eggs.ProductWarehouse.Warehouse")
|
Preload("Eggs.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Eggs.ProductWarehouse.Warehouse.Area").
|
||||||
|
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
||||||
@@ -433,6 +460,67 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) {
|
||||||
|
var row struct {
|
||||||
|
HenDayTotal float64
|
||||||
|
HenHouseTotal float64
|
||||||
|
EggWeightTotal float64
|
||||||
|
EggMassTotal float64
|
||||||
|
FeedIntakeTotal float64
|
||||||
|
FcrTotal float64
|
||||||
|
CumDepletionRateTotal float64
|
||||||
|
TotalCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
selectParts := []string{
|
||||||
|
"COALESCE(SUM(feed_intake), 0) AS feed_intake_total",
|
||||||
|
"COALESCE(SUM(fcr_value), 0) AS fcr_total",
|
||||||
|
"COALESCE(SUM(cum_depletion_rate), 0) AS cum_depletion_rate_total",
|
||||||
|
"COUNT(*) AS total_count",
|
||||||
|
}
|
||||||
|
if includeTargets {
|
||||||
|
selectParts = append([]string{
|
||||||
|
"COALESCE(SUM(hen_day), 0) AS hen_day_total",
|
||||||
|
"COALESCE(SUM(hen_house), 0) AS hen_house_total",
|
||||||
|
"COALESCE(SUM(egg_weight), 0) AS egg_weight_total",
|
||||||
|
"COALESCE(SUM(egg_mass), 0) AS egg_mass_total",
|
||||||
|
}, selectParts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Table("recordings").
|
||||||
|
Select(strings.Join(selectParts, ", ")).
|
||||||
|
Where("project_flock_kandangs_id = ? AND deleted_at IS NULL", projectFlockKandangID).
|
||||||
|
Scan(&row).Error; err != nil {
|
||||||
|
return RecordingTargetAverages{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := RecordingTargetAverages{
|
||||||
|
FeedIntakeCount: row.TotalCount,
|
||||||
|
FcrCount: row.TotalCount,
|
||||||
|
CumDepletionRateCount: row.TotalCount,
|
||||||
|
}
|
||||||
|
if includeTargets {
|
||||||
|
result.HenDayCount = row.TotalCount
|
||||||
|
result.HenHouseCount = row.TotalCount
|
||||||
|
result.EggWeightCount = row.TotalCount
|
||||||
|
result.EggMassCount = row.TotalCount
|
||||||
|
}
|
||||||
|
if row.TotalCount > 0 {
|
||||||
|
if includeTargets {
|
||||||
|
result.HenDayAvg = row.HenDayTotal / float64(row.TotalCount)
|
||||||
|
result.HenHouseAvg = row.HenHouseTotal / float64(row.TotalCount)
|
||||||
|
result.EggWeightAvg = row.EggWeightTotal / float64(row.TotalCount)
|
||||||
|
result.EggMassAvg = row.EggMassTotal / float64(row.TotalCount)
|
||||||
|
}
|
||||||
|
result.FeedIntakeAvg = row.FeedIntakeTotal / float64(row.TotalCount)
|
||||||
|
result.FcrAvg = row.FcrTotal / float64(row.TotalCount)
|
||||||
|
result.CumDepletionRateAvg = row.CumDepletionRateTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func nextRecordingDay(days []int) int {
|
func nextRecordingDay(days []int) int {
|
||||||
if len(days) == 0 {
|
if len(days) == 0 {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
|
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
@@ -53,6 +54,7 @@ type recordingService struct {
|
|||||||
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||||
ApprovalRepo commonRepo.ApprovalRepository
|
ApprovalRepo commonRepo.ApprovalRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ func NewRecordingService(
|
|||||||
approvalRepo commonRepo.ApprovalRepository,
|
approvalRepo commonRepo.ApprovalRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoSvc commonSvc.FifoService,
|
||||||
|
productionStandardSvc sProductionStandard.ProductionStandardService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) RecordingService {
|
) RecordingService {
|
||||||
return &recordingService{
|
return &recordingService{
|
||||||
@@ -75,6 +78,7 @@ func NewRecordingService(
|
|||||||
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
|
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
|
||||||
ApprovalRepo: approvalRepo,
|
ApprovalRepo: approvalRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
ProductionStandardSvc: productionStandardSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +173,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
recordTime := time.Now().UTC()
|
||||||
|
if req.RecordDate != nil && strings.TrimSpace(*req.RecordDate) != "" {
|
||||||
|
parsed, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordDate))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
|
||||||
|
}
|
||||||
|
recordTime = parsed.UTC()
|
||||||
|
}
|
||||||
|
|
||||||
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, req.ProjectFlockKandangId)
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, req.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,6 +200,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
if err := s.ensureChickInExists(ctx, pfk.Id); err != nil {
|
if err := s.ensureChickInExists(ctx, pfk.Id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if s.ProductionStandardSvc != nil {
|
||||||
|
if err := s.ProductionStandardSvc.EnsureWeekStart(ctx, pfk.ProjectFlock.ProductionStandardId, category); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
@@ -210,8 +227,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if s.ProductionStandardSvc != nil {
|
||||||
|
if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, nextDay); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recordTime := time.Now().UTC()
|
|
||||||
existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime)
|
existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to verify existing recording on date: %+v", err)
|
s.Log.Errorf("Failed to verify existing recording on date: %+v", err)
|
||||||
@@ -1157,7 +1178,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var fcrValue float64
|
var fcrValue float64
|
||||||
if usageInGrams > 0 && totalEggWeightGrams > 0 {
|
if usageInGrams > 0 && totalEggWeightGrams > 0 {
|
||||||
fcrValue = totalEggWeightGrams / usageInGrams
|
fcrValue = usageInGrams / totalEggWeightGrams
|
||||||
updates["fcr_value"] = fcrValue
|
updates["fcr_value"] = fcrValue
|
||||||
recording.FcrValue = &fcrValue
|
recording.FcrValue = &fcrValue
|
||||||
} else {
|
} else {
|
||||||
@@ -1330,12 +1351,16 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
week := ((int(*item.Day) - 1) / 7) + 1
|
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
|
||||||
|
weekBase := 1
|
||||||
|
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
weekBase = 18
|
||||||
|
}
|
||||||
|
week := ((int(*item.Day) - 1) / 7) + weekBase
|
||||||
if week <= 0 {
|
if week <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
|
|
||||||
db := s.Repository.DB()
|
db := s.Repository.DB()
|
||||||
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type (
|
|||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
|
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Stocks []Stock `json:"stocks" validate:"dive"`
|
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||||
Depletions []Depletion `json:"depletions" validate:"dive"`
|
Depletions []Depletion `json:"depletions" validate:"dive"`
|
||||||
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||||
|
|||||||
@@ -302,6 +302,16 @@ func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
|
|||||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suppliers := make([]supplierDTO.SupplierRelationDTO, len(p.ProductRelationDTO.Suppliers))
|
||||||
|
for i, ps := range p.ProductRelationDTO.Suppliers {
|
||||||
|
suppliers[i] = supplierDTO.SupplierRelationDTO{
|
||||||
|
Id: ps.Id,
|
||||||
|
Name: ps.Name,
|
||||||
|
Alias: ps.Alias,
|
||||||
|
Category: ps.Category,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return json.Marshal(&Alias{
|
return json.Marshal(&Alias{
|
||||||
Id: p.ProductRelationDTO.Id,
|
Id: p.ProductRelationDTO.Id,
|
||||||
Name: p.ProductRelationDTO.Name,
|
Name: p.ProductRelationDTO.Name,
|
||||||
@@ -310,6 +320,6 @@ func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
|
|||||||
Uom: p.ProductRelationDTO.Uom,
|
Uom: p.ProductRelationDTO.Uom,
|
||||||
Flags: p.ProductRelationDTO.Flags,
|
Flags: p.ProductRelationDTO.Flags,
|
||||||
ProductCategory: p.ProductRelationDTO.ProductCategory,
|
ProductCategory: p.ProductRelationDTO.ProductCategory,
|
||||||
Suppliers: p.ProductRelationDTO.Suppliers,
|
Suppliers: suppliers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,9 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
|||||||
|
|
||||||
func resolveDebtSupplierDateColumn(filterBy string) string {
|
func resolveDebtSupplierDateColumn(filterBy string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||||
case "receive_date":
|
|
||||||
return "purchases.receive_date"
|
|
||||||
case "po_date":
|
case "po_date":
|
||||||
return "purchases.po_date"
|
return "purchases.po_date"
|
||||||
case "do_date", "received_date", "":
|
case "received_date", "":
|
||||||
return "purchase_items.received_date"
|
return "purchase_items.received_date"
|
||||||
default:
|
default:
|
||||||
return "purchase_items.received_date"
|
return "purchase_items.received_date"
|
||||||
@@ -130,7 +128,7 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context
|
|||||||
Preload("Warehouse.Area").
|
Preload("Warehouse.Area").
|
||||||
Order("purchase_items.id ASC")
|
Order("purchase_items.id ASC")
|
||||||
|
|
||||||
if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "do_date") || strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" {
|
if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" {
|
||||||
if filters.StartDate != "" {
|
if filters.StartDate != "" {
|
||||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
db = db.Where("DATE(purchase_items.received_date) >= ?", dateFrom)
|
db = db.Where("DATE(purchase_items.received_date) >= ?", dateFrom)
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
|
|||||||
dataQuery := r.db.WithContext(ctx).
|
dataQuery := r.db.WithContext(ctx).
|
||||||
Model(&entity.Recording{}).
|
Model(&entity.Recording{}).
|
||||||
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
|
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||||
Preload("BodyWeights").
|
|
||||||
Preload("Eggs", func(db *gorm.DB) *gorm.DB {
|
Preload("Eggs", func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Select("recording_eggs.*, f.name AS product_flag_name").
|
return db.Select("recording_eggs.*, f.name AS product_flag_name").
|
||||||
Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").
|
Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").
|
||||||
|
|||||||
@@ -858,7 +858,7 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
|
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
|
||||||
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
|
if params.FilterBy == "" {
|
||||||
params.FilterBy = "received_date"
|
params.FilterBy = "received_date"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,25 +897,8 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
}
|
}
|
||||||
|
|
||||||
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
||||||
references := make([]string, 0)
|
|
||||||
seenRefs := make(map[string]struct{})
|
|
||||||
for _, purchase := range purchases {
|
for _, purchase := range purchases {
|
||||||
supplierID := purchase.SupplierId
|
purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase)
|
||||||
purchasesBySupplier[supplierID] = append(purchasesBySupplier[supplierID], purchase)
|
|
||||||
|
|
||||||
reference := purchase.PrNumber
|
|
||||||
if purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" {
|
|
||||||
reference = *purchase.PoNumber
|
|
||||||
}
|
|
||||||
if _, exists := seenRefs[reference]; !exists {
|
|
||||||
seenRefs[reference] = struct{}{}
|
|
||||||
references = append(references, reference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsByReferences(c.Context(), supplierIDs, references)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
||||||
@@ -940,6 +923,14 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
now := time.Now().In(location)
|
now := time.Now().In(location)
|
||||||
|
|
||||||
result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs))
|
result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs))
|
||||||
|
type debtSupplierRowItem struct {
|
||||||
|
Row dto.DebtSupplierRowDTO
|
||||||
|
SortTime time.Time
|
||||||
|
Order int
|
||||||
|
DeltaBalance float64
|
||||||
|
CountTotals bool
|
||||||
|
}
|
||||||
|
|
||||||
for _, supplierID := range supplierIDs {
|
for _, supplierID := range supplierIDs {
|
||||||
supplier, exists := supplierMap[supplierID]
|
supplier, exists := supplierMap[supplierID]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -947,23 +938,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||||
|
|
||||||
items := purchasesBySupplier[supplierID]
|
items := purchasesBySupplier[supplierID]
|
||||||
paymentItems := paymentsBySupplier[supplierID]
|
paymentItems := paymentsBySupplier[supplierID]
|
||||||
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
|
|
||||||
total := dto.DebtSupplierTotalDTO{}
|
total := dto.DebtSupplierTotalDTO{}
|
||||||
|
|
||||||
type debtSupplierRowItem struct {
|
|
||||||
Row dto.DebtSupplierRowDTO
|
|
||||||
SortTime time.Time
|
|
||||||
Order int
|
|
||||||
DeltaBalance float64
|
|
||||||
CountTotals bool
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||||
for _, purchase := range items {
|
for _, purchase := range items {
|
||||||
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
|
row := buildDebtSupplierRow(purchase, now, location)
|
||||||
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||||
Row: row,
|
Row: row,
|
||||||
@@ -996,6 +977,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
balance := initialBalance
|
balance := initialBalance
|
||||||
for i := range combinedRows {
|
for i := range combinedRows {
|
||||||
balance += combinedRows[i].DeltaBalance
|
balance += combinedRows[i].DeltaBalance
|
||||||
|
combinedRows[i].Row.DebtPrice = balance
|
||||||
combinedRows[i].Row.Balance = balance
|
combinedRows[i].Row.Balance = balance
|
||||||
|
|
||||||
if combinedRows[i].CountTotals {
|
if combinedRows[i].CountTotals {
|
||||||
@@ -1004,13 +986,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
total.Aging = row.Aging
|
total.Aging = row.Aging
|
||||||
}
|
}
|
||||||
total.TotalPrice += row.TotalPrice
|
total.TotalPrice += row.TotalPrice
|
||||||
total.PaymentPrice += row.PaymentPrice
|
|
||||||
total.DebtPrice += row.DebtPrice
|
|
||||||
} else {
|
} else {
|
||||||
combinedRows[i].Row.DebtPrice = balance
|
total.PaymentPrice += combinedRows[i].Row.PaymentPrice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
total.DebtPrice = balance
|
||||||
|
|
||||||
|
rows := make([]dto.DebtSupplierRowDTO, 0, len(combinedRows))
|
||||||
sortDesc := strings.EqualFold(params.SortOrder, "desc")
|
sortDesc := strings.EqualFold(params.SortOrder, "desc")
|
||||||
if sortDesc {
|
if sortDesc {
|
||||||
for i := len(combinedRows) - 1; i >= 0; i-- {
|
for i := len(combinedRows) - 1; i >= 0; i-- {
|
||||||
@@ -1039,18 +1021,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return result, totalSuppliers, nil
|
return result, totalSuppliers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]float64, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
|
func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
|
||||||
prNumber := purchase.PrNumber
|
prNumber := purchase.PrNumber
|
||||||
poNumber := ""
|
poNumber := ""
|
||||||
if purchase.PoNumber != nil {
|
if purchase.PoNumber != nil {
|
||||||
poNumber = *purchase.PoNumber
|
poNumber = *purchase.PoNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
reference := prNumber
|
|
||||||
if strings.TrimSpace(poNumber) != "" {
|
|
||||||
reference = poNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
prDate := purchase.CreatedAt.In(loc)
|
prDate := purchase.CreatedAt.In(loc)
|
||||||
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
|
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
|
||||||
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||||
@@ -1093,9 +1070,6 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentPrice := paymentTotals[reference]
|
|
||||||
debtPrice := paymentPrice - totalPrice
|
|
||||||
|
|
||||||
dueDate := ""
|
dueDate := ""
|
||||||
dueStatus := "-"
|
dueStatus := "-"
|
||||||
if purchase.DueDate != nil && !purchase.DueDate.IsZero() {
|
if purchase.DueDate != nil && !purchase.DueDate.IsZero() {
|
||||||
@@ -1109,10 +1083,6 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
}
|
}
|
||||||
|
|
||||||
status := "Belum Lunas"
|
status := "Belum Lunas"
|
||||||
if debtPrice >= 0 {
|
|
||||||
status = "Lunas"
|
|
||||||
}
|
|
||||||
|
|
||||||
poDate := ""
|
poDate := ""
|
||||||
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
||||||
poDate = purchase.PoDate.In(loc).Format("2006-01-02")
|
poDate = purchase.PoDate.In(loc).Format("2006-01-02")
|
||||||
@@ -1129,10 +1099,11 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
DueStatus: dueStatus,
|
DueStatus: dueStatus,
|
||||||
TotalPrice: totalPrice,
|
TotalPrice: totalPrice,
|
||||||
PaymentPrice: paymentPrice,
|
PaymentPrice: 0,
|
||||||
DebtPrice: debtPrice,
|
DebtPrice: 0,
|
||||||
Status: status,
|
Status: status,
|
||||||
TravelNumber: travelNumber,
|
TravelNumber: travelNumber,
|
||||||
|
Balance: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,32 +1133,30 @@ func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto
|
|||||||
DebtPrice: 0,
|
DebtPrice: 0,
|
||||||
Status: "Pembayaran",
|
Status: "Pembayaran",
|
||||||
TravelNumber: "-",
|
TravelNumber: "-",
|
||||||
|
Balance: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
|
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
|
||||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
if strings.EqualFold(strings.TrimSpace(filterBy), "po_date") {
|
||||||
case "po_date":
|
|
||||||
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
||||||
return purchase.PoDate.In(loc)
|
return purchase.PoDate.In(loc)
|
||||||
}
|
}
|
||||||
case "pr_date":
|
}
|
||||||
return purchase.CreatedAt.In(loc)
|
|
||||||
default:
|
earliest := time.Time{}
|
||||||
earliest := time.Time{}
|
for _, item := range purchase.Items {
|
||||||
for _, item := range purchase.Items {
|
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||||
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
received := item.ReceivedDate.In(loc)
|
|
||||||
if earliest.IsZero() || received.Before(earliest) {
|
|
||||||
earliest = received
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !earliest.IsZero() {
|
received := item.ReceivedDate.In(loc)
|
||||||
return earliest
|
if earliest.IsZero() || received.Before(earliest) {
|
||||||
|
earliest = received
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !earliest.IsZero() {
|
||||||
|
return earliest
|
||||||
|
}
|
||||||
|
|
||||||
return purchase.CreatedAt.In(loc)
|
return purchase.CreatedAt.In(loc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type DebtSupplierQuery struct {
|
|||||||
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
|
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
|
||||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"`
|
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"`
|
||||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user